Commit c629d2a11cbc5863c4a68eaafa8bf6a99b263f26

Edward Thomson 2022-01-29T21:02:15

merge: support zdiff3 conflict styles

diff --git a/include/git2/checkout.h b/include/git2/checkout.h
index f026d5b..9f83411 100644
--- a/include/git2/checkout.h
+++ b/include/git2/checkout.h
@@ -182,7 +182,10 @@ typedef enum {
 	 * notifications; don't update the working directory or index.
 	 */
 	GIT_CHECKOUT_DRY_RUN = (1u << 24),
-	
+
+	/** Include common ancestor data in zdiff3 format for conflicts */
+	GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3 = (1u << 25),
+
 	/**
 	 * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
 	 */
diff --git a/include/git2/merge.h b/include/git2/merge.h
index 3b3132f..edd090c 100644
--- a/include/git2/merge.h
+++ b/include/git2/merge.h
@@ -159,7 +159,10 @@ typedef enum {
 	GIT_MERGE_FILE_DIFF_PATIENCE = (1 << 6),
 
 	/** Take extra time to find minimal diff */
-	GIT_MERGE_FILE_DIFF_MINIMAL = (1 << 7)
+	GIT_MERGE_FILE_DIFF_MINIMAL = (1 << 7),
+
+	/** Create zdiff3 ("zealous diff3")-style files */
+	GIT_MERGE_FILE_STYLE_ZDIFF3 = (1 << 8)
 } git_merge_file_flag_t;
 
 #define GIT_MERGE_CONFLICT_MARKER_SIZE	7
diff --git a/src/checkout.c b/src/checkout.c
index bc3048c..6a46431 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -2070,6 +2070,9 @@ static int checkout_write_merge(
 	if (data->opts.checkout_strategy & GIT_CHECKOUT_CONFLICT_STYLE_DIFF3)
 		opts.flags |= GIT_MERGE_FILE_STYLE_DIFF3;
 
+	if (data->opts.checkout_strategy & GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3)
+		opts.flags |= GIT_MERGE_FILE_STYLE_ZDIFF3;
+
 	opts.ancestor_label = data->opts.ancestor_label ?
 		data->opts.ancestor_label : "ancestor";
 	opts.our_label = data->opts.our_label ?
@@ -2493,6 +2496,8 @@ static int checkout_data_init(
 			data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_MERGE;
 		else if (strcmp(conflict_style->value, "diff3") == 0)
 			data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_DIFF3;
+		else if (strcmp(conflict_style->value, "zdiff3") == 0)
+			data->opts.checkout_strategy |= GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3;
 		else {
 			git_error_set(GIT_ERROR_CHECKOUT, "unknown style '%s' given for 'merge.conflictstyle'",
 				conflict_style->value);
diff --git a/src/merge_file.c b/src/merge_file.c
index 7285884..732a047 100644
--- a/src/merge_file.c
+++ b/src/merge_file.c
@@ -123,6 +123,8 @@ static int merge_file__xdiff(
 
 	if (options.flags & GIT_MERGE_FILE_STYLE_DIFF3)
 		xmparam.style = XDL_MERGE_DIFF3;
+	if (options.flags & GIT_MERGE_FILE_STYLE_ZDIFF3)
+		xmparam.style = XDL_MERGE_ZEALOUS_DIFF3;
 
 	if (options.flags & GIT_MERGE_FILE_IGNORE_WHITESPACE)
 		xmparam.xpp.flags |= XDF_IGNORE_WHITESPACE;
diff --git a/tests/merge/conflict_data.h b/tests/merge/conflict_data.h
index 27f19c1..0b1e7ee 100644
--- a/tests/merge/conflict_data.h
+++ b/tests/merge/conflict_data.h
@@ -36,6 +36,15 @@
 	"this file is changed in branch and master\n" \
 	">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
 
+#define CONFLICTING_ZDIFF3_FILE \
+	"<<<<<<< HEAD\n" \
+	"this file is changed in master and branch\n" \
+	"||||||| initial\n" \
+	"this file is a conflict\n" \
+	"=======\n" \
+	"this file is changed in branch and master\n" \
+	">>>>>>> 7cb63eed597130ba4abb87b3e544b85021905520\n"
+
 #define CONFLICTING_UNION_FILE \
 	"this file is changed in master and branch\n" \
 	"this file is changed in branch and master\n"
diff --git a/tests/merge/files.c b/tests/merge/files.c
index fbc54e1..6296f3b 100644
--- a/tests/merge/files.c
+++ b/tests/merge/files.c
@@ -424,3 +424,42 @@ void test_merge_files__crlf_conflict_markers_for_crlf_files(void)
 	cl_assert(memcmp(expected_diff3, result.ptr, expected_len) == 0);
 	git_merge_file_result_free(&result);
 }
+
+void test_merge_files__conflicts_in_zdiff3(void)
+{
+	git_merge_file_input ancestor = GIT_MERGE_FILE_INPUT_INIT,
+		ours = GIT_MERGE_FILE_INPUT_INIT,
+		theirs = GIT_MERGE_FILE_INPUT_INIT;
+	git_merge_file_options opts = GIT_MERGE_FILE_OPTIONS_INIT;
+	git_merge_file_result result = {0};
+
+	const char *expected_zdiff3 =
+		"1,\nfoo,\nbar,\n" \
+		"<<<<<<< file.txt\n" \
+		"||||||| file.txt\n# add more here\n" \
+		"=======\nquux,\nwoot,\n" \
+		">>>>>>> file.txt\nbaz,\n3,\n";
+	size_t expected_zdiff3_len = strlen(expected_zdiff3);
+
+	ancestor.ptr = "1,\n# add more here\n3,\n";
+	ancestor.size = strlen(ancestor.ptr);
+	ancestor.path = "file.txt";
+	ancestor.mode = 0100644;
+
+	ours.ptr = "1,\nfoo,\nbar,\nbaz,\n3,\n";
+	ours.size = strlen(ours.ptr);
+	ours.path = "file.txt";
+	ours.mode = 0100644;
+
+	theirs.ptr = "1,\nfoo,\nbar,\nquux,\nwoot,\nbaz,\n3,\n";
+	theirs.size = strlen(theirs.ptr);
+	theirs.path = "file.txt";
+	theirs.mode = 0100644;
+
+	opts.flags |= GIT_MERGE_FILE_STYLE_ZDIFF3;
+	cl_git_pass(git_merge_file(&result, &ancestor, &ours, &theirs, &opts));
+	cl_assert_equal_i(0, result.automergeable);
+	cl_assert_equal_i(expected_zdiff3_len, result.len);
+	cl_assert(memcmp(expected_zdiff3, result.ptr, expected_zdiff3_len) == 0);
+	git_merge_file_result_free(&result);
+}
diff --git a/tests/merge/workdir/simple.c b/tests/merge/workdir/simple.c
index f51ff09..0b672d6 100644
--- a/tests/merge/workdir/simple.c
+++ b/tests/merge/workdir/simple.c
@@ -323,6 +323,42 @@ void test_merge_workdir_simple__diff3(void)
 	cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3));
 }
 
+void test_merge_workdir_simple__zdiff3(void)
+{
+	git_str conflicting_buf = GIT_STR_INIT;
+
+	struct merge_index_entry merge_index_entries[] = {
+		ADDED_IN_MASTER_INDEX_ENTRY,
+		AUTOMERGEABLE_INDEX_ENTRY,
+		CHANGED_IN_BRANCH_INDEX_ENTRY,
+		CHANGED_IN_MASTER_INDEX_ENTRY,
+
+		{ 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" },
+		{ 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" },
+		{ 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" },
+
+		UNCHANGED_INDEX_ENTRY,
+	};
+
+	struct merge_reuc_entry merge_reuc_entries[] = {
+		AUTOMERGEABLE_REUC_ENTRY,
+		REMOVED_IN_BRANCH_REUC_ENTRY,
+		REMOVED_IN_MASTER_REUC_ENTRY
+	};
+
+	set_core_autocrlf_to(repo, false);
+
+	merge_simple_branch(0, GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3);
+
+	cl_git_pass(git_futils_readbuffer(&conflicting_buf,
+		TEST_REPO_PATH "/conflicting.txt"));
+	cl_assert_equal_s(CONFLICTING_ZDIFF3_FILE, conflicting_buf.ptr);
+	git_str_dispose(&conflicting_buf);
+
+	cl_assert(merge_test_index(repo_index, merge_index_entries, 8));
+	cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3));
+}
+
 void test_merge_workdir_simple__union(void)
 {
 	git_str conflicting_buf = GIT_STR_INIT;
@@ -436,6 +472,48 @@ void test_merge_workdir_simple__diff3_from_config(void)
 	git_config_free(config);
 }
 
+void test_merge_workdir_simple__zdiff3_from_config(void)
+{
+	git_config *config;
+	git_str conflicting_buf = GIT_STR_INIT;
+
+	struct merge_index_entry merge_index_entries[] = {
+		ADDED_IN_MASTER_INDEX_ENTRY,
+		AUTOMERGEABLE_INDEX_ENTRY,
+		CHANGED_IN_BRANCH_INDEX_ENTRY,
+		CHANGED_IN_MASTER_INDEX_ENTRY,
+
+		{ 0100644, "d427e0b2e138501a3d15cc376077a3631e15bd46", 1, "conflicting.txt" },
+		{ 0100644, "4e886e602529caa9ab11d71f86634bd1b6e0de10", 2, "conflicting.txt" },
+		{ 0100644, "2bd0a343aeef7a2cf0d158478966a6e587ff3863", 3, "conflicting.txt" },
+
+		UNCHANGED_INDEX_ENTRY,
+	};
+
+	struct merge_reuc_entry merge_reuc_entries[] = {
+		AUTOMERGEABLE_REUC_ENTRY,
+		REMOVED_IN_BRANCH_REUC_ENTRY,
+		REMOVED_IN_MASTER_REUC_ENTRY
+	};
+
+	cl_git_pass(git_repository_config(&config, repo));
+	cl_git_pass(git_config_set_string(config, "merge.conflictstyle", "zdiff3"));
+
+	set_core_autocrlf_to(repo, false);
+
+	merge_simple_branch(0, 0);
+
+	cl_git_pass(git_futils_readbuffer(&conflicting_buf,
+		TEST_REPO_PATH "/conflicting.txt"));
+	cl_assert(strcmp(conflicting_buf.ptr, CONFLICTING_ZDIFF3_FILE) == 0);
+	git_str_dispose(&conflicting_buf);
+
+	cl_assert(merge_test_index(repo_index, merge_index_entries, 8));
+	cl_assert(merge_test_reuc(repo_index, merge_reuc_entries, 3));
+
+	git_config_free(config);
+}
+
 void test_merge_workdir_simple__merge_overrides_config(void)
 {
 	git_config *config;