Skip to content

Commit df6bba0

Browse files
tgummerergitster
authored andcommitted
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 <[email protected]> Signed-off-by: Thomas Gummerer <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 9ca6326 commit df6bba0

File tree

4 files changed

+153
-12
lines changed

4 files changed

+153
-12
lines changed

Documentation/git-stash.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ SYNOPSIS
1717
[-u|--include-untracked] [-a|--all] [<message>]]
1818
'git stash' push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
1919
[-u|--include-untracked] [-a|--all] [-m|--message <message>]]
20+
[--] [<pathspec>...]
2021
'git stash' clear
2122
'git stash' create [<message>]
2223
'git stash' store [-m|--message <message>] [-q|--quiet] <commit>
@@ -48,7 +49,7 @@ OPTIONS
4849
-------
4950

5051
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>...]::
5253

5354
Save your local modifications to a new 'stash' and roll them
5455
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
5859
only <message> does not trigger this action to prevent a misspelled
5960
subcommand from making an unwanted stash.
6061
+
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+
+
6168
If the `--keep-index` option is used, all changes already added to the
6269
index are left intact.
6370
+

git-stash.sh

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ USAGE="list [<options>]
1111
[-u|--include-untracked] [-a|--all] [<message>]]
1212
or: $dashless push [--patch] [-k|--[no-]keep-index] [-q|--quiet]
1313
[-u|--include-untracked] [-a|--all] [-m <message>]
14+
[-- <pathspec>...]
1415
or: $dashless clear"
1516

1617
SUBDIRECTORY_OK=Yes
@@ -35,15 +36,15 @@ else
3536
fi
3637

3738
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 -- "$@" &&
4041
(test -z "$untracked" || test -z "$(untracked_files)")
4142
}
4243

4344
untracked_files () {
4445
excl_opt=--exclude-standard
4546
test "$untracked" = "all" && excl_opt=
46-
git ls-files -o -z $excl_opt
47+
git ls-files -o -z $excl_opt -- "$@"
4748
}
4849

4950
clear_stash () {
@@ -71,12 +72,16 @@ create_stash () {
7172
shift
7273
untracked=${1?"BUG: create_stash () -u requires an argument"}
7374
;;
75+
--)
76+
shift
77+
break
78+
;;
7479
esac
7580
shift
7681
done
7782

7883
git update-index -q --refresh
79-
if no_changes
84+
if no_changes "$@"
8085
then
8186
exit 0
8287
fi
@@ -108,7 +113,7 @@ create_stash () {
108113
# Untracked files are stored by themselves in a parentless commit, for
109114
# ease of unpacking later.
110115
u_commit=$(
111-
untracked_files | (
116+
untracked_files "$@" | (
112117
GIT_INDEX_FILE="$TMPindex" &&
113118
export GIT_INDEX_FILE &&
114119
rm -f "$TMPindex" &&
@@ -131,7 +136,7 @@ create_stash () {
131136
git read-tree --index-output="$TMPindex" -m $i_tree &&
132137
GIT_INDEX_FILE="$TMPindex" &&
133138
export GIT_INDEX_FILE &&
134-
git diff-index --name-only -z HEAD -- >"$TMP-stagenames" &&
139+
git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" &&
135140
git update-index -z --add --remove --stdin <"$TMP-stagenames" &&
136141
git write-tree &&
137142
rm -f "$TMPindex"
@@ -145,7 +150,7 @@ create_stash () {
145150

146151
# find out what the user wants
147152
GIT_INDEX_FILE="$TMP-index" \
148-
git add--interactive --patch=stash -- &&
153+
git add--interactive --patch=stash -- "$@" &&
149154

150155
# state of the working tree
151156
w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
@@ -273,27 +278,38 @@ push_stash () {
273278
die "$(gettext "Can't use --patch and --include-untracked or --all at the same time")"
274279
fi
275280

281+
test -n "$untracked" || git ls-files --error-unmatch -- "$@" >/dev/null || exit 1
282+
276283
git update-index -q --refresh
277-
if no_changes
284+
if no_changes "$@"
278285
then
279286
say "$(gettext "No local changes to save")"
280287
exit 0
281288
fi
289+
282290
git reflog exists $ref_stash ||
283291
clear_stash || die "$(gettext "Cannot initialize stash")"
284292

285-
create_stash -m "$stash_msg" -u "$untracked"
293+
create_stash -m "$stash_msg" -u "$untracked" -- "$@"
286294
store_stash -m "$stash_msg" -q $w_commit ||
287295
die "$(gettext "Cannot save the current status")"
288296
say "$(eval_gettext "Saved working directory and index state \$stash_msg")"
289297

290298
if test -z "$patch_mode"
291299
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
293309
test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION=
294310
if test -n "$untracked"
295311
then
296-
git clean --force --quiet -d $CLEAN_X_OPTION
312+
git clean --force --quiet -d $CLEAN_X_OPTION -- "$@"
297313
fi
298314

299315
if test "$keep_index" = "t" && test -n "$i_tree"

t/t3903-stash.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,4 +802,96 @@ test_expect_success 'create with multiple arguments for the message' '
802802
test_cmp expect actual
803803
'
804804

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+
805897
test_done

t/t3905-stash-include-untracked.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,30 @@ test_expect_success 'stash save --all is stash poppable' '
185185
test -s .gitignore
186186
'
187187

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+
188214
test_done

0 commit comments

Comments
 (0)