Skip to content

Commit dda1f2a

Browse files
trastgitster
authored andcommitted
Implement 'git stash save --patch'
This adds a hunk-based mode to git-stash. You can select hunks from the difference between HEAD and worktree, and git-stash will build a stash that reflects these changes. The index state of the stash is the same as your current index, and we also let --patch imply --keep-index. Note that because the selected hunks are rolled back from the worktree but not the index, the resulting state may appear somewhat confusing if you had also staged these changes. This is not entirely satisfactory, but due to the way stashes are applied, other solutions would require a change to the stash format. Signed-off-by: Thomas Rast <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 4f35365 commit dda1f2a

File tree

4 files changed

+145
-18
lines changed

4 files changed

+145
-18
lines changed

Documentation/git-stash.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ SYNOPSIS
1313
'git stash' drop [-q|--quiet] [<stash>]
1414
'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
1515
'git stash' branch <branchname> [<stash>]
16-
'git stash' [save [--keep-index] [-q|--quiet] [<message>]]
16+
'git stash' [save [--patch] [--[no-]keep-index] [-q|--quiet] [<message>]]
1717
'git stash' clear
1818
'git stash' create
1919

@@ -42,7 +42,7 @@ is also possible).
4242
OPTIONS
4343
-------
4444

45-
save [--keep-index] [-q|--quiet] [<message>]::
45+
save [--patch] [--[no-]keep-index] [-q|--quiet] [<message>]::
4646

4747
Save your local modifications to a new 'stash', and run `git reset
4848
--hard` to revert them. This is the default action when no
@@ -51,6 +51,16 @@ save [--keep-index] [-q|--quiet] [<message>]::
5151
+
5252
If the `--keep-index` option is used, all changes already added to the
5353
index are left intact.
54+
+
55+
With `--patch`, you can interactively select hunks from in the diff
56+
between HEAD and the working tree to be stashed. The stash entry is
57+
constructed such that its index state is the same as the index state
58+
of your repository, and its worktree contains only the changes you
59+
selected interactively. The selected changes are then rolled back
60+
from your worktree.
61+
+
62+
The `--patch` option implies `--keep-index`. You can use
63+
`--no-keep-index` to override this.
5464

5565
list [<options>]::
5666

git-add--interactive.perl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ sub colored {
7676

7777
sub apply_patch;
7878
sub apply_patch_for_checkout_commit;
79+
sub apply_patch_for_stash;
7980

8081
my %patch_modes = (
8182
'stage' => {
@@ -87,6 +88,15 @@ sub colored {
8788
PARTICIPLE => 'staging',
8889
FILTER => 'file-only',
8990
},
91+
'stash' => {
92+
DIFF => 'diff-index -p HEAD',
93+
APPLY => sub { apply_patch 'apply --cached', @_; },
94+
APPLY_CHECK => 'apply --cached',
95+
VERB => 'Stash',
96+
TARGET => '',
97+
PARTICIPLE => 'stashing',
98+
FILTER => undef,
99+
},
90100
'reset_head' => {
91101
DIFF => 'diff-index -p --cached',
92102
APPLY => sub { apply_patch 'apply -R --cached', @_; },
@@ -1493,8 +1503,8 @@ sub process_args {
14931503
'checkout_head' : 'checkout_nothead');
14941504
$arg = shift @ARGV or die "missing --";
14951505
}
1496-
} elsif ($1 eq 'stage') {
1497-
$patch_mode = 'stage';
1506+
} elsif ($1 eq 'stage' or $1 eq 'stash') {
1507+
$patch_mode = $1;
14981508
$arg = shift @ARGV or die "missing --";
14991509
} else {
15001510
die "unknown --patch mode: $1";

git-stash.sh

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ trap 'rm -f "$TMP-*"' 0
2121

2222
ref_stash=refs/stash
2323

24+
if git config --get-colorbool color.interactive; then
25+
help_color="$(git config --get-color color.interactive.help 'red bold')"
26+
reset_color="$(git config --get-color '' reset)"
27+
else
28+
help_color=
29+
reset_color=
30+
fi
31+
2432
no_changes () {
2533
git diff-index --quiet --cached HEAD --ignore-submodules -- &&
2634
git diff-files --quiet --ignore-submodules
@@ -68,19 +76,44 @@ create_stash () {
6876
git commit-tree $i_tree -p $b_commit) ||
6977
die "Cannot save the current index state"
7078

71-
# state of the working tree
72-
w_tree=$( (
79+
if test -z "$patch_mode"
80+
then
81+
82+
# state of the working tree
83+
w_tree=$( (
84+
rm -f "$TMP-index" &&
85+
cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
86+
GIT_INDEX_FILE="$TMP-index" &&
87+
export GIT_INDEX_FILE &&
88+
git read-tree -m $i_tree &&
89+
git add -u &&
90+
git write-tree &&
91+
rm -f "$TMP-index"
92+
) ) ||
93+
die "Cannot save the current worktree state"
94+
95+
else
96+
7397
rm -f "$TMP-index" &&
74-
cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
75-
GIT_INDEX_FILE="$TMP-index" &&
76-
export GIT_INDEX_FILE &&
77-
git read-tree -m $i_tree &&
78-
git add -u &&
79-
git write-tree &&
80-
rm -f "$TMP-index"
81-
) ) ||
98+
GIT_INDEX_FILE="$TMP-index" git read-tree HEAD &&
99+
100+
# find out what the user wants
101+
GIT_INDEX_FILE="$TMP-index" \
102+
git add--interactive --patch=stash -- &&
103+
104+
# state of the working tree
105+
w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
82106
die "Cannot save the current worktree state"
83107

108+
git diff-tree -p HEAD $w_tree > "$TMP-patch" &&
109+
test -s "$TMP-patch" ||
110+
die "No changes selected"
111+
112+
rm -f "$TMP-index" ||
113+
die "Cannot remove temporary index (can't happen)"
114+
115+
fi
116+
84117
# create the stash
85118
if test -z "$stash_msg"
86119
then
@@ -95,12 +128,20 @@ create_stash () {
95128

96129
save_stash () {
97130
keep_index=
131+
patch_mode=
98132
while test $# != 0
99133
do
100134
case "$1" in
101135
--keep-index)
102136
keep_index=t
103137
;;
138+
--no-keep-index)
139+
keep_index=
140+
;;
141+
-p|--patch)
142+
patch_mode=t
143+
keep_index=t
144+
;;
104145
-q|--quiet)
105146
GIT_QUIET=t
106147
;;
@@ -131,11 +172,22 @@ save_stash () {
131172
die "Cannot save the current status"
132173
say Saved working directory and index state "$stash_msg"
133174

134-
git reset --hard ${GIT_QUIET:+-q}
135-
136-
if test -n "$keep_index" && test -n $i_tree
175+
if test -z "$patch_mode"
137176
then
138-
git read-tree --reset -u $i_tree
177+
git reset --hard ${GIT_QUIET:+-q}
178+
179+
if test -n "$keep_index" && test -n $i_tree
180+
then
181+
git read-tree --reset -u $i_tree
182+
fi
183+
else
184+
git apply -R < "$TMP-patch" ||
185+
die "Cannot remove worktree changes"
186+
187+
if test -z "$keep_index"
188+
then
189+
git reset
190+
fi
139191
fi
140192
}
141193

t/t3904-stash-patch.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
3+
test_description='git checkout --patch'
4+
. ./lib-patch-mode.sh
5+
6+
test_expect_success 'setup' '
7+
mkdir dir &&
8+
echo parent > dir/foo &&
9+
echo dummy > bar &&
10+
git add bar dir/foo &&
11+
git commit -m initial &&
12+
test_tick &&
13+
test_commit second dir/foo head &&
14+
echo index > dir/foo &&
15+
git add dir/foo &&
16+
set_and_save_state bar bar_work bar_index &&
17+
save_head
18+
'
19+
20+
# note: bar sorts before dir, so the first 'n' is always to skip 'bar'
21+
22+
test_expect_success 'saying "n" does nothing' '
23+
set_state dir/foo work index
24+
(echo n; echo n) | test_must_fail git stash save -p &&
25+
verify_state dir/foo work index &&
26+
verify_saved_state bar
27+
'
28+
29+
test_expect_success 'git stash -p' '
30+
(echo n; echo y) | git stash save -p &&
31+
verify_state dir/foo head index &&
32+
verify_saved_state bar &&
33+
git reset --hard &&
34+
git stash apply &&
35+
verify_state dir/foo work head &&
36+
verify_state bar dummy dummy
37+
'
38+
39+
test_expect_success 'git stash -p --no-keep-index' '
40+
set_state dir/foo work index &&
41+
set_state bar bar_work bar_index &&
42+
(echo n; echo y) | git stash save -p --no-keep-index &&
43+
verify_state dir/foo head head &&
44+
verify_state bar bar_work dummy &&
45+
git reset --hard &&
46+
git stash apply --index &&
47+
verify_state dir/foo work index &&
48+
verify_state bar dummy bar_index
49+
'
50+
51+
test_expect_success 'none of this moved HEAD' '
52+
verify_saved_head
53+
'
54+
55+
test_done

0 commit comments

Comments
 (0)