Merge branch 'kk/merge-base-exhaustion'

The merge-base computation has been optimized by stopping the walk
early when one side's exclusive commits in the queue are exhausted,
yielding significant speedups for queries with one-sided histories.

* kk/merge-base-exhaustion:
  commit-reach: remove commit-date ordering fallback
  commit-reach: move min_generation check into paint_queue_get()
  commit-reach: terminate merge-base walk when one paint side is exhausted
  commit-reach: introduce struct paint_state with per-side counters
  t6600: add clock-skew topologies and step counts for edge cases
  commit-reach: add trace2 instrumentation to paint_down_to_common()
  t6099: add side-exhaustion regression test
  t6600: add test cases for side-exhaustion edge cases
  test-lib-functions: improve diagnostic output for trace2 data assertions
  Documentation/technical: add paint-down-to-common doc
main
Junio C Hamano 2026-08-23 18:01:46 -07:00
commit 679a72c6b8
8 changed files with 628 additions and 54 deletions

View File

@ -129,6 +129,7 @@ TECH_DOCS += technical/long-running-process-protocol
TECH_DOCS += technical/multi-pack-index
TECH_DOCS += technical/packfile-uri
TECH_DOCS += technical/pack-heuristics
TECH_DOCS += technical/paint-down-to-common
TECH_DOCS += technical/parallel-checkout
TECH_DOCS += technical/partial-clone
TECH_DOCS += technical/platform-support

View File

@ -18,6 +18,7 @@ articles = [
'multi-pack-index.adoc',
'packfile-uri.adoc',
'pack-heuristics.adoc',
'paint-down-to-common.adoc',
'parallel-checkout.adoc',
'partial-clone.adoc',
'platform-support.adoc',

View File

@ -0,0 +1,155 @@
Merge-Base Computation and paint_down_to_common()
==================================================

The function `paint_down_to_common()` in `commit-reach.c` computes merge
bases by walking the commit graph backwards from two sets of tips and
finding where their ancestry meets.

Use cases
---------

Computing merge bases is used in two different ways:

1. *Finding all merge bases* (`merge-base --all`, `merge-tree`,
`merge`, `rebase`). A merge base is a common ancestor that is
not itself an ancestor of another common ancestor.

2. *Ancestry checks* (`in_merge_bases`, used by `merge-base
--is-ancestor`, `branch -d`, `fetch`). These ask: "is commit A
an ancestor of commit B?" If a common ancestor equals one of the
inputs, that input is necessarily the only merge base -- no other
common ancestor can be both as recent and not an ancestor of it.

Both use cases share the same algorithm and implementation.

Algorithm
---------

Given a commit `one` and a set of commits `twos[]`, the walk paints
commits with two colors:

- PARENT1: reachable from `one`
- PARENT2: reachable from any commit in `twos[]`

The walk uses a priority queue ordered by generation number
(highest first), breaking ties by commit date. Each step dequeues
the highest-priority commit and propagates its paint flags to its
parents, enqueuing any parent that gained new flags. When a
commit receives both PARENT1 and PARENT2, it is a merge-base
candidate. A candidate gains the STALE flag so its ancestors
propagate staleness -- any deeper common ancestor is necessarily
redundant.

[[generation-regions]]
Topologically ordered and unordered generation regions
------------------------------------------------------

Commits fall into two regions based on whether their generation
numbers provide a topological ordering guarantee:

....
+------------------------------------------+
| Unordered region |
| generation = INFINITY or V1_MAX |
| queue order: heuristic (commit date) |
+------------------------------------------+
|
v
+------------------------------------------+
| Ordered region |
| generation = finite, unsaturated |
| queue order: topological |
+------------------------------------------+
....

In the ordered region, a child's generation is strictly greater
than its parent's. Same-generation commits are necessarily
independent, so the queue always processes children before
their parents.

In the unordered region, parent-child pairs can share the same
generation number, so topological order is not guaranteed. The
queue uses commit-date as a heuristic, which typically produces
a reasonable traversal order but may process a parent before
its child.

Commits not in the commit-graph have generation INFINITY; v1
commit-graphs saturate at V1_MAX. Both place commits in the
unordered region. Any optimization that depends on generation
ordering must account for this saturation boundary. The early
exit gates compare against a topological ceiling --
`GENERATION_NUMBER_V1_MAX` for v1 graphs and
`GENERATION_NUMBER_INFINITY` for v2 graphs -- so that saturated
commits are treated as unordered.

With generation ordering, values in the unordered region exceed
those in the ordered region. The walk may therefore transition
from the unordered region into the ordered region, but never in
the reverse direction. Without a commit-graph, every commit has INFINITY
and the walk operates entirely in the unordered region.

In the ordered region, paint on a dequeued commit is final --
no future step can add flags to it. In the unordered region,
a dequeued commit may later gain additional paint. Paint flags
are only added, never removed, bounding the number of
re-enqueues per commit.

Termination
-----------

The walk tracks the number of commits of each type in the queue
(PARENT1-only, PARENT2-only, pending merge-base). The main loop
ends when one of the following conditions holds:

1. The queue is empty.
2. The queue contains only stale entries.
3. Generation cutoff: the dequeued commit's generation is below
a caller-supplied `min_generation` threshold.
4. Single result: the caller only needs one merge base, one has
been found, and the walk has entered the ordered 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 ordered region.

Stale entry condition
~~~~~~~~~~~~~~~~~~~~~
Once all queued entries are stale, no new merge-base candidates can
be discovered -- that requires at least one non-stale commit from
each side meeting. Continuing the walk could still invalidate
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 ordered region,
where paint flags are final at visit time; in the unordered region,
a side that appears exhausted could reappear through late paint
propagation.

Generation cutoff
~~~~~~~~~~~~~~~~~
Some callers (notably `remove_redundant()`) supply a `min_generation`
threshold equal to the minimum generation of the input commits.
These callers only need to determine reachability among the inputs,
not find deep merge bases, so the walk can safely terminate when it
dequeues a commit below this threshold.

Single result
~~~~~~~~~~~~~
When only one merge base is needed and the walk is in the
ordered region with generation ordering, the first candidate
found is necessarily the highest-generation common ancestor.
No remaining commit in the queue can be a descendant of this
candidate (generation ordering guarantees children are visited
first), so it cannot be redundant and the walk can stop
immediately.

Related documentation
---------------------

- `Documentation/technical/commit-graph.adoc` -- generation numbers
and the reachability closure property.

View File

@ -11,6 +11,7 @@
#include "tag.h"
#include "commit-reach.h"
#include "ewah/ewok.h"
#include "trace2.h"

/* Remember to update object flag allocation in object.h */
#define PARENT1 (1u<<16)
@ -78,25 +79,111 @@ static void clear_nonstale_queue(struct nonstale_queue *queue)
queue->max_nonstale = NULL;
}

static void nonstale_queue_put_dedup(struct nonstale_queue *queue,
struct commit *c)
/*
* Priority queue with per-side commit counters for paint_down_to_common().
* Each non-stale queued commit occupies exactly one bucket: PARENT1-only,
* PARENT2-only, or both (a pending merge-base candidate).
*/
struct paint_state {
struct prio_queue queue;
size_t parent1_count;
size_t parent2_count;
size_t mb_candidate_count;
timestamp_t min_generation;
timestamp_t last_gen;
timestamp_t topo_ceiling;
};

static void paint_count_update(struct paint_state *state,
unsigned flags, int delta)
{
if (c->object.flags & ENQUEUED)
return;
c->object.flags |= ENQUEUED;
nonstale_queue_put(queue, c);
switch (flags & (PARENT1 | PARENT2 | STALE)) {
case PARENT1:
state->parent1_count += delta;
break;

case PARENT2:
state->parent2_count += delta;
break;

case PARENT1 | PARENT2:
state->mb_candidate_count += delta;
break;

case PARENT1 | PARENT2 | STALE:
break;

default:
BUG("unexpected paint state");
}
}

static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue)
static void paint_queue_put(struct paint_state *state,
struct commit *c, unsigned add_flags)
{
struct commit *commit = nonstale_queue_get(queue);
unsigned old_flags = c->object.flags;
c->object.flags |= add_flags;

if (old_flags & ENQUEUED) {
paint_count_update(state, old_flags, -1);
paint_count_update(state, c->object.flags, 1);
} else {
c->object.flags |= ENQUEUED;
prio_queue_put(&state->queue, c);
paint_count_update(state, c->object.flags, 1);
}
}

/*
* 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);
timestamp_t generation;

if (!commit)
return NULL;

if (commit)
commit->object.flags &= ~ENQUEUED;
generation = commit_graph_generation(commit);

if (state->min_generation && generation > state->last_gen)
BUG("bad generation skip %"PRItime" > %"PRItime" at %s",
generation, state->last_gen,
oid_to_hex(&commit->object.oid));
state->last_gen = generation;

/* generation cutoff */
if (generation < state->min_generation)
return NULL;

/*
* Check exit condition before decrementing: the counters
* still include this commit, so the last non-stale commit
* sees a non-zero count and is returned for processing.
*/
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) &&
generation < state->topo_ceiling)
return NULL;
}

paint_count_update(state, commit->object.flags, -1);
return commit;
}

/* all input commits in one and twos[] must have been parsed! */
/*
* See Documentation/technical/paint-down-to-common.adoc
*
* All input commits in one and twos[] must have been parsed!
*/
static int paint_down_to_common(struct repository *r,
struct commit *one, int n,
struct commit **twos,
@ -104,45 +191,40 @@ static int paint_down_to_common(struct repository *r,
enum merge_base_flags mb_flags,
struct commit_list **result)
{
struct nonstale_queue queue = {
{ compare_commits_by_gen_then_commit_date }
/*
* Generation ordering is required for the side-exhaustion and
* single-result early exits, which rely on topological traversal
* order (children visited before parents) in the ordered region.
*/
struct paint_state state = {
.queue = { compare_commits_by_gen_then_commit_date }
};
struct commit *commit;
int i;
int gen_ordered = 1;
timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
int steps = 0;
struct commit_list **tail = result;

if (!min_generation && !corrected_commit_dates_enabled(r)) {
queue.pq.compare = compare_commits_by_commit_date;
gen_ordered = 0;
}
state.min_generation = min_generation;
state.last_gen = GENERATION_NUMBER_INFINITY;
state.topo_ceiling = corrected_commit_dates_enabled(r)
? GENERATION_NUMBER_INFINITY
: GENERATION_NUMBER_V1_MAX;


one->object.flags |= PARENT1;
if (!n) {
commit_list_append(one, result);
return 0;
}
nonstale_queue_put_dedup(&queue, one);
paint_queue_put(&state, one, 0);

for (i = 0; i < n; i++) {
twos[i]->object.flags |= PARENT2;
nonstale_queue_put_dedup(&queue, twos[i]);
}
for (i = 0; i < n; i++)
paint_queue_put(&state, twos[i], PARENT2);

while (queue.max_nonstale) {
struct commit *commit = nonstale_queue_get_dedup(&queue);
while ((commit = paint_queue_get(&state))) {
struct commit_list *parents;
int flags;
timestamp_t generation = commit_graph_generation(commit);

if (min_generation && generation > last_gen)
BUG("bad generation skip %"PRItime" > %"PRItime" at %s",
generation, last_gen,
oid_to_hex(&commit->object.oid));
last_gen = generation;

if (generation < min_generation)
break;
steps++;

flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
if (flags == (PARENT1 | PARENT2)) {
@ -155,8 +237,7 @@ static int paint_down_to_common(struct repository *r,
* descendant of this one.
*/
if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
gen_ordered &&
generation < GENERATION_NUMBER_INFINITY)
state.last_gen < state.topo_ceiling)
break;
}
/* Mark parents of a found merge stale */
@ -169,7 +250,7 @@ static int paint_down_to_common(struct repository *r,
if ((p->object.flags & flags) == flags)
continue;
if (repo_parse_commit(r, p)) {
clear_nonstale_queue(&queue);
clear_prio_queue(&state.queue);
commit_list_free(*result);
*result = NULL;
/*
@ -184,12 +265,13 @@ static int paint_down_to_common(struct repository *r,
return error(_("could not parse commit %s"),
oid_to_hex(&p->object.oid));
}
p->object.flags |= flags;
nonstale_queue_put_dedup(&queue, p);
paint_queue_put(&state, p, flags);
}
}

clear_nonstale_queue(&queue);
clear_prio_queue(&state.queue);
trace2_data_intmax("paint_down_to_common", r,
"steps", steps);
commit_list_sort_by_date(result);
return 0;
}

View File

@ -796,6 +796,7 @@ integration_tests = [
't6041-bisect-submodule.sh',
't6050-replace.sh',
't6060-merge-index.sh',
't6099-merge-base-side-exhaustion.sh',
't6100-rev-list-in-order.sh',
't6101-rev-parse-parents.sh',
't6102-rev-list-unexpected-objects.sh',

View File

@ -0,0 +1,82 @@
#!/bin/sh

test_description='merge-base with ancestor among merge-base candidates

Test that merge-base --all correctly handles cases where
multiple merge-base candidates exist and one is an ancestor
of another. The side-exhaustion optimization in
paint_down_to_common may exit before STALE propagation
removes the ancestor, but remove_redundant catches it.

Graph shape (parents are below children):

A ----- X
|\ /|
| B---/ |
| \ |
e2 \ f2
| | |
e1 d1 f1
\ | /
\ | /
\|/
C

A and X are the two tips.
B and C are both reachable from A and X.
B reaches C through d1.
Only B should appear in merge-base --all output.
'

GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh

test_expect_success 'setup ancestor merge-base candidate' '
test_commit C &&

git checkout -b d-chain HEAD &&
test_commit d1 &&
test_commit B &&

git checkout -b e-path C &&
test_commit e1 &&
test_commit e2 &&

git checkout -b f-path C &&
test_commit f1 &&
test_commit f2 &&

git checkout -b branch-A e-path &&
test_merge A B &&

git checkout -b branch-X f-path &&
test_merge X B &&

git commit-graph write --reachable
'

test_expect_success 'merge-base --all excludes ancestor candidate' '
git rev-parse B >expected &&
git merge-base --all A X >actual &&
test_cmp expected actual
'

test_expect_success 'merge-base (single) finds shallowest' '
git rev-parse B >expected &&
git merge-base A X >actual &&
test_cmp expected actual
'

# Without commit-graph: generation numbers are INFINITY,
# side-exhaustion optimization does not fire.
test_expect_success 'merge-base --all without commit-graph' '
rm -f .git/objects/info/commit-graph &&
git rev-parse B >expected &&
git merge-base --all A X >actual &&
test_cmp expected actual
'

test_done

View File

@ -85,6 +85,103 @@ test_expect_success 'setup' '
git branch -f skew-P2 "$skew_P2" &&
git tag skew-M2 "$skew_M2" &&

# Build a small side topology to exercise the (PARENT1|PARENT2) ->
# (PARENT1|PARENT2|STALE) transition in paint_down_to_common(); the
# 10x10 grid above does not exercise it because no merge-base candidate
# there is a descendant of another, so STALE never reaches a
# still-pending candidate.
#
# ps-X
# /|\
# / | \
# ps-Z ps-B ps-W
# | / \ |
# | / \ |
# |/ \|
# ps-T1 ps-T2
#
# where ps-T1=merge(ps-Z,ps-B), ps-T2=merge(ps-W,ps-B), so
# merge-base(ps-T1,ps-T2) = ps-B. During the walk, ps-X transitions
# to (PARENT1|PARENT2) via ps-Z and ps-W before ps-B is dequeued;
# then the STALE-walk from ps-B transitions ps-X to
# (PARENT1|PARENT2|STALE).
git checkout --orphan ps-orphan &&
test_commit ps-X &&
git checkout -b ps-B-br ps-X && test_commit ps-B &&
git checkout -b ps-Z-br ps-X && test_commit ps-Z &&
git checkout -b ps-W-br ps-X && test_commit ps-W &&
git checkout -b ps-T1 ps-Z &&
git merge --no-ff -m ps-T1 ps-B &&
git checkout -b ps-T2 ps-W &&
git merge --no-ff -m ps-T2 ps-B &&

# Build a side topology that lives entirely outside the half
# commit-graph and has non-monotonic commit dates, to exercise the
# INFINITY-gate in paint_down_to_common. With both tips outside
# the graph, generation is INFINITY and the queue falls back to
# commit-date order, which here is non-monotonic.
#
# pi-X (date 500, PARENT1 tip) --> pi-P, pi-D
# pi-D (date 480) --> pi-C
# pi-C (date 200) --> pi-B
# pi-B (date 100, PARENT2 tip) --> pi-P
# pi-P (date 450, root)
#
# merge-base(pi-X, pi-B) = pi-B (it is an ancestor of pi-X and is
# itself one of the queried tips).
git checkout --orphan pi-orphan &&
test_commit --date "@450 +0000" pi-P &&
test_commit --date "@100 +0000" pi-B &&
test_commit --date "@200 +0000" pi-C &&
test_commit --date "@480 +0000" pi-D &&
GIT_AUTHOR_DATE="@500 +0000" GIT_COMMITTER_DATE="@500 +0000" \
git commit-tree -p pi-D -p pi-P -m pi-X pi-D^{tree} >pi-X-oid &&
pi_x="$(cat pi-X-oid)" &&
git branch -f pi-X-br "$pi_x" &&
git tag pi-X "$pi_x" &&

# Clock-skew topology for side-exhaustion testing.
# D is the correct merge base but has a higher committer date
# than C (its child). With date ordering, D would be dequeued
# before C, causing side-exhaustion to fire too early.
# Generation ordering prevents this by visiting children
# before parents regardless of dates.
#
# se-A (date 7000) --> se-C (date 3000) --> se-D (date 5000) --> se-root (date 4000)
# se-B (date 6000) --> se-D
#
se_root=$(skew_commit 4000 se-root) &&
se_D=$(skew_commit 5000 se-D -p "$se_root") &&
se_C=$(skew_commit 3000 se-C -p "$se_D") &&
se_A=$(skew_commit 7000 se-A -p "$se_C") &&
se_B=$(skew_commit 6000 se-B -p "$se_D") &&
git branch -f se-A "$se_A" &&
git branch -f se-B "$se_B" &&
git tag se-D "$se_D" &&

# Clock-skew topology with redundant ancestor for
# side-exhaustion testing. MB1 is the correct merge base;
# MB2 is its parent. A reaches MB2 via E (high date) and
# MB1 via C (low date). B reaches MB1 via D. With date
# ordering, side-exhaustion would fire before C is dequeued,
# missing MB1. Generation ordering ensures both are found.
#
# se2-A (date 8000) --> se2-C (date 2000) --> se2-MB1 (date 5000) --> se2-MB2 (date 4000) --> se2-root (date 1000)
# se2-A --> se2-E (date 6500) --> se2-MB2
# se2-B (date 7000) --> se2-D (date 6000) --> se2-MB1
#
se2_root=$(skew_commit 1000 se2-root) &&
se2_MB2=$(skew_commit 4000 se2-MB2 -p "$se2_root") &&
se2_MB1=$(skew_commit 5000 se2-MB1 -p "$se2_MB2") &&
se2_C=$(skew_commit 2000 se2-C -p "$se2_MB1") &&
se2_D=$(skew_commit 6000 se2-D -p "$se2_MB1") &&
se2_E=$(skew_commit 6500 se2-E -p "$se2_MB2") &&
se2_A=$(skew_commit 8000 se2-A -p "$se2_C" -p "$se2_E") &&
se2_B=$(skew_commit 7000 se2-B -p "$se2_D") &&
git branch -f se2-A "$se2_A" &&
git branch -f se2-B "$se2_B" &&
git tag se2-MB1 "$se2_MB1" &&

git commit-graph write --reachable &&
mv .git/objects/info/commit-graph commit-graph-full &&
chmod u+w commit-graph-full &&
@ -98,24 +195,34 @@ test_expect_success 'setup' '
'

run_all_modes () {
test_when_finished rm -rf .git/objects/info/commit-graph &&
graph=.git/objects/info/commit-graph &&
test_when_finished rm -rf "$graph" "${graph}s" &&
rm -f trace-mode-*.txt &&

for mode in none full half no-gdat
do
rm -rf "$graph" "${graph}s" &&
cp "commit-graph-${mode}" "$graph" 2>/dev/null ||
true &&
GIT_TRACE2_EVENT="$(pwd)/trace-mode-${mode}.txt" \
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-full .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-half .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual &&
cp commit-graph-no-gdat .git/objects/info/commit-graph &&
"$@" <input >actual &&
test_cmp expect actual
test_cmp expect actual || return 1
done
}

test_all_modes () {
run_all_modes test-tool reach "$@"
}

test_paint_down_steps () {
for mode in none full half no-gdat
do
test_trace2_data_singular paint_down_to_common steps "$1" \
"mode=$mode" <"trace-mode-${mode}.txt" || return 1
shift
done
}

test_expect_success 'ref_newer:miss' '
cat >input <<-\EOF &&
A:commit-5-7
@ -182,6 +289,17 @@ test_expect_success 'in_merge_bases_many:miss-heuristic' '
test_all_modes in_merge_bases_many
'

test_expect_success 'in_merge_bases_many:self' '
cat >input <<-\EOF &&
A:commit-6-8
X:commit-5-9
X:commit-6-8
EOF
echo "in_merge_bases_many(A,X):1" >expect &&
test_all_modes in_merge_bases_many &&
test_paint_down_steps 45 1 25 1
'

test_expect_success 'is_descendant_of:hit' '
cat >input <<-\EOF &&
A:commit-5-7
@ -219,6 +337,105 @@ test_expect_success 'get_merge_bases_many' '
test_all_modes get_merge_bases_many
'

test_expect_success 'get_merge_bases_many:duplicate-twos' '
cat >input <<-\EOF &&
A:commit-5-7
X:commit-4-8
X:commit-4-8
X:commit-6-6
X:commit-6-6
X:commit-8-3
EOF
{
echo "get_merge_bases_many(A,X):" &&
git rev-parse commit-5-6 \
commit-4-7 | sort
} >expect &&
test_all_modes get_merge_bases_many
'

test_expect_success 'get_merge_bases_many:pending-stale' '
# Exercises the (PARENT1|PARENT2) -> (...|STALE) transition path in
# paint_down_to_common(). See the topology comment in the setup test.
cat >input <<-\EOF &&
A:ps-T1
X:ps-T2
EOF
{
echo "get_merge_bases_many(A,X):" &&
git rev-parse ps-B
} >expect &&
test_all_modes get_merge_bases_many &&
test_paint_down_steps 5 5 5 5
'

test_expect_success 'get_merge_bases_many:infinity-both-sides' '
# Exercises the push-time INFINITY-gate in paint_down_to_common(). See
# the pi-* topology comment in the setup test.
cat >input <<-\EOF &&
A:pi-X
X:pi-B
EOF
{
echo "get_merge_bases_many(A,X):" &&
git rev-parse pi-B
} >expect &&
test_all_modes get_merge_bases_many &&
test_paint_down_steps 5 4 5 4
'

test_expect_success 'setup mixed finite/INFINITY topology' '
# Create a commit outside all saved commit-graph files so it always
# has INFINITY generation, while its parent (ps-X) is in the graph
# with a finite generation. Use the ps-* orphan topology so we do
# not pollute the grid-based rev-list tests.
git checkout ps-X &&
test_env GIT_TEST_COMMIT_GRAPH= test_commit pm-INF
'

test_expect_success 'get_merge_bases_many:mixed-finite-infinity' '
# One tip (pm-INF) is outside the commit-graph with INFINITY
# generation; the other (ps-B) is in the graph with finite
# generation. The walk starts in the INFINITY region and crosses
# into the finite region where side-exhaustion can fire.
cat >input <<-\EOF &&
A:pm-INF
X:ps-B
EOF
{
echo "get_merge_bases_many(A,X):" &&
git rev-parse ps-X
} >expect &&
test_all_modes get_merge_bases_many &&
test_paint_down_steps 3 3 3 3
'

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 9 57 37
'

test_expect_success 'merge-base --all with clock skew (side-exhaustion)' '
# Verify that the merge base is computed correctly even
# when commits have non-monotonic commit dates.
>input &&
git rev-parse se-D >expect &&
run_all_modes git merge-base --all se-A se-B &&
test_paint_down_steps 6 4 6 4
'

test_expect_success 'merge-base --all with clock skew and redundant ancestor (side-exhaustion)' '
# Verify that the correct merge base is found even when
# non-monotonic commit dates could cause a redundant
# ancestor to be visited first.
>input &&
git rev-parse se2-MB1 >expect &&
run_all_modes git merge-base --all se2-A se2-B &&
test_paint_down_steps 8 6 8 6
'

test_expect_success 'reduce_heads' '
cat >input <<-\EOF &&
X:commit-1-10

View File

@ -2004,6 +2004,41 @@ test_trace2_data () {
grep -e '"category":"'"$1"'","key":"'"$2"'","value":"'"$3"'"'
}

# Check that the given trace2 data event has the expected value and
# appears exactly once. Produces a diagnostic on failure.
#
# test_trace2_data_singular <category> <key> <value> [<label>]
test_trace2_data_singular () {
local category="$1" key="$2" expect_val="$3"
local label_suffix="${4:+ [$4]}"
local kv_pattern='"category":"'"$category"'","key":"'"$key"'","value":"\([^"]*\)"'
local actual

actual=$(sed -n "s|.*${kv_pattern}.*|\1|p") &&

if test -z "$actual"
then
echo >&4 "error: trace2 data '$category/$key'$label_suffix not found"
return 1
fi &&

case "$actual" in
*"$LF"*)
echo >&4 "error: trace2 data '$category/$key'$label_suffix has multiple entries, expected 1"
printf '%s\n' "$actual" | sed 's/^/ actual: /' >&4
return 1
;;
esac &&

if test "$actual" != "$expect_val"
then
echo >&4 "error: trace2 data '$category/$key'$label_suffix"
echo >&4 " expected: $expect_val"
echo >&4 " actual: $actual"
return 1
fi
}

# Given a GIT_TRACE2_EVENT log over stdin, writes to stdout a list of URLs
# sent to git-remote-https child processes.
test_remote_https_urls() {