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

[PATCH v6 4/6] stash: teach 'push' (and 'create_stash') to honor pathspec

From
Thomas Gummerer <t.gummerer@gmail.com>
Date
Feb 19, 2017, 11:03 UTC
Message-ID
<20170219110313.24070-5-t.gummerer@gmail.com>
In-Reply-To
<20170219110313.24070-1-t.gummerer@gmail.com>

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>
---
 Documentation/git-stash.txt        |  9 ++++-
 git-stash.sh                       | 48 +++++++++++++++++++------
 t/t3903-stash.sh                   | 72 ++++++++++++++++++++++++++++++++++++++
 t/t3905-stash-include-untracked.sh | 26 ++++++++++++++
 4 files changed, 143 insertions(+), 12 deletions(-)
diff --git a/Documentation/git-stash.txt b/Documentation/git-stash.txt
index 0f602ea0c8..b7db939a06 100644
--- a/Documentation/git-stash.txt
+++ b/Documentation/git-stash.txt
@@ -17,6 +17,7 @@ SYNOPSIS
 	     [-u|--include-untracked] [-a|--all] [<message>]]
 'git stash' push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
 	     [-u|--include-untracked] [-a|--all] [-m|--message <message>]]
+	     [--] [<pathspec>...]
 'git stash' clear
 'git stash' create [<message>]
 'git stash' store [-m|--message <message>] [-q|--quiet] <commit>
@@ -48,7 +49,7 @@ OPTIONS
 -------
 
 save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
-push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>]::
+push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--] [<pathspec>...]::
 
 	Save your local modifications to a new 'stash', and run `git reset
 	--hard` to revert them.  The <message> part is optional and gives
@@ -57,6 +58,12 @@ push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q
 	only <message> does not trigger this action to prevent a misspelled
 	subcommand from making an unwanted stash.
 +
+When pathspec is given to 'git stash push', the new stash records the
+modified states only for the files that match the pathspec.  The index
+entries and working tree files are then rolled back to the state in
+HEAD only for these files, too, leaving files that do not match the
+pathspec intact.
++
 If the `--keep-index` option is used, all changes already added to the
 index are left intact.
 +
diff --git a/git-stash.sh b/git-stash.sh
index ef5d1b45be..b55983d1fd 100755
--- a/git-stash.sh
+++ b/git-stash.sh
@@ -11,6 +11,7 @@ USAGE="list [<options>]
 		       [-u|--include-untracked] [-a|--all] [<message>]]
    or: $dashless push [--patch] [-k|--[no-]keep-index] [-q|--quiet]
 		      [-u|--include-untracked] [-a|--all] [-m <message>]
+		      [-- <pathspec>...]
    or: $dashless clear"
 
 SUBDIRECTORY_OK=Yes
@@ -35,15 +36,15 @@ else
 fi
 
 no_changes () {
-	git diff-index --quiet --cached HEAD --ignore-submodules -- &&
-	git diff-files --quiet --ignore-submodules &&
+	git diff-index --quiet --cached HEAD --ignore-submodules -- "$@" &&
+	git diff-files --quiet --ignore-submodules -- "$@" &&
 	(test -z "$untracked" || test -z "$(untracked_files)")
 }
 
 untracked_files () {
 	excl_opt=--exclude-standard
 	test "$untracked" = "all" && excl_opt=
-	git ls-files -o -z $excl_opt
+	git ls-files -o -z $excl_opt -- "$@"
 }
 
 clear_stash () {
@@ -71,12 +72,16 @@ create_stash () {
 			shift
 			untracked=${1?"BUG: create_stash () -u requires an argument"}
 			;;
+		--)
+			shift
+			break
+			;;
 		esac
 		shift
 	done
 
 	git update-index -q --refresh
-	if no_changes
+	if no_changes "$@"
 	then
 		exit 0
 	fi
@@ -108,7 +113,7 @@ create_stash () {
 		# Untracked files are stored by themselves in a parentless commit, for
 		# ease of unpacking later.
 		u_commit=$(
-			untracked_files | (
+			untracked_files "$@" | (
 				GIT_INDEX_FILE="$TMPindex" &&
 				export GIT_INDEX_FILE &&
 				rm -f "$TMPindex" &&
@@ -131,7 +136,7 @@ create_stash () {
 			git read-tree --index-output="$TMPindex" -m $i_tree &&
 			GIT_INDEX_FILE="$TMPindex" &&
 			export GIT_INDEX_FILE &&
-			git diff-index --name-only -z HEAD -- >"$TMP-stagenames" &&
+			git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" &&
 			git update-index -z --add --remove --stdin <"$TMP-stagenames" &&
 			git write-tree &&
 			rm -f "$TMPindex"
@@ -145,7 +150,7 @@ create_stash () {
 
 		# find out what the user wants
 		GIT_INDEX_FILE="$TMP-index" \
-			git add--interactive --patch=stash -- &&
+			git add--interactive --patch=stash -- "$@" &&
 
 		# state of the working tree
 		w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
@@ -274,7 +279,7 @@ push_stash () {
 	fi
 
 	git update-index -q --refresh
-	if no_changes
+	if no_changes "$@"
 	then
 		say "$(gettext "No local changes to save")"
 		exit 0
@@ -282,18 +287,39 @@ push_stash () {
 	git reflog exists $ref_stash ||
 		clear_stash || die "$(gettext "Cannot initialize stash")"
 
-	create_stash -m "$stash_msg" -u "$untracked"
+	create_stash -m "$stash_msg" -u "$untracked" -- "$@"
 	store_stash -m "$stash_msg" -q $w_commit ||
 	die "$(gettext "Cannot save the current status")"
 	say "$(eval_gettext "Saved working directory and index state \$stash_msg")"
 
 	if test -z "$patch_mode"
 	then
-		git reset --hard ${GIT_QUIET:+-q}
+		if test $# != 0
+		then
+			saved_untracked=
+			if test -n "$(git ls-files --others -- "$@")"
+			then
+				saved_untracked=$(
+					git ls-files -z --others -- "$@" |
+					    xargs -0 git stash create -u all --)
+			fi
+			git ls-files -z -- "$@" | xargs -0 git reset ${GIT_QUIET:+-q} --
+			git ls-files -z --modified -- "$@" | xargs -0 git checkout ${GIT_QUIET:+-q} HEAD --
+			if test -n "$(git ls-files -z --others -- "$@")"
+			then
+				git ls-files -z --others -- "$@" | xargs -0 git clean --force -d ${GIT_QUIET:+-q} --
+			fi
+			if test -n "$saved_untracked"
+			then
+				git stash pop -q $saved_untracked
+			fi
+		else
+			git reset --hard ${GIT_QUIET:+-q}
+		fi
 		test "$untracked" = "all" && CLEAN_X_OPTION=-x || CLEAN_X_OPTION=
 		if test -n "$untracked"
 		then
-			git clean --force --quiet -d $CLEAN_X_OPTION
+			git clean --force --quiet -d $CLEAN_X_OPTION -- "$@"
 		fi
 
 		if test "$keep_index" = "t" && test -n "$i_tree"
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index ffe3549ea5..4fb800eec8 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -802,4 +802,76 @@ test_expect_success 'create with multiple arguments for the message' '
 	test_cmp expect actual
 '
 
+test_expect_success 'stash -- <filename> stashes and restores the file' '
+	>foo &&
+	>bar &&
+	git add foo bar &&
+	git stash push -- foo &&
+	test_path_is_file bar &&
+	test_path_is_missing foo &&
+	git stash pop &&
+	test_path_is_file foo &&
+	test_path_is_file bar
+'
+
+test_expect_success 'stash with multiple filename arguments' '
+	>foo &&
+	>bar &&
+	>extra &&
+	git add foo bar extra &&
+	git stash push -- foo bar &&
+	test_path_is_missing bar &&
+	test_path_is_missing foo &&
+	test_path_is_file extra &&
+	git stash pop &&
+	test_path_is_file foo &&
+	test_path_is_file bar &&
+	test_path_is_file extra
+'
+
+test_expect_success 'stash with file including $IFS character' '
+	>"foo bar" &&
+	>foo &&
+	>bar &&
+	git add foo* &&
+	git stash push -- "foo b*" &&
+	test_path_is_missing "foo bar" &&
+	test_path_is_file foo &&
+	test_path_is_file bar &&
+	git stash pop &&
+	test_path_is_file "foo bar" &&
+	test_path_is_file foo &&
+	test_path_is_file bar
+'
+
+test_expect_success 'stash push -p with pathspec shows no changes only onece' '
+	>file &&
+	git add file &&
+	git stash push -p not-file >actual &&
+	echo "No local changes to save" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'stash push with pathspec shows no changes when there are none' '
+	>file &&
+	git add file &&
+	git stash push not-file >actual &&
+	echo "No local changes to save" >expect &&
+	test_cmp expect actual
+'
+
+test_expect_success 'untracked file is not removed when using pathspecs' '
+	>untracked &&
+	git stash push untracked &&
+	test_path_is_file untracked
+'
+
+test_expect_success 'untracked files are left in place when -u is not given' '
+	>file &&
+	git add file &&
+	>untracked &&
+	git stash push file &&
+	test_path_is_file untracked
+'
+
 test_done
diff --git a/t/t3905-stash-include-untracked.sh b/t/t3905-stash-include-untracked.sh
index f372fc8ca8..193adc7b68 100755
--- a/t/t3905-stash-include-untracked.sh
+++ b/t/t3905-stash-include-untracked.sh
@@ -185,4 +185,30 @@ test_expect_success 'stash save --all is stash poppable' '
 	test -s .gitignore
 '
 
+test_expect_success 'stash push --include-untracked with pathspec' '
+	>foo &&
+	>bar &&
+	git stash push --include-untracked -- foo &&
+	test_path_is_file bar &&
+	test_path_is_missing foo &&
+	git stash pop &&
+	test_path_is_file bar &&
+	test_path_is_file foo
+'
+
+test_expect_success 'stash push with $IFS character' '
+	>"foo bar" &&
+	>foo &&
+	>bar &&
+	git add foo* &&
+	git stash push --include-untracked -- "foo b*" &&
+	test_path_is_missing "foo bar" &&
+	test_path_is_file foo &&
+	test_path_is_file bar &&
+	git stash pop &&
+	test_path_is_file "foo bar" &&
+	test_path_is_file foo &&
+	test_path_is_file bar
+'
+
 test_done
-- 
2.12.0.rc2.399.g0ca89a282
Previous: Thomas GummererNext: Junio C Hamano
Message 29 of 61 in “stash: support pathspec argument”
  1. 0/7 stash: support pathspec argumentThomas Gummerer, Feb 12, 2017
  2. 1/7 Documentation/stash: remove mention of git reset --hardThomas Gummerer, Feb 12, 2017
  3. 2/7 stash: introduce push verbThomas Gummerer, Feb 12, 2017
  4. 6/7 stash: use stash_push for no verb formThomas Gummerer, Feb 12, 2017
  5. 5/7 stash: teach 'push' (and 'create') to honor pathspecThomas Gummerer, Feb 12, 2017
  6. 4/7 stash: introduce new format createThomas Gummerer, Feb 12, 2017
  7. 3/7 stash: add test for the create command line argumentsThomas Gummerer, Feb 12, 2017
  8. 7/7 stash: allow pathspecs in the no verb formThomas Gummerer, Feb 12, 2017
  9. Thomas GummererFeb 12, 2017
  10. 0/6 stash: support pathspec argumentThomas Gummerer, Feb 17, 2017
  11. 2/6 stash: add test for the create command line argumentsThomas Gummerer, Feb 17, 2017
  12. 4/6 stash: teach 'push' (and 'create_stash') to honor pathspecThomas Gummerer, Feb 17, 2017
  13. 5/6 stash: use stash_push for no verb formThomas Gummerer, Feb 17, 2017
  14. 6/6 stash: allow pathspecs in the no verb formThomas Gummerer, Feb 17, 2017
  15. Jeff KingFeb 17, 2017
  16. Thomas GummererFeb 19, 2017
  17. 1/6 stash: introduce push verbThomas Gummerer, Feb 17, 2017
  18. 3/6 stash: refactor stash_createThomas Gummerer, Feb 17, 2017
  19. Jeff KingFeb 17, 2017
  20. Thomas GummererFeb 19, 2017
  21. Junio C HamanoFeb 17, 2017
  22. Thomas GummererFeb 17, 2017
  23. Junio C HamanoFeb 17, 2017
  24. 0/6 stash: support pathspec argumentThomas Gummerer, Feb 19, 2017
  25. 5/6 stash: use stash_push for no verb formThomas Gummerer, Feb 19, 2017
  26. 2/6 stash: add test for the create command line argumentsThomas Gummerer, Feb 19, 2017
  27. 3/6 stash: refactor stash_createThomas Gummerer, Feb 19, 2017
  28. 1/6 stash: introduce push verbThomas Gummerer, Feb 19, 2017
  29. 4/6 stash: teach 'push' (and 'create_stash') to honor pathspecThomas Gummerer, Feb 19, 2017
  30. Junio C HamanoFeb 21, 2017
  31. Thomas GummererFeb 25, 2017
  32. 6/6 stash: allow pathspecs in the no verb formThomas Gummerer, Feb 19, 2017
  33. Jeff KingFeb 20, 2017
  34. Junio C HamanoFeb 20, 2017
  35. Junio C HamanoFeb 20, 2017
  36. Thomas GummererFeb 25, 2017
  37. 0/6 stash: support pathspec argumentThomas Gummerer, Feb 25, 2017
  38. 2/6 stash: add test for the create command line argumentsThomas Gummerer, Feb 25, 2017
  39. 3/6 stash: refactor stash_createThomas Gummerer, Feb 25, 2017
  40. 6/6 stash: allow pathspecs in the no verb formThomas Gummerer, Feb 25, 2017
  41. 1/6 stash: introduce push verbThomas Gummerer, Feb 25, 2017
  42. 4/6 stash: teach 'push' (and 'create_stash') to honor pathspecThomas Gummerer, Feb 25, 2017
  43. Junio C HamanoFeb 27, 2017
  44. Thomas GummererFeb 27, 2017
  45. Junio C HamanoFeb 27, 2017
  46. Junio C HamanoFeb 27, 2017
  47. Junio C HamanoFeb 27, 2017
  48. Thomas GummererFeb 27, 2017
  49. 5/6 stash: use stash_push for no verb formThomas Gummerer, Feb 25, 2017
  50. 0/6 stash: support pathspec argumentThomas Gummerer, Feb 28, 2017
  51. 3/6 stash: refactor stash_createThomas Gummerer, Feb 28, 2017
  52. 6/6 stash: allow pathspecs in the no verb formThomas Gummerer, Feb 28, 2017
  53. 1/6 stash: introduce push verbThomas Gummerer, Feb 28, 2017
  54. 4/6 stash: teach 'push' (and 'create_stash') to honor pathspecThomas Gummerer, Feb 28, 2017
  55. Junio C HamanoFeb 28, 2017
  56. Thomas GummererMar 1, 2017
  57. Junio C HamanoMar 1, 2017
  58. 2/6 stash: add test for the create command line argumentsThomas Gummerer, Feb 28, 2017
  59. 5/6 stash: use stash_push for no verb formThomas Gummerer, Feb 28, 2017
  60. Thomas GummererFeb 14, 2017
  61. Thomas GummererFeb 17, 2017

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.