@@ -21,9 +21,293 @@ This edition covers what happened during the months of July and August 2026.
2121### General
2222-->
2323
24- <!-- -
2524### Reviews
26- -->
25+
26+ + [[ PATCH/RFC 0/6] commit-reach: terminate merge-base walk when one side is exhausted] ( https://lore.kernel.org/git/pull.2149.git.1781951820.gitgitgadget@gmail.com )
27+
28+ Kristofer Karlsson had a monorepo of around 2.6 million commits in
29+ which a very ordinary operation was painfully slow: computing the
30+ merge base between a pull request branch and the mainline took several
31+ seconds. He had described the problem in
32+ [ an earlier RFC discussion] ( https://lore.kernel.org/git/CAL71e4Ps-2_0+uuZu43N9pFnXBemoAohPs_eyRJf8taXHJPAXQ@mail.gmail.com/ ) ,
33+ and in June he followed it up with actual code, writing that he
34+ expected "the design to still be scrutinized, but that may be easier
35+ with actual code to look at".
36+
37+ The culprit is ` paint_down_to_common() ` in ` commit-reach.c ` , the
38+ function underlying ` git merge-base ` , ` git merge-tree ` , and every
39+ command that needs to know where two histories diverged. Kristofer's
40+ cover letter included an ASCII diagram of the shape that triggers the
41+ pathology: a repository import that grafts a separate history with its
42+ own root commit. When the walk from one side reaches a commit with a
43+ very low generation number that the other side never paints, the walk
44+ is forced to drain nearly the whole graph before it can convince itself
45+ that it is done. Any merge that introduces a low-generation commit
46+ never painted by the other side has the same effect.
47+
48+ The key observation, and the whole idea behind the series, is that a
49+ new merge-base candidate can only be discovered when exclusive
50+ ` PARENT1 ` and ` PARENT2 ` paint meet. In the initial numbers Kristofer
51+ reported, this turned a 4.293 second ` git merge-base --all ` across the
52+ import into 8 milliseconds, a 537x improvement, and a 5.345 second
53+ ` git merge-tree ` into 13 milliseconds.
54+
55+ ## Some background on the paint walk and generation numbers
56+
57+ ` paint_down_to_common() ` is a single traversal driven by one priority
58+ queue, which holds the frontier of commits waiting to be visited. It
59+ paints the first commit with the flag ` PARENT1 ` and each of the other
60+ commits with ` PARENT2 ` , puts them all in the queue, and then repeats
61+ one step until it can stop: pop the commit with the highest generation
62+ number, and pass its flags to its parents, putting each parent in the
63+ queue if it gained a flag it did not already have.
64+
65+ Painting and enqueuing are therefore the same operation: a commit is
66+ painted precisely when one of its children passes it a flag, and
67+ nothing else can change its paint. A commit holding both ` PARENT1 ` and
68+ ` PARENT2 ` has been reached from both sides, making it a merge-base
69+ candidate. A third flag, ` STALE ` , then spreads to its ancestors, which
70+ cannot be merge bases themselves.
71+
72+ A generation number, stored in the commit-graph file, records how far
73+ a commit is from a root, and a child's is always greater than its
74+ parent's. Since each pop returns the highest generation left in the
75+ queue, and every child of the popped commit has a higher generation
76+ still, no child of it can ever be popped later, so its paint is
77+ final. That is the property the new optimization depends on: it means
78+ that when one side has no exclusive commits left in the queue, none
79+ can ever reappear, so no new merge-base candidate can turn up and the
80+ walk can stop there.
81+
82+ ## The first round of review
83+
84+ Derrick Stolee reviewed the RFC thoroughly and set the tone for
85+ everything that followed: "Overall, I believe that this implementation
86+ is functionally correct and everything I have to say is about
87+ presentation and data gathering."
88+
89+ His most consequential structural objection was about the first two
90+ patches. Kristofer had moved ` ahead_behind() ` off the shared
91+ ` nonstale_queue ` abstraction in order to replace that abstraction with
92+ a new one. Stolee argued this was "essentially recreating its logic
93+ in a more disjointed way here, leaving this code in a worse state",
94+ and asked for a * new* data structure to be introduced alongside the
95+ existing one rather than replacing something that already worked for
96+ multiple callers. Kristofer agreed to leave ` ahead_behind() `
97+ untouched.
98+
99+ Stolee also asked for the switch statements tracking paint transitions
100+ to be reformatted per the coding guidelines, questioned whether the
101+ ` pq ` field name was wise when it could stand for either ` prio_queue ` or
102+ ` paint_queue ` , and made a suggestion that shaped the rest of the
103+ series: rather than testing the termination condition in the loop body,
104+ ` paint_queue_get() ` should return NULL when it detects that no further
105+ merge base can be found, so that the loop has a single exit. He
106+ preferred ` !count ` over summing counters and comparing to zero, too.
107+
108+ Separately, Elijah Newren had independently discovered the same
109+ optimization and had an implementation of his own in
110+ [ gitgitgadget PR #2150 ] ( https://github.com/gitgitgadget/git/pull/2150 ) .
111+ Rather than compete, the two combined efforts: Elijah's test cases were
112+ folded into the series as a patch authored by him, and Elijah's
113+ criss-cross counterexample from the earlier RFC thread, along with
114+ Stolee's, had already sharpened the halt condition.
115+
116+ When Kristofer wondered how to benchmark reliably given the noise from
117+ commit parsing, Stolee pointed him at
118+ [ hyperfine] ( https://github.com/sharkdp/hyperfine ) and showed the exact
119+ invocation for comparing two builds.
120+
121+ ## Measuring the walk instead of the clock
122+
123+ [ Version 2] ( https://lore.kernel.org/git/pull.2149.v2.git.1782303254.gitgitgadget@gmail.com )
124+ reordered the series so that the documentation came first, describing
125+ the algorithm as it already existed, and the tests came before the code
126+ changes so they could be shown passing with the old logic. The
127+ ` ahead_behind() ` patch was dropped, the new struct was renamed from
128+ ` paint_queue ` to ` paint_state ` , and all termination conditions moved
129+ into ` paint_queue_get() ` as Stolee had asked.
130+
131+ The most useful addition was one Stolee had implicitly asked for by
132+ requesting better "data gathering": a ` trace2_data_intmax() ` call
133+ reporting how many commits the paint walk visited. Because step counts
134+ are deterministic, unlike wall-clock times, they can be asserted in the
135+ test suite. Stolee was enthusiastic saying "This is great data", and
136+ suggested going further by reordering the instrumentation patch before
137+ the new tests, so that those tests would carry step-count assertions
138+ from birth and visibly update when the implementation changed. He also
139+ noticed that Kristofer had omitted step counts for one benchmark row.
140+ Kristofer confessed "I will have to attribute to laziness I suppose :)"
141+ and filled in the table.
142+
143+ Junio Hamano, the Git maintainer, read the new technical document and
144+ called it a "Great write-up that very clearly and concisely explains
145+ what goes on inside the merge-base computation. Thanks for a pleasant
146+ read."
147+
148+ ## A self-reported breakage, and a rename
149+
150+ [ Version 3] ( https://lore.kernel.org/git/pull.2149.v3.git.1782479286.gitgitgadget@gmail.com )
151+ consolidated the ` min_generation ` check and the generation-monotonicity
152+ ` BUG() ` assertion into ` paint_queue_get() ` as well, so that
153+ ` commit_graph_generation() ` is called exactly once per dequeued commit.
154+ Stolee worked through the change out loud, initially doubting that
155+ ` last_gen ` belonged in the struct and then concluding "This is an
156+ appropriate use of this value. My concerns are no longer valid. Thanks
157+ for letting me think out loud."
158+
159+ This round also broke ` t6600 ` , and Junio ejected it from ` seen ` .
160+ Kristofer had spotted the mistake himself and self-reported it, but
161+ only in a reply to the individual patch rather than to the cover
162+ letter. Junio was relaxed about it: "Mistakes happen, and do not need
163+ to rush, as collaboration is asynchronous around here anyway, and we
164+ may read our e-mails in different order ;-)", adding that "It would
165+ have been a more troubling experience if only my set-up were seeing the
166+ issue".
167+
168+ René Scharfe reviewed the new ` paint_state ` struct and asked whether
169+ its counters could ever go negative, suggesting ` size_t ` to match ` nr `
170+ from ` struct prio_queue ` , and then, in his words, indulged in "some
171+ bikeshedding" about the field names: why abbreviate ` p1_count ` when
172+ ` parent1_count ` reads more easily and pairs visibly with the ` PARENT1 `
173+ flag? [ Version 4] ( https://lore.kernel.org/git/pull.2149.v4.git.1782649547.gitgitgadget@gmail.com )
174+ adopted both suggestions, renaming the counters to ` parent1_count ` ,
175+ ` parent2_count ` and ` mb_candidate_count ` and switching them to ` size_t ` .
176+
177+ ## Growing scope: a test helper and an eight-year-old workaround
178+
179+ SZEDER Gábor made a brief but useful appearance on version 4, pointing
180+ out that the patch removing the now-unused ` nonstale_queue_put_dedup() `
181+ and ` nonstale_queue_get_dedup() ` wrappers had to be squashed into the
182+ preceding one: because the last callers disappeared in that earlier
183+ commit, the tree could no longer be built with ` DEVELOPER=1 ` without
184+ tripping ` -Wunused-function ` . Kristofer agreed, noting it was
185+ "unfortunate that this means the commit itself becomes less clean, but
186+ I don't have any other good solution -- and having each commit compile
187+ cleanly is more important."
188+
189+ [ Version 5] ( https://lore.kernel.org/git/pull.2149.v5.git.1782923832.gitgitgadget@gmail.com )
190+ grew the series to ten patches with two notable additions. The first
191+ was a ` test_trace2_data_singular() ` helper for
192+ ` t/test-lib-functions.sh ` : the existing ` test_trace2_data() ` is a bare
193+ ` grep ` that fails silently, whereas the new helper reports whether the
194+ key was missing, appeared more than once, or simply held the wrong
195+ value. Kristofer offered to drop it, calling it possibly "unnecessary
196+ infrastructure", but it survived. New test topologies with deliberate
197+ clock skew were also added, to exercise precisely the cases where
198+ date ordering would break the optimization.
199+
200+ The second addition was much bolder: removing the commit-date ordering
201+ fallback introduced by 091f4cf3 (commit: don't use generation numbers
202+ if not needed, 2018-08-30). That fallback existed because v1
203+ commit-graphs, which store topological levels rather than corrected
204+ commit dates, could make ` git merge-base v4.8 v4.9 ` on the Linux
205+ kernel walk 636k commits instead of 167k. Side exhaustion solves the
206+ same problem far better. Kristofer measured the step count for that
207+ query dropping to 5,725 on a v1 graph and 3,887 on a v2 graph. And
208+ removing the fallback means the queue is always generation-ordered, so
209+ every termination condition can rely on a single invariant. He noted
210+ that if this patch were kept, his separate
211+ ` kk/commit-reach-find-all-fix ` topic would become unnecessary.
212+
213+ ## Waiting for reviewers
214+
215+ [ Version 6] ( https://lore.kernel.org/git/pull.2149.v6.git.1783776466.gitgitgadget@gmail.com )
216+ prompted a process aside. Kristofer had written that the series was
217+ "rebased on next", and Junio responded firmly: "As always, do * not*
218+ base your patches on 'next'. I cannot apply such a patch series to my
219+ tree, as merging the resulting topic down to 'master' will pull _ all_
220+ the other topics, including those that are not ready", recommending
221+ instead a synthetic base built by merging only the topics actually
222+ depended upon. Kristofer explained he had merely * verified* against
223+ ` next ` and had in fact prepared exactly such a synthetic base, and that
224+ he should have said so more clearly. Junio replied "Well that is how I
225+ wiggled the series in my tree after all ;-)".
226+
227+ Then the topic stalled for several weeks, and Junio asked the list
228+ plainly: "we really need to get somebody take a look at these patches
229+ to move them forward. Any takers?". Kristofer, unfazed, offered to
230+ shrink the series, dropping the date-ordering cleanup, dropping the
231+ test helper, or squashing test commits, while wondering whether "this
232+ is simply the time of year where people take more vacation and are
233+ thus spending less time on code reviews". Elijah answered the call: "I
234+ started looking at the series and left a couple comments. I'll
235+ continue looking at it on Monday."
236+
237+ ## Naming the region
238+
239+ Elijah's review of
240+ [ version 7] ( https://lore.kernel.org/git/pull.2149.v7.git.1786013982.gitgitgadget@gmail.com )
241+
242+ was detailed and warm, and it repeatedly praised the series structure
243+ rather than just the code. Of the ` paint_state ` patch, he wrote "Ooh,
244+ I like this setup for what comes later; it sets the stage perfectly
245+ for the key insight behind the optimization". Of the optimization
246+ patch itself, he said "...this is the insight behind this
247+ optimization, which the previous patch set up so nicely."
248+
249+ His substantive concern was about a case the series had glossed over.
250+ With v1 commit-graphs, generation numbers * saturate* at
251+ ` GENERATION_NUMBER_V1_MAX ` , so many commits at genuinely different
252+ depths share one value, breaking ordering guarantees in exactly the
253+ same way as ` GENERATION_NUMBER_INFINITY ` does. Elijah asked "What
254+ about ` GENERATION_NUMBER_V1_MAX ` ?" and objected that the documentation
255+ mentioned the problem while the code at that point in the series only
256+ gated on infinity, so patch 8 was internally inconsistent. He also
257+ pushed on vocabulary: rather than "finite-generation region", why not
258+ call it the "reliably-ordered region"? Kristofer liked the coinage,
259+ replying that he would rewrite the documentation to speak of ordered
260+ versus unordered regions, "I think you coined it in one of the other
261+ emails, and I quite prefer that over infinite/finite", and confessed
262+ that "I've always found it easier to write correct code than useful
263+ documentation, so now the real work starts". Version 7 had already
264+ introduced a ` topo_ceiling ` (` V1_MAX ` for v1 graphs, ` INFINITY ` for
265+ v2) that the early-exit gates compare against, so saturated commits
266+ are treated as unordered.
267+
268+ Junio also spotted that one test patch's commit message described
269+ changes to ` t6600 ` that the diffstat showed happening two patches
270+ later. Kristofer traced it to a reorganisation back at version 5 and
271+ fixed it. Junio, apologising for "nitpicking", added "Maybe others can
272+ give more serious reviews on the topic. This gives us an important
273+ optimization."
274+
275+ ## Conclusion
276+
277+ [ Version 8] ( https://lore.kernel.org/git/pull.2149.v8.git.1786440533.gitgitgadget@gmail.com )
278+ moved ` topo_ceiling ` into the patch where the side-exhaustion gate
279+ first needs it, so V1 saturation is handled correctly at every commit
280+ in the series, and renamed the "finite/INFINITY region" to
281+ "ordered/unordered region" throughout. Elijah's response was
282+ unreserved:
283+
284+ > I am quite pleased with how this series has turned out. Not only does
285+ > it provide nice speedups, I think the structure of the series is
286+ > particularly nicely set up in a way that helps guide the discovery of
287+ > the idea behind the optimization for others to read, documents and
288+ > tests everything logically and thoroughly, and was a pleasant read.
289+
290+ He added a ` Reviewed-by: ` trailer, and then a mock complaint that
291+ Kristofer had normalised some double spaces after periods in the
292+ documentation: "Don't think for a second that I didn't notice you
293+ murdering these double spaces. You villain! ;-)" That prompted
294+ Kristofer to wonder whether the project should codify a preference.
295+ Junio ruled that this "is a thing that is best left for 'match the
296+ surrounding area' rule". Junio then asked "We can declare victory and
297+ mark the topic for 'next' now?", and both author and reviewer agreed.
298+
299+ The topic was merged to ` master ` on 23 August 2026 and should be
300+ released as part of Git 2.56.0 around the end of September 2026.
301+
302+ Beyond the speedups themselves, which will be most visible to anyone
303+ working in a large monorepo or a repository with a grafted import, the
304+ discussion left Git with three lasting artefacts: a new
305+ ` Documentation/technical/paint-down-to-common.adoc ` explaining an
306+ algorithm that had never been written down, trace2 step-count
307+ instrumentation that makes future work on the paint walk measurable
308+ without benchmarking wall-clock time, and one fewer special case, now
309+ that the 2018 commit-date ordering fallback has been removed and the
310+ merge-base queue is always generation-ordered.
27311
28312<!-- -
29313### Support
0 commit comments