Surface source HEAD symref in Result and ProbeResult · Entire
Surface source HEAD symref in Result and ProbeResult
164174f·
Soph·2mo ago·2 files·+98 added/-92 removed
The source's symref HEAD target (e.g. refs/heads/main) is already captured during the source advertisement parse but never escapes the session. Surface it on syncer.Result and syncer.ProbeResult so library callers and the CLI can compare against the target's intended default branch.
The test-server scope grew slightly: the v2 ls-refs handler now honors the symrefs argument and emits HEAD with a symref-target attribute, mirroring what real git advertises. Two new integration tests pin the field through both Sync and Probe.
Target HEAD is deliberately not exposed here. The receive-pack advertisement we already perform doesn't include HEAD (per go-git's serve.go: addReferences passes addHead=false for forPush), and detecting it would need a separate upload-pack round-trip against the target. Tracked as a follow-up.
Refs #45.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Sessions
5e441c7c5d73View transcript
Changes
2
internal/syncer
Mintegration_test.go+54/-63
Msyncer.go+44/-29
1849 unmodified lines
// ProbeResult.SourceHEAD and TargetHEAD let `git-sync probe` preview the
default-branch mismatch without actually mutating the target.
func TestProbe_IntegrationSurfacesBothHEADs(t *testing.T) {
// ProbeResult.SourceHEAD lets `git-sync probe` show the source's default
// branch without performing a sync. Same protocol limitation as the sync
// path: target HEAD isn't exposed by receive-pack advertisements.
func TestProbe_IntegrationSurfacesSourceHEAD(t *testing.T) {
sourceRepo, sourceFS := newSourceRepo(t)
makeCommits(t, sourceRepo, sourceFS, 1)
targetRepo, err := git.Init(memory.NewStorage(), nil, git.WithDefaultBranch("refs/heads/main"))
if err != nil {
t.Fatalf("init target repo: %v", err)
}
sourceServer := newSmartHTTPRepoServerV2(t, sourceRepo)
targetServer := newSmartHTTPRepoServer(t, targetRepo)
defersourceServer.Close()
defertargetServer.Close()
result, err := Probe(context.Background(), Config{
Source: Endpoint{URL: sourceServer.RepoURL()},
Target: Endpoint{URL: targetServer.RepoURL()},
ProtocolMode: protocolModeAuto,
})
if err != nil {
t.Fatalf("probe: %v", err)
}
wantSource := plumbing.NewBranchReferenceName(testBranch)
if result.SourceHEAD != wantSource {
t.Errorf("SourceHEAD = %q, want %q", result.SourceHEAD, wantSource)
}
wantTarget := plumbing.ReferenceName("refs/heads/main")
if result.TargetHEAD != wantTarget {
t.Errorf("TargetHEAD = %q, want %q", result.TargetHEAD, wantTarget)
}
if got, want := result.SourceHEAD, plumbing.NewBranchReferenceName(testBranch); got != want {
t.Errorf("SourceHEAD = %q, want %q", got, want)
}
}
968 unmodified lines
// Result.SourceHEAD and Result.TargetHEAD capture each side's symref HEAD
target, so library callers can detect a default-branch mismatch without
scraping the notice stream. Bootstrap into an empty target whose bare-repo
HEAD default differs from the source's surfaces this case.
func TestRun_IntegrationBootstrapSurfacesHEADInResult(t *testing.T) {
// Result.SourceHEAD carries the source's symref HEAD target, parsed from
// the v2 ls-refs response (or v1 advertisement's symref capability), so
// library callers can compare against the target's intended default
// branch without out-of-band metadata. Target HEAD is not exposed: the
// receive-pack advertisement we already query doesn't include HEAD, and
// detecting it would need a separate upload-pack round-trip.
func TestRun_IntegrationSyncSurfacesSourceHEADInResult(t *testing.T) {
sourceRepo, sourceFS := newSourceRepo(t)
makeCommits(t, sourceRepo, sourceFS, 1)
// go-git Init defaults HEAD to refs/heads/master; pin target to
// refs/heads/main so the symrefs diverge.
targetRepo, err := git.Init(memory.NewStorage(), nil, git.WithDefaultBranch("refs/heads/main"))
if err != nil {
t.Fatalf("init target repo: %v", err)
}
result, err := Run(context.Background(), Config{
Source: Endpoint{URL: sourceServer.RepoURL()},
Target: Endpoint{URL: targetServer.RepoURL()},
ProtocolMode: protocolModeAuto,
})
if err != nil {
t.Fatalf("sync: %v", err)
}
wantSourceHEAD := plumbing.NewBranchReferenceName(testBranch)
if result.SourceHEAD != wantSourceHEAD {
t.Errorf("SourceHEAD = %q, want %q", result.SourceHEAD, wantSourceHEAD)
}
wantTargetHEAD := plumbing.ReferenceName("refs/heads/main")
if result.TargetHEAD != wantTargetHEAD {
t.Errorf("TargetHEAD = %q, want %q", result.TargetHEAD, wantTargetHEAD)
}
}
// When source HEAD and target HEAD match, the sync surfaces both fields
// identically — no mismatch from the consumer's perspective.
func TestRun_IntegrationBootstrapHEADMatch(t *testing.T) {
sourceRepo, sourceFS := newSourceRepo(t)
makeCommits(t, sourceRepo, sourceFS, 1)
// Both source and target default to refs/heads/master via go-git's
// plumbing.Master.
targetRepo, err := git.Init(memory.NewStorage())
if err != nil {
t.Fatalf("init target repo: %v", err)
}
12 unmodified lines
// lsRefsCoversHead returns true when prefixes is empty (matches everything)
// or any prefix is a prefix of "HEAD".
func lsRefsCoversHead(prefixes []string) bool {
if len(prefixes) == 0 {
return true
}
for _, p := range prefixes {
if strings.HasPrefix("HEAD", p) {
return true
}
}
return false
}
// lsRefsHeadLine formats a v2 ls-refs HEAD line with symref-target attribute,
// matching what real git advertises when a client requests "symrefs".
func (s *smartHTTPRepoServer) lsRefsHeadLine() (string, bool) {
head, err := s.repo.Storer.Reference(plumbing.HEAD)
if err != nil || head.Type() != plumbing.SymbolicReference {
return "", false
}
resolved, err := s.repo.Reference(head.Target(), true)
if err != nil {
return "", false
}
return fmt.Sprintf("%s HEAD symref-target:%s\n", resolved.Hash(), head.Target()), true
}
func (s *smartHTTPRepoServer) refsMatchingPrefixes(prefixes []string) ([]*plumbing.Reference, error) {
iter, err := s.repo.Storer.IterReferences()
if err != nil {
Minternal/syncer/integration_test.go+54/-63
129 unmodified lines
"