git stash: avoid data loss when "git stash save" kills a directory

"stash save" is about saving the local change to the working tree, but also about restoring the state of the last commit to the working tree. When a local change is to turn a non-directory to a directory, in order to restore the non-directory, everything in the directory needs to be removed. Which is fine when running "git stash save --include-untracked", but without that option, untracked, newly created files in the directory will have to be discarded, if the state you are restoring to has a non-directory at the same path as the directory. Introduce a safety valve to fail the operation in such case, using the "ls-files --killed" which was designed for this exact purpose. The "stash save" is stopped when untracked files need to be discarded because their leading path ceased to be a directory, and the user is required to pass --force to really have the data removed. Signed-off-by: Petr Baudis <pasky@ucw.cz> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Petr Baudis committed Jun 28, 2013 at 17:05 UTC a73653130edd6a8977106d45a8092c09040f9132
3 files changed +40 -2
Documentation/git-stash.txt
+10 -2
@@ -14,7 +14,8 @@ SYNOPSIS
14 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
15 'git stash' branch <branchname> [<stash>]
16 'git stash' [save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
17 - [-u|--include-untracked] [-a|--all] [<message>]]
17 + [-u|--include-untracked] [-a|--all] [-f|--force]
18 + [<message>]]
19 'git stash' clear
20 'git stash' create [<message>]
21 'git stash' store [-m|--message <message>] [-q|--quiet] <commit>
@@ -44,7 +45,7 @@ is also possible).
45 OPTIONS
46 -------
47
47 -save [-p|--patch] [--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
48 +save [-p|--patch] [--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-f|--force] [<message>]::
49
50 Save your local modifications to a new 'stash', and run `git reset
51 --hard` to revert them. The <message> part is optional and gives
@@ -71,6 +72,13 @@ linkgit:git-add[1] to learn how to operate the `--patch` mode.
72 +
73 The `--patch` option implies `--keep-index`. You can use
74 `--no-keep-index` to override this.
75 ++
76 +In some cases, saving a stash could mean irretrievably removing some
77 +data - if a directory with untracked files replaces a tracked file of
78 +the same name, the new untracked files are not saved (except in case
79 +of `--include-untracked`) but the original tracked file shall be restored.
80 +By default, `stash save` will abort in such a case; `--force` will allow
81 +it to remove the untracked files.
82
83 list [<options>]::
84
git-stash.sh
+12
@@ -195,6 +195,7 @@ save_stash () {
195 keep_index=
196 patch_mode=
197 untracked=
198 + force=
199 while test $# != 0
200 do
201 case "$1" in
@@ -215,6 +216,9 @@ save_stash () {
216 -u|--include-untracked)
217 untracked=untracked
218 ;;
219 + -f|--force)
220 + force=t
221 + ;;
222 -a|--all)
223 untracked=all
224 ;;
@@ -258,6 +262,14 @@ save_stash () {
262 say "$(gettext "No local changes to save")"
263 exit 0
264 fi
265 + if test -z "$untracked$force" &&
266 + test -n "$(git ls-files --killed | head -n 1)"
267 + then
268 + say "$(gettext "The following untracked files would NOT be saved but need to be removed by stash save:")"
269 + test -n "$GIT_QUIET" || git ls-files --killed | sed 's/^/\t/'
270 + say "$(gettext "Aborting. Consider using either the --force or --include-untracked option.")" >&2
271 + exit 1
272 + fi
273 test -f "$GIT_DIR/logs/$ref_stash" ||
274 clear_stash || die "$(gettext "Cannot initialize stash")"
275
t/t3903-stash.sh
+18
@@ -673,4 +673,22 @@ test_expect_success 'store updates stash ref and reflog' '
673 grep quux bazzy
674 '
675
676 +test_expect_success 'stash a change to turn a non-directory to a directory' '
677 + git reset --hard &&
678 + >testfile &&
679 + git add testfile &&
680 + git commit -m "add testfile as a regular file" &&
681 + rm testfile &&
682 + mkdir testfile &&
683 + >testfile/file &&
684 + test_must_fail git stash save "recover regular file" &&
685 + test -f testfile/file
686 +'
687 +
688 +test_expect_success 'stash a change to turn a non-directory to a directory (forced)' '
689 + git stash save --force "recover regular file (forced)" &&
690 + ! test -f testfile/file &&
691 + test -f testfile
692 +'
693 +
694 test_done