Commit 5a7d454bf657fdf4d726f2319bf554ffd4486ed3

David Turner 2018-06-04T12:56:08

Fix stash save bug with fast path index check If the index contains stat data for a modified file, and the file is not racily dirty, and there exists an untracked working tree directory alphabetically after that file, and there are no other changes to the repo, then git_stash_save would fail. It would confuse the untracked working tree directory for the modified file, because they have the same sha: zero. The wt directory has a sha of zero because it's a directory, and the file would have a zero sha because we wouldn't read the file -- we would just know that it doesn't match the index. To fix this confusion, we simply check mode as well as SHA.

diff --git a/src/diff_generate.c b/src/diff_generate.c
index e11cbe4..0105c17 100644
--- a/src/diff_generate.c
+++ b/src/diff_generate.c
@@ -273,7 +273,8 @@ static git_diff_delta *diff_delta__last_for_item(
 		break;
 	case GIT_DELTA_MODIFIED:
 		if (git_oid__cmp(&delta->old_file.id, &item->id) == 0 ||
-			git_oid__cmp(&delta->new_file.id, &item->id) == 0)
+		    (delta->new_file.mode == item->mode &&
+			git_oid__cmp(&delta->new_file.id, &item->id) == 0))
 			return delta;
 		break;
 	default:
diff --git a/tests/stash/save.c b/tests/stash/save.c
index edcee82..5c2d494 100644
--- a/tests/stash/save.c
+++ b/tests/stash/save.c
@@ -188,6 +188,46 @@ void test_stash_save__can_include_untracked_and_ignored_files(void)
 	cl_assert(!git_path_exists("stash/just.ignore"));
 }
 
+/*
+ * Note: this test was flaky prior to fixing #4101 -- run it several
+ * times to get a failure.  The issues is that whether the fast
+ * (stat-only) codepath is used inside stash's diff operation depends
+ * on whether files are "racily clean", and there doesn't seem to be
+ * an easy way to force the exact required state.
+ */
+void test_stash_save__untracked_regression(void)
+{
+	git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
+	const char *paths[] = {"what", "where", "how", "why"};
+	git_reference *head;
+	git_commit *head_commit;
+	git_buf untracked_dir;
+
+	const char* workdir = git_repository_workdir(repo);
+
+	git_buf_init(&untracked_dir, 0);
+	git_buf_printf(&untracked_dir, "%sz", workdir);
+
+	cl_assert(!p_mkdir(untracked_dir.ptr, 0777));
+
+	cl_git_pass(git_repository_head(&head, repo));
+
+	cl_git_pass(git_reference_peel((git_object **)&head_commit, head, GIT_OBJ_COMMIT));
+
+	opts.checkout_strategy = GIT_CHECKOUT_FORCE;
+
+	opts.paths.strings = (char **)paths;
+	opts.paths.count = 4;
+
+	cl_git_pass(git_checkout_tree(repo, (git_object*)head_commit, &opts));
+
+	cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT));
+
+	assert_commit_message_contains("refs/stash", "WIP on master");
+
+	git_buf_free(&untracked_dir);
+}
+
 #define MESSAGE "Look Ma! I'm on TV!"
 void test_stash_save__can_accept_a_message(void)
 {