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>
seen
Kristofer Karlsson 2026-07-11 13:27:43 +00:00 committed by Junio C Hamano
parent 351dfe5aa7
commit 8df944d32a
3 changed files with 39 additions and 6 deletions

View File

@ -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
---------------------


View File

@ -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;

View File

@ -297,7 +297,7 @@ test_expect_success 'in_merge_bases_many:self' '
EOF
echo "in_merge_bases_many(A,X):1" >expect &&
test_all_modes in_merge_bases_many &&
test_paint_down_steps 45 2 25 3
test_paint_down_steps 45 1 25 1
'

test_expect_success 'is_descendant_of:hit' '
@ -414,7 +414,7 @@ test_expect_success 'merge-base --all commit-walk steps' '
>input &&
git rev-parse commit-9-1 >expect &&
run_all_modes git merge-base --all commit-9-9 commit-9-1 &&
test_paint_down_steps 81 80 81 81
test_paint_down_steps 81 9 57 81
'

test_expect_success 'merge-base --all with clock skew (side-exhaustion)' '