fix: a bare's HEAD symref outlived the branch it named - #512
Conversation
`git symbolic-ref HEAD` prints what HEAD points at and exits 0 whether or not that ref is there, so a bare cache cloned when the default was `master` kept answering `master` after the upstream rename and the prune that deleted the ref. Nothing repoints it: dl has never written a bare's HEAD, and #477 decided it should not start. The reading verifies the ref before it believes the name, and treats a name that is not a ref exactly as it treats a refusal: fall through to the next probe. That leaves default_branch_of the whole of the reading, so no caller can get a default branch read off a ref the repository has not got. The adopt path is where it was measured: register_existing_bare rebuilds a record by reading the clone, so deleting metadata.json wrote the dead branch straight back with every later fallback unreachable behind it.
Reviewer's GuideFixes stale default-branch detection in bare clones by verifying each symbolic ref’s full target before accepting it, allowing fallback probes to identify a branch that still exists; adds focused fake-Git, integration, and sequence coverage plus changelog documentation. Sequence diagram for verified default-branch detectionsequenceDiagram
participant RepositoryManager
participant Git
RepositoryManager->>Git: symbolic_ref(repo_path, reference)
Git-->>RepositoryManager: named ref
RepositoryManager->>Git: verify_ref(repo_path, named)
alt target ref exists
Git-->>RepositoryManager: success
RepositoryManager-->>RepositoryManager: return branch_in_symbolic_ref(named)
else target ref is missing
Git-->>RepositoryManager: refusal
RepositoryManager->>Git: symbolic_ref(repo_path, next reference)
Git-->>RepositoryManager: next named ref
RepositoryManager->>Git: verify_ref(repo_path, next named ref)
end
opt both symbolic refs are invalid
RepositoryManager->>Git: remote_branch_listing(repo_path)
Git-->>RepositoryManager: existing remote branch
end
Flow diagram for stale symbolic-ref fallbackflowchart TD
A[default_branch_of] --> B[symbolic_ref HEAD]
B --> C[verify_ref full target]
C -->|exists| D[return branch name]
C -->|missing| E[symbolic_ref refs/remotes/origin/HEAD]
E --> F[verify_ref full target]
F -->|exists| G[return branch name]
F -->|missing| H[remote_branch_listing]
H -->|branch found| I[return listed branch]
H -->|no answer| J[return main fallback]
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
Fresh-context adversarial review (session devlaunch-2b, orchestrator, not the author; GitHub refuses a self-approval under the shared account, so the review lands as a comment). Hunted three specific holes and could construct none: (1) probe 2 reintroducing the dangling-name lie one probe later — refuted, |
Closes #477.
The lie
A bare clone remembers the default branch it was cloned with, in its
HEADsymref, and nothing repoints it afterwards.
dlhas never written a bare'sHEAD(noset-head, noremote showanywhere in the tree), and #477 decidedit should not start: repointing at fetch time is a state mutation standing in
for a reader that is not total, it costs a remote round trip, and it is wrong
between fetches anyway.
So a repository that renames
mastertomainleaves the cache pointing atrefs/heads/masterlong after the prune that deleted the ref, andgit symbolic-ref HEADkeeps answeringmaster, exit 0, forever. A symbolicref is a name, not a branch, and
symbolic-refdoes not check its target.default_branch_ofread that first answer and returned it, so every laterprobe that could have answered correctly sat unreachable behind it.
The fix
The two symref probes now check that the name is a ref the clone really has
(
show-ref --verifyon the full ref, prefix and all, sincerefs/heads/mainand
refs/remotes/origin/mainare different refs), and a name that is not oneis treated exactly as a refusal is: ask the next probe, then
branch -r, thenthe caller's
main.The seam is the flow layer,
RepositoryManager::default_branch_of, anddeliberately not the git client.
clients/gitstates that a verb never fallsback to another verb and decides nothing about sequence, and there is no single
git command that both dereferences a symref and verifies its target
(
symbolic-refdoes the first,show-ref --verifythe second;rev-parse --verify --symbolic-full-namewould do both but answersHEADfora detached HEAD, which is a new wrong name in place of the old one). Since
default_branch_ofis the only reader ofGit::symbolic_refin the tree andthe only place a default branch is read off a clone, making it total leaves no
caller able to obtain a name that was read off a ref that is not there.
Cost: one extra local
show-refper clone or adopt. It reads refs off the disknext to a
git clone, and the cold-launch sequence test records it.Where it was measured
register_existing_bare(the adopt path). It rebuilds a record by reading theclone, so a user who deletes
metadata.jsonhad the dead branch writtenstraight back.
real_git_does_not_re_record_a_default_branch_the_remote_has_deletedbuilds that state the way it actually arrives: clone the cache while the default
is
master, rename the remote's default tomain, deletemasterupstream,prune-fetch. Before this change the rebuilt record said
master, a ref theclone has not got; after it,
main, which the clone has.Not in scope
The recorded
default_branchis written once and never revalidated, so a moveddefault goes unnoticed until something fails on it. That is #507, with its own
trace, and nothing here changes it.
Tests
a_symbolic_ref_whose_branch_is_gone_is_not_an_answer(fake git: HEAD names adead ref, the answer comes from
refs/remotes/origin/HEAD; the argv listpins which ref is checked)
both_symbolic_refs_being_gone_falls_through_to_the_listingreal_git_does_not_re_record_a_default_branch_the_remote_has_deletedthe_default_branch_is_read_from_head_then_the_remote_head_then_the_listingand
a_cold_launch_issues_exactly_this_sequenceupdated for the added probecargo test --workspace,cargo clippy --locked --all-targets -- -D warningsand
cargo fmt --checkare green. No public API change, so no snapshot moved.🤖 Generated with Claude Code
Summary by Sourcery
Ensure default-branch discovery ignores dangling symbolic refs so caches do not preserve deleted branch names.
Bug Fixes:
Enhancements:
Documentation:
Tests: