# 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)
