stash: teach 'push' (and 'create_stash') to honor pathspec
authorThomas Gummerer <t.gummerer@gmail.com>
Tue, 28 Feb 2017 20:33:38 +0000 (20:33 +0000)
committerJunio C Hamano <gitster@pobox.com>
Tue, 28 Feb 2017 22:21:05 +0000 (14:21 -0800)
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>
Documentation/git-stash.txt
git-stash.sh
t/t3903-stash.sh
t/t3905-stash-include-untracked.sh

index d240df4..88369ed 100644 (file)
@@ -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.
 +
index ef5d1b4..35f7a09 100755 (executable)
@@ -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"
index ffe3549..f934993 100755 (executable)
@@ -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
index f372fc8..193adc7 100755 (executable)
@@ -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