diff --git a/builtin/merge-base.c b/builtin/merge-base.c index c7ee97fa6ac62a..6b9d42f59675d8 100644 --- a/builtin/merge-base.c +++ b/builtin/merge-base.c @@ -14,7 +14,8 @@ static int show_merge_base(struct commit **rev, size_t rev_nr, int show_all) struct commit_list *result = NULL, *r; if (repo_get_merge_bases_many_dirty(the_repository, rev[0], - rev_nr - 1, rev + 1, &result) < 0) { + rev_nr - 1, rev + 1, + show_all, &result) < 0) { commit_list_free(result); return -1; } diff --git a/commit-reach.c b/commit-reach.c index d3a9b3ed6fe561..c9d2d594debe43 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -55,14 +55,16 @@ static int paint_down_to_common(struct repository *r, struct commit **twos, timestamp_t min_generation, int ignore_missing_commits, + int find_all, struct commit_list **result) { struct prio_queue queue = { compare_commits_by_gen_then_commit_date }; int i; + int has_gens = min_generation || corrected_commit_dates_enabled(r); timestamp_t last_gen = GENERATION_NUMBER_INFINITY; struct commit_list **tail = result; - if (!min_generation && !corrected_commit_dates_enabled(r)) + if (!has_gens) queue.compare = compare_commits_by_commit_date; one->object.flags |= PARENT1; @@ -97,6 +99,11 @@ static int paint_down_to_common(struct repository *r, if (!(commit->object.flags & RESULT)) { commit->object.flags |= RESULT; tail = commit_list_append(commit, tail); + /* Generation-ordered queue: no later + * commit can dominate this one. */ + if (!find_all && has_gens && + generation < GENERATION_NUMBER_INFINITY) + break; } /* Mark parents of a found merge stale */ flags |= STALE; @@ -136,6 +143,7 @@ static int paint_down_to_common(struct repository *r, static int merge_bases_many(struct repository *r, struct commit *one, int n, struct commit **twos, + int find_all, struct commit_list **result) { struct commit_list *list = NULL, **tail = result; @@ -165,7 +173,7 @@ static int merge_bases_many(struct repository *r, oid_to_hex(&twos[i]->object.oid)); } - if (paint_down_to_common(r, one, n, twos, 0, 0, &list)) { + if (paint_down_to_common(r, one, n, twos, 0, 0, find_all, &list)) { commit_list_free(list); return -1; } @@ -246,7 +254,7 @@ static int remove_redundant_no_gen(struct repository *r, min_generation = curr_generation; } if (paint_down_to_common(r, array[i], filled, - work, min_generation, 0, &common)) { + work, min_generation, 0, 1, &common)) { clear_commit_marks(array[i], all_flags); clear_commit_marks_many(filled, work, all_flags); commit_list_free(common); @@ -425,6 +433,7 @@ static int get_merge_bases_many_0(struct repository *r, size_t n, struct commit **twos, int cleanup, + int find_all, struct commit_list **result) { struct commit_list *list, **tail = result; @@ -432,7 +441,7 @@ static int get_merge_bases_many_0(struct repository *r, size_t cnt, i; int ret; - if (merge_bases_many(r, one, n, twos, result) < 0) + if (merge_bases_many(r, one, n, twos, find_all, result) < 0) return -1; for (i = 0; i < n; i++) { if (one == twos[i]) @@ -475,16 +484,17 @@ int repo_get_merge_bases_many(struct repository *r, struct commit **twos, struct commit_list **result) { - return get_merge_bases_many_0(r, one, n, twos, 1, result); + return get_merge_bases_many_0(r, one, n, twos, 1, 1, result); } int repo_get_merge_bases_many_dirty(struct repository *r, struct commit *one, size_t n, struct commit **twos, + int find_all, struct commit_list **result) { - return get_merge_bases_many_0(r, one, n, twos, 0, result); + return get_merge_bases_many_0(r, one, n, twos, 0, find_all, result); } int repo_get_merge_bases(struct repository *r, @@ -492,7 +502,7 @@ int repo_get_merge_bases(struct repository *r, struct commit *two, struct commit_list **result) { - return get_merge_bases_many_0(r, one, 1, &two, 1, result); + return get_merge_bases_many_0(r, one, 1, &two, 1, 1, result); } /* @@ -555,7 +565,7 @@ int repo_in_merge_bases_many(struct repository *r, struct commit *commit, if (paint_down_to_common(r, commit, nr_reference, reference, - generation, ignore_missing_commits, &bases)) + generation, ignore_missing_commits, 1, &bases)) ret = -1; else if (commit->object.flags & PARENT2) ret = 1; diff --git a/commit-reach.h b/commit-reach.h index 6012402dfcfe45..908b9539c5e344 100644 --- a/commit-reach.h +++ b/commit-reach.h @@ -17,10 +17,13 @@ int repo_get_merge_bases_many(struct repository *r, struct commit *one, size_t n, struct commit **twos, struct commit_list **result); -/* To be used only when object flags after this call no longer matter */ +/* To be used only when object flags after this call no longer matter. + * When find_all is false and generation numbers are available, returns + * after finding the first merge-base, skipping the STALE drain. */ int repo_get_merge_bases_many_dirty(struct repository *r, struct commit *one, size_t n, struct commit **twos, + int find_all, struct commit_list **result); int get_octopus_merge_bases(struct commit_list *in, struct commit_list **result); diff --git a/t/t6010-merge-base.sh b/t/t6010-merge-base.sh index 44c726ea397cf1..f6c85d4f5360c3 100755 --- a/t/t6010-merge-base.sh +++ b/t/t6010-merge-base.sh @@ -305,4 +305,123 @@ test_expect_success 'merge-base --octopus --all for complex tree' ' test_cmp expected actual ' +# The following tests verify that "git merge-base" (without --all) +# returns the same result with and without a commit-graph. +# This exercises the early-exit optimisation in paint_down_to_common +# that skips the STALE drain when generation numbers are available. + +test_expect_success 'setup for commit-graph tests' ' + git init graph-repo && + ( + cd graph-repo && + + # Build a forked DAG: + # + # L1---L2 (left) + # / + # S + # \ + # R1---R2 (right) + # + test_commit GS && + git checkout -b left && + test_commit L1 && + test_commit L2 && + git checkout GS && + git checkout -b right && + test_commit GR1 && + test_commit GR2 + ) +' + +test_expect_success 'merge-base without commit-graph' ' + ( + cd graph-repo && + rm -f .git/objects/info/commit-graph && + git merge-base left right >actual && + git rev-parse GS >expected && + test_cmp expected actual + ) +' + +test_expect_success 'merge-base with commit-graph' ' + ( + cd graph-repo && + git commit-graph write --reachable && + git merge-base left right >actual && + git rev-parse GS >expected && + test_cmp expected actual + ) +' + +test_expect_success 'merge-base --all with commit-graph' ' + ( + cd graph-repo && + git merge-base --all left right >actual && + git rev-parse GS >expected && + test_cmp expected actual + ) +' + +test_expect_success 'merge-base agrees with --all for single result' ' + ( + cd graph-repo && + git commit-graph write --reachable && + git merge-base left right >actual.single && + git merge-base --all left right >actual.all && + test_cmp actual.all actual.single + ) +' + +test_expect_success 'setup for deep chain commit-graph test' ' + git init deep-repo && + ( + cd deep-repo && + + # Build a deep forked DAG: + # + # L1--L2--...--L20 (left) + # / + # S + # \ + # R1--R2--...--R20 (right) + # + test_commit DS && + git checkout -b left && + for i in $(test_seq 1 20) + do + test_commit DL$i || return 1 + done && + git checkout DS && + git checkout -b right && + for i in $(test_seq 1 20) + do + test_commit DR$i || return 1 + done + ) +' + +test_expect_success 'deep chain: merge-base matches with and without commit-graph' ' + ( + cd deep-repo && + rm -f .git/objects/info/commit-graph && + git merge-base left right >actual.no-graph && + git rev-parse DS >expected && + test_cmp expected actual.no-graph && + git commit-graph write --reachable && + git merge-base left right >actual.graph && + test_cmp expected actual.graph + ) +' + +test_expect_success 'deep chain: --all and non---all agree with commit-graph' ' + ( + cd deep-repo && + git commit-graph write --reachable && + git merge-base left right >actual.single && + git merge-base --all left right >actual.all && + test_cmp actual.all actual.single + ) +' + test_done diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh index dc0421ed2f3726..51c23b76833126 100755 --- a/t/t6600-test-reach.sh +++ b/t/t6600-test-reach.sh @@ -882,4 +882,44 @@ test_expect_success 'rev-list --maximal-only matches merge-base --independent' ' test_cmp expect.sorted actual.sorted ' +# The following tests verify the early-exit optimisation in +# paint_down_to_common when merge-base is invoked without --all. +# Each test checks all four commit-graph configurations. + +merge_base_all_modes () { + test_when_finished rm -rf .git/objects/info/commit-graph && + git merge-base "$@" >actual && + test_cmp expect actual && + cp commit-graph-full .git/objects/info/commit-graph && + git merge-base "$@" >actual && + test_cmp expect actual && + cp commit-graph-half .git/objects/info/commit-graph && + git merge-base "$@" >actual && + test_cmp expect actual && + cp commit-graph-no-gdat .git/objects/info/commit-graph && + git merge-base "$@" >actual && + test_cmp expect actual +} + +test_expect_success 'merge-base without --all (unique base)' ' + git rev-parse commit-5-3 >expect && + merge_base_all_modes commit-5-7 commit-8-3 +' + +test_expect_success 'merge-base without --all is one of --all results' ' + test_when_finished rm -rf .git/objects/info/commit-graph && + + cp commit-graph-full .git/objects/info/commit-graph && + git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all && + git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single && + test_line_count = 1 single && + grep -F -f single all && + + cp commit-graph-half .git/objects/info/commit-graph && + git merge-base --all commit-5-7 commit-4-8 commit-6-6 commit-8-3 >all && + git merge-base commit-5-7 commit-4-8 commit-6-6 commit-8-3 >single && + test_line_count = 1 single && + grep -F -f single all +' + test_done