stash push: support the --pathspec-from-file option
authorAlexandr Miloslavskiy <redacted>
Mon, 17 Feb 2020 17:25:22 +0000 (17:25 +0000)
committerJunio C Hamano <redacted>
Wed, 19 Feb 2020 18:56:49 +0000 (10:56 -0800)
Decisions taken for simplicity:
1) For now, `--pathspec-from-file` is declared incompatible with
   `--patch`, even when <file> is not `-`. Such use case is not
   really expected.
2) It is not allowed to pass pathspec in both args and file.

Signed-off-by: Alexandr Miloslavskiy <redacted>
Signed-off-by: Junio C Hamano <redacted>
Documentation/git-stash.txt
builtin/stash.c
t/t3909-stash-pathspec-file.sh [new file with mode: 0755]

index f10d8a2a5c66a00378a653524869e50ea06c1f5b..31f1beb65baf213908e93d0e90c379d9fb0e7d84 100644 (file)
@@ -15,6 +15,7 @@ SYNOPSIS
 'git stash' branch <branchname> [<stash>]
 'git stash' [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]
             [-u|--include-untracked] [-a|--all] [-m|--message <message>]
+            [--pathspec-from-file=<file> [--pathspec-file-nul]]
             [--] [<pathspec>...]]
 'git stash' clear
 'git stash' create [<message>]
@@ -46,7 +47,7 @@ stash index (e.g. the integer `n` is equivalent to `stash@{n}`).
 COMMANDS
 --------
 
-push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--] [<pathspec>...]::
+push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [-m|--message <message>] [--pathspec-from-file=<file> [--pathspec-file-nul]] [--] [<pathspec>...]::
 
        Save your local modifications to a new 'stash entry' and roll them
        back to HEAD (in the working tree and in the index).
@@ -194,6 +195,23 @@ to learn how to operate the `--patch` mode.
 The `--patch` option implies `--keep-index`.  You can use
 `--no-keep-index` to override this.
 
+--pathspec-from-file=<file>::
+       This option is only valid for `push` command.
++
+Pathspec is passed in `<file>` instead of commandline args. If
+`<file>` is exactly `-` then standard input is used. Pathspec
+elements are separated by LF or CR/LF. Pathspec elements can be
+quoted as explained for the configuration variable `core.quotePath`
+(see linkgit:git-config[1]). See also `--pathspec-file-nul` and
+global `--literal-pathspecs`.
+
+--pathspec-file-nul::
+       This option is only valid for `push` command.
++
+Only meaningful with `--pathspec-from-file`. Pathspec elements are
+separated with NUL character and all other characters are taken
+literally (including newlines and quotes).
+
 -q::
 --quiet::
        This option is only valid for `apply`, `drop`, `pop`, `push`,
index 7bc4c5d69657cc74b65c485d0bf2583cf1c61b9e..74d92595a2c7105e0ac558fcb223dd57429034be 100644 (file)
@@ -27,6 +27,7 @@ static const char * const git_stash_usage[] = {
        N_("git stash clear"),
        N_("git stash [push [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
           "          [-u|--include-untracked] [-a|--all] [-m|--message <message>]\n"
+          "          [--pathspec-from-file=<file> [--pathspec-file-nul]]\n"
           "          [--] [<pathspec>...]]"),
        N_("git stash save [-p|--patch] [-k|--[no-]keep-index] [-q|--quiet]\n"
           "          [-u|--include-untracked] [-a|--all] [<message>]"),
@@ -1456,7 +1457,9 @@ static int push_stash(int argc, const char **argv, const char *prefix,
        int patch_mode = 0;
        int include_untracked = 0;
        int quiet = 0;
+       int pathspec_file_nul = 0;
        const char *stash_msg = NULL;
+       const char *pathspec_from_file = NULL;
        struct pathspec ps;
        struct option options[] = {
                OPT_BOOL('k', "keep-index", &keep_index,
@@ -1470,6 +1473,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
                            N_("include ignore files"), 2),
                OPT_STRING('m', "message", &stash_msg, N_("message"),
                           N_("stash message")),
+               OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
+               OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
                OPT_END()
        };
 
@@ -1492,6 +1497,21 @@ static int push_stash(int argc, const char **argv, const char *prefix,
 
        parse_pathspec(&ps, 0, PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN,
                       prefix, argv);
+
+       if (pathspec_from_file) {
+               if (patch_mode)
+                       die(_("--pathspec-from-file is incompatible with --patch"));
+
+               if (ps.nr)
+                       die(_("--pathspec-from-file is incompatible with pathspec arguments"));
+
+               parse_pathspec_file(&ps, 0,
+                                   PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN,
+                                   prefix, pathspec_from_file, pathspec_file_nul);
+       } else if (pathspec_file_nul) {
+               die(_("--pathspec-file-nul requires --pathspec-from-file"));
+       }
+
        return do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
                             include_untracked);
 }
diff --git a/t/t3909-stash-pathspec-file.sh b/t/t3909-stash-pathspec-file.sh
new file mode 100755 (executable)
index 0000000..55e050c
--- /dev/null
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+test_description='stash --pathspec-from-file'
+
+. ./test-lib.sh
+
+test_tick
+
+test_expect_success setup '
+       >fileA.t &&
+       >fileB.t &&
+       >fileC.t &&
+       >fileD.t &&
+       git add fileA.t fileB.t fileC.t fileD.t &&
+       git commit -m "Files" &&
+
+       git tag checkpoint
+'
+
+restore_checkpoint () {
+       git reset --hard checkpoint
+}
+
+verify_expect () {
+       git stash show --name-status >actual &&
+       test_cmp expect actual
+}
+
+test_expect_success 'simplest' '
+       restore_checkpoint &&
+
+       # More files are written to make sure that git didnt ignore
+       # --pathspec-from-file, stashing everything
+       echo A >fileA.t &&
+       echo B >fileB.t &&
+       echo C >fileC.t &&
+       echo D >fileD.t &&
+
+       cat >expect <<-\EOF &&
+       M       fileA.t
+       EOF
+
+       echo fileA.t | git stash push --pathspec-from-file=- &&
+       verify_expect
+'
+
+test_expect_success '--pathspec-file-nul' '
+       restore_checkpoint &&
+
+       # More files are written to make sure that git didnt ignore
+       # --pathspec-from-file, stashing everything
+       echo A >fileA.t &&
+       echo B >fileB.t &&
+       echo C >fileC.t &&
+       echo D >fileD.t &&
+
+       cat >expect <<-\EOF &&
+       M       fileA.t
+       M       fileB.t
+       EOF
+
+       printf "fileA.t\0fileB.t\0" | git stash push --pathspec-from-file=- --pathspec-file-nul &&
+       verify_expect
+'
+
+test_expect_success 'only touches what was listed' '
+       restore_checkpoint &&
+
+       # More files are written to make sure that git didnt ignore
+       # --pathspec-from-file, stashing everything
+       echo A >fileA.t &&
+       echo B >fileB.t &&
+       echo C >fileC.t &&
+       echo D >fileD.t &&
+
+       cat >expect <<-\EOF &&
+       M       fileB.t
+       M       fileC.t
+       EOF
+
+       printf "fileB.t\nfileC.t\n" | git stash push --pathspec-from-file=- &&
+       verify_expect
+'
+
+test_expect_success 'error conditions' '
+       restore_checkpoint &&
+       echo A >fileA.t &&
+       echo fileA.t >list &&
+
+       test_must_fail git stash push --pathspec-from-file=list --patch 2>err &&
+       test_i18ngrep -e "--pathspec-from-file is incompatible with --patch" err &&
+
+       test_must_fail git stash push --pathspec-from-file=list -- fileA.t 2>err &&
+       test_i18ngrep -e "--pathspec-from-file is incompatible with pathspec arguments" err &&
+
+       test_must_fail git stash push --pathspec-file-nul 2>err &&
+       test_i18ngrep -e "--pathspec-file-nul requires --pathspec-from-file" err
+'
+
+test_done
git clone https://git.99rst.org/PROJECT