Discover target HEAD via concurrent upload-pack info-refs · Entire
Discover target HEAD via concurrent upload-pack info-refs
03819d2·
Soph·2mo ago·2 files·+44 added/-7 removed
receive-pack advertisements don't include HEAD (server-side go-git's addReferences passes addHead=false for forPush, matching real git servers). So the session now does a second info-refs against the target's upload-pack endpoint, runs it concurrently with the existing receive-pack info-refs, and parses HEAD's symref capability from the result.
Cost: one extra HTTP GET per session, parallelised with the existing target request → effectively zero added latency. Failures are non-fatal: push-only auth that 401s on upload-pack just leaves targetSession.headTarget empty.
gitproto.DiscoverHEAD wraps the v1 advertisement parse. The session goroutine swallows errors; future telemetry could surface them if useful.
Refs #45.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Sessions
bf6948e65b2dView transcript
[?
do we have integration tests?Claude Code·Opus 4.7[1m]·1 step](/content/gh/entireio/git-sync/session/d0406407-d612-489d-b375-1372d062af82#timeline-bf6948e65b2d/index.html)
Changes
2
internal
gitproto
Mrefs.go+17
syncer
Msyncer.go+27/-7
200 unmodified lines
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
200 unmodified lines
}
// DiscoverHEAD reads HEAD's symref target from an upload-pack info-refs
// advertisement against conn. Used to find the target's default branch
// since the receive-pack advertisement we already query doesn't include
// HEAD (server-side addReferences passes addHead=false for forPush).
//
// Returns empty when HEAD is detached, the target advertisement omits it
// (typical for empty bare repos where HEAD's underlying ref doesn't yet
// exist), or any error occurs. Errors are returned for telemetry but
// callers typically treat this as best-effort.
func DiscoverHEAD(ctx context.Context, conn *Conn) (plumbing.ReferenceName, error) {
adv, err := AdvertisedRefsV1(ctx, conn, transport.UploadPackService)
if err != nil {
return "", fmt.Errorf("upload-pack info-refs: %w", err)
}
return headTargetFromAdv(adv), nil
}
// headTargetFromAdv extracts the branch HEAD points to from v1 advertised // capabilities. Returns empty when HEAD is detached or no symref is advertised. func headTargetFromAdv(adv *packp.AdvRefs) plumbing.ReferenceName {
Minternal/gitproto/refs.go+17
540 unmodified lines
541 542 543 544 545 546 547 548 549 544 545 546 547 548 549 550 551 552 553 55 unmodified lines
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 7 unmodified lines
646 647 648 630 649 650 651 652 653
540 unmodified lines
}
type targetSession struct { conn *gitproto.Conn adv *packp.AdvRefs refMap map[plumbing.ReferenceName]plumbing.Hash features gitproto.TargetFeatures policy planner.RelayTargetPolicy pusher *gitproto.Pusher conn *gitproto.Conn adv *packp.AdvRefs refMap map[plumbing.ReferenceName]plumbing.Hash features gitproto.TargetFeatures policy planner.RelayTargetPolicy pusher *gitproto.Pusher headTarget plumbing.ReferenceName }
// newSession performs the shared setup: protocol validation, mapping validation, 55 unmodified lines
return nil, fmt.Errorf("create target transport: %w", err) } targetConn.ProgressOut = &sessionStderr{s: s}
// Run upload-pack discovery concurrently with the receive-pack // advertisement: receive-pack tells us capabilities and current // refs (for push), upload-pack tells us HEAD's symref target // (receive-pack omits HEAD by protocol design). Errors on the // upload-pack side are non-fatal — push-only auth, for example, // would 401 and we just leave headTarget empty. headCh := make(chan plumbing.ReferenceName, 1) go func() { head, err := gitproto.DiscoverHEAD(ctx, targetConn) if err != nil { head = "" } headCh <- head }()
targetAdv, err := gitproto.AdvertisedRefsV1(ctx, targetConn, transport.ReceivePackService) if err != nil { <-headCh return nil, fmt.Errorf("list target refs: %w", err) } targetRefSlice, err := gitproto.AdvRefsToSlice(targetAdv) if err != nil { <-headCh return nil, fmt.Errorf("decode target refs: %w", err) } targetRefMap := gitproto.RefHashMap(targetRefSlice) 7 unmodified lines
CapabilitiesKnown: targetFeatures.Known, NoThin: targetFeatures.NoThin, }, pusher: gitproto.NewPusher(targetConn, targetAdv, cfg.Verbose), pusher: gitproto.NewPusher(targetConn, targetAdv, cfg.Verbose), headTarget: <-headCh, } if cfg.BestEffort { s.rejections = make(map[plumbing.ReferenceName]string)