stash: teach 'push' (and 'create_stash') to honor pathspec

While working on a repository, it's often helpful to stash the changes of a single or multiple files, and leave others alone. Unfortunately git currently offers no such option. git stash -p can be used to work around this, but it's often impractical when there are a lot of changes over multiple files. Allow 'git stash push' to take pathspec to specify which paths to stash. Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Thomas Gummerer <t.gummerer@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Thomas Gummerer committed Feb 28, 2017 at 20:33 UTC df6bba0937209d679a06addd26975593fef744f2
4 files changed +153 -12
Documentation/git-stash.txt
+8 -1
@@ -17,6 +17,7 @@ SYNOPSIS
17 [-u|--include-untracked] [-a|--all] [<message>]]
18 'git stash' push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
19 [-u|--include-untracked] [-a|--all] [-m|--message <message>]]
20 + [--] [<pathspec>...]
21 'git stash' clear
22 'git stash' create [<message>]
23 'git stash' store [-m|--message <message>] [-q|--quiet] <commit>
@@ -48,7 +49,7 @@ OPTIONS
49 -------
50
51 save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
51 -push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>]::
52 +push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--] [<pathspec>...]::
53
54 Save your local modifications to a new 'stash' and roll them
55 back to HEAD (in the working tree and in the index).
@@ -58,6 +59,12 @@ push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q
59 only <message> does not trigger this action to prevent a misspelled
60 subcommand from making an unwanted stash.
61 +
62 +When pathspec is given to 'git stash push', the new stash records the
63 +modified states only for the files that match the pathspec. The index
64 +entries and working tree files are then rolled back to the state in
65 +HEAD only for these files, too, leaving files that do not match the
66 +pathspec intact.
67 ++
68 If the `--keep-index` option is used, all changes already added to the
69 index are left intact.
70 +
git-stash.sh
+27 -11
@@ -11,6 +11,7 @@ USAGE="list [<options>]
11 [-u|--include-untracked] [-a|--all] [<message>]]
12 or: $dashless push [--patch] [-k|--[no-]keep-index] [-q|--quiet]
13 [-u|--include-untracked] [-a|--all] [-m <message>]
14 + [-- <pathspec>...]
15 or: $dashless clear"
16
17 SUBDIRECTORY_OK=Yes
@@ -35,15 +36,15 @@ else
36 fi
37
38 no_changes () {
38 - git diff-index --quiet --cached HEAD --ignore-submodules -- &&
39 - git diff-files --quiet --ignore-submodules &&
39 + git diff-index --quiet --cached HEAD --ignore-submodules -- "$@" &&
40 + git diff-files --quiet --ignore-submodules -- "$@" &&
41 (test -z "$untracked" || test -z "$(untracked_files)")
42 }
43
44 untracked_files () {
45 excl_opt=--exclude-standard
46 test "$untracked" = "all" && excl_opt=
46 - git ls-files -o -z $excl_opt
47 + git ls-files -o -z $excl_opt -- "$@"
48 }
49
50 clear_stash () {
@@ -71,12 +72,16 @@ create_stash () {
72 shift
73 untracked=${1?"BUG: create_stash () -u requires an argument"}
74 ;;
75 + --)
76 + shift
77 + break
78 + ;;
79 esac
80 shift
81 done
82
83 git update-index -q --refresh
79 - if no_changes
84 + if no_changes "$@"
85 then
86 exit 0
87 fi
@@ -108,7 +113,7 @@ create_stash () {
113 # Untracked files are stored by themselves in a parentless commit, for
114 # ease of unpacking later.
115 u_commit=$(
111 - untracked_files | (
116 + untracked_files "$@" | (
117 GIT_INDEX_FILE="$TMPindex" &&
118 export GIT_INDEX_FILE &&
119 rm -f "$TMPindex" &&
@@ -131,7 +136,7 @@ create_stash () {
136 git read-tree --index-output="$TMPindex" -m $i_tree &&
137 GIT_INDEX_FILE="$TMPindex" &&
138 export GIT_INDEX_FILE &&
134 - git diff-index --name-only -z HEAD -- >"$TMP-stagenames" &&
139 + git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" &&
140 git update-index -z --add --remove --stdin <"$TMP-stagenames" &&
141 git write-tree &&
142 rm -f "$TMPindex"
@@ -145,7 +150,7 @@ create_stash () {
150
151 # find out what the user wants
152 GIT_INDEX_FILE="$TMP-index" \
148 - git add--interactive --patch=stash -- &&
153 + git add--interactive --patch=stash -- "$@" &&
154
155 # state of the working tree
156 w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
@@ -273,27 +278,38 @@ push_stash () {
278 die "$(gettext "Can't use --patch and --include-untracked or --all at the same time")"
279 fi
280
281 + test -n "$untracked" || git ls-files --error-unmatch -- "$@" >/dev/null || exit 1
282 +
283 git update-index -q --refresh
277 - if no_changes
284 + if no_changes "$@"
285 then
286 say "$(gettext "No local changes to save")"
287 exit 0
288 fi
289 +
290 git reflog exists $ref_stash ||
291 clear_stash || die "$(gettext "Cannot initialize stash")"
292
285 - create_stash -m "$stash_msg" -u "$untracked"
293 + create_stash -m "$stash_msg" -u "$untracked" -- "$@"
294 store_stash -m "$stash_msg" -q $w_commit ||
295 die "$(gettext "Cannot save the current status")"
296 say "$(eval_gettext "Saved working directory and index state \$stash_msg")"
297
298 if test -z "$patch_mode"
299 then
292 - git reset --hard ${GIT_QUIET:+-q}
300 + if test $# != 0
301 + then
302 + git reset ${GIT_QUIET:+-q} -- "$@"
303 + git ls-files -z --modified -- "$@" |
304 + git checkout-index -z --force --stdin
305 + git clean --force ${GIT_QUIET:+-q} -d -- "$@"
306 + else
307 + git reset --hard ${GIT_QUIET:+-q}
308 + fi
309 test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION=
310 if test -n "$untracked"
311 then
296 - git clean --force --quiet -d $CLEAN_X_OPTION
312 + git clean --force --quiet -d $CLEAN_X_OPTION -- "$@"
313 fi
314
315 if test "$keep_index" = "t" && test -n "$i_tree"
t/t3903-stash.sh
+92
@@ -802,4 +802,96 @@ test_expect_success 'create with multiple arguments for the message' '
802 test_cmp expect actual
803 '
804
805 +test_expect_success 'stash -- <pathspec> stashes and restores the file' '
806 + >foo &&
807 + >bar &&
808 + git add foo bar &&
809 + git stash push -- foo &&
810 + test_path_is_file bar &&
811 + test_path_is_missing foo &&
812 + git stash pop &&
813 + test_path_is_file foo &&
814 + test_path_is_file bar
815 +'
816 +
817 +test_expect_success 'stash with multiple pathspec arguments' '
818 + >foo &&
819 + >bar &&
820 + >extra &&
821 + git add foo bar extra &&
822 + git stash push -- foo bar &&
823 + test_path_is_missing bar &&
824 + test_path_is_missing foo &&
825 + test_path_is_file extra &&
826 + git stash pop &&
827 + test_path_is_file foo &&
828 + test_path_is_file bar &&
829 + test_path_is_file extra
830 +'
831 +
832 +test_expect_success 'stash with file including $IFS character' '
833 + >"foo bar" &&
834 + >foo &&
835 + >bar &&
836 + git add foo* &&
837 + git stash push -- "foo b*" &&
838 + test_path_is_missing "foo bar" &&
839 + test_path_is_file foo &&
840 + test_path_is_file bar &&
841 + git stash pop &&
842 + test_path_is_file "foo bar" &&
843 + test_path_is_file foo &&
844 + test_path_is_file bar
845 +'
846 +
847 +test_expect_success 'stash with pathspec matching multiple paths' '
848 + echo original >file &&
849 + echo original >other-file &&
850 + git commit -m "two" file other-file &&
851 + echo modified >file &&
852 + echo modified >other-file &&
853 + git stash push -- "*file" &&
854 + echo original >expect &&
855 + test_cmp expect file &&
856 + test_cmp expect other-file &&
857 + git stash pop &&
858 + echo modified >expect &&
859 + test_cmp expect file &&
860 + test_cmp expect other-file
861 +'
862 +
863 +test_expect_success 'stash push -p with pathspec shows no changes only once' '
864 + >foo &&
865 + git add foo &&
866 + git commit -m "tmp" &&
867 + git stash push -p foo >actual &&
868 + echo "No local changes to save" >expect &&
869 + git reset --hard HEAD~ &&
870 + test_cmp expect actual
871 +'
872 +
873 +test_expect_success 'stash push with pathspec shows no changes when there are none' '
874 + >foo &&
875 + git add foo &&
876 + git commit -m "tmp" &&
877 + git stash push foo >actual &&
878 + echo "No local changes to save" >expect &&
879 + git reset --hard HEAD~ &&
880 + test_cmp expect actual
881 +'
882 +
883 +test_expect_success 'stash push with pathspec not in the repository errors out' '
884 + >untracked &&
885 + test_must_fail git stash push untracked &&
886 + test_path_is_file untracked
887 +'
888 +
889 +test_expect_success 'untracked files are left in place when -u is not given' '
890 + >file &&
891 + git add file &&
892 + >untracked &&
893 + git stash push file &&
894 + test_path_is_file untracked
895 +'
896 +
897 test_done
t/t3905-stash-include-untracked.sh
+26
@@ -185,4 +185,30 @@ test_expect_success 'stash save --all is stash poppable' '
185 test -s .gitignore
186 '
187
188 +test_expect_success 'stash push --include-untracked with pathspec' '
189 + >foo &&
190 + >bar &&
191 + git stash push --include-untracked -- foo &&
192 + test_path_is_file bar &&
193 + test_path_is_missing foo &&
194 + git stash pop &&
195 + test_path_is_file bar &&
196 + test_path_is_file foo
197 +'
198 +
199 +test_expect_success 'stash push with $IFS character' '
200 + >"foo bar" &&
201 + >foo &&
202 + >bar &&
203 + git add foo* &&
204 + git stash push --include-untracked -- "foo b*" &&
205 + test_path_is_missing "foo bar" &&
206 + test_path_is_file foo &&
207 + test_path_is_file bar &&
208 + git stash pop &&
209 + test_path_is_file "foo bar" &&
210 + test_path_is_file foo &&
211 + test_path_is_file bar
212 +'
213 +
214 test_done