Commit 5b9c63c3f673cbc209e53627be2a0e87c17ccb3c

Edward Thomson 2015-11-20T19:01:42

recursive merge: add a recursion limit

diff --git a/include/git2/merge.h b/include/git2/merge.h
index a272e8b..af53ead 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 f05e45c..9eb3b09 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 693c910..c5b129b 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);
+}
+