Skip to content

Commit 5a7d454

Browse files
committed
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.
1 parent bae6ed6 commit 5a7d454

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

src/diff_generate.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ static git_diff_delta *diff_delta__last_for_item(
273273
break;
274274
case GIT_DELTA_MODIFIED:
275275
if (git_oid__cmp(&delta->old_file.id, &item->id) == 0 ||
276-
git_oid__cmp(&delta->new_file.id, &item->id) == 0)
276+
(delta->new_file.mode == item->mode &&
277+
git_oid__cmp(&delta->new_file.id, &item->id) == 0))
277278
return delta;
278279
break;
279280
default:

tests/stash/save.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,46 @@ void test_stash_save__can_include_untracked_and_ignored_files(void)
188188
cl_assert(!git_path_exists("stash/just.ignore"));
189189
}
190190

191+
/*
192+
* Note: this test was flaky prior to fixing #4101 -- run it several
193+
* times to get a failure. The issues is that whether the fast
194+
* (stat-only) codepath is used inside stash's diff operation depends
195+
* on whether files are "racily clean", and there doesn't seem to be
196+
* an easy way to force the exact required state.
197+
*/
198+
void test_stash_save__untracked_regression(void)
199+
{
200+
git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
201+
const char *paths[] = {"what", "where", "how", "why"};
202+
git_reference *head;
203+
git_commit *head_commit;
204+
git_buf untracked_dir;
205+
206+
const char* workdir = git_repository_workdir(repo);
207+
208+
git_buf_init(&untracked_dir, 0);
209+
git_buf_printf(&untracked_dir, "%sz", workdir);
210+
211+
cl_assert(!p_mkdir(untracked_dir.ptr, 0777));
212+
213+
cl_git_pass(git_repository_head(&head, repo));
214+
215+
cl_git_pass(git_reference_peel((git_object **)&head_commit, head, GIT_OBJ_COMMIT));
216+
217+
opts.checkout_strategy = GIT_CHECKOUT_FORCE;
218+
219+
opts.paths.strings = (char **)paths;
220+
opts.paths.count = 4;
221+
222+
cl_git_pass(git_checkout_tree(repo, (git_object*)head_commit, &opts));
223+
224+
cl_git_pass(git_stash_save(&stash_tip_oid, repo, signature, NULL, GIT_STASH_DEFAULT));
225+
226+
assert_commit_message_contains("refs/stash", "WIP on master");
227+
228+
git_buf_free(&untracked_dir);
229+
}
230+
191231
#define MESSAGE "Look Ma! I'm on TV!"
192232
void test_stash_save__can_accept_a_message(void)
193233
{

0 commit comments

Comments
 (0)