Commit 0eedacb06ae07fd0d784066ad41383276e05d92e

Russell Belfer 2013-12-11T10:39:36

Merge pull request #1985 from libgit2/diff-rename-config Rename detection using diff.renames

diff --git a/include/git2/diff.h b/include/git2/diff.h
index db6bce2..315cc12 100644
--- a/include/git2/diff.h
+++ b/include/git2/diff.h
@@ -468,6 +468,9 @@ typedef int (*git_diff_line_cb)(
  * Flags to control the behavior of diff rename/copy detection.
  */
 typedef enum {
+	/** Obey `diff.renames`. This is overridden by any other GIT_DIFF_FIND_ALL flag. */
+	GIT_DIFF_FIND_BY_CONFIG = 0,
+
 	/** Look for renames? (`--find-renames`) */
 	GIT_DIFF_FIND_RENAMES = (1u << 0),
 
@@ -573,7 +576,11 @@ typedef struct {
 typedef struct {
 	unsigned int version;
 
-	/** Combination of git_diff_find_t values (default FIND_RENAMES) */
+	/**
+	 * Combination of git_diff_find_t values (default FIND_BY_CONFIG).
+	 * Note that if you don't explicitly set this, `diff.renames` could be set
+	 * to false, resulting in `git_diff_find_similar` doing nothing. 
+	 */
 	uint32_t flags;
 
 	/** Similarity to consider a file renamed (default 50) */
diff --git a/src/diff_tform.c b/src/diff_tform.c
index 0a28e58..702e43b 100644
--- a/src/diff_tform.c
+++ b/src/diff_tform.c
@@ -275,28 +275,36 @@ static int normalize_find_opts(
 {
 	git_config *cfg = NULL;
 
+	GITERR_CHECK_VERSION(given, GIT_DIFF_FIND_OPTIONS_VERSION, "git_diff_find_options");
+
 	if (diff->repo != NULL &&
 		git_repository_config__weakptr(&cfg, diff->repo) < 0)
 		return -1;
 
-	if (given != NULL)
+	if (given) {
 		memcpy(opts, given, sizeof(*opts));
-	else {
-		const char *val = NULL;
-
+	} else {
 		GIT_INIT_STRUCTURE(opts, GIT_DIFF_FIND_OPTIONS_VERSION);
+	}
 
-		opts->flags = GIT_DIFF_FIND_RENAMES;
+	if (!given ||
+		 (given->flags & GIT_DIFF_FIND_ALL) == GIT_DIFF_FIND_BY_CONFIG)
+	{
+		const char *val = NULL;
 
 		if (git_config_get_string(&val, cfg, "diff.renames") < 0)
 			giterr_clear();
-		else if (val &&
-			(!strcasecmp(val, "copies") || !strcasecmp(val, "copy")))
-			opts->flags = GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES;
+		else if (val) {
+			int boolval;
+			if (!git__parse_bool(&boolval, val) && !boolval) {
+				/* do nothing */
+			} else if (!strcasecmp(val, "copies") || !strcasecmp(val, "copy"))
+				opts->flags |= (GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES);
+			else
+				opts->flags |= GIT_DIFF_FIND_RENAMES;
+		}
 	}
 
-	GITERR_CHECK_VERSION(opts, GIT_DIFF_FIND_OPTIONS_VERSION, "git_diff_find_options");
-
 	/* some flags imply others */
 
 	if (opts->flags & GIT_DIFF_FIND_EXACT_MATCH_ONLY) {
@@ -830,6 +838,10 @@ int git_diff_find_similar(
 	if ((error = normalize_find_opts(diff, &opts, given_opts)) < 0)
 		return error;
 
+	/* No flags set; nothing to do */
+	if ((opts.flags & GIT_DIFF_FIND_ALL) == 0)
+		return 0;
+
 	num_deltas = diff->deltas.length;
 
 	/* TODO: maybe abort if deltas.length > rename_limit ??? */
diff --git a/tests/diff/rename.c b/tests/diff/rename.c
index 919f513..ca6d076 100644
--- a/tests/diff/rename.c
+++ b/tests/diff/rename.c
@@ -919,6 +919,7 @@ void test_diff_rename__rejected_match_can_match_others(void)
 	char *ptr;
 
 	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+	findopts.flags = GIT_DIFF_FIND_RENAMES;
 
 	cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD"));
 	cl_git_pass(git_reference_symbolic_set_target(
@@ -1003,6 +1004,7 @@ void test_diff_rename__rejected_match_can_match_others_two(void)
 	struct rename_expected expect = { 2, status, sources, targets };
 
 	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+	findopts.flags = GIT_DIFF_FIND_RENAMES;
 
 	cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD"));
 	cl_git_pass(git_reference_symbolic_set_target(
@@ -1060,6 +1062,7 @@ void test_diff_rename__rejected_match_can_match_others_three(void)
 	struct rename_expected expect = { 2, status, sources, targets };
 
 	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+	findopts.flags = GIT_DIFF_FIND_RENAMES;
 
 	cl_git_pass(git_reference_lookup(&head, g_repo, "HEAD"));
 	cl_git_pass(git_reference_symbolic_set_target(
@@ -1381,3 +1384,183 @@ void test_diff_rename__can_delete_unmodified_deltas(void)
 
 	git_buf_free(&c1);
 }
+
+void test_diff_rename__matches_config_behavior(void)
+{
+	const char *sha0 = "31e47d8c1fa36d7f8d537b96158e3f024de0a9f2";
+	const char *sha1 = "2bc7f351d20b53f1c72c16c4b036e491c478c49a";
+	const char *sha2 = "1c068dee5790ef1580cfc4cd670915b48d790084";
+
+	git_tree *tree0, *tree1, *tree2;
+	git_config *cfg;
+	git_diff *diff;
+	git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
+	git_diff_find_options opts = GIT_DIFF_FIND_OPTIONS_INIT;
+	diff_expects exp;
+
+	opts.flags = GIT_DIFF_FIND_BY_CONFIG;
+	tree0 = resolve_commit_oid_to_tree(g_repo, sha0);
+	tree1 = resolve_commit_oid_to_tree(g_repo, sha1);
+	tree2 = resolve_commit_oid_to_tree(g_repo, sha2);
+
+	diffopts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
+	cl_git_pass(git_repository_config(&cfg, g_repo));
+
+	/* diff.renames = false; no rename detection should happen */
+	cl_git_pass(git_config_set_bool(cfg, "diff.renames", false));
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree0, tree1, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(4, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_ADDED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
+	git_diff_free(diff);
+
+	/* diff.renames = true; should act like -M */
+	cl_git_pass(git_config_set_bool(cfg, "diff.renames", true));
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree0, tree1, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(3, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_ADDED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_RENAMED]);
+	git_diff_free(diff);
+
+	/* diff.renames = copies; should act like -M -C */
+	cl_git_pass(git_config_set_string(cfg, "diff.renames", "copies"));
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree1, tree2, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(4, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_MODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_COPIED]);
+	git_diff_free(diff);
+
+	/* NULL find options is the same as GIT_DIFF_FIND_BY_CONFIG */
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree1, tree2, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, NULL));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(4, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_MODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_COPIED]);
+	git_diff_free(diff);
+
+	/* Cleanup */
+	git_tree_free(tree0);
+	git_tree_free(tree1);
+	git_tree_free(tree2);
+	git_config_free(cfg);
+}
+
+void test_diff_rename__can_override_thresholds_when_obeying_config(void)
+{
+	const char *sha1 = "2bc7f351d20b53f1c72c16c4b036e491c478c49a";
+	const char *sha2 = "1c068dee5790ef1580cfc4cd670915b48d790084";
+
+	git_tree *tree1, *tree2;
+	git_config *cfg;
+	git_diff *diff;
+	git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
+	git_diff_find_options opts = GIT_DIFF_FIND_OPTIONS_INIT;
+	diff_expects exp;
+
+	tree1 = resolve_commit_oid_to_tree(g_repo, sha1);
+	tree2 = resolve_commit_oid_to_tree(g_repo, sha2);
+
+	diffopts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
+	opts.flags = GIT_DIFF_FIND_BY_CONFIG;
+
+	cl_git_pass(git_repository_config(&cfg, g_repo));
+	cl_git_pass(git_config_set_string(cfg, "diff.renames", "copies"));
+	git_config_free(cfg);
+
+	/* copy threshold = 96%, should see creation of ikeepsix.txt */
+	opts.copy_threshold = 96;
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree1, tree2, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(4, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_MODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_ADDED]);
+	git_diff_free(diff);
+
+	/* copy threshold = 20%, should see sixserving.txt => ikeepsix.txt */
+	opts.copy_threshold = 20;
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree1, tree2, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(4, exp.files);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_UNMODIFIED]);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_MODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_COPIED]);
+	git_diff_free(diff);
+
+	/* Cleanup */
+	git_tree_free(tree1);
+	git_tree_free(tree2);
+}
+
+void test_diff_rename__by_config_doesnt_mess_with_whitespace_settings(void)
+{
+	const char *sha1 = "1c068dee5790ef1580cfc4cd670915b48d790084";
+	const char *sha2 = "19dd32dfb1520a64e5bbaae8dce6ef423dfa2f13";
+
+	git_tree *tree1, *tree2;
+	git_config *cfg;
+	git_diff *diff;
+	git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT;
+	git_diff_find_options opts = GIT_DIFF_FIND_OPTIONS_INIT;
+	diff_expects exp;
+
+	tree1 = resolve_commit_oid_to_tree(g_repo, sha1);
+	tree2 = resolve_commit_oid_to_tree(g_repo, sha2);
+
+	diffopts.flags |= GIT_DIFF_INCLUDE_UNMODIFIED;
+	opts.flags = GIT_DIFF_FIND_BY_CONFIG;
+
+	cl_git_pass(git_repository_config(&cfg, g_repo));
+	cl_git_pass(git_config_set_string(cfg, "diff.renames", "copies"));
+	git_config_free(cfg);
+
+	/* Don't ignore whitespace; this should find a change in sixserving.txt */
+	opts.flags |= 0 | GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE;
+	cl_git_pass(git_diff_tree_to_tree(
+				&diff, g_repo, tree1, tree2, &diffopts));
+	memset(&exp, 0, sizeof(exp));
+	cl_git_pass(git_diff_find_similar(diff, &opts));
+	cl_git_pass(git_diff_foreach(diff,
+				diff_file_cb, diff_hunk_cb, diff_line_cb, &exp));
+	cl_assert_equal_i(5, exp.files);
+	cl_assert_equal_i(2, exp.file_status[GIT_DELTA_MODIFIED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_RENAMED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_DELETED]);
+	cl_assert_equal_i(1, exp.file_status[GIT_DELTA_ADDED]);
+	git_diff_free(diff);
+
+	/* Cleanup */
+	git_tree_free(tree1);
+	git_tree_free(tree2);
+}