diff --git a/include/git2/merge.h b/include/git2/merge.h index a272e8be4..af53ead22 100644 --- a/include/git2/merge.h +++ b/include/git2/merge.h @@ -265,6 +265,14 @@ typedef struct { /** Pluggable similarity metric; pass NULL to use internal metric */ git_diff_similarity_metric *metric; + /** + * Maximum number of times to merge common ancestors to build a + * virtual merge base when faced with criss-cross merges. When this + * limit is reached, the next ancestor will simply be used instead of + * attempting to merge it. The default is unlimited. + */ + unsigned int recursion_limit; + /** Flags for handling conflicting content. */ git_merge_file_favor_t file_favor; diff --git a/src/merge.c b/src/merge.c index f05e45c9f..9eb3b0904 100644 --- a/src/merge.c +++ b/src/merge.c @@ -2019,17 +2019,21 @@ static int compute_base( git_repository *repo, const git_annotated_commit *one, const git_annotated_commit *two, - const git_merge_options *opts, + const git_merge_options *given_opts, size_t recursion_level) { git_array_oid_t head_ids = GIT_ARRAY_INIT; git_oidarray bases = {0}; git_annotated_commit *base = NULL, *other = NULL, *new_base = NULL; + git_merge_options opts = GIT_MERGE_OPTIONS_INIT; size_t i; int error; *out = NULL; + if (given_opts) + memcpy(&opts, given_opts, sizeof(git_merge_options)); + if ((error = insert_head_ids(&head_ids, one)) < 0 || (error = insert_head_ids(&head_ids, two)) < 0) goto done; @@ -2037,15 +2041,18 @@ static int compute_base( if ((error = git_merge_bases_many(&bases, repo, head_ids.size, head_ids.ptr)) < 0 || (error = git_annotated_commit_lookup(&base, repo, &bases.ids[0])) < 0 || - (opts && (opts->flags & GIT_MERGE_NO_RECURSIVE))) + (opts.flags & GIT_MERGE_NO_RECURSIVE)) goto done; for (i = 1; i < bases.count; i++) { recursion_level++; + if (opts.recursion_limit && recursion_level > opts.recursion_limit) + break; + if ((error = git_annotated_commit_lookup(&other, repo, &bases.ids[i])) < 0 || - (error = create_virtual_base(&new_base, repo, base, other, opts, + (error = create_virtual_base(&new_base, repo, base, other, &opts, recursion_level)) < 0) goto done; diff --git a/tests/merge/trees/recursive.c b/tests/merge/trees/recursive.c index 693c91065..c5b129bf8 100644 --- a/tests/merge/trees/recursive.c +++ b/tests/merge/trees/recursive.c @@ -377,3 +377,34 @@ void test_merge_trees_recursive__conflicting_merge_base_since_resolved(void) git_index_free(index); } + +/* There are multiple levels of criss-cross merges, and multiple recursive + * merges would create a common ancestor that allows the merge to complete + * successfully. Test that we can build a single virtual base, then stop, + * which will produce a conflicting merge. + */ +void test_merge_trees_recursive__recursionlimit(void) +{ + git_index *index; + git_merge_options opts = GIT_MERGE_OPTIONS_INIT; + + struct merge_index_entry merge_index_entries[] = { + { 0100644, "ffb36e513f5fdf8a6ba850a20142676a2ac4807d", 0, "asparagus.txt" }, + { 0100644, "68f6182f4c85d39e1309d97c7e456156dc9c0096", 0, "beef.txt" }, + { 0100644, "4b7c5650008b2e747fe1809eeb5a1dde0e80850a", 0, "bouilli.txt" }, + { 0100644, "ce7e553c6feb6e5f3bd67e3c3be04182fe3094b4", 1, "gravy.txt" }, + { 0100644, "d8dd349b78f19a4ebe3357bacb8138f00bf5ed41", 2, "gravy.txt" }, + { 0100644, "e50fbbd701458757bdfe9815f58ed717c588d1b5", 3, "gravy.txt" }, + { 0100644, "68af1fc7407fd9addf1701a87eb1c95c7494c598", 0, "oyster.txt" }, + { 0100644, "a7b066537e6be7109abfe4ff97b675d4e077da20", 0, "veal.txt" }, + }; + + opts.recursion_limit = 1; + + cl_git_pass(merge_commits_from_branches(&index, repo, "branchE-1", "branchE-2", &opts)); + + cl_assert(merge_test_index(index, merge_index_entries, 8)); + + git_index_free(index); +} +