Make the empty-source policy reachable, scoped, and honestly reported

main

Commit

nodo2w ago

Follow-up review of the empty-source work found the policy inert or wrong on several of its own headline paths.

Reachability. The emptiness decision hung off len(desiredRefs) == 0, after planning. For any request pinning refs by mapping, planner.BuildDesiredRefs errors on the absent mapped source ref first, so a mapping-scoped mirror of a genuinely empty repository got "source ref X not found" — matching none of the new sentinels — on exactly the state the policy exists to make succeed. An empty advertisement is now resolved before planning, where under AllRefs it is already a complete observation. Gated on the opt-in, so a caller that never asked for this still gets the planner's error verbatim.

Scope. The target-populated check counted every advertised target ref, ignoring exclusions and zero hashes, unlike every other consumer of the target ref map (replicateCanBootstrap, addPruneCandidates). A mirror that trims refs/pull/* whose target held only refs/pull/1/head was reported as permanently diverged over a ref the run would neither push nor prune — and refs/pull/* is the namespace this package's own docs cite as the benign case.

Contract. ExecutionSummary.SourceEmpty is renamed Converged: it requires both sides verified, so it was false in every other outcome including the diverged one where the source WAS verified empty. It also loses omitempty — for the field whose whole purpose is separating a converged run from a no-op, "false" and "this binary has no such field" must not be the same JSON — and is now rendered by Result.Lines(), so the text output cmd/git-sync actually prints is no longer byte-identical to an ordinary zero-work sync. The converged result carries the Relay fields every other successful replicate return sets.

Validation. AllowEmptySource silently required AllRefs, was silently discarded outside replicate mode, and could never succeed over protocol v1, whose ls-refs has no unborn signal. All three are now rejected at the request edge instead of threaded in and dropped. The v1 case validation cannot see — an "auto" SSH source that falls back mid-run — reports that the protocol cannot carry the signal, rather than "did not report an unborn HEAD", which reads as the server withholding refs and points an operator at a hideRefs misconfiguration or a compromised source. A v2 source not advertising ls-refs=unborn was misreported the same way and is now distinguished too.

Corroboration. RefService.SkippedRefNames was populated at one of four construction sites, leaving the invalid-name cross-check vacuous on v1; it fails closed today only because the !HeadUnborn check happens to run first. It is now a count set on every path through newV1RefService. A count also stops the slice being pinned to a struct that outlives the pack transfer — megabytes retained on a source advertising many invalid names, to answer a boolean.

Coverage. Every test hand-built a syncSession, so the chain the design rests on (ls-refs "unborn" -> decodeV2LSRefs -> RefService.HeadUnborn -> Config -> converged Result) had none: deleting the request argument left the whole suite green. The in-package fake v2 server advertised ls-refs=unborn but never emitted an unborn line; it does now, and Run is exercised end to end. Reverting any fix here turns a test red. The argument staying ungated is deliberate — it costs no round trip and lets Probe report an unborn HEAD without a convergence policy — and is pinned by tests, along with the advertisement gate that does matter.

Also: guard s.target, which is legitimately nil on Fetch and target-less Probe sessions; correct ErrNoRefsSelected's doc, which named two causes unreachable by construction; label the divergence count; document the unborn argument in docs/protocol.md and correct the CHANGELOG's "nothing changes for existing callers", which the ungated request falsifies for every v2 caller.

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

Checkpoints

Fix Silent Field Drops in Git Sync Configuration

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