Skip to content

Commit cae5b73

Browse files
committed
Merge branch 'en/stash-apply-sparse-checkout' into next
"git stash" did not work well in a sparsely checked out working tree. * en/stash-apply-sparse-checkout: stash: fix stash application in sparse-checkouts stash: remove unnecessary process forking t7012: add a testcase demonstrating stash apply bugs in sparse checkouts
2 parents 45f1b43 + ba359fd commit cae5b73

File tree

3 files changed

+212
-57
lines changed

3 files changed

+212
-57
lines changed

builtin/stash.c

Lines changed: 116 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -325,35 +325,6 @@ static void add_diff_to_buf(struct diff_queue_struct *q,
325325
}
326326
}
327327

328-
static int get_newly_staged(struct strbuf *out, struct object_id *c_tree)
329-
{
330-
struct child_process cp = CHILD_PROCESS_INIT;
331-
const char *c_tree_hex = oid_to_hex(c_tree);
332-
333-
/*
334-
* diff-index is very similar to diff-tree above, and should be
335-
* converted together with update_index.
336-
*/
337-
cp.git_cmd = 1;
338-
strvec_pushl(&cp.args, "diff-index", "--cached", "--name-only",
339-
"--diff-filter=A", NULL);
340-
strvec_push(&cp.args, c_tree_hex);
341-
return pipe_command(&cp, NULL, 0, out, 0, NULL, 0);
342-
}
343-
344-
static int update_index(struct strbuf *out)
345-
{
346-
struct child_process cp = CHILD_PROCESS_INIT;
347-
348-
/*
349-
* Update-index is very complicated and may need to have a public
350-
* function exposed in order to remove this forking.
351-
*/
352-
cp.git_cmd = 1;
353-
strvec_pushl(&cp.args, "update-index", "--add", "--stdin", NULL);
354-
return pipe_command(&cp, out->buf, out->len, NULL, 0, NULL, 0);
355-
}
356-
357328
static int restore_untracked(struct object_id *u_tree)
358329
{
359330
int res;
@@ -385,6 +356,121 @@ static int restore_untracked(struct object_id *u_tree)
385356
return res;
386357
}
387358

359+
static void unstage_changes_unless_new(struct object_id *orig_tree)
360+
{
361+
/*
362+
* When we enter this function, there has been a clean merge of
363+
* relevant trees, and the merge logic always stages whatever merges
364+
* cleanly. We want to unstage those changes, unless it corresponds
365+
* to a file that didn't exist as of orig_tree.
366+
*
367+
* However, if any SKIP_WORKTREE path is modified relative to
368+
* orig_tree, then we want to clear the SKIP_WORKTREE bit and write
369+
* it to the worktree before unstaging.
370+
*/
371+
372+
struct checkout state = CHECKOUT_INIT;
373+
struct diff_options diff_opts;
374+
struct lock_file lock = LOCK_INIT;
375+
int i;
376+
377+
/* If any entries have skip_worktree set, we'll have to check 'em out */
378+
state.force = 1;
379+
state.quiet = 1;
380+
state.refresh_cache = 1;
381+
state.istate = &the_index;
382+
383+
/*
384+
* Step 1: get a difference between orig_tree (which corresponding
385+
* to the index before a merge was run) and the current index
386+
* (reflecting the changes brought in by the merge).
387+
*/
388+
diff_setup(&diff_opts);
389+
diff_opts.flags.recursive = 1;
390+
diff_opts.detect_rename = 0;
391+
diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT;
392+
diff_setup_done(&diff_opts);
393+
394+
do_diff_cache(orig_tree, &diff_opts);
395+
diffcore_std(&diff_opts);
396+
397+
/* Iterate over the paths that changed due to the merge... */
398+
for (i = 0; i < diff_queued_diff.nr; i++) {
399+
struct diff_filepair *p;
400+
struct cache_entry *ce;
401+
int pos;
402+
403+
/* Look up the path's position in the current index. */
404+
p = diff_queued_diff.queue[i];
405+
pos = index_name_pos(&the_index, p->two->path,
406+
strlen(p->two->path));
407+
408+
/*
409+
* Step 2: Place changes in the working tree
410+
*
411+
* Stash is about restoring changes *to the working tree*.
412+
* So if the merge successfully got a new version of some
413+
* path, but left it out of the working tree, then clear the
414+
* SKIP_WORKTREE bit and write it to the working tree.
415+
*/
416+
if (pos >= 0 && ce_skip_worktree(active_cache[pos])) {
417+
struct stat st;
418+
419+
ce = active_cache[pos];
420+
if (!lstat(ce->name, &st)) {
421+
/* Conflicting path present; relocate it */
422+
struct strbuf new_path = STRBUF_INIT;
423+
int fd;
424+
425+
strbuf_addf(&new_path,
426+
"%s.stash.XXXXXX", ce->name);
427+
fd = xmkstemp(new_path.buf);
428+
close(fd);
429+
printf(_("WARNING: Untracked file in way of "
430+
"tracked file! Renaming\n "
431+
" %s -> %s\n"
432+
" to make room.\n"),
433+
ce->name, new_path.buf);
434+
if (rename(ce->name, new_path.buf))
435+
die("Failed to move %s to %s\n",
436+
ce->name, new_path.buf);
437+
strbuf_release(&new_path);
438+
}
439+
checkout_entry(ce, &state, NULL, NULL);
440+
ce->ce_flags &= ~CE_SKIP_WORKTREE;
441+
}
442+
443+
/*
444+
* Step 3: "unstage" changes, as long as they are still tracked
445+
*/
446+
if (p->one->oid_valid) {
447+
/*
448+
* Path existed in orig_tree; restore index entry
449+
* from that tree in order to "unstage" the changes.
450+
*/
451+
int option = ADD_CACHE_OK_TO_REPLACE;
452+
if (pos < 0)
453+
option = ADD_CACHE_OK_TO_ADD;
454+
455+
ce = make_cache_entry(&the_index,
456+
p->one->mode,
457+
&p->one->oid,
458+
p->one->path,
459+
0, 0);
460+
add_index_entry(&the_index, ce, option);
461+
}
462+
}
463+
diff_flush(&diff_opts);
464+
465+
/*
466+
* Step 4: write the new index to disk
467+
*/
468+
repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR);
469+
if (write_locked_index(&the_index, &lock,
470+
COMMIT_LOCK | SKIP_IF_UNCHANGED))
471+
die(_("Unable to write index."));
472+
}
473+
388474
static int do_apply_stash(const char *prefix, struct stash_info *info,
389475
int index, int quiet)
390476
{
@@ -467,26 +553,7 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
467553
if (reset_tree(&index_tree, 0, 0))
468554
return -1;
469555
} else {
470-
struct strbuf out = STRBUF_INIT;
471-
472-
if (get_newly_staged(&out, &c_tree)) {
473-
strbuf_release(&out);
474-
return -1;
475-
}
476-
477-
if (reset_tree(&c_tree, 0, 1)) {
478-
strbuf_release(&out);
479-
return -1;
480-
}
481-
482-
ret = update_index(&out);
483-
strbuf_release(&out);
484-
if (ret)
485-
return -1;
486-
487-
/* read back the result of update_index() back from the disk */
488-
discard_cache();
489-
read_cache();
556+
unstage_changes_unless_new(&c_tree);
490557
}
491558

492559
if (!quiet) {

t/lib-submodule-update.sh

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,7 @@ test_submodule_switch_common () {
316316
command="$1"
317317
######################### Appearing submodule #########################
318318
# Switching to a commit letting a submodule appear creates empty dir ...
319-
if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1
320-
then
321-
# Restoring stash fails to restore submodule index entry
322-
RESULT="failure"
323-
else
324-
RESULT="success"
325-
fi
326-
test_expect_$RESULT "$command: added submodule creates empty directory" '
319+
test_expect_success "$command: added submodule creates empty directory" '
327320
prolog &&
328321
reset_work_tree_to no_submodule &&
329322
(
@@ -337,6 +330,13 @@ test_submodule_switch_common () {
337330
)
338331
'
339332
# ... and doesn't care if it already exists.
333+
if test "$KNOWN_FAILURE_STASH_DOES_IGNORE_SUBMODULE_CHANGES" = 1
334+
then
335+
# Restoring stash fails to restore submodule index entry
336+
RESULT="failure"
337+
else
338+
RESULT="success"
339+
fi
340340
test_expect_$RESULT "$command: added submodule leaves existing empty directory alone" '
341341
prolog &&
342342
reset_work_tree_to no_submodule &&

t/t7012-skip-worktree-writing.sh

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,94 @@ test_expect_success '--ignore-skip-worktree-entries leaves worktree alone' '
149149
--diff-filter=D -- keep-me.t
150150
'
151151

152+
test_expect_success 'stash restore in sparse checkout' '
153+
test_create_repo stash-restore &&
154+
(
155+
cd stash-restore &&
156+
157+
mkdir subdir &&
158+
echo A >subdir/A &&
159+
echo untouched >untouched &&
160+
echo removeme >removeme &&
161+
echo modified >modified &&
162+
git add . &&
163+
git commit -m Initial &&
164+
165+
echo AA >>subdir/A &&
166+
echo addme >addme &&
167+
echo tweaked >>modified &&
168+
rm removeme &&
169+
git add addme &&
170+
171+
git stash push &&
172+
173+
git sparse-checkout set subdir &&
174+
175+
# Ensure after sparse-checkout we only have expected files
176+
cat >expect <<-EOF &&
177+
S modified
178+
S removeme
179+
H subdir/A
180+
S untouched
181+
EOF
182+
git ls-files -t >actual &&
183+
test_cmp expect actual &&
184+
185+
test_path_is_missing addme &&
186+
test_path_is_missing modified &&
187+
test_path_is_missing removeme &&
188+
test_path_is_file subdir/A &&
189+
test_path_is_missing untouched &&
190+
191+
# Put a file in the working directory in the way
192+
echo in the way >modified &&
193+
git stash apply &&
194+
195+
# Ensure stash vivifies modifies paths...
196+
cat >expect <<-EOF &&
197+
H addme
198+
H modified
199+
H removeme
200+
H subdir/A
201+
S untouched
202+
EOF
203+
git ls-files -t >actual &&
204+
test_cmp expect actual &&
205+
206+
# ...and that the paths show up in status as changed...
207+
cat >expect <<-EOF &&
208+
A addme
209+
M modified
210+
D removeme
211+
M subdir/A
212+
?? actual
213+
?? expect
214+
?? modified.stash.XXXXXX
215+
EOF
216+
git status --porcelain | \
217+
sed -e s/stash......./stash.XXXXXX/ >actual &&
218+
test_cmp expect actual &&
219+
220+
# ...and that working directory reflects the files correctly
221+
test_path_is_file addme &&
222+
test_path_is_file modified &&
223+
test_path_is_missing removeme &&
224+
test_path_is_file subdir/A &&
225+
test_path_is_missing untouched &&
226+
227+
# ...including that we have the expected "modified" file...
228+
cat >expect <<-EOF &&
229+
modified
230+
tweaked
231+
EOF
232+
test_cmp expect modified &&
233+
234+
# ...and that the other "modified" file is still present...
235+
echo in the way >expect &&
236+
test_cmp expect modified.stash.*
237+
)
238+
'
239+
152240
#TODO test_expect_failure 'git-apply adds file' false
153241
#TODO test_expect_failure 'git-apply updates file' false
154242
#TODO test_expect_failure 'git-apply removes file' false

0 commit comments

Comments
 (0)