Skip to content

Commit e4dada8

Browse files
committed
commit-reach: early exit paint_down_to_common for single merge-base
Commits not in the commit-graph get GENERATION_NUMBER_INFINITY and sort to the top of the priority queue. After those, commits with finite generation numbers are popped in non-increasing order. When MERGE_BASE_FIND_ALL is not set the first doubly-painted commit with a finite generation is therefore a best merge-base: no commit still in the queue can be a descendant of it. Skip the expensive STALE drain in this case. Introduce enum merge_base_flags with MERGE_BASE_FIND_ALL and MERGE_BASE_IGNORE_MISSING_COMMITS, replacing the two boolean parameters in paint_down_to_common(). Thread the flags through merge_bases_many(), get_merge_bases_many_0(), and the public repo_get_merge_bases_many_dirty() API. git merge-base (without --all) passes 0, triggering the early exit. On a 2.2M-commit merge-heavy monorepo with commit-graph: HEAD vs ~500: 5,229ms -> 24ms HEAD vs ~1000: 4,214ms -> 39ms HEAD vs ~5000: 3,799ms -> 46ms HEAD vs ~10000: 3,827ms -> 61ms Signed-off-by: Kristofer Karlsson <krka@spotify.com>
1 parent 94f0577 commit e4dada8

5 files changed

Lines changed: 200 additions & 11 deletions

File tree

builtin/merge-base.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
static int show_merge_base(struct commit **rev, size_t rev_nr, int show_all)
1313
{
14+
enum merge_base_flags flags = show_all ? MERGE_BASE_FIND_ALL : 0;
1415
struct commit_list *result = NULL, *r;
1516

1617
if (repo_get_merge_bases_many_dirty(the_repository, rev[0],
17-
rev_nr - 1, rev + 1, &result) < 0) {
18+
rev_nr - 1, rev + 1,
19+
flags, &result) < 0) {
1820
commit_list_free(result);
1921
return -1;
2022
}

commit-reach.c

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ static int paint_down_to_common(struct repository *r,
5454
struct commit *one, int n,
5555
struct commit **twos,
5656
timestamp_t min_generation,
57-
int ignore_missing_commits,
57+
enum merge_base_flags mb_flags,
5858
struct commit_list **result)
5959
{
6060
struct prio_queue queue = { compare_commits_by_gen_then_commit_date };
@@ -97,6 +97,14 @@ static int paint_down_to_common(struct repository *r,
9797
if (!(commit->object.flags & RESULT)) {
9898
commit->object.flags |= RESULT;
9999
tail = commit_list_append(commit, tail);
100+
/*
101+
* The queue is generation-ordered; no
102+
* remaining common ancestor can be a
103+
* descendant of this one.
104+
*/
105+
if (!(mb_flags & MERGE_BASE_FIND_ALL) &&
106+
generation < GENERATION_NUMBER_INFINITY)
107+
break;
100108
}
101109
/* Mark parents of a found merge stale */
102110
flags |= STALE;
@@ -118,7 +126,7 @@ static int paint_down_to_common(struct repository *r,
118126
* corrupt commits would already have been
119127
* dispatched with a `die()`.
120128
*/
121-
if (ignore_missing_commits)
129+
if (mb_flags & MERGE_BASE_IGNORE_MISSING_COMMITS)
122130
return 0;
123131
return error(_("could not parse commit %s"),
124132
oid_to_hex(&p->object.oid));
@@ -136,6 +144,7 @@ static int paint_down_to_common(struct repository *r,
136144
static int merge_bases_many(struct repository *r,
137145
struct commit *one, int n,
138146
struct commit **twos,
147+
enum merge_base_flags mb_flags,
139148
struct commit_list **result)
140149
{
141150
struct commit_list *list = NULL, **tail = result;
@@ -165,7 +174,7 @@ static int merge_bases_many(struct repository *r,
165174
oid_to_hex(&twos[i]->object.oid));
166175
}
167176

168-
if (paint_down_to_common(r, one, n, twos, 0, 0, &list)) {
177+
if (paint_down_to_common(r, one, n, twos, 0, mb_flags, &list)) {
169178
commit_list_free(list);
170179
return -1;
171180
}
@@ -246,7 +255,8 @@ static int remove_redundant_no_gen(struct repository *r,
246255
min_generation = curr_generation;
247256
}
248257
if (paint_down_to_common(r, array[i], filled,
249-
work, min_generation, 0, &common)) {
258+
work, min_generation,
259+
MERGE_BASE_FIND_ALL, &common)) {
250260
clear_commit_marks(array[i], all_flags);
251261
clear_commit_marks_many(filled, work, all_flags);
252262
commit_list_free(common);
@@ -425,14 +435,15 @@ static int get_merge_bases_many_0(struct repository *r,
425435
size_t n,
426436
struct commit **twos,
427437
int cleanup,
438+
enum merge_base_flags mb_flags,
428439
struct commit_list **result)
429440
{
430441
struct commit_list *list, **tail = result;
431442
struct commit **rslt;
432443
size_t cnt, i;
433444
int ret;
434445

435-
if (merge_bases_many(r, one, n, twos, result) < 0)
446+
if (merge_bases_many(r, one, n, twos, mb_flags, result) < 0)
436447
return -1;
437448
for (i = 0; i < n; i++) {
438449
if (one == twos[i])
@@ -475,24 +486,27 @@ int repo_get_merge_bases_many(struct repository *r,
475486
struct commit **twos,
476487
struct commit_list **result)
477488
{
478-
return get_merge_bases_many_0(r, one, n, twos, 1, result);
489+
return get_merge_bases_many_0(r, one, n, twos, 1,
490+
MERGE_BASE_FIND_ALL, result);
479491
}
480492

481493
int repo_get_merge_bases_many_dirty(struct repository *r,
482494
struct commit *one,
483495
size_t n,
484496
struct commit **twos,
497+
enum merge_base_flags mb_flags,
485498
struct commit_list **result)
486499
{
487-
return get_merge_bases_many_0(r, one, n, twos, 0, result);
500+
return get_merge_bases_many_0(r, one, n, twos, 0, mb_flags, result);
488501
}
489502

490503
int repo_get_merge_bases(struct repository *r,
491504
struct commit *one,
492505
struct commit *two,
493506
struct commit_list **result)
494507
{
495-
return get_merge_bases_many_0(r, one, 1, &two, 1, result);
508+
return get_merge_bases_many_0(r, one, 1, &two, 1,
509+
MERGE_BASE_FIND_ALL, result);
496510
}
497511

498512
/*
@@ -537,6 +551,10 @@ int repo_in_merge_bases_many(struct repository *r, struct commit *commit,
537551
struct commit_list *bases = NULL;
538552
int ret = 0, i;
539553
timestamp_t generation, max_generation = GENERATION_NUMBER_ZERO;
554+
enum merge_base_flags mb_flags = MERGE_BASE_FIND_ALL;
555+
556+
if (ignore_missing_commits)
557+
mb_flags |= MERGE_BASE_IGNORE_MISSING_COMMITS;
540558

541559
if (repo_parse_commit(r, commit))
542560
return ignore_missing_commits ? 0 : -1;
@@ -555,7 +573,7 @@ int repo_in_merge_bases_many(struct repository *r, struct commit *commit,
555573

556574
if (paint_down_to_common(r, commit,
557575
nr_reference, reference,
558-
generation, ignore_missing_commits, &bases))
576+
generation, mb_flags, &bases))
559577
ret = -1;
560578
else if (commit->object.flags & PARENT2)
561579
ret = 1;

commit-reach.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@ int repo_get_merge_bases_many(struct repository *r,
1717
struct commit *one, size_t n,
1818
struct commit **twos,
1919
struct commit_list **result);
20-
/* To be used only when object flags after this call no longer matter */
20+
enum merge_base_flags {
21+
MERGE_BASE_FIND_ALL = (1 << 0),
22+
MERGE_BASE_IGNORE_MISSING_COMMITS = (1 << 1),
23+
};
24+
25+
/*
26+
* To be used only when object flags after this call no longer matter.
27+
* Without MERGE_BASE_FIND_ALL and with generation numbers available,
28+
* returns after finding the first merge-base, skipping the STALE drain.
29+
*/
2130
int repo_get_merge_bases_many_dirty(struct repository *r,
2231
struct commit *one, size_t n,
2332
struct commit **twos,
33+
enum merge_base_flags mb_flags,
2434
struct commit_list **result);
2535

2636
int get_octopus_merge_bases(struct commit_list *in, struct commit_list **result);

t/t6010-merge-base.sh

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,123 @@ test_expect_success 'merge-base --octopus --all for complex tree' '
305305
test_cmp expected actual
306306
'
307307

308+
# The following tests verify that "git merge-base" (without --all)
309+
# returns the same result with and without a commit-graph.
310+
# This exercises the early-exit optimisation in paint_down_to_common
311+
# that skips the STALE drain when generation numbers are available.
312+
313+
test_expect_success 'setup for commit-graph tests' '
314+
git init graph-repo &&
315+
(
316+
cd graph-repo &&
317+
318+
# Build a forked DAG:
319+
#
320+
# L1---L2 (left)
321+
# /
322+
# S
323+
# \
324+
# R1---R2 (right)
325+
#
326+
test_commit GS &&
327+
git checkout -b left &&
328+
test_commit L1 &&
329+
test_commit L2 &&
330+
git checkout GS &&
331+
git checkout -b right &&
332+
test_commit GR1 &&
333+
test_commit GR2
334+
)
335+
'
336+
337+
test_expect_success 'merge-base without commit-graph' '
338+
(
339+
cd graph-repo &&
340+
rm -f .git/objects/info/commit-graph &&
341+
git merge-base left right >actual &&
342+
git rev-parse GS >expected &&
343+
test_cmp expected actual
344+
)
345+
'
346+
347+
test_expect_success 'merge-base with commit-graph' '
348+
(
349+
cd graph-repo &&
350+
git commit-graph write --reachable &&
351+
git merge-base left right >actual &&
352+
git rev-parse GS >expected &&
353+
test_cmp expected actual
354+
)
355+
'
356+
357+
test_expect_success 'merge-base --all with commit-graph' '
358+
(
359+
cd graph-repo &&
360+
git merge-base --all left right >actual &&
361+
git rev-parse GS >expected &&
362+
test_cmp expected actual
363+
)
364+
'
365+
366+
test_expect_success 'merge-base agrees with --all for single result' '
367+
(
368+
cd graph-repo &&
369+
git commit-graph write --reachable &&
370+
git merge-base left right >actual.single &&
371+
git merge-base --all left right >actual.all &&
372+
test_cmp actual.all actual.single
373+
)
374+
'
375+
376+
test_expect_success 'setup for deep chain commit-graph test' '
377+
git init deep-repo &&
378+
(
379+
cd deep-repo &&
380+
381+
# Build a deep forked DAG:
382+
#
383+
# L1--L2--...--L20 (left)
384+
# /
385+
# S
386+
# \
387+
# R1--R2--...--R20 (right)
388+
#
389+
test_commit DS &&
390+
git checkout -b left &&
391+
for i in $(test_seq 1 20)
392+
do
393+
test_commit DL$i || return 1
394+
done &&
395+
git checkout DS &&
396+
git checkout -b right &&
397+
for i in $(test_seq 1 20)
398+
do
399+
test_commit DR$i || return 1
400+
done
401+
)
402+
'
403+
404+
test_expect_success 'deep chain: merge-base matches with and without commit-graph' '
405+
(
406+
cd deep-repo &&
407+
rm -f .git/objects/info/commit-graph &&
408+
git merge-base left right >actual.no-graph &&
409+
git rev-parse DS >expected &&
410+
test_cmp expected actual.no-graph &&
411+
git commit-graph write --reachable &&
412+
git merge-base left right >actual.graph &&
413+
test_cmp expected actual.graph
414+
)
415+
'
416+
417+
test_expect_success 'deep chain: --all and non---all agree with commit-graph' '
418+
(
419+
cd deep-repo &&
420+
git commit-graph write --reachable &&
421+
git merge-base left right >actual.single &&
422+
git merge-base --all left right >actual.all &&
423+
test_cmp actual.all actual.single
424+
)
425+
'
426+
308427
test_done

t/t6600-test-reach.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,4 +882,44 @@ test_expect_success 'rev-list --maximal-only matches merge-base --independent' '
882882
test_cmp expect.sorted actual.sorted
883883
'
884884

885+
# The following tests verify the early-exit optimisation in
886+
# paint_down_to_common when merge-base is invoked without --all.
887+
# Each test checks all four commit-graph configurations.
888+
889+
merge_base_all_modes () {
890+
test_when_finished rm -rf .git/objects/info/commit-graph &&
891+
git merge-base "$@" >actual &&
892+
test_cmp expect actual &&
893+
cp commit-graph-full .git/objects/info/commit-graph &&
894+
git merge-base "$@" >actual &&
895+
test_cmp expect actual &&
896+
cp commit-graph-half .git/objects/info/commit-graph &&
897+
git merge-base "$@" >actual &&
898+
test_cmp expect actual &&
899+
cp commit-graph-no-gdat .git/objects/info/commit-graph &&
900+
git merge-base "$@" >actual &&
901+
test_cmp expect actual
902+
}
903+
904+
test_expect_success 'merge-base without --all (unique base)' '
905+
git rev-parse commit-5-3 >expect &&
906+
merge_base_all_modes commit-5-7 commit-8-3
907+
'
908+
909+
test_expect_success 'merge-base without --all is one of --all results' '
910+
test_when_finished rm -rf .git/objects/info/commit-graph &&
911+
912+
cp commit-graph-full .git/objects/info/commit-graph &&
913+
git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
914+
git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
915+
test_line_count = 1 single &&
916+
grep -F -f single all &&
917+
918+
cp commit-graph-half .git/objects/info/commit-graph &&
919+
git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all &&
920+
git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single &&
921+
test_line_count = 1 single &&
922+
grep -F -f single all
923+
'
924+
885925
test_done

0 commit comments

Comments
 (0)