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>maint
							parent
							
								
									9ca6326dff
								
							
						
					
					
						commit
						df6bba0937
					
				|  | @ -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 roll them | ||||
| 	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 | |||
| 	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. | ||||
| + | ||||
|  |  | |||
							
								
								
									
										38
									
								
								git-stash.sh
								
								
								
								
							
							
						
						
									
										38
									
								
								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) || | ||||
|  | @ -273,27 +278,38 @@ push_stash () { | |||
| 		die "$(gettext "Can't use --patch and --include-untracked or --all at the same time")" | ||||
| 	fi | ||||
|  | ||||
| 	test -n "$untracked" || git ls-files --error-unmatch -- "$@" >/dev/null || exit 1 | ||||
|  | ||||
| 	git update-index -q --refresh | ||||
| 	if no_changes | ||||
| 	if no_changes "$@" | ||||
| 	then | ||||
| 		say "$(gettext "No local changes to save")" | ||||
| 		exit 0 | ||||
| 	fi | ||||
|  | ||||
| 	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 | ||||
| 			git reset ${GIT_QUIET:+-q} -- "$@" | ||||
| 			git ls-files -z --modified -- "$@" | | ||||
| 			git checkout-index -z --force --stdin | ||||
| 			git clean --force ${GIT_QUIET:+-q} -d -- "$@" | ||||
| 		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" | ||||
|  |  | |||
|  | @ -802,4 +802,96 @@ test_expect_success 'create with multiple arguments for the message' ' | |||
| 	test_cmp expect actual | ||||
| ' | ||||
|  | ||||
| test_expect_success 'stash -- <pathspec> 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 pathspec 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 with pathspec matching multiple paths' ' | ||||
|        echo original >file && | ||||
|        echo original >other-file && | ||||
|        git commit -m "two" file other-file && | ||||
|        echo modified >file && | ||||
|        echo modified >other-file && | ||||
|        git stash push -- "*file" && | ||||
|        echo original >expect && | ||||
|        test_cmp expect file && | ||||
|        test_cmp expect other-file && | ||||
|        git stash pop && | ||||
|        echo modified >expect && | ||||
|        test_cmp expect file && | ||||
|        test_cmp expect other-file | ||||
| ' | ||||
|  | ||||
| test_expect_success 'stash push -p with pathspec shows no changes only once' ' | ||||
| 	>foo && | ||||
| 	git add foo && | ||||
| 	git commit -m "tmp" && | ||||
| 	git stash push -p foo >actual && | ||||
| 	echo "No local changes to save" >expect && | ||||
| 	git reset --hard HEAD~ && | ||||
| 	test_cmp expect actual | ||||
| ' | ||||
|  | ||||
| test_expect_success 'stash push with pathspec shows no changes when there are none' ' | ||||
| 	>foo && | ||||
| 	git add foo && | ||||
| 	git commit -m "tmp" && | ||||
| 	git stash push foo >actual && | ||||
| 	echo "No local changes to save" >expect && | ||||
| 	git reset --hard HEAD~ && | ||||
| 	test_cmp expect actual | ||||
| ' | ||||
|  | ||||
| test_expect_success 'stash push with pathspec not in the repository errors out' ' | ||||
| 	>untracked && | ||||
| 	test_must_fail 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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Thomas Gummerer
						Thomas Gummerer