Commit e38f0d69aba011d02ba5cabc648fee6c6f4dcc29

Edward Thomson 2013-08-05T14:06:41

Add rename from rewrites to status In git_diff_paired_foreach, temporarily resort the index->workdir diff list by index path so that we can track a rename in the workdir from head->index->workdir.

diff --git a/include/git2/status.h b/include/git2/status.h
index 2f7c067..aa934d9 100644
--- a/include/git2/status.h
+++ b/include/git2/status.h
@@ -107,7 +107,7 @@ typedef enum {
  * - GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX indicates that rename detection
  *   should be processed between the head and the index and enables
  *   the GIT_STATUS_INDEX_RENAMED as a possible status flag.
- * - GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR indicates tha rename
+ * - GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR indicates that rename
  *   detection should be run between the index and the working directory
  *   and enabled GIT_STATUS_WT_RENAMED as a possible status flag.
  * - GIT_STATUS_OPT_SORT_CASE_SENSITIVELY overrides the native case
@@ -116,6 +116,8 @@ typedef enum {
  * - GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY overrides the native case
  *   sensitivity for the file system and forces the output to be in
  *   case-insensitive order
+ * - GIT_STATUS_OPT_RENAMES_FROM_REWRITES indicates that rename detection
+ *   should include rewritten files
  *
  * Calling `git_status_foreach()` is like calling the extended version
  * with: GIT_STATUS_OPT_INCLUDE_IGNORED, GIT_STATUS_OPT_INCLUDE_UNTRACKED,
@@ -134,6 +136,7 @@ typedef enum {
 	GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR = (1u << 8),
 	GIT_STATUS_OPT_SORT_CASE_SENSITIVELY    = (1u << 9),
 	GIT_STATUS_OPT_SORT_CASE_INSENSITIVELY  = (1u << 10),
+	GIT_STATUS_OPT_RENAMES_FROM_REWRITES    = (1u << 11),
 } git_status_opt_t;
 
 #define GIT_STATUS_OPT_DEFAULTS \
diff --git a/src/diff.c b/src/diff.c
index e875d09..77dbbd8 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -258,6 +258,26 @@ int git_diff_delta__casecmp(const void *a, const void *b)
 	return val ? val : ((int)da->status - (int)db->status);
 }
 
+GIT_INLINE(const char *) diff_delta__i2w_path(const git_diff_delta *delta)
+{
+	return delta->old_file.path ?
+		delta->old_file.path : delta->new_file.path;
+}
+
+int git_diff_delta__i2w_cmp(const void *a, const void *b)
+{
+	const git_diff_delta *da = a, *db = b;
+	int val = strcmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db));
+	return val ? val : ((int)da->status - (int)db->status);
+}
+
+int git_diff_delta__i2w_casecmp(const void *a, const void *b)
+{
+	const git_diff_delta *da = a, *db = b;
+	int val = strcasecmp(diff_delta__i2w_path(da), diff_delta__i2w_path(db));
+	return val ? val : ((int)da->status - (int)db->status);
+}
+
 bool git_diff_delta__should_skip(
 	const git_diff_options *opts, const git_diff_delta *delta)
 {
@@ -1276,7 +1296,7 @@ int git_diff__paired_foreach(
 	git_diff_delta *h2i, *i2w;
 	size_t i, j, i_max, j_max;
 	int (*strcomp)(const char *, const char *) = git__strcmp;
-	bool icase_mismatch;
+	bool h2i_icase, i2w_icase, icase_mismatch;
 
 	i_max = head2idx ? head2idx->deltas.length : 0;
 	j_max = idx2wd ? idx2wd->deltas.length : 0;
@@ -1291,24 +1311,35 @@ int git_diff__paired_foreach(
 	 * Therefore the main thing we need to do here is make sure the diffs
 	 * are traversed in a compatible order.  To do this, we temporarily
 	 * resort a mismatched diff to get the order correct.
+	 *
+	 * In order to traverse renames in the index->workdir, we need to
+	 * ensure that we compare the index name on both sides, so we
+	 * always sort by the old name in the i2w list.
 	 */
+	h2i_icase = head2idx != NULL &&
+		(head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0;
+
+	i2w_icase = idx2wd != NULL &&
+		(idx2wd->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0;
+
 	icase_mismatch =
-		(head2idx != NULL && idx2wd != NULL &&
-		 ((head2idx->opts.flags ^ idx2wd->opts.flags) & GIT_DIFF_DELTAS_ARE_ICASE));
-
-	/* force case-sensitive delta sort */
-	if (icase_mismatch) {
-		if (head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) {
-			git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp);
-			git_vector_sort(&head2idx->deltas);
-		} else {
-			git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__cmp);
-			git_vector_sort(&idx2wd->deltas);
-		}
+		(head2idx != NULL && idx2wd != NULL && h2i_icase != i2w_icase);
+
+	if (icase_mismatch && h2i_icase) {
+		git_vector_set_cmp(&head2idx->deltas, git_diff_delta__cmp);
+		git_vector_sort(&head2idx->deltas);
 	}
-	else if (head2idx != NULL && head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE)
+
+	if (i2w_icase && !icase_mismatch) {
 		strcomp = git__strcasecmp;
 
+		git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_casecmp);
+		git_vector_sort(&idx2wd->deltas);
+	} else if (idx2wd != NULL) {
+		git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__i2w_cmp);
+		git_vector_sort(&idx2wd->deltas);
+	}
+
 	for (i = 0, j = 0; i < i_max || j < j_max; ) {
 		h2i = head2idx ? GIT_VECTOR_GET(&head2idx->deltas, i) : NULL;
 		i2w = idx2wd ? GIT_VECTOR_GET(&idx2wd->deltas, j) : NULL;
@@ -1332,14 +1363,16 @@ int git_diff__paired_foreach(
 	}
 
 	/* restore case-insensitive delta sort */
-	if (icase_mismatch) {
-		if (head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) {
-			git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp);
-			git_vector_sort(&head2idx->deltas);
-		} else {
-			git_vector_set_cmp(&idx2wd->deltas, git_diff_delta__casecmp);
-			git_vector_sort(&idx2wd->deltas);
-		}
+	if (icase_mismatch && h2i_icase) {
+		git_vector_set_cmp(&head2idx->deltas, git_diff_delta__casecmp);
+		git_vector_sort(&head2idx->deltas);
+	}
+
+	/* restore idx2wd sort by new path */
+	if (idx2wd != NULL) {
+		git_vector_set_cmp(&idx2wd->deltas,
+			i2w_icase ? git_diff_delta__casecmp : git_diff_delta__cmp);
+		git_vector_sort(&idx2wd->deltas);
 	}
 
 	return 0;
diff --git a/src/status.c b/src/status.c
index ccb8d37..b235325 100644
--- a/src/status.c
+++ b/src/status.c
@@ -225,24 +225,6 @@ static git_status_list *git_status_list_alloc(git_index *index)
 	return status;
 }
 
-/*
-static int newfile_cmp(const void *a, const void *b)
-{
-	const git_diff_delta *delta_a = a;
-	const git_diff_delta *delta_b = b;
-
-	return git__strcmp(delta_a->new_file.path, delta_b->new_file.path);
-}
-
-static int newfile_casecmp(const void *a, const void *b)
-{
-	const git_diff_delta *delta_a = a;
-	const git_diff_delta *delta_b = b;
-
-	return git__strcasecmp(delta_a->new_file.path, delta_b->new_file.path);
-}
-*/
-
 int git_status_list_new(
 	git_status_list **out,
 	git_repository *repo,
@@ -251,7 +233,7 @@ int git_status_list_new(
 	git_index *index = NULL;
 	git_status_list *status = NULL;
 	git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT;
-	git_diff_find_options findopts_i2w = GIT_DIFF_FIND_OPTIONS_INIT;
+	git_diff_find_options findopt = GIT_DIFF_FIND_OPTIONS_INIT;
 	git_tree *head = NULL;
 	git_status_show_t show =
 		opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
@@ -284,6 +266,7 @@ int git_status_list_new(
 	}
 
 	diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
+	findopt.flags = GIT_DIFF_FIND_FOR_UNTRACKED;
 
 	if ((flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0)
 		diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
@@ -300,7 +283,9 @@ int git_status_list_new(
 	if ((flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
 		diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES;
 
-	findopts_i2w.flags |= GIT_DIFF_FIND_FOR_UNTRACKED;
+	if ((flags & GIT_STATUS_OPT_RENAMES_FROM_REWRITES) != 0)
+		findopt.flags = findopt.flags | GIT_DIFF_FIND_AND_BREAK_REWRITES |
+			GIT_DIFF_FIND_RENAMES_FROM_REWRITES;
 
 	if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
 		if ((error = git_diff_tree_to_index(
@@ -308,7 +293,7 @@ int git_status_list_new(
 			goto done;
 
 		if ((flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
-			(error = git_diff_find_similar(status->head2idx, NULL)) < 0)
+			(error = git_diff_find_similar(status->head2idx, &findopt)) < 0)
 			goto done;
 	}
 
@@ -318,7 +303,7 @@ int git_status_list_new(
 			goto done;
 
 		if ((flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
-			(error = git_diff_find_similar(status->idx2wd, &findopts_i2w)) < 0)
+			(error = git_diff_find_similar(status->idx2wd, &findopt)) < 0)
 			goto done;
 	}
 
diff --git a/tests-clar/status/renames.c b/tests-clar/status/renames.c
index 80ff260..836e65c 100644
--- a/tests-clar/status/renames.c
+++ b/tests-clar/status/renames.c
@@ -153,6 +153,65 @@ void test_status_renames__head2index_two(void)
 	git_index_free(index);
 }
 
+void test_status_renames__head2index_no_rename_from_rewrite(void)
+{
+	git_index *index;
+	git_status_list *statuslist;
+	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
+	struct status_entry expected[] = {
+		{ GIT_STATUS_INDEX_MODIFIED, "ikeepsix.txt", "ikeepsix.txt" },
+		{ GIT_STATUS_INDEX_MODIFIED, "sixserving.txt", "sixserving.txt" },
+	};
+
+	opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
+
+	cl_git_pass(git_repository_index(&index, g_repo));
+
+	rename_file(g_repo, "ikeepsix.txt", "_temp_.txt");
+	rename_file(g_repo, "sixserving.txt", "ikeepsix.txt");
+	rename_file(g_repo, "_temp_.txt", "sixserving.txt");
+
+	cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt"));
+	cl_git_pass(git_index_add_bypath(index, "sixserving.txt"));
+	cl_git_pass(git_index_write(index));
+
+	cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
+	test_status(statuslist, expected, 2);
+	git_status_list_free(statuslist);
+
+	git_index_free(index);
+}
+
+void test_status_renames__head2index_rename_from_rewrite(void)
+{
+	git_index *index;
+	git_status_list *statuslist;
+	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
+	struct status_entry expected[] = {
+		{ GIT_STATUS_INDEX_RENAMED, "sixserving.txt", "ikeepsix.txt" },
+		{ GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "sixserving.txt" },
+	};
+
+	opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
+	opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES;
+
+	cl_git_pass(git_repository_index(&index, g_repo));
+
+	rename_file(g_repo, "ikeepsix.txt", "_temp_.txt");
+	rename_file(g_repo, "sixserving.txt", "ikeepsix.txt");
+	rename_file(g_repo, "_temp_.txt", "sixserving.txt");
+
+	cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt"));
+	cl_git_pass(git_index_add_bypath(index, "sixserving.txt"));
+	cl_git_pass(git_index_write(index));
+
+	cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
+	test_status(statuslist, expected, 2);
+	git_status_list_free(statuslist);
+
+	git_index_free(index);
+}
+
 void test_status_renames__index2workdir_one(void)
 {
 	git_status_list *statuslist;
@@ -197,6 +256,32 @@ void test_status_renames__index2workdir_two(void)
 	git_status_list_free(statuslist);
 }
 
+void test_status_renames__index2workdir_rename_from_rewrite(void)
+{
+	git_index *index;
+	git_status_list *statuslist;
+	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
+	struct status_entry expected[] = {
+		{ GIT_STATUS_WT_RENAMED, "sixserving.txt", "ikeepsix.txt" },
+		{ GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "sixserving.txt" },
+	};
+
+	opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
+	opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES;
+
+	cl_git_pass(git_repository_index(&index, g_repo));
+
+	rename_file(g_repo, "ikeepsix.txt", "_temp_.txt");
+	rename_file(g_repo, "sixserving.txt", "ikeepsix.txt");
+	rename_file(g_repo, "_temp_.txt", "sixserving.txt");
+
+	cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
+	test_status(statuslist, expected, 2);
+	git_status_list_free(statuslist);
+
+	git_index_free(index);
+}
+
 void test_status_renames__both_one(void)
 {
 	git_index *index;
@@ -274,6 +359,50 @@ void test_status_renames__both_two(void)
 	git_index_free(index);
 }
 
+
+void test_status_renames__both_rename_from_rewrite(void)
+{
+	git_index *index;
+	git_status_list *statuslist;
+	git_status_options opts = GIT_STATUS_OPTIONS_INIT;
+	struct status_entry expected[] = {
+		{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED,
+		  "songof7cities.txt", "ikeepsix.txt" },
+		{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED,
+		  "ikeepsix.txt", "sixserving.txt" },
+		{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED,
+		  "sixserving.txt", "songof7cities.txt" },
+	};
+
+	opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
+	opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
+	opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
+	opts.flags |= GIT_STATUS_OPT_RENAMES_FROM_REWRITES;
+
+	cl_git_pass(git_repository_index(&index, g_repo));
+
+	rename_file(g_repo, "ikeepsix.txt", "_temp_.txt");
+	rename_file(g_repo, "sixserving.txt", "ikeepsix.txt");
+	rename_file(g_repo, "songof7cities.txt", "sixserving.txt");
+	rename_file(g_repo, "_temp_.txt", "songof7cities.txt");
+
+	cl_git_pass(git_index_add_bypath(index, "ikeepsix.txt"));
+	cl_git_pass(git_index_add_bypath(index, "sixserving.txt"));
+	cl_git_pass(git_index_add_bypath(index, "songof7cities.txt"));
+	cl_git_pass(git_index_write(index));
+
+	rename_file(g_repo, "songof7cities.txt", "_temp_.txt");
+	rename_file(g_repo, "ikeepsix.txt", "songof7cities.txt");
+	rename_file(g_repo, "sixserving.txt", "ikeepsix.txt");
+	rename_file(g_repo, "_temp_.txt", "sixserving.txt");
+
+	cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
+	test_status(statuslist, expected, 3);
+	git_status_list_free(statuslist);
+
+	git_index_free(index);
+}
+
 void test_status_renames__both_casechange_one(void)
 {
 	git_index *index;