Commit 1a79cd959ba2991dd3295f9940b28b606e494ccf

Edward Thomson 2016-04-26T01:18:01

patch: show copy information for identical copies When showing copy information because we are duplicating contents, for example, when performing a `diff --find-copies-harder -M100 -B100`, then show copy from/to lines in a patch, and do not show context. Ensure that we can also parse such patches.

diff --git a/src/diff_print.c b/src/diff_print.c
index 5a5a70b..f72ca89 100644
--- a/src/diff_print.c
+++ b/src/diff_print.c
@@ -331,11 +331,12 @@ static int diff_delta_format_with_paths(
 	return git_buf_printf(out, template, oldpath, newpath);
 }
 
-int diff_delta_format_rename_header(
+int diff_delta_format_similarity_header(
 	git_buf *out,
 	const git_diff_delta *delta)
 {
 	git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT;
+	const char *type;
 	int error = 0;
 
 	if (delta->similarity > 100) {
@@ -344,6 +345,13 @@ int diff_delta_format_rename_header(
 		goto done;
 	}
 
+	if (delta->status == GIT_DELTA_RENAMED)
+		type = "rename";
+	else if (delta->status == GIT_DELTA_COPIED)
+		type = "copy";
+	else
+		abort();
+
 	if ((error = git_buf_puts(&old_path, delta->old_file.path)) < 0 ||
 		(error = git_buf_puts(&new_path, delta->new_file.path)) < 0 ||
 		(error = git_buf_quote(&old_path)) < 0 ||
@@ -352,11 +360,11 @@ int diff_delta_format_rename_header(
 
 	git_buf_printf(out,
 		"similarity index %d%%\n"
-		"rename from %s\n"
-		"rename to %s\n",
+		"%s from %s\n"
+		"%s to %s\n",
 		delta->similarity,
-		old_path.ptr,
-		new_path.ptr);
+		type, old_path.ptr,
+		type, new_path.ptr);
 
 	if (git_buf_oom(out))
 		error = -1;
@@ -368,6 +376,22 @@ done:
 	return error;
 }
 
+static bool delta_is_unchanged(const git_diff_delta *delta)
+{
+	if (git_oid_iszero(&delta->old_file.id) &&
+		git_oid_iszero(&delta->new_file.id))
+		return true;
+
+	if (delta->old_file.mode == GIT_FILEMODE_COMMIT ||
+		delta->new_file.mode == GIT_FILEMODE_COMMIT)
+		return false;
+
+	if (git_oid_equal(&delta->old_file.id, &delta->new_file.id))
+		return true;
+
+	return false;
+}
+
 int git_diff_delta__format_file_header(
 	git_buf *out,
 	const git_diff_delta *delta,
@@ -376,7 +400,7 @@ int git_diff_delta__format_file_header(
 	int id_strlen)
 {
 	git_buf old_path = GIT_BUF_INIT, new_path = GIT_BUF_INIT;
-	bool unchanged;
+	bool unchanged = delta_is_unchanged(delta);
 	int error = 0;
 
 	if (!oldpfx)
@@ -397,14 +421,12 @@ int git_diff_delta__format_file_header(
 	git_buf_printf(out, "diff --git %s %s\n",
 		old_path.ptr, new_path.ptr);
 
-	if (delta->status == GIT_DELTA_RENAMED) {
-		if ((error = diff_delta_format_rename_header(out, delta)) < 0)
+	if (delta->status == GIT_DELTA_RENAMED ||
+		(delta->status == GIT_DELTA_COPIED && unchanged)) {
+		if ((error = diff_delta_format_similarity_header(out, delta)) < 0)
 			goto done;
 	}
 
-	unchanged = (git_oid_iszero(&delta->old_file.id) &&
-		git_oid_iszero(&delta->new_file.id));
-
 	if (!unchanged) {
 		if ((error = diff_print_oid_range(out, delta, id_strlen)) < 0)
 			goto done;
diff --git a/src/patch_parse.c b/src/patch_parse.c
index 72c4d14..7f21e3f 100644
--- a/src/patch_parse.c
+++ b/src/patch_parse.c
@@ -310,6 +310,20 @@ static int parse_header_renameto(
 	return parse_header_rename(&patch->rename_new_path, ctx);
 }
 
+static int parse_header_copyfrom(
+	git_patch_parsed *patch, git_patch_parse_ctx *ctx)
+{
+	patch->base.delta->status = GIT_DELTA_COPIED;
+	return parse_header_rename(&patch->rename_old_path, ctx);
+}
+
+static int parse_header_copyto(
+	git_patch_parsed *patch, git_patch_parse_ctx *ctx)
+{
+	patch->base.delta->status = GIT_DELTA_COPIED;
+	return parse_header_rename(&patch->rename_new_path, ctx);
+}
+
 static int parse_header_percent(uint16_t *out, git_patch_parse_ctx *ctx)
 {
 	int32_t val;
@@ -375,6 +389,8 @@ static const header_git_op header_git_ops[] = {
 	{ "rename to ", parse_header_renameto },
 	{ "rename old ", parse_header_renamefrom },
 	{ "rename new ", parse_header_renameto },
+	{ "copy from ", parse_header_copyfrom },
+	{ "copy to ", parse_header_copyto },
 	{ "similarity index ", parse_header_similarity },
 	{ "dissimilarity index ", parse_header_dissimilarity },
 };
diff --git a/tests/diff/diff_helpers.c b/tests/diff/diff_helpers.c
index 8fa8e3e..50752b2 100644
--- a/tests/diff/diff_helpers.c
+++ b/tests/diff/diff_helpers.c
@@ -242,18 +242,44 @@ void diff_print_raw(FILE *fp, git_diff *diff)
 			git_diff_print_callback__to_file_handle, fp ? fp : stderr));
 }
 
+static size_t num_modified_deltas(git_diff *diff)
+{
+	const git_diff_delta *delta;
+	size_t i, cnt = 0;
+
+	for (i = 0; i < git_diff_num_deltas(diff); i++) {
+		delta = git_diff_get_delta(diff, i);
+
+		if (delta->status != GIT_DELTA_UNMODIFIED)
+			cnt++;
+	}
+
+	return cnt;
+}
+
 void diff_assert_equal(git_diff *a, git_diff *b)
 {
 	const git_diff_delta *ad, *bd;
-	size_t i;
+	size_t i, j;
 
 	assert(a && b);
 
-	cl_assert_equal_i(git_diff_num_deltas(a), git_diff_num_deltas(b));
+	cl_assert_equal_i(num_modified_deltas(a), num_modified_deltas(b));
+
+	for (i = 0, j = 0;
+		i < git_diff_num_deltas(a) && j < git_diff_num_deltas(b); ) {
 
-	for (i = 0; i < git_diff_num_deltas(a); i++) {
 		ad = git_diff_get_delta(a, i);
-		bd = git_diff_get_delta(b, i);
+		bd = git_diff_get_delta(b, j);
+
+		if (ad->status == GIT_DELTA_UNMODIFIED) {
+			i++;
+			continue;
+		}
+		if (bd->status == GIT_DELTA_UNMODIFIED) {
+			j++;
+			continue;
+		}
 
 		cl_assert_equal_i(ad->status, bd->status);
 		cl_assert_equal_i(ad->flags, bd->flags);
@@ -265,15 +291,26 @@ void diff_assert_equal(git_diff *a, git_diff *b)
 		 * computed deltas will have flags of `VALID_ID` and
 		 * `EXISTS` (parsed deltas will not query the ODB.)
 		 */
-		cl_assert_equal_oid(&ad->old_file.id, &bd->old_file.id);
-		cl_assert_equal_i(ad->old_file.id_abbrev, bd->old_file.id_abbrev);
+
+		/* an empty id indicates that it wasn't presented, because
+		 * the diff was identical.  (eg, pure rename, mode change only, etc)
+		 */
+		if (ad->old_file.id_abbrev && bd->old_file.id_abbrev) {
+			cl_assert_equal_i(ad->old_file.id_abbrev, bd->old_file.id_abbrev);
+			cl_assert_equal_oid(&ad->old_file.id, &bd->old_file.id);
+			cl_assert_equal_i(ad->old_file.mode, bd->old_file.mode);
+		}
 		cl_assert_equal_s(ad->old_file.path, bd->old_file.path);
-		cl_assert_equal_i(ad->old_file.mode, bd->old_file.mode);
 
-		cl_assert_equal_oid(&ad->new_file.id, &bd->new_file.id);
-		cl_assert_equal_i(ad->new_file.id_abbrev, bd->new_file.id_abbrev);
+		if (ad->new_file.id_abbrev && bd->new_file.id_abbrev) {
+			cl_assert_equal_oid(&ad->new_file.id, &bd->new_file.id);
+			cl_assert_equal_i(ad->new_file.id_abbrev, bd->new_file.id_abbrev);
+			cl_assert_equal_i(ad->new_file.mode, bd->new_file.mode);
+		}
 		cl_assert_equal_s(ad->new_file.path, bd->new_file.path);
-		cl_assert_equal_i(ad->new_file.mode, bd->new_file.mode);
+
+		i++;
+		j++;
 	}
 }
 
diff --git a/tests/diff/format_email.c b/tests/diff/format_email.c
index 2e6d036..9f8fe31 100644
--- a/tests/diff/format_email.c
+++ b/tests/diff/format_email.c
@@ -351,9 +351,6 @@ void test_diff_format_email__mode_change(void)
 	"diff --git a/file1.txt.renamed b/file1.txt.renamed\n" \
 	"old mode 100644\n" \
 	"new mode 100755\n" \
-	"index a97157a..a97157a\n" \
-	"--- a/file1.txt.renamed\n" \
-	"+++ b/file1.txt.renamed\n" \
 	"--\n" \
 	"libgit2 " LIBGIT2_VERSION "\n" \
 	"\n";
diff --git a/tests/diff/parse.c b/tests/diff/parse.c
index 56b9890..83000a9 100644
--- a/tests/diff/parse.c
+++ b/tests/diff/parse.c
@@ -143,6 +143,11 @@ void test_diff_parse__can_parse_generated_diff(void)
 		"31e47d8c1fa36d7f8d537b96158e3f024de0a9f2",
 		"2bc7f351d20b53f1c72c16c4b036e491c478c49a",
 		GIT_DIFF_INCLUDE_UNMODIFIED,
+		0);
+	test_tree_to_tree_computed_to_parsed("renames",
+		"31e47d8c1fa36d7f8d537b96158e3f024de0a9f2",
+		"2bc7f351d20b53f1c72c16c4b036e491c478c49a",
+		GIT_DIFF_INCLUDE_UNMODIFIED,
 		GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED | GIT_DIFF_FIND_EXACT_MATCH_ONLY);
 }
 
diff --git a/tests/diff/rename.c b/tests/diff/rename.c
index 5cfd8e2..c1cd252 100644
--- a/tests/diff/rename.c
+++ b/tests/diff/rename.c
@@ -1702,3 +1702,49 @@ void test_diff_rename__blank_files_not_renamed_when_not_ignoring_whitespace(void
 	expect_files_not_renamed("", "\n\n\n\n",  GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE);
 	expect_files_not_renamed("\n\n\n\n", "\r\n\r\n\r\n",  GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE);
 }
+
+/* test that 100% renames and copies emit the correct patch file
+ * git diff --find-copies-harder -M100 -B100 \
+ *          31e47d8c1fa36d7f8d537b96158e3f024de0a9f2 \
+ *          2bc7f351d20b53f1c72c16c4b036e491c478c49a
+ */
+void test_diff_rename__identical(void)
+{
+	const char *old_sha = "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2";
+	const char *new_sha = "2bc7f351d20b53f1c72c16c4b036e491c478c49a";
+	git_tree *old_tree, *new_tree;
+    git_diff *diff;
+	git_diff_options diff_opts = GIT_DIFF_OPTIONS_INIT;
+	git_diff_find_options find_opts = GIT_DIFF_FIND_OPTIONS_INIT;
+	git_buf diff_buf = GIT_BUF_INIT;
+	const char *expected =
+		"diff --git a/serving.txt b/sixserving.txt\n"
+		"similarity index 100%\n"
+		"rename from serving.txt\n"
+		"rename to sixserving.txt\n"
+		"diff --git a/sevencities.txt b/songofseven.txt\n"
+		"similarity index 100%\n"
+		"copy from sevencities.txt\n"
+		"copy to songofseven.txt\n";
+
+	old_tree = resolve_commit_oid_to_tree(g_repo, old_sha);
+	new_tree = resolve_commit_oid_to_tree(g_repo, new_sha);
+
+	diff_opts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
+	find_opts.flags = GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED |
+		GIT_DIFF_FIND_EXACT_MATCH_ONLY;
+
+	cl_git_pass(git_diff_tree_to_tree(&diff,
+		g_repo, old_tree, new_tree, &diff_opts));
+	cl_git_pass(git_diff_find_similar(diff, &find_opts));
+
+	cl_git_pass(git_diff_to_buf(&diff_buf, diff, GIT_DIFF_FORMAT_PATCH));
+
+	cl_assert_equal_s(expected, diff_buf.ptr);
+
+	git_buf_free(&diff_buf);
+	git_diff_free(diff);
+	git_tree_free(old_tree);
+	git_tree_free(new_tree);
+}
+