Merge pull request #41 from entireio/soph/topo-bootstrap · Entire

Merge pull request #41 from entireio/soph/topo-bootstrap

4fd509e→main·

Soph·2mo ago·10 files·+585 added/-19 removed

Add topo bootstrap strategy for merge-heavy repos

Changes

10

79 unmodified lines

80
81
82
83
84
85
86

79 unmodified lines

cmd.Flags().BoolVar(&jsonOutput, "json", false, "print JSON output")
    cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap if the streamed source pack exceeds this many bytes")
    cmd.Flags().Int64Var(&req.Options.TargetMaxPackBytes, "target-max-pack-bytes", 0, "target receive-pack body size limit; batches are planned and auto-subdivided to fit")
    cmd.Flags().StringVar(&req.Options.BootstrapStrategy, "bootstrap-strategy", "", "checkpoint chain ordering: \"first-parent\" (default) or \"topo\". Use \"topo\" for merge-heavy repos where individual first-parent steps drag in unboundedly large side branches; requires the target to allow non-fast-forward updates on the refs/gitsync/ namespace")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVarP(&req.Options.Verbose, "verbose", "v", false, "verbose logging")

Bootstrap chain ordering

Batched bootstrap walks a chain of source commits and places sub-pack boundaries (checkpoints) along it, so each push fits under --target-max-pack-bytes. Two orderings are available via --bootstrap-strategy:

The default is fine for most repos. topo is for the case where a single first-parent step pulls in a large side-branch ancestry and cannot be subdivided. Concrete example: assume the target's pack-body limit fits about two commits' worth of objects.

     root ── A ──────────────── M ── tip       (first-parent backbone)
              \                /
               S1 ─ S2 ─ S3 ─ S4              (side branch, merged at M)

Under first-parent, the planner only knows root → A → M → tip:

checkpoint 1:  root → A   pack {A}                  ✅ fits
checkpoint 2:  A → M      pack {S1, S2, S3, S4, M}  ❌ 5 commits, too big
checkpoint 3:  M → tip    pack {tip}                ✅ fits

The A → M step is one indivisible unit because no checkpoint can land on S2 — S2 isn't on the backbone. The bootstrap fails: the pack exceeds the limit and can't be split further.

Under topo, every reachable commit is a candidate checkpoint:

chain:  root → A → S1 → S2 → S3 → S4 → M → tip

checkpoint 1:  root → A    pack {A}        ✅
checkpoint 2:  A → S2      pack {S1, S2}   ✅
checkpoint 3:  S2 → S4     pack {S3, S4}   ✅
checkpoint 4:  S4 → M      pack {M}        ✅ tiny — merge content already pushed
checkpoint 5:  M → tip     pack {tip}      ✅

Trade-offs:

git-sync sync \
  --target-max-pack-bytes 100000000 \
  --bootstrap-strategy topo \
  <source-url> \
  <target-url>

Add --measure-memory to any command to sample elapsed time and Go heap usage:


TestTopoChainStoppingAtIncludesSideBranches

The test verifies the topo walk emits every reachable commit (not just first-parent), parents always before children. This is the property that lets bootstrap place sub-pack boundaries inside merge-pulled side branches instead of being limited to first-parent granularity.