# Merge pull request \#51 from entireio/soph/head-bootstrap-default-branch

`52006e6`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·2mo ago·6 files·+357 added/-42 removed

Bootstrap: push source HEAD's branch first; surface source HEAD in results

## Changes

6

- docs

- Musage.md+26

- internal

- strategy/bootstrap

- Mbootstrap.go+44/-13

- Mbootstrap\_test.go+62

- syncer

- Mintegration\_test.go+178

- Msyncer.go+43/-29

- internalbridge

- Mmodel.go+4

````
221 unmodified lines

222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
4 unmodified lines

257
258
259
260
261
262
263

221 unmodified lines

<target-url>
```

## HEAD / Default Branch

git-sync surfaces the source's symref HEAD target — the source's default
branch — in result output:

- JSON: `execution.sourceHead` (sync / plan / replicate / bootstrap); `sourceHead` (probe).
- Human: a `source-head: <ref>` line.

```
$ git-sync probe --json <source-url> | jq .sourceHead
"refs/heads/main"
```

**Bootstrap pushes the source HEAD's branch first.** When initializing a new
repository, the default branch is based on the first push to a fresh repo (GitHub, GitLab,
others), this makes the mirror's default branch match the source
without any manual step. No flag needed — the ordering is always applied.

git's wire protocol has no command for updating a remote symref, so for
hosts that *don't* infer the default from first-push (raw bare repos,
some self-hosted setups), the default branch has to be set out of band:

1. **Match the default at init time** — `git init --bare --initial-branch=<source-default>` on the target before the first sync.
2. **Set HEAD post-sync** — `git symbolic-ref HEAD refs/heads/<source-default>` on the bare repo, or the host's API/UI (GitHub `PATCH /repos/{owner}/{repo}` with `default_branch`; GitLab `PUT /projects/:id` with `default_branch`).

## JSON Output

Add `--json` to any command to emit machine-readable output instead of the default text format.
4 unmodified lines

- refs and hashes are serialized as strings, not raw byte arrays
- top-level keys include `plans`, `pushed`, `skipped`, `blocked`, `deleted`, `warned`, `dryRun`, `protocol`, and `stats`, plus `relay`, `relayMode`, `relayReason`, `batching`, `batchCount`, `plannedBatchCount`, and `tempRefs`
- each item in `plans` includes stable string fields such as `branch`, `sourceRef`, `targetRef`, `sourceHash`, `targetHash`, `kind`, `action`, and `reason`
- `execution.sourceHead` (sync/plan/replicate/bootstrap) and `sourceHead` (probe) carry the source's symref HEAD target when advertised

## Auth
````

Mdocs/usage.md+26

```
175 unmodified lines

176
177
178
179
179
180
181
182
13 unmodified lines

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
565 unmodified lines

804
805
806
770
771
772
773
774
775
776
777
778
779
780
807
808
809
810
811
812
782
813
814
815
816

175 unmodified lines

if p.OnPhase != nil {
		p.OnPhase("pushing pack")
	}
	cmds := convert.PlansToPushCommands(plans)
	cmds := convert.PlansToPushCommands(hoistSourceHeadPlan(plans, p.SourceHeadTarget))
	pushErr := p.TargetPusher.PushPack(ctx, cmds, packReader)
	_ = packReader.Close()
	if pushErr != nil {
13 unmodified lines

return result, nil
}

// hoistSourceHeadPlan moves the plan whose source ref matches the
// source's symref HEAD target to the front, so the resulting push
// commands send that ref first. Hosts that pick the default branch
// from the first push on a fresh repo (GitHub, GitLab) end up with the
// right default. Matching is on SourceRef rather than TargetRef so
// --map remappings push the correct (mapped) target ref first.
func hoistSourceHeadPlan(plans []planner.BranchPlan, sourceHEAD plumbing.ReferenceName) []planner.BranchPlan {
	if sourceHEAD == "" {
		return plans
	}
	out, _ := hoistFirstMatch(plans, func(p planner.BranchPlan) bool {
		return p.SourceRef == sourceHEAD
	})
	return out
}

// hoistFirstMatch moves the first element satisfying match to the front
// of xs. Returns the (possibly new) slice and the original index of the
// hoisted element (-1 when no match). When the match is already at
// position 0, returns the input slice unchanged.
func hoistFirstMatch[T any](xs []T, match func(T) bool) ([]T, int) {
	for i, x := range xs {
		if !match(x) {
			continue
		}
		if i == 0 {
			return xs, 0
		}
		out := make([]T, 0, len(xs))
		out = append(out, x)
		out = append(out, xs[:i]...)
		out = append(out, xs[i+1:]...)
		return out, i
	}
	return xs, -1
}

func adjustedBootstrapTargetRefs(
	desiredRefs map[plumbing.ReferenceName]planner.DesiredRef,
	targetRefs map[plumbing.ReferenceName]plumbing.Hash,
565 unmodified lines

if sourceHeadTarget == "" {
		return desired, -1
	}
	for i, ref := range desired {
		if ref.SourceRef == sourceHeadTarget {
			if i == 0 {
				return desired, 0
			}
		reordered := make([]planner.DesiredRef, 0, len(desired))
		reordered = append(reordered, ref)
		reordered = append(reordered, desired[:i]...)
		reordered = append(reordered, desired[i+1:]...)
		return reordered, 0
	}
	reordered, idx := hoistFirstMatch(desired, func(r planner.DesiredRef) bool {
		return r.SourceRef == sourceHeadTarget
	})
	if idx < 0 {
		return desired, -1
	}
	return desired, -1
	return reordered, 0
}

// PlanCheckpoints plans the checkpoint hashes for a single branch during batched bootstrap.
