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