Merge branch 'kk/merge-base-exhaustion' into seen
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, t6600: add side-exhaustion regression tests 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 docseen
commit
46a667095f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
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 (this is when we say a commit is
|
||||
"visited") and propagates its paint flags to its parents, enqueuing
|
||||
them if they 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]]
|
||||
INFINITY and finite generation regions
|
||||
--------------------------------------
|
||||
|
||||
The commit-graph stores a generation number for each commit.
|
||||
Commits not in the commit-graph have generation
|
||||
`GENERATION_NUMBER_INFINITY`. The graph is closed under
|
||||
reachability: if a commit is in the graph, all its ancestors are
|
||||
too. This partitions the commit graph into two regions:
|
||||
|
||||
....
|
||||
+---------------------------------------+
|
||||
| INFINITY region |
|
||||
| generation = INFINITY |
|
||||
| queue order: heuristic (commit date) |
|
||||
+---------------------------------------+
|
||||
|
|
||||
v
|
||||
+---------------------------------------+
|
||||
| Finite region |
|
||||
| generation = finite |
|
||||
| queue order: topological |
|
||||
+---------------------------------------+
|
||||
....
|
||||
|
||||
When the commit-graph is enabled, the INFINITY region is typically
|
||||
very small -- it only contains commits added since the last
|
||||
commit-graph refresh.
|
||||
|
||||
All reachable INFINITY-generation commits are visited before any
|
||||
finite-generation commit, because INFINITY is larger than any finite
|
||||
value. Once the walk crosses into the finite region, it stays there.
|
||||
|
||||
In the finite region, generation ordering guarantees topological
|
||||
traversal: children are always visited before their parents. This
|
||||
means that paint on already-visited commits is final -- no future
|
||||
traversal step can add paint to them.
|
||||
|
||||
In the INFINITY region, all commits share the same generation
|
||||
value, so the queue breaks ties by commit date. This can violate
|
||||
topological ordering: a parent with a later date can be visited
|
||||
before a child with an earlier date. Paint flags are therefore
|
||||
NOT final at visit time, and a commit visited with only one
|
||||
side's paint may later gain the other.
|
||||
|
||||
Paint flags are only added, never removed. Since each flag can be set
|
||||
at most once per commit, the number of times a commit can be
|
||||
re-enqueued is bounded by the number of flag transitions.
|
||||
|
||||
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 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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
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 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`
|
||||
threshold -- the minimum generation of the input commits. No merge
|
||||
base can have a generation below this threshold, so the walk
|
||||
terminates as soon as it dequeues such a commit.
|
||||
|
||||
Single result
|
||||
~~~~~~~~~~~~~
|
||||
When only one merge base is needed, the walk is in the
|
||||
finite-generation region, and the queue uses 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.
|
||||
|
|
@ -793,17 +793,6 @@ int generation_numbers_enabled(struct repository *r)
|
|||
return !!first_generation;
|
||||
}
|
||||
|
||||
int corrected_commit_dates_enabled(struct repository *r)
|
||||
{
|
||||
struct commit_graph *g;
|
||||
|
||||
g = prepare_commit_graph(r);
|
||||
if (!g || !g->num_commits)
|
||||
return 0;
|
||||
|
||||
return g->read_generation_data;
|
||||
}
|
||||
|
||||
struct bloom_filter_settings *get_bloom_filter_settings(struct repository *r)
|
||||
{
|
||||
struct commit_graph *g;
|
||||
|
|
|
|||
|
|
@ -136,12 +136,6 @@ struct commit_graph *parse_commit_graph(struct repository *r,
|
|||
*/
|
||||
int generation_numbers_enabled(struct repository *r);
|
||||
|
||||
/*
|
||||
* Return 1 if and only if the repository has a commit-graph
|
||||
* file and generation data chunk has been written for the file.
|
||||
*/
|
||||
int corrected_commit_dates_enabled(struct repository *r);
|
||||
|
||||
struct bloom_filter_settings *get_bloom_filter_settings(struct repository *r);
|
||||
|
||||
enum commit_graph_write_flags {
|
||||
|
|
|
|||
156
commit-reach.c
156
commit-reach.c
|
|
@ -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,105 @@ 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;
|
||||
};
|
||||
|
||||
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 (commit)
|
||||
commit->object.flags &= ~ENQUEUED;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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 < GENERATION_NUMBER_INFINITY)
|
||||
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 +185,36 @@ 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 finite 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;
|
||||
|
||||
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 +227,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 < GENERATION_NUMBER_INFINITY)
|
||||
break;
|
||||
}
|
||||
/* Mark parents of a found merge stale */
|
||||
|
|
@ -169,7 +240,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 +255,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -797,6 +797,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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 &&
|
||||
"$@" <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
|
||||
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 || 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
|
||||
|
|
|
|||
|
|
@ -1996,6 +1996,42 @@ 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
|
||||
*"
|
||||
"*)
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue