Separate an empty source from an unmatched scope, and let a verified-empty source converge

main

Commit

nodo2w ago

Replicate failed every run whose planning produced no desired refs, with one message covering two unrelated conditions: the source has no refs, and the source has refs that the requested scope excluded. A caller could not tell them apart, and the first is not always a failure — a mirror of a repository that has never been pushed to is trivially up to date, yet it read as an error forever.

SyncPolicy.AllowEmptySource (off by default) opts into the distinction. With it set, Replicate reports ErrNoRefsSelected when the source does advertise refs, ErrSourceEmptyUnverified when it advertised none but never confirmed it is empty, ErrSourceEmptyTargetPopulated when it is confirmed empty while the target still holds refs, and a zero-plan success carrying ExecutionSummary.SourceEmpty when source and target are both empty and therefore already agree.

Emptiness is established from what the source asserts, never inferred from a response that merely carried no refs. git-sync now requests protocol v2's ls-refs=unborn where the server advertises it, so a repository with no commits answers with an explicit "unborn HEAD" line; only that assertion, under an all-refs scope, qualifies. The distinction is the point: a blank body behind a valid header, a server-side ref-listing or hide-pattern regression, or a narrowed ref-prefix all produce the same silence as an empty repository, and a caller acting on silence would act on every affected repository at once. The unborn line's symref-target is deliberately not surfaced as SourceHEAD, which consumers read as a branch that exists.

The divergent case refuses rather than converging. Converging means deleting every ref on the target, and the states that produce that signature — a source restored from backup, a wiped data plane, an out-of-band emptying — are the ones where the target may hold the only surviving copy.

The opt-in gate is checked first, so "off" is structurally identical to the behavior that predates this and not merely identical in the cases someone thought to test: a caller that has not opted in cannot receive a sentinel it has never heard of. The sentinels' messages deliberately avoid the historical "no source refs matched" text, so a caller that substring-matches that phrase cannot read one as the other and the order the checks run in is not load-bearing.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01714HJZAqpgwuwp6fcMWEhG Entire-Checkpoint: 01M0JBEEG33N57NPRZ1MAT4DY6

Checkpoints

can you change the status line so I can see the `Usage credits` of the month?

Claude CodeOpus 5.[1m]
View session
Checkpoint 1

Fix Git Sync Source Scope Handling

Claude CodeOpus 5
View session
Checkpoint 1