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 docmain
commit
679a72c6b8
|
|
@ -129,6 +129,7 @@ TECH_DOCS += technical/long-running-process-protocol
|
||||||
TECH_DOCS += technical/multi-pack-index
|
TECH_DOCS += technical/multi-pack-index
|
||||||
TECH_DOCS += technical/packfile-uri
|
TECH_DOCS += technical/packfile-uri
|
||||||
TECH_DOCS += technical/pack-heuristics
|
TECH_DOCS += technical/pack-heuristics
|
||||||
|
TECH_DOCS += technical/paint-down-to-common
|
||||||
TECH_DOCS += technical/parallel-checkout
|
TECH_DOCS += technical/parallel-checkout
|
||||||
TECH_DOCS += technical/partial-clone
|
TECH_DOCS += technical/partial-clone
|
||||||
TECH_DOCS += technical/platform-support
|
TECH_DOCS += technical/platform-support
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ articles = [
|
||||||
'multi-pack-index.adoc',
|
'multi-pack-index.adoc',
|
||||||
'packfile-uri.adoc',
|
'packfile-uri.adoc',
|
||||||
'pack-heuristics.adoc',
|
'pack-heuristics.adoc',
|
||||||
|
'paint-down-to-common.adoc',
|
||||||
'parallel-checkout.adoc',
|
'parallel-checkout.adoc',
|
||||||
'partial-clone.adoc',
|
'partial-clone.adoc',
|
||||||
'platform-support.adoc',
|
'platform-support.adoc',
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
166
commit-reach.c
166
commit-reach.c
|
|
@ -11,6 +11,7 @@
|
||||||
#include "tag.h"
|
#include "tag.h"
|
||||||
#include "commit-reach.h"
|
#include "commit-reach.h"
|
||||||
#include "ewah/ewok.h"
|
#include "ewah/ewok.h"
|
||||||
|
#include "trace2.h"
|
||||||
|
|
||||||
/* Remember to update object flag allocation in object.h */
|
/* Remember to update object flag allocation in object.h */
|
||||||
#define PARENT1 (1u<<16)
|
#define PARENT1 (1u<<16)
|
||||||
|
|
@ -78,25 +79,111 @@ static void clear_nonstale_queue(struct nonstale_queue *queue)
|
||||||
queue->max_nonstale = NULL;
|
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)
|
switch (flags & (PARENT1 | PARENT2 | STALE)) {
|
||||||
return;
|
case PARENT1:
|
||||||
c->object.flags |= ENQUEUED;
|
state->parent1_count += delta;
|
||||||
nonstale_queue_put(queue, c);
|
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)
|
if (old_flags & ENQUEUED) {
|
||||||
commit->object.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;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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;
|
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,
|
static int paint_down_to_common(struct repository *r,
|
||||||
struct commit *one, int n,
|
struct commit *one, int n,
|
||||||
struct commit **twos,
|
struct commit **twos,
|
||||||
|
|
@ -104,45 +191,40 @@ static int paint_down_to_common(struct repository *r,
|
||||||
enum merge_base_flags mb_flags,
|
enum merge_base_flags mb_flags,
|
||||||
struct commit_list **result)
|
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 i;
|
||||||
int gen_ordered = 1;
|
int steps = 0;
|
||||||
timestamp_t last_gen = GENERATION_NUMBER_INFINITY;
|
|
||||||
struct commit_list **tail = result;
|
struct commit_list **tail = result;
|
||||||
|
|
||||||
if (!min_generation && !corrected_commit_dates_enabled(r)) {
|
state.min_generation = min_generation;
|
||||||
queue.pq.compare = compare_commits_by_commit_date;
|
state.last_gen = GENERATION_NUMBER_INFINITY;
|
||||||
gen_ordered = 0;
|
state.topo_ceiling = corrected_commit_dates_enabled(r)
|
||||||
}
|
? GENERATION_NUMBER_INFINITY
|
||||||
|
: GENERATION_NUMBER_V1_MAX;
|
||||||
|
|
||||||
|
|
||||||
one->object.flags |= PARENT1;
|
one->object.flags |= PARENT1;
|
||||||
if (!n) {
|
if (!n) {
|
||||||
commit_list_append(one, result);
|
commit_list_append(one, result);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
nonstale_queue_put_dedup(&queue, one);
|
paint_queue_put(&state, one, 0);
|
||||||
|
|
||||||
for (i = 0; i < n; i++) {
|
for (i = 0; i < n; i++)
|
||||||
twos[i]->object.flags |= PARENT2;
|
paint_queue_put(&state, twos[i], PARENT2);
|
||||||
nonstale_queue_put_dedup(&queue, twos[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (queue.max_nonstale) {
|
while ((commit = paint_queue_get(&state))) {
|
||||||
struct commit *commit = nonstale_queue_get_dedup(&queue);
|
|
||||||
struct commit_list *parents;
|
struct commit_list *parents;
|
||||||
int flags;
|
int flags;
|
||||||
timestamp_t generation = commit_graph_generation(commit);
|
steps++;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
|
flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
|
||||||
if (flags == (PARENT1 | PARENT2)) {
|
if (flags == (PARENT1 | PARENT2)) {
|
||||||
|
|
@ -155,8 +237,7 @@ static int paint_down_to_common(struct repository *r,
|
||||||
* descendant of this one.
|
* descendant of this one.
|
||||||
*/
|
*/
|
||||||
if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
|
if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
|
||||||
gen_ordered &&
|
state.last_gen < state.topo_ceiling)
|
||||||
generation < GENERATION_NUMBER_INFINITY)
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
/* Mark parents of a found merge stale */
|
/* 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)
|
if ((p->object.flags & flags) == flags)
|
||||||
continue;
|
continue;
|
||||||
if (repo_parse_commit(r, p)) {
|
if (repo_parse_commit(r, p)) {
|
||||||
clear_nonstale_queue(&queue);
|
clear_prio_queue(&state.queue);
|
||||||
commit_list_free(*result);
|
commit_list_free(*result);
|
||||||
*result = NULL;
|
*result = NULL;
|
||||||
/*
|
/*
|
||||||
|
|
@ -184,12 +265,13 @@ static int paint_down_to_common(struct repository *r,
|
||||||
return error(_("could not parse commit %s"),
|
return error(_("could not parse commit %s"),
|
||||||
oid_to_hex(&p->object.oid));
|
oid_to_hex(&p->object.oid));
|
||||||
}
|
}
|
||||||
p->object.flags |= flags;
|
paint_queue_put(&state, p, flags);
|
||||||
nonstale_queue_put_dedup(&queue, p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
commit_list_sort_by_date(result);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -796,6 +796,7 @@ integration_tests = [
|
||||||
't6041-bisect-submodule.sh',
|
't6041-bisect-submodule.sh',
|
||||||
't6050-replace.sh',
|
't6050-replace.sh',
|
||||||
't6060-merge-index.sh',
|
't6060-merge-index.sh',
|
||||||
|
't6099-merge-base-side-exhaustion.sh',
|
||||||
't6100-rev-list-in-order.sh',
|
't6100-rev-list-in-order.sh',
|
||||||
't6101-rev-parse-parents.sh',
|
't6101-rev-parse-parents.sh',
|
||||||
't6102-rev-list-unexpected-objects.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 branch -f skew-P2 "$skew_P2" &&
|
||||||
git tag skew-M2 "$skew_M2" &&
|
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 &&
|
git commit-graph write --reachable &&
|
||||||
mv .git/objects/info/commit-graph commit-graph-full &&
|
mv .git/objects/info/commit-graph commit-graph-full &&
|
||||||
chmod u+w commit-graph-full &&
|
chmod u+w commit-graph-full &&
|
||||||
|
|
@ -98,24 +195,34 @@ test_expect_success 'setup' '
|
||||||
'
|
'
|
||||||
|
|
||||||
run_all_modes () {
|
run_all_modes () {
|
||||||
test_when_finished rm -rf .git/objects/info/commit-graph &&
|
graph=.git/objects/info/commit-graph &&
|
||||||
"$@" <input >actual &&
|
test_when_finished rm -rf "$graph" "${graph}s" &&
|
||||||
test_cmp expect actual &&
|
rm -f trace-mode-*.txt &&
|
||||||
cp commit-graph-full .git/objects/info/commit-graph &&
|
|
||||||
"$@" <input >actual &&
|
for mode in none full half no-gdat
|
||||||
test_cmp expect actual &&
|
do
|
||||||
cp commit-graph-half .git/objects/info/commit-graph &&
|
rm -rf "$graph" "${graph}s" &&
|
||||||
"$@" <input >actual &&
|
cp "commit-graph-${mode}" "$graph" 2>/dev/null ||
|
||||||
test_cmp expect actual &&
|
true &&
|
||||||
cp commit-graph-no-gdat .git/objects/info/commit-graph &&
|
GIT_TRACE2_EVENT="$(pwd)/trace-mode-${mode}.txt" \
|
||||||
"$@" <input >actual &&
|
"$@" <input >actual &&
|
||||||
test_cmp expect actual
|
test_cmp expect actual || return 1
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
test_all_modes () {
|
test_all_modes () {
|
||||||
run_all_modes test-tool reach "$@"
|
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' '
|
test_expect_success 'ref_newer:miss' '
|
||||||
cat >input <<-\EOF &&
|
cat >input <<-\EOF &&
|
||||||
A:commit-5-7
|
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_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' '
|
test_expect_success 'is_descendant_of:hit' '
|
||||||
cat >input <<-\EOF &&
|
cat >input <<-\EOF &&
|
||||||
A:commit-5-7
|
A:commit-5-7
|
||||||
|
|
@ -219,6 +337,105 @@ test_expect_success 'get_merge_bases_many' '
|
||||||
test_all_modes 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' '
|
test_expect_success 'reduce_heads' '
|
||||||
cat >input <<-\EOF &&
|
cat >input <<-\EOF &&
|
||||||
X:commit-1-10
|
X:commit-1-10
|
||||||
|
|
|
||||||
|
|
@ -2004,6 +2004,41 @@ test_trace2_data () {
|
||||||
grep -e '"category":"'"$1"'","key":"'"$2"'","value":"'"$3"'"'
|
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
|
# Given a GIT_TRACE2_EVENT log over stdin, writes to stdout a list of URLs
|
||||||
# sent to git-remote-https child processes.
|
# sent to git-remote-https child processes.
|
||||||
test_remote_https_urls() {
|
test_remote_https_urls() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue