git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v4 5/5] Implement 'git stash save --patch'

From
Thomas Rast <trast@student.ethz.ch>
Date
Jul 28, 2009, 21:20 UTC
Message-ID
<fab366a3117f00e8987dc0bdb2be80729f13cd8f.1248815580.git.trast@student.ethz.ch>
In-Reply-To
<cover.1248815580.git.trast@student.ethz.ch>

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 <trast@student.ethz.ch>
---
 Documentation/git-stash.txt |   14 ++++++-
 git-add--interactive.perl   |   13 ++++++-
 git-stash.sh                |   80 +++++++++++++++++++++++++++++++++++-------
 t/t3904-stash-patch.sh      |   66 +++++++++++++++++++++++++++++++++++
 4 files changed, 155 insertions(+), 18 deletions(-)
 create mode 100755 t/t3904-stash-patch.sh
diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt
index 1c64a02..4b15459 100644
--- a/Documentation/git-stash.txt
+++ b/Documentation/git-stash.txt
@@ -13,7 +13,7 @@ SYNOPSIS
 'git stash' drop [-q|--quiet] [<stash>]
 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
 'git stash' branch <branchname> [<stash>]
-'git stash' [save [--keep-index] [-q|--quiet] [<message>]]
+'git stash' [save [--patch] [--[no-]keep-index] [-q|--quiet] [<message>]]
 'git stash' clear
 'git stash' create
 
@@ -42,7 +42,7 @@ is also possible).
 OPTIONS
 -------
 
-save [--keep-index] [-q|--quiet] [<message>]::
+save [--patch] [--[no-]keep-index] [-q|--quiet] [<message>]::
 
 	Save your local modifications to a new 'stash', and run `git reset
 	--hard` to revert them.  This is the default action when no
@@ -51,6 +51,16 @@ save [--keep-index] [-q|--quiet] [<message>]::
 +
 If the `--keep-index` option is used, all changes already added to the
 index are left intact.
++
+With `--patch`, you can interactively select hunks from in the diff
+between HEAD and the working tree to be stashed.  The stash entry is
+constructed such that its index state is the same as the index state
+of your repository, and its worktree contains only the changes you
+selected interactively.  The selected changes are then rolled back
+from your worktree.
++
+The `--patch` option implies `--keep-index`.  You can use
+`--no-keep-index` to override this.
 
 list [<options>]::
 
diff --git a/git-add--interactive.perl b/git-add--interactive.perl
index bb76c37..91f1657 100755
--- a/git-add--interactive.perl
+++ b/git-add--interactive.perl
@@ -76,6 +76,7 @@
 
 sub apply_patch;
 sub apply_patch_for_checkout_commit;
+sub apply_patch_for_stash;
 
 my %patch_modes = (
 	'stage' => {
@@ -86,6 +87,14 @@
 		PARTICIPLE => 'staging',
 		FILTER => 'file-only',
 	},
+	'stash' => {
+		DIFF => 'diff-index -p HEAD',
+		APPLY => sub { apply_patch 'apply --cached', @_; },
+		APPLY_CHECK => 'apply --cached',
+		VERB => 'Stash',
+		PARTICIPLE => 'stashing',
+		FILTER => undef,
+	},
 	'reset' => {
 		DIFF => 'diff-index -p --cached',
 		APPLY => sub { apply_patch 'apply -R --cached', @_; },
@@ -1465,8 +1474,8 @@
 					$patch_mode_revision = $arg;
 					$arg = shift @ARGV or die "missing --";
 				}
-			} elsif ($1 eq 'stage') {
-				$patch_mode = 'stage';
+			} elsif ($1 eq 'stage' or $1 eq 'stash') {
+				$patch_mode = $1;
 				$arg = shift @ARGV or die "missing --";
 			} else {
 				die "unknown --patch mode: $1";
diff --git a/git-stash.sh b/git-stash.sh
index 03e589f..567aa5d 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -21,6 +21,14 @@ trap 'rm -f "$TMP-*"' 0
 
 ref_stash=refs/stash
 
+if git config --get-colorbool color.interactive; then
+       help_color="$(git config --get-color color.interactive.help 'red bold')"
+       reset_color="$(git config --get-color '' reset)"
+else
+       help_color=
+       reset_color=
+fi
+
 no_changes () {
 	git diff-index --quiet --cached HEAD --ignore-submodules -- &&
 	git diff-files --quiet --ignore-submodules
@@ -68,19 +76,44 @@ create_stash () {
 		git commit-tree $i_tree -p $b_commit) ||
 		die "Cannot save the current index state"
 
-	# state of the working tree
-	w_tree=$( (
+	if test -z "$patch_mode"
+	then
+
+		# state of the working tree
+		w_tree=$( (
+			rm -f "$TMP-index" &&
+			cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
+			GIT_INDEX_FILE="$TMP-index" &&
+			export GIT_INDEX_FILE &&
+			git read-tree -m $i_tree &&
+			git add -u &&
+			git write-tree &&
+			rm -f "$TMP-index"
+		) ) ||
+			die "Cannot save the current worktree state"
+
+	else
+
 		rm -f "$TMP-index" &&
-		cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
-		GIT_INDEX_FILE="$TMP-index" &&
-		export GIT_INDEX_FILE &&
-		git read-tree -m $i_tree &&
-		git add -u &&
-		git write-tree &&
-		rm -f "$TMP-index"
-	) ) ||
+		GIT_INDEX_FILE="$TMP-index" git read-tree HEAD &&
+
+		# find out what the user wants
+		GIT_INDEX_FILE="$TMP-index" \
+			git add--interactive --patch=stash -- &&
+
+		# state of the working tree
+		w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
 		die "Cannot save the current worktree state"
 
+		git diff-tree -p HEAD $w_tree > "$TMP-patch" &&
+		test -s "$TMP-patch" ||
+		die "No changes selected"
+
+		rm -f "$TMP-index" ||
+		die "Cannot remove temporary index (can't happen)"
+
+	fi
+
 	# create the stash
 	if test -z "$stash_msg"
 	then
@@ -95,12 +128,20 @@ create_stash () {
 
 save_stash () {
 	keep_index=
+	patch_mode=
 	while test $# != 0
 	do
 		case "$1" in
 		--keep-index)
 			keep_index=t
 			;;
+		--no-keep-index)
+			keep_index=
+			;;
+		-p|--patch)
+			patch_mode=t
+			keep_index=t
+			;;
 		-q|--quiet)
 			GIT_QUIET=t
 			;;
@@ -131,11 +172,22 @@ save_stash () {
 		die "Cannot save the current status"
 	say Saved working directory and index state "$stash_msg"
 
-	git reset --hard ${GIT_QUIET:+-q}
-
-	if test -n "$keep_index" && test -n $i_tree
+	if test -z "$patch_mode"
 	then
-		git read-tree --reset -u $i_tree
+		git reset --hard ${GIT_QUIET:+-q}
+
+		if test -n "$keep_index" && test -n $i_tree
+		then
+			git read-tree --reset -u $i_tree
+		fi
+	else
+		git apply -R < "$TMP-patch" ||
+		die "Cannot remove worktree changes"
+
+		if test -z "$keep_index"
+		then
+			git reset
+		fi
 	fi
 }
 
diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh
new file mode 100755
index 0000000..c7b8e5d
--- /dev/null
+++ b/t/t3904-stash-patch.sh
@@ -0,0 +1,66 @@
+#!/bin/sh
+
+test_description='git checkout --patch'
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+	mkdir dir &&
+	echo parent > dir/foo &&
+	echo dummy > dir/bar &&
+	git add dir &&
+	git commit -m initial &&
+	test_tick &&
+	test_commit second dir/foo head &&
+	echo index > dir/foo &&
+	git add dir/foo &&
+	echo work > dir/foo &&
+	echo bar_index > dir/bar &&
+	git add dir/bar &&
+	echo bar_work > dir/bar
+'
+
+# note: bar sorts before foo, so the first 'n' is always to skip 'bar'
+
+test_expect_success 'saying "n" does nothing' '
+	(echo n; echo n) | test_must_fail git stash save -p &&
+	test "$(git show :dir/foo)" = index &&
+	test "$(cat dir/foo)" = work &&
+	test "$(git show :dir/bar)" = bar_index &&
+	test "$(cat dir/bar)" = bar_work
+'
+
+test_expect_success 'git stash -p' '
+	(echo n; echo y) | git stash save -p &&
+	test "$(git show :dir/foo)" = index &&
+	test "$(cat dir/foo)" = head &&
+	test "$(git show :dir/bar)" = bar_index &&
+	test "$(cat dir/bar)" = bar_work &&
+	git reset --hard &&
+	git stash apply &&
+	test "$(git show :dir/foo)" = head &&
+	test "$(cat dir/foo)" = work &&
+	test "$(git show :dir/bar)" = dummy &&
+	test "$(cat dir/bar)" = dummy
+'
+
+test_expect_success 'git stash -p --no-keep-index' '
+	echo index > dir/foo &&
+	git add dir/foo &&
+	echo work > dir/foo &&
+	echo bar_index > dir/bar &&
+	git add dir/bar &&
+	echo bar_work > dir/bar &&
+	(echo n; echo y) | git stash save -p --no-keep-index &&
+	test "$(git show :dir/foo)" = head &&
+	test "$(cat dir/foo)" = head &&
+	test "$(git show :dir/bar)" = dummy &&
+	test "$(cat dir/bar)" = bar_work &&
+	git reset --hard &&
+	git stash apply --index &&
+	test "$(git show :dir/foo)" = index &&
+	test "$(cat dir/foo)" = work &&
+	test "$(git show :dir/bar)" = bar_index &&
+	test "$(cat dir/bar)" = dummy
+'
+
+test_done
-- 
1.6.4.rc3.215.g18405
Previous: Thomas RastNext: Thomas Rast
Message 35 of 76 in “git-add -p: be able to undo a given hunk”
  1. git-add -p: be able to undo a given hunkPierre Habouzit, Jul 23, 2009
  2. Thomas RastJul 23, 2009
  3. Pierre HabouzitJul 23, 2009
  4. Implement unstage and reset modes for git-add--interactiveThomas Rast, Jul 24, 2009
  5. 1/3 Introduce git-unstageThomas Rast, Jul 24, 2009
  6. Bert WesargJul 24, 2009
  7. Bert WesargJul 24, 2009
  8. Elijah NewrenJul 24, 2009
  9. 2/3 Introduce git-discardThomas Rast, Jul 24, 2009
  10. Elijah NewrenJul 24, 2009
  11. Bert WesargJul 24, 2009
  12. Elijah NewrenJul 24, 2009
  13. Pierre HabouzitJul 25, 2009
  14. 3/3 Implement unstage --patch and discard --patchThomas Rast, Jul 24, 2009
  15. Matthias KestenholzJul 24, 2009
  16. Bert WesargJul 24, 2009
  17. Junio C HamanoJul 24, 2009
  18. Nanako ShiraishiJul 24, 2009
  19. Thomas RastJul 24, 2009
  20. Junio C HamanoJul 24, 2009
  21. 0/5 {checkout,reset,stash} --patchThomas Rast, Jul 25, 2009
  22. 1/5 git-apply--interactive: Refactor patch mode codeThomas Rast, Jul 25, 2009
  23. 2/5 builtin-add: refactor the meat of interactive_add()Thomas Rast, Jul 25, 2009
  24. 3/5 Implement 'git reset --patch'Thomas Rast, Jul 25, 2009
  25. 4/5 Implement 'git checkout --patch'Thomas Rast, Jul 25, 2009
  26. 5/5 Implement 'git stash save --patch'Thomas Rast, Jul 25, 2009
  27. Sverre RabbelierJul 26, 2009
  28. Thomas RastJul 26, 2009
  29. Thomas RastJul 27, 2009
  30. 0/5 {checkout,reset,stash} --patchThomas Rast, Jul 28, 2009
  31. 1/5 git-apply--interactive: Refactor patch mode codeThomas Rast, Jul 28, 2009
  32. 2/5 builtin-add: refactor the meat of interactive_add()Thomas Rast, Jul 28, 2009
  33. 3/5 Implement 'git reset --patch'Thomas Rast, Jul 28, 2009
  34. 4/5 Implement 'git checkout --patch'Thomas Rast, Jul 28, 2009
  35. 5/5 Implement 'git stash save --patch'Thomas Rast, Jul 28, 2009
  36. 6/5 DWIM 'git stash save -p' for 'git stash -p'Thomas Rast, Jul 28, 2009
  37. Jeff KingAug 9, 2009
  38. Thomas RastAug 9, 2009
  39. 0/5 Re: {checkout,reset,stash} --patchNicolas Sebrecht, Aug 9, 2009
  40. Thomas RastAug 9, 2009
  41. 0/5 Re: {checkout,reset,stash} --patchNicolas Sebrecht, Aug 9, 2009
  42. Thomas RastAug 9, 2009
  43. 0/5 Re: {checkout,reset,stash} --patchNicolas Sebrecht, Aug 9, 2009
  44. Thomas RastAug 10, 2009
  45. 0/6 {checkout,reset,stash} --patchThomas Rast, Aug 13, 2009
  46. 1/6 git-apply--interactive: Refactor patch mode codeThomas Rast, Aug 13, 2009
  47. 2/6 Add a small patch-mode testing libraryThomas Rast, Aug 13, 2009
  48. 3/6 builtin-add: refactor the meat of interactive_add()Thomas Rast, Aug 13, 2009
  49. 4/6 Implement 'git reset --patch'Thomas Rast, Aug 13, 2009
  50. 4/6 Implement 'git reset --patch'Thomas Rast, Aug 15, 2009
  51. 5/6 Implement 'git checkout --patch'Thomas Rast, Aug 13, 2009
  52. 5/6 Implement 'git checkout --patch'Thomas Rast, Aug 15, 2009
  53. 6/6 Implement 'git stash save --patch'Thomas Rast, Aug 13, 2009
  54. 7/6 DWIM 'git stash save -p' for 'git stash -p'Thomas Rast, Aug 13, 2009
  55. 0/6 Re: {checkout,reset,stash} --patchNicolas Sebrecht, Aug 14, 2009
  56. Jeff KingAug 15, 2009
  57. Junio C HamanoAug 15, 2009
  58. Thomas RastAug 15, 2009
  59. Thomas RastAug 15, 2009
  60. Jeff KingAug 18, 2009
  61. Thomas RastAug 19, 2009
  62. Jeff KingAug 19, 2009
  63. Junio C HamanoJul 23, 2009
  64. Nanako ShiraishiJul 24, 2009
  65. Junio C HamanoJul 24, 2009
  66. Jeff KingJul 24, 2009
  67. Junio C HamanoJul 25, 2009
  68. Thomas RastJul 25, 2009
  69. Pierre HabouzitJul 25, 2009
  70. Pierre HabouzitJul 25, 2009
  71. Jeff KingJul 26, 2009
  72. Pierre HabouzitJul 27, 2009
  73. Jeff KingJul 27, 2009
  74. Thomas RastJul 27, 2009
  75. Jeff KingJul 27, 2009
  76. Pierre HabouzitJul 24, 2009

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.