commit-reach: terminate merge-base walk when one paint side is exhausted
Add an early termination check to paint_down_to_common() using the
per-side counters introduced earlier. Once the walk enters the
finite-generation region, terminate early when one side's exclusive
count drops to zero -- no new merge-base can form without both paint
sides meeting.
The check also waits for pending_merge_bases to reach zero, ensuring
all merge-base candidates have been dequeued and recorded before
exiting.
The INFINITY gate ensures correctness: commits without a commit-graph
entry have GENERATION_NUMBER_INFINITY and are ordered by commit date,
which is not topologically reliable. The optimization only fires
once the walk enters the finite-generation region where ordering
guarantees hold.
Step counts measured with trace2 on git.git with commit-graph:
merge-base --all v2.0.0 v2.55.0-rc1:
before: 72264 steps after: 44589 steps
merge-base --all v2.55.0-rc1 v2.55.0-rc1~5:
before: 110 steps after: 7 steps
Helped-by: Derrick Stolee <stolee@gmail.com>
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Kristofer Karlsson <krka@spotify.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Kristofer Karlsson committedJul 11, 2026 at 13:27 UTC8df944d32a9792d07e60081222c7b31fd98e6c8b
3 files changed+39-6
Documentation/technical/paint-down-to-common.adoc
+22-1
index 3b3f7ac8af..7ae9b11529 100644--- a/Documentation/technical/paint-down-to-common.adoc+++ b/Documentation/technical/paint-down-to-common.adoc@@ -105,6 +105,9 @@ ends when one of the following conditions holds: 4. Single result: the caller only needs one merge base, one has been found, and the walk has entered the finite-generation region.+ 5. Side exhaustion: no pure PARENT1 or pure PARENT2 commits+ remain in the queue, no pending merge-base candidates exist,+ and the walk has entered the finite-generation region. Stale entry condition ~~~~~~~~~~~~~~~~~~~~~@@ -115,6 +118,20 @@ existing candidates by proving one is an ancestor of another, but `remove_redundant()` handles that as a post-processing step, so it is safe to exit early.+Side-exhaustion condition+~~~~~~~~~~~~~~~~~~~~~~~~~+A new merge-base requires commits from both sides to meet. When one+side's exclusive counter reaches zero and there are no pending+merge-base candidates, no future traversal step can produce a new+candidate.++This optimization only activates in the finite-generation region+where topological ordering holds. In that region, children are+always visited before parents, so paint flags are final at visit+time and an exhausted side cannot reappear. In the INFINITY region,+commit-date ordering can violate this guarantee, so the check is+skipped.+ Generation cutoff ~~~~~~~~~~~~~~~~~ Some callers (notably `remove_redundant()`) supply a `min_generation`@@ -159,12 +176,16 @@ ordering via `compare_commits_by_commit_date`. Because commit dates are not monotonic (clock skew, rebases, etc.), the queue may visit commits out of topological order.-This disables the optimization that depends on generation ordering:+This disables the optimizations that depend on generation ordering: - *Single result*: the first merge-base candidate found may not be the shallowest, because a deeper ancestor with a higher commit date can be dequeued first.+ - *Side exhaustion*: one paint side can appear to drain from the+ queue while commits from that side are still waiting with lower+ dates, causing premature termination.+ Related documentation ---------------------
commit-reach.c
+15-3
index 7a2629336d..1b445399e9 100644--- a/commit-reach.c+++ b/commit-reach.c@@ -132,6 +132,10 @@ static void paint_queue_put(struct paint_state *state, } }+/*+ * Dequeue the next commit for the paint walk, or return NULL when+ * no more merge bases can be discovered.+ */ static struct commit *paint_queue_get(struct paint_state *state) { struct commit *commit = prio_queue_get(&state->queue);@@ -141,9 +145,17 @@ static struct commit *paint_queue_get(struct paint_state *state) commit->object.flags &= ~ENQUEUED;- if (!state->parent1_count && !state->parent2_count &&- !state->mb_candidate_count)- return NULL;+ if (!state->mb_candidate_count) {+ /* only stale entries remain */+ if (!state->parent1_count && !state->parent2_count)+ return NULL;++ /* one side is exhausted */+ if ((!state->parent1_count || !state->parent2_count) &&+ state->gen_ordered &&+ commit_graph_generation(commit) < GENERATION_NUMBER_INFINITY)+ return NULL;+ } paint_count_update(state, commit->object.flags, -1); return commit;