# Plumb TargetHEAD through Result/ProbeResult/bridge

`e470b83`·

Soph·2mo ago·2 files·+24 added/-9 removed

syncer.Result and syncer.ProbeResult re-gain the TargetHEAD field.
internalbridge.ProbeResult.TargetHEAD and ExecutionSummary.TargetHEAD
expose it to library callers / JSON output. Human Lines() emit a
"target-head: <ref>" line when populated.

All Result construction sites (sync, replicate, bootstrap wrapper)
pick up s.target.headTarget. ProbeResult does the same in the
s.target != nil branch.

Together with the previous commit's upload-pack discovery, callers
can now see both sides of the HEAD picture and decide what to do —
git-sync still doesn't actively propagate HEAD over the wire (no
go-git symref-update support yet), but the field-equality check is
enough for a CLI script or downstream tool to detect a default-branch
mismatch.

Refs #45.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

## Sessions

0b4edbe8a55fView 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-0b4edbe8a55f/index.html)

## Changes

2

- internal/syncer

- Msyncer.go+20/-9

- internalbridge

- Mmodel.go+4

```
130 unmodified lines

131
132
133
134
135
136
137
138
139
134
135
136
137
138
139
140
141
142
12 unmodified lines

155
156
157
158
159
160
161
23 unmodified lines

185
186
187
188
189
190
191
192
193
8 unmodified lines

202
203
204
205
206
207
208
10 unmodified lines

219
220
221
222
223
224
225
226
227
503 unmodified lines

731
732
733
726
734
735
736
737
37 unmodified lines

775
776
777
770
778
779
780
781
80 unmodified lines

862
863
864
865
866
867
868
12 unmodified lines

881
882
883
884
885
886
887
190 unmodified lines

1078
1079
1080
1071
1081
1082
1083
1084
100 unmodified lines

1185
1186
1187
1188
1189
1190
1191

130 unmodified lines

// Result holds the outcome of a sync or bootstrap operation.
//
// SourceHEAD carries the source's symref HEAD target as read from the
// upload-pack advertisement. It lets callers compare against the target's
// intended default branch without scraping the notice stream. Target HEAD
// is not exposed here because the receive-pack advertisement we already
// perform doesn't include HEAD; surfacing target HEAD would need a
// separate upload-pack round-trip and is tracked as a follow-up.
// SourceHEAD and TargetHEAD carry each side's symref HEAD target. Source
// is parsed from the upload-pack advertisement we already make. Target
// requires an additional upload-pack info-refs round-trip (the
// receive-pack advertisement omits HEAD by protocol design); when that
// fails — push-only auth, empty target where HEAD's underlying ref
// doesn't exist — TargetHEAD is empty.
type Result struct {
	Plans              []BranchPlan           `json:"plans"`
	Pushed             int                    `json:"pushed"`
12 unmodified lines

TempRefs           []string               `json:"tempRefs"`
	BootstrapSuggested bool                   `json:"bootstrapSuggested"`
	SourceHEAD         plumbing.ReferenceName `json:"sourceHead,omitempty"`
	TargetHEAD         plumbing.ReferenceName `json:"targetHead,omitempty"`
	Stats              Stats                  `json:"stats"`
	Measurement        Measurement            `json:"measurement"`
	Protocol           string                 `json:"protocol"`
23 unmodified lines

if r.SourceHEAD != "" {
		lines = append(lines, "source-head: "+r.SourceHEAD.String())
	}
	if r.TargetHEAD != "" {
		lines = append(lines, "target-head: "+r.TargetHEAD.String())
	}
	return lines
}

8 unmodified lines

TargetCaps    []string               `json:"targetCapabilities,omitempty"`
	Refs          []RefInfo              `json:"refs"`
	SourceHEAD    plumbing.ReferenceName `json:"sourceHead,omitempty"`
	TargetHEAD    plumbing.ReferenceName `json:"targetHead,omitempty"`
	Stats         Stats                  `json:"stats"`
	Measurement   Measurement            `json:"measurement"`
}
10 unmodified lines

if r.SourceHEAD != "" {
		lines = append(lines, "source-head: "+r.SourceHEAD.String())
	}
	if r.TargetHEAD != "" {
		lines = append(lines, "target-head: "+r.TargetHEAD.String())
	}
	if len(r.Capabilities) > 0 {
		lines = append(lines, "source-capabilities: "+strings.Join(r.Capabilities, ", "))
	}
503 unmodified lines

Plans: plans, DryRun: true, RelayReason: reason,
		OperationMode: modeSync, BootstrapSuggested: true, Stats: stats.snapshot(),
		Measurement: measurementDone(), Protocol: sourceService.Protocol,
		SourceHEAD: sourceService.HeadTarget,
		SourceHEAD: sourceService.HeadTarget, TargetHEAD: s.target.headTarget,
		}, nil
	}
	return bootstrapWithInputs(ctx, s, desiredRefs, targetRefMap, reason)
37 unmodified lines

result := Result{
		Plans: plans, DryRun: s.cfg.DryRun, OperationMode: modeSync, Protocol: sourceService.Protocol,
		Stats: stats.snapshot(), Measurement: measurementDone(),
		SourceHEAD: sourceService.HeadTarget,
		SourceHEAD: sourceService.HeadTarget, TargetHEAD: s.target.headTarget,
	}

pushPlans := make([]BranchPlan, 0, len(plans))
80 unmodified lines

Measurement:        s.measurementDone(),
		Protocol:           s.sourceService.Protocol,
		SourceHEAD:         s.sourceService.HeadTarget,
		TargetHEAD:         s.target.headTarget,
		}, nil
	}
	return bootstrapWithInputs(ctx, s, desiredRefs, s.target.refMap, "empty-target-managed-refs")
12 unmodified lines

Stats:         s.stats.snapshot(),
		Measurement:   s.measurementDone(),
		SourceHEAD:    s.sourceService.HeadTarget,
		TargetHEAD:    s.target.headTarget,
	}

pushPlans := make([]BranchPlan, 0, len(plans))
190 unmodified lines

Batching: bResult.Batching, BatchCount: bResult.BatchCount,
		PlannedBatchCount: bResult.PlannedBatchCount, TempRefs: bResult.TempRefs,
		Stats: s.stats.snapshot(), Measurement: s.measurementDone(), Protocol: s.sourceService.Protocol,
		SourceHEAD: s.sourceService.HeadTarget,
		SourceHEAD: s.sourceService.HeadTarget, TargetHEAD: s.target.headTarget,
		}, nil
	}
}

100 unmodified lines

if s.target != nil {
		result.TargetURL = s.cfg.Target.URL
		result.TargetCaps = gitproto.AdvRefsCaps(s.target.adv)
		result.TargetHEAD = s.target.headTarget
		result.Stats = s.stats.snapshot()
		result.Measurement = s.measurementDone()
	}
``

Minternal/syncer/syncer.go+20/-9

``
87 unmodified lines

88
89
90
91
92
93
94
21 unmodified lines

116
117
118
119
120
121
122
18 unmodified lines

141
142
143
144
145
146
147
22 unmodified lines

170
171
172
173
174
175
176

87 unmodified lines

TargetCaps    []string    `json:"targetCapabilities,omitempty"`
	Refs          []RefInfo   `json:"refs"`
	SourceHEAD    string      `json:"sourceHead,omitempty"`
	TargetHEAD    string      `json:"targetHead,omitempty"`
	Stats         Stats       `json:"stats"`
	Measurement   Measurement `json:"measurement"`
}
21 unmodified lines

Reason             string       `json:"reason"`
	BootstrapSuggested bool         `json:"bootstrapSuggested"`
	SourceHEAD         string       `json:"sourceHead,omitempty"`
	TargetHEAD         string       `json:"targetHead,omitempty"`
	Batch              BatchSummary `json:"batch"`
}

18 unmodified lines

TargetCaps:    append([]string(nil), result.TargetCaps...),
		Refs:          make([]RefInfo, 0, len(result.Refs)),
		SourceHEAD:    result.SourceHEAD.String(),
		TargetHEAD:    result.TargetHEAD.String(),
		Stats:         FromStats(result.Stats),
		Measurement:   FromMeasurement(result.Measurement),
	}
22 unmodified lines

Reason:             result.RelayReason,
		BootstrapSuggested: result.BootstrapSuggested,
		SourceHEAD:         result.SourceHEAD.String(),
		TargetHEAD:         result.TargetHEAD.String(),
		Batch: BatchSummary{
			Enabled: result.Batching,
			Planned: result.PlannedBatchCount,
