stash: Add --include-untracked option to stash and remove all untracked files

The --include-untracked option acts like the normal "git stash save" but also adds all untracked files in the working directory to the stash and then calls "git clean --force --quiet" to restore the working directory to a pristine state. This is useful for projects that need to run release scripts. With this option, the release scripts can be from the main working directory so one does not have to maintain a "clean" directory in parallel just for releasing. Basically the work-flow becomes: $ git tag release-1.0 $ git stash --include-untracked $ make release $ git clean -f $ git stash pop "git stash" alone is not enough in this case--it leaves untracked files lying around that might mess up a release process that expects everything to be very clean or might let a release succeed that should actually fail (due to a new source file being created that hasn't been committed yet). Signed-off-by: David Caldwell <david@porkrind.org> Signed-off-by: Junio C Hamano <gitster@pobox.com>

David Caldwell committed Jun 24, 2011 at 17:56 UTC 787513027a7d0af3c2cd2f04b85bc7136d580586
3 files changed +227 -6
Documentation/git-stash.txt
+8 -2
@@ -13,7 +13,8 @@ SYNOPSIS
13 'git stash' drop [-q|--quiet] [<stash>]
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] [<message>]]
16 +'git stash' [save [--patch] [-k|--[no-]keep-index] [-q|--quiet]
17 + [-u|--include-untracked] [-a|--all] [<message>]]
18 'git stash' clear
19 'git stash' create
20
@@ -42,7 +43,7 @@ is also possible).
43 OPTIONS
44 -------
45
45 -save [-p|--patch] [--[no-]keep-index] [-q|--quiet] [<message>]::
46 +save [-p|--patch] [--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
47
48 Save your local modifications to a new 'stash', and run `git reset
49 --hard` to revert them. The <message> part is optional and gives
@@ -54,6 +55,11 @@ save [-p|--patch] [--[no-]keep-index] [-q|--quiet] [<message>]::
55 If the `--keep-index` option is used, all changes already added to the
56 index are left intact.
57 +
58 +If the `--include-untracked` option is used, all untracked files are also
59 +stashed and then cleaned up with `git clean`, leaving the working directory
60 +in a very clean state. If the `--all` option is used instead then the
61 +ignored files are stashed and cleaned in addition to the untracked files.
62 ++
63 With `--patch`, you can interactively select hunks from the diff
64 between HEAD and the working tree to be stashed. The stash entry is
65 constructed such that its index state is the same as the index state
git-stash.sh
+64 -4
@@ -7,7 +7,8 @@ USAGE="list [<options>]
7 or: $dashless drop [-q|--quiet] [<stash>]
8 or: $dashless ( pop | apply ) [--index] [-q|--quiet] [<stash>]
9 or: $dashless branch <branchname> [<stash>]
10 - or: $dashless [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] [<message>]]
10 + or: $dashless [save [--patch] [-k|--[no-]keep-index] [-q|--quiet]
11 + [-u|--include-untracked] [-a|--all] [<message>]]
12 or: $dashless clear"
13
14 SUBDIRECTORY_OK=Yes
@@ -33,7 +34,14 @@ fi
34
35 no_changes () {
36 git diff-index --quiet --cached HEAD --ignore-submodules -- &&
36 - git diff-files --quiet --ignore-submodules
37 + git diff-files --quiet --ignore-submodules &&
38 + (test -z "$untracked" || test -z "$(untracked_files)")
39 +}
40 +
41 +untracked_files () {
42 + excl_opt=--exclude-standard
43 + test "$untracked" = "all" && excl_opt=
44 + git ls-files -o -z $excl_opt
45 }
46
47 clear_stash () {
@@ -49,6 +57,7 @@ clear_stash () {
57
58 create_stash () {
59 stash_msg="$1"
60 + untracked="$2"
61
62 git update-index -q --refresh
63 if no_changes
@@ -78,6 +87,25 @@ create_stash () {
87 git commit-tree $i_tree -p $b_commit) ||
88 die "Cannot save the current index state"
89
90 + if test -n "$untracked"
91 + then
92 + # Untracked files are stored by themselves in a parentless commit, for
93 + # ease of unpacking later.
94 + u_commit=$(
95 + untracked_files | (
96 + export GIT_INDEX_FILE="$TMPindex"
97 + rm -f "$TMPindex" &&
98 + git update-index -z --add --remove --stdin &&
99 + u_tree=$(git write-tree) &&
100 + printf 'untracked files on %s\n' "$msg" | git commit-tree $u_tree &&
101 + rm -f "$TMPindex"
102 + ) ) || die "Cannot save the untracked files"
103 +
104 + untracked_commit_option="-p $u_commit";
105 + else
106 + untracked_commit_option=
107 + fi
108 +
109 if test -z "$patch_mode"
110 then
111
@@ -122,13 +150,14 @@ create_stash () {
150 stash_msg=$(printf 'On %s: %s' "$branch" "$stash_msg")
151 fi
152 w_commit=$(printf '%s\n' "$stash_msg" |
125 - git commit-tree $w_tree -p $b_commit -p $i_commit) ||
153 + git commit-tree $w_tree -p $b_commit -p $i_commit $untracked_commit_option) ||
154 die "Cannot record working tree state"
155 }
156
157 save_stash () {
158 keep_index=
159 patch_mode=
160 + untracked=
161 while test $# != 0
162 do
163 case "$1" in
@@ -146,6 +175,12 @@ save_stash () {
175 -q|--quiet)
176 GIT_QUIET=t
177 ;;
178 + -u|--include-untracked)
179 + untracked=untracked
180 + ;;
181 + -a|--all)
182 + untracked=all
183 + ;;
184 --)
185 shift
186 break
@@ -162,6 +197,11 @@ save_stash () {
197 shift
198 done
199
200 + if test -n "$patch_mode" && test -n "$untracked"
201 + then
202 + die "Can't use --patch and ---include-untracked or --all at the same time"
203 + fi
204 +
205 stash_msg="$*"
206
207 git update-index -q --refresh
@@ -173,7 +213,7 @@ save_stash () {
213 test -f "$GIT_DIR/logs/$ref_stash" ||
214 clear_stash || die "Cannot initialize stash"
215
176 - create_stash "$stash_msg"
216 + create_stash "$stash_msg" $untracked
217
218 # Make sure the reflog for stash is kept.
219 : >>"$GIT_DIR/logs/$ref_stash"
@@ -185,6 +225,11 @@ save_stash () {
225 if test -z "$patch_mode"
226 then
227 git reset --hard ${GIT_QUIET:+-q}
228 + test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION=
229 + if test -n "$untracked"
230 + then
231 + git clean --force --quiet $CLEAN_X_OPTION
232 + fi
233
234 if test "$keep_index" = "t" && test -n $i_tree
235 then
@@ -234,9 +279,11 @@ show_stash () {
279 # w_commit is set to the commit containing the working tree
280 # b_commit is set to the base commit
281 # i_commit is set to the commit containing the index tree
282 +# u_commit is set to the commit containing the untracked files tree
283 # w_tree is set to the working tree
284 # b_tree is set to the base tree
285 # i_tree is set to the index tree
286 +# u_tree is set to the untracked files tree
287 #
288 # GIT_QUIET is set to t if -q is specified
289 # INDEX_OPTION is set to --index if --index is specified.
@@ -261,9 +308,11 @@ parse_flags_and_rev()
308 w_commit=
309 b_commit=
310 i_commit=
311 + u_commit=
312 w_tree=
313 b_tree=
314 i_tree=
315 + u_tree=
316
317 REV=$(git rev-parse --no-flags --symbolic "$@") || exit 1
318
@@ -311,6 +360,9 @@ parse_flags_and_rev()
360 IS_STASH_LIKE=t &&
361 test "$ref_stash" = "$(git rev-parse --symbolic-full-name "${REV%@*}")" &&
362 IS_STASH_REF=t
363 +
364 + u_commit=$(git rev-parse --quiet --verify $REV^3 2>/dev/null) &&
365 + u_tree=$(git rev-parse $REV^3: 2>/dev/null)
366 }
367
368 is_stash_like()
@@ -353,6 +405,14 @@ apply_stash () {
405 git reset
406 fi
407
408 + if test -n "$u_tree"
409 + then
410 + GIT_INDEX_FILE="$TMPindex" git-read-tree "$u_tree" &&
411 + GIT_INDEX_FILE="$TMPindex" git checkout-index --all &&
412 + rm -f "$TMPindex" ||
413 + die 'Could not restore untracked files from stash'
414 + fi
415 +
416 eval "
417 GITHEAD_$w_tree='Stashed changes' &&
418 GITHEAD_$c_tree='Updated upstream' &&
t/t3905-stash-include-untracked.sh new
+155
@@ -0,0 +1,155 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2011 David Caldwell
4 +#
5 +
6 +test_description='Test git stash --include-untracked'
7 +
8 +. ./test-lib.sh
9 +
10 +test_expect_success 'stash save --include-untracked some dirty working directory' '
11 + echo 1 > file &&
12 + git add file &&
13 + test_tick &&
14 + git commit -m initial &&
15 + echo 2 > file &&
16 + git add file &&
17 + echo 3 > file &&
18 + test_tick &&
19 + echo 1 > file2 &&
20 + git stash --include-untracked &&
21 + git diff-files --quiet &&
22 + git diff-index --cached --quiet HEAD
23 +'
24 +
25 +cat > expect <<EOF
26 +?? expect
27 +?? output
28 +EOF
29 +
30 +test_expect_success 'stash save --include-untracked cleaned the untracked files' '
31 + git status --porcelain > output
32 + test_cmp output expect
33 +'
34 +
35 +cat > expect.diff <<EOF
36 +diff --git a/file2 b/file2
37 +new file mode 100644
38 +index 0000000..d00491f
39 +--- /dev/null
40 ++++ b/file2
41 +@@ -0,0 +1 @@
42 ++1
43 +EOF
44 +cat > expect.lstree <<EOF
45 +file2
46 +EOF
47 +
48 +test_expect_success 'stash save --include-untracked stashed the untracked files' '
49 + test "!" -f file2 &&
50 + git diff HEAD..stash^3 -- file2 > output &&
51 + test_cmp output expect.diff &&
52 + git ls-tree --name-only stash^3: > output &&
53 + test_cmp output expect.lstree
54 +'
55 +test_expect_success 'stash save --patch --include-untracked fails' '
56 + test_must_fail git stash --patch --include-untracked
57 +'
58 +
59 +test_expect_success 'stash save --patch --all fails' '
60 + test_must_fail git stash --patch --all
61 +'
62 +
63 +git clean --force --quiet
64 +
65 +cat > expect <<EOF
66 + M file
67 +?? expect
68 +?? file2
69 +?? output
70 +EOF
71 +
72 +test_expect_success 'stash pop after save --include-untracked leaves files untracked again' '
73 + git stash pop &&
74 + git status --porcelain > output
75 + test_cmp output expect
76 +'
77 +
78 +git clean --force --quiet
79 +
80 +test_expect_success 'stash save -u dirty index' '
81 + echo 4 > file3 &&
82 + git add file3 &&
83 + test_tick &&
84 + git stash -u
85 +'
86 +
87 +cat > expect <<EOF
88 +diff --git a/file3 b/file3
89 +new file mode 100644
90 +index 0000000..b8626c4
91 +--- /dev/null
92 ++++ b/file3
93 +@@ -0,0 +1 @@
94 ++4
95 +EOF
96 +
97 +test_expect_success 'stash save --include-untracked dirty index got stashed' '
98 + git stash pop --index &&
99 + git diff --cached > output &&
100 + test_cmp output expect
101 +'
102 +
103 +git reset > /dev/null
104 +
105 +test_expect_success 'stash save --include-untracked -q is quiet' '
106 + echo 1 > file5 &&
107 + git stash save --include-untracked --quiet > output.out 2>&1 &&
108 + test ! -s output.out
109 +'
110 +
111 +test_expect_success 'stash save --include-untracked removed files' '
112 + rm -f file &&
113 + git stash save --include-untracked &&
114 + echo 1 > expect &&
115 + test_cmp file expect
116 +'
117 +
118 +rm -f expect
119 +
120 +test_expect_success 'stash save --include-untracked removed files got stashed' '
121 + git stash pop &&
122 + test ! -f file
123 +'
124 +
125 +cat > .gitignore <<EOF
126 +.gitignore
127 +ignored
128 +EOF
129 +
130 +test_expect_success 'stash save --include-untracked respects .gitignore' '
131 + echo ignored > ignored &&
132 + git stash -u &&
133 + test -s ignored &&
134 + test -s .gitignore
135 +'
136 +
137 +test_expect_success 'stash save -u can stash with only untracked files different' '
138 + echo 4 > file4 &&
139 + git stash -u
140 + test "!" -f file4
141 +'
142 +
143 +test_expect_success 'stash save --all does not respect .gitignore' '
144 + git stash -a &&
145 + test "!" -f ignored &&
146 + test "!" -f .gitignore
147 +'
148 +
149 +test_expect_success 'stash save --all is stash poppable' '
150 + git stash pop &&
151 + test -s ignored &&
152 + test -s .gitignore
153 +'
154 +
155 +test_done