Add topo bootstrap strategy for merge-heavy repos · Entire

Add topo bootstrap strategy for merge-heavy repos

67af394→main· Soph·2mo ago·7 files·+265 added/-10 removed

Bootstrap currently checkpoints along the first-parent chain. For repos where each first-parent step is a merge that pulls in a large side-branch ancestry (the "Checkpoint: " pattern), the smallest possible sub-pack is "everything one merge brings in" — which can be hundreds of MB and indivisible by the existing subdivision logic. Sub-packs hit the target's body limit with no finer-grained option short of changing where checkpoints land.

Add planner.TopoChainStoppingAt: a deterministic topological walk that includes every reachable commit (parents before children, hash-tie-broken for stable resume positioning). Plumb a new bootstrap.Params.Strategy ("first-parent" default, "topo" opt-in) through syncer.Config, unstable.AdvancedOptions, and a --bootstrap-strategy flag on sync/replicate/bootstrap.

Under "topo" the chain length grows by all merge-pulled commits, so sub-pack boundaries can land inside side branches. Cost: more source fetches and source-side enumeration work proportional to the extra commit count. Worth it when the first-parent floor is above the target body limit; otherwise first-parent is leaner.

Bootstrap loop is otherwise unchanged — chain[i] is still a hash in the source repo and "want chain[i] have current" still produces the right pack regardless of whether chain[i] is on the first-parent backbone or a side branch. Tag-phase logic and resume from temp refs both work as-is because the temp ref still points to whichever commit was last successfully pushed.

Sessions

b43bf65a227fView transcript

?\can you rebase soph/progress-indicators onto soph/smart-subdivisionClaude Code·Opus 4.7[1m]·1 step

Changes

7

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")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVarP(&req.Options.Verbose, "verbose", "v", false, "verbose logging")

Mcmd/git-sync/bootstrap.go+1

117 unmodified lines

118
119
120
121
122
123
124

117 unmodified lines

cmd.Flags().IntVar(&req.Options.MaterializedMaxObjects, "materialized-max-objects", unstable.DefaultMaterializedMaxObjects, "abort non-relay materialized syncs above this many objects")
    cmd.Flags().Int64Var(&req.Options.MaxPackBytes, "max-pack-bytes", 0, "abort bootstrap-relay push 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 for bootstrap: \"first-parent\" (default) or \"topo\". Use \"topo\" for merge-heavy repos where individual first-parent steps drag in unboundedly large side branches")
    addProtocolFlag(cmd, &protocolVal)
    cmd.Flags().BoolVarP(&req.Options.Verbose, "verbose", "v", false, "verbose logging")

Mcmd/git-sync/syncplan.go+1

60 unmodified lines

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
60 unmodified lines

return chain, nil
}

// TopoChainStoppingAt returns every commit reachable from tip
// (excluding any in stopAt and their ancestors) in a deterministic
// topological order: every commit's parents appear before the commit
// itself in the result. Ties are broken by hash so the order is stable
// across runs of the same source graph — important for resume, which
// looks up a temp-ref commit's position in the rebuilt chain.
//
// Where FirstParentChainStoppingAt walks only the first-parent
// backbone, this includes every merge-pulled side-branch commit too.
// For repos where individual first-parent steps drag in unboundedly
// large second-parent ancestries (the merge-heavy "checkpoint" pattern),
// topo order lets the bootstrap place sub-pack boundaries inside those
// side branches instead of being limited to first-parent granularity.
func TopoChainStoppingAt(store storer.EncodedObjectStorer, tip plumbing.Hash, stopAt map[plumbing.Hash]struct{}) ([]plumbing.Hash, error) {
    if _, stop := stopAt[tip]; stop {
        return nil, nil
    }

// BFS reachability collection. We need every commit and its parent
    // list to compute in-degrees for the topological sort below.
    type entry struct {
        commit  *object.Commit
        parents []plumbing.Hash
    }
    reachable := map[plumbing.Hash]entry{}
    queue := []plumbing.Hash{tip}
    for len(queue) > 0 {
        h := queue[0]
        queue = queue[1:]
        if _, ok := reachable[h]; ok {
            continue
        }
        if _, stop := stopAt[h]; stop {
            continue
        }
        commit, err := object.GetCommit(store, h)
        if err != nil {
            return nil, fmt.Errorf("load commit %s: %w", h, err)
        }
        reachable[h] = entry{commit: commit, parents: commit.ParentHashes}
        for _, p := range commit.ParentHashes {
            if _, stop := stopAt[p]; stop {
                continue
            }
            if _, ok := reachable[p]; ok {
                continue
            }
            queue = append(queue, p)
        }
    }

// Kahn's algorithm: emit commits whose every reachable parent has
    // already been emitted. children[p] is the list of reachable
    // commits that consider p a parent — so when we emit p, we can
    // decrement each child's in-degree.
    inDeg := make(map[plumbing.Hash]int, len(reachable))
    children := make(map[plumbing.Hash][]plumbing.Hash, len(reachable))
    for h, e := range reachable {
        for _, p := range e.parents {
            if _, ok := reachable[p]; !ok {
                continue
            }
            inDeg[h]++
            children[p] = append(children[p], h)
        }
    }

ready := make([]plumbing.Hash, 0)
    for h := range reachable {
        if inDeg[h] == 0 {
            ready = append(ready, h)
        }
    }
    sortHashes(ready)

chain := make([]plumbing.Hash, 0, len(reachable))
    for len(ready) > 0 {
        h := ready[0]
        ready = ready[1:]
        chain = append(chain, h)
        for _, child := range children[h] {
            inDeg[child]--
            if inDeg[child] == 0 {
                ready = appendSortedHash(ready, child)
            }
        }
    }
    if len(chain) != len(reachable) {
        return nil, fmt.Errorf("cycle detected in commit graph (emitted %d of %d)",
            len(chain), len(reachable))
    }
    return chain, nil
}

// sortHashes sorts a hash slice in lexicographic order. Used as the
// tie-breaker in topological emission so the chain is deterministic
// across runs even when several commits are simultaneously ready.
func sortHashes(hs []plumbing.Hash) {
    sort.Slice(hs, func(i, j int) bool {
        return hashLess(hs[i], hs[j])
    })
}

// appendSortedHash inserts h into an already-sorted slice while
// preserving sort order. Cheaper than re-sorting after each append in the
// topological-emission loop where we add one element at a time.
func appendSortedHash(hs []plumbing.Hash, h plumbing.Hash) []plumbing.Hash {
    idx := sort.Search(len(hs), func(i int) bool {
        return !hashLess(hs[i], h)
    })
    hs = append(hs, plumbing.ZeroHash)
    copy(hs[idx+1:], hs[idx:])
    hs[idx] = h
    return hs
}

func hashLess(a, b plumbing.Hash) bool {
    return a.Compare(b.Bytes()) < 0
}

// FirstParentChainFromMap walks a first-parent map from tip back to root.
// The map key is a commit hash, the value is its first parent hash.
// A zero-value parent marks the root. Returns the chain in root-to-tip order.