]> git.ipfire.org Git - thirdparty/git.git/commitdiff
commit-reach: terminate merge-base walk when one paint side is exhausted
authorKristofer Karlsson <krka@spotify.com>
Wed, 24 Jun 2026 12:14:13 +0000 (12:14 +0000)
committerJunio C Hamano <gitster@pobox.com>
Wed, 24 Jun 2026 16:58:50 +0000 (09:58 -0700)
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>
Documentation/technical/paint-down-to-common.adoc
commit-reach.c
t/t6600-test-reach.sh

index 0f4e1892a52df70883e95a033dca99ef890b4057..983dfcf2332e96798b366bd849b0e794bf7b7b5d 100644 (file)
@@ -94,6 +94,9 @@ ends when one of the following conditions holds:
 
   1. The queue is empty.
   2. The queue contains only stale entries.
+  3. 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
 ~~~~~~~~~~~~~~~~~~~~~
@@ -104,6 +107,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.
+
 Related documentation
 ---------------------
 
index e0d9874f99e320a922b02d1487bf470c7b269620..f79d0b64d65bc80c55b40ed7827da1d074d25aa9 100644 (file)
@@ -133,17 +133,30 @@ static void paint_queue_put(struct paint_state *state,
 
 static struct commit *paint_queue_get(struct paint_state *state)
 {
-       struct commit *commit;
+       struct commit *commit = prio_queue_get(&state->queue);
 
-       if (!state->p1_count && !state->p2_count &&
-           !state->pending_merge_bases)
+       if (!commit)
                return NULL;
 
-       commit = prio_queue_get(&state->queue);
-       if (commit) {
-               commit->object.flags &= ~ENQUEUED;
-               paint_count_update(state, commit->object.flags, -1);
+       commit->object.flags &= ~ENQUEUED;
+
+       if (!state->pending_merge_bases) {
+               if (!state->p1_count && !state->p2_count)
+                       return NULL;
+               /*
+                * Side exhaustion: a new merge-base can only form
+                * when both PARENT1-only and PARENT2-only commits
+                * remain in the queue. In the finite-generation
+                * region the queue is ordered topologically, so
+                * no future step can add paint to visited commits
+                * and an exhausted side cannot reappear.
+                */
+               if ((!state->p1_count || !state->p2_count) &&
+                   commit_graph_generation(commit) < GENERATION_NUMBER_INFINITY)
+                       return NULL;
        }
+
+       paint_count_update(state, commit->object.flags, -1);
        return commit;
 }
 
index c1109fb42f94cddf71faace6c7f75ccf0efc1958..03175befb3159eeeded46c063bd1ffc23fb7620f 100755 (executable)
@@ -332,12 +332,12 @@ test_expect_success 'merge-base --all commit-walk steps' '
        cp commit-graph-full .git/objects/info/commit-graph &&
        GIT_TRACE2_EVENT="$(pwd)/trace-full.txt" \
                git merge-base --all commit-9-9 commit-9-1 >actual &&
-       test_trace2_data paint_down_to_common steps 80 <trace-full.txt &&
+       test_trace2_data paint_down_to_common steps 9 <trace-full.txt &&
 
        cp commit-graph-half .git/objects/info/commit-graph &&
        GIT_TRACE2_EVENT="$(pwd)/trace-half.txt" \
                git merge-base --all commit-9-9 commit-9-1 >actual &&
-       test_trace2_data paint_down_to_common steps 81 <trace-half.txt
+       test_trace2_data paint_down_to_common steps 57 <trace-half.txt
 '
 
 test_expect_success 'reduce_heads' '