summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2019-12-25 19:21:57 (GMT)
committerJunio C Hamano <gitster@pobox.com>2019-12-25 19:21:57 (GMT)
commit135365dd991f9520406012e30ab27a8d22951df8 (patch)
treee4737fc48d547395e10b4c8acd939c7ec2abe958
parentff0cb70d456f586c94f15daa67d2ff1bb8e75fb6 (diff)
parenta9aecc7abb52c4dcd616ee8f43ce5741a15e14a6 (diff)
downloadgit-135365dd991f9520406012e30ab27a8d22951df8.zip
git-135365dd991f9520406012e30ab27a8d22951df8.tar.gz
git-135365dd991f9520406012e30ab27a8d22951df8.tar.bz2
Merge branch 'am/pathspec-f-f-checkout'
A few more commands learned the "--pathspec-from-file" command line option. * am/pathspec-f-f-checkout: checkout, restore: support the --pathspec-from-file option doc: restore: synchronize <pathspec> description doc: checkout: synchronize <pathspec> description doc: checkout: fix broken text reference doc: checkout: remove duplicate synopsis add: support the --pathspec-from-file option cmd_add: prepare for next patch
-rw-r--r--Documentation/git-add.txt16
-rw-r--r--Documentation/git-checkout.txt50
-rw-r--r--Documentation/git-restore.txt26
-rw-r--r--builtin/add.c60
-rw-r--r--builtin/checkout.c31
-rwxr-xr-xt/t2026-checkout-pathspec-file.sh139
-rwxr-xr-xt/t2072-restore-pathspec-file.sh139
-rwxr-xr-xt/t3704-add-pathspec-file.sh127
-rwxr-xr-xt/t9902-completion.sh2
9 files changed, 548 insertions, 42 deletions
diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt
index 8b0e4c7..be5e3ac 100644
--- a/Documentation/git-add.txt
+++ b/Documentation/git-add.txt
@@ -11,7 +11,8 @@ SYNOPSIS
'git add' [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]
[--edit | -e] [--[no-]all | --[no-]ignore-removal | [--update | -u]]
[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]
- [--chmod=(+|-)x] [--] [<pathspec>...]
+ [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]
+ [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -187,6 +188,19 @@ for "git add --no-all <pathspec>...", i.e. ignored removed files.
bit is only changed in the index, the files on disk are left
unchanged.
+--pathspec-from-file=<file>::
+ 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::
+ Only meaningful with `--pathspec-from-file`. Pathspec elements are
+ separated with NUL character and all other characters are taken
+ literally (including newlines and quotes).
+
\--::
This option can be used to separate command-line options from
the list of files, (useful when filenames might be mistaken
diff --git a/Documentation/git-checkout.txt b/Documentation/git-checkout.txt
index cf3cac0..c8fb995 100644
--- a/Documentation/git-checkout.txt
+++ b/Documentation/git-checkout.txt
@@ -12,14 +12,14 @@ SYNOPSIS
'git checkout' [-q] [-f] [-m] --detach [<branch>]
'git checkout' [-q] [-f] [-m] [--detach] <commit>
'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
-'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
-'git checkout' [<tree-ish>] [--] <pathspec>...
-'git checkout' (-p|--patch) [<tree-ish>] [--] [<paths>...]
+'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...
+'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
+'git checkout' (-p|--patch) [<tree-ish>] [--] [<pathspec>...]
DESCRIPTION
-----------
Updates files in the working tree to match the version in the index
-or the specified tree. If no paths are given, 'git checkout' will
+or the specified tree. If no pathspec was given, 'git checkout' will
also update `HEAD` to set the specified branch as the current
branch.
@@ -79,13 +79,14 @@ be used to detach `HEAD` at the tip of the branch (`git checkout
+
Omitting `<branch>` detaches `HEAD` at the tip of the current branch.
-'git checkout' [<tree-ish>] [--] <pathspec>...::
+'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...::
+'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]::
- Overwrite paths in the working tree by replacing with the
- contents in the index or in the `<tree-ish>` (most often a
- commit). When a `<tree-ish>` is given, the paths that
- match the `<pathspec>` are updated both in the index and in
- the working tree.
+ Overwrite the contents of the files that match the pathspec.
+ When the `<tree-ish>` (most often a commit) is not given,
+ overwrite working tree with the contents in the index.
+ When the `<tree-ish>` is given, overwrite both the index and
+ the working tree with the contents at the `<tree-ish>`.
+
The index may contain unmerged entries because of a previous failed merge.
By default, if you try to check out such an entry from the index, the
@@ -96,12 +97,10 @@ using `--ours` or `--theirs`. With `-m`, changes made to the working tree
file can be discarded to re-create the original conflicted merge result.
'git checkout' (-p|--patch) [<tree-ish>] [--] [<pathspec>...]::
- This is similar to the "check out paths to the working tree
- from either the index or from a tree-ish" mode described
- above, but lets you use the interactive interface to show
- the "diff" output and choose which hunks to use in the
- result. See below for the description of `--patch` option.
-
+ This is similar to the previous mode, but lets you use the
+ interactive interface to show the "diff" output and choose which
+ hunks to use in the result. See below for the description of
+ `--patch` option.
OPTIONS
-------
@@ -309,6 +308,19 @@ Note that this option uses the no overlay mode by default (see also
working tree, but not in `<tree-ish>` are removed, to make them
match `<tree-ish>` exactly.
+--pathspec-from-file=<file>::
+ 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::
+ Only meaningful with `--pathspec-from-file`. Pathspec elements are
+ separated with NUL character and all other characters are taken
+ literally (including newlines and quotes).
+
<branch>::
Branch to checkout; if it refers to a branch (i.e., a name that,
when prepended with "refs/heads/", is a valid ref), then that
@@ -339,7 +351,13 @@ leave out at most one of `A` and `B`, in which case it defaults to `HEAD`.
Tree to checkout from (when paths are given). If not specified,
the index will be used.
+\--::
+ Do not interpret any more arguments as options.
+<pathspec>...::
+ Limits the paths affected by the operation.
++
+For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
DETACHED HEAD
-------------
diff --git a/Documentation/git-restore.txt b/Documentation/git-restore.txt
index 1ab2e40..5bf60d4 100644
--- a/Documentation/git-restore.txt
+++ b/Documentation/git-restore.txt
@@ -8,8 +8,9 @@ git-restore - Restore working tree files
SYNOPSIS
--------
[verse]
-'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] <pathspec>...
-'git restore' (-p|--patch) [<options>] [--source=<tree>] [--staged] [--worktree] [<pathspec>...]
+'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] [--] <pathspec>...
+'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] --pathspec-from-file=<file> [--pathspec-file-nul]
+'git restore' (-p|--patch) [<options>] [--source=<tree>] [--staged] [--worktree] [--] [<pathspec>...]
DESCRIPTION
-----------
@@ -113,6 +114,27 @@ in linkgit:git-checkout[1] for details.
appear in the `--source` tree are removed, to make them match
`<tree>` exactly. The default is no-overlay mode.
+--pathspec-from-file=<file>::
+ 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::
+ Only meaningful with `--pathspec-from-file`. Pathspec elements are
+ separated with NUL character and all other characters are taken
+ literally (including newlines and quotes).
+
+\--::
+ Do not interpret any more arguments as options.
+
+<pathspec>...::
+ Limits the paths affected by the operation.
++
+For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
+
EXAMPLES
--------
diff --git a/builtin/add.c b/builtin/add.c
index d4686d5..7c21ad4 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -29,6 +29,8 @@ static const char * const builtin_add_usage[] = {
static int patch_interactive, add_interactive, edit_interactive;
static int take_worktree_changes;
static int add_renormalize;
+static int pathspec_file_nul;
+static const char *pathspec_from_file;
struct update_callback_data {
int flags;
@@ -320,6 +322,8 @@ static struct option builtin_add_options[] = {
N_("override the executable bit of the listed files")),
OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo,
N_("warn when adding an embedded repository")),
+ OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
+ OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
OPT_END(),
};
@@ -414,11 +418,17 @@ int cmd_add(int argc, const char **argv, const char *prefix)
builtin_add_usage, PARSE_OPT_KEEP_ARGV0);
if (patch_interactive)
add_interactive = 1;
- if (add_interactive)
+ if (add_interactive) {
+ if (pathspec_from_file)
+ die(_("--pathspec-from-file is incompatible with --interactive/--patch"));
exit(interactive_add(argc - 1, argv + 1, prefix, patch_interactive));
+ }
- if (edit_interactive)
+ if (edit_interactive) {
+ if (pathspec_from_file)
+ die(_("--pathspec-from-file is incompatible with --edit"));
return(edit_patch(argc, argv, prefix));
+ }
argc--;
argv++;
@@ -430,10 +440,6 @@ int cmd_add(int argc, const char **argv, const char *prefix)
if (addremove && take_worktree_changes)
die(_("-A and -u are mutually incompatible"));
- if (!take_worktree_changes && addremove_explicit < 0 && argc)
- /* Turn "git add pathspec..." to "git add -A pathspec..." */
- addremove = 1;
-
if (!show_only && ignore_missing)
die(_("Option --ignore-missing can only be used together with --dry-run"));
@@ -446,19 +452,6 @@ int cmd_add(int argc, const char **argv, const char *prefix)
hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
- flags = ((verbose ? ADD_CACHE_VERBOSE : 0) |
- (show_only ? ADD_CACHE_PRETEND : 0) |
- (intent_to_add ? ADD_CACHE_INTENT : 0) |
- (ignore_add_errors ? ADD_CACHE_IGNORE_ERRORS : 0) |
- (!(addremove || take_worktree_changes)
- ? ADD_CACHE_IGNORE_REMOVAL : 0));
-
- if (require_pathspec && argc == 0) {
- fprintf(stderr, _("Nothing specified, nothing added.\n"));
- fprintf(stderr, _("Maybe you wanted to say 'git add .'?\n"));
- return 0;
- }
-
/*
* Check the "pathspec '%s' did not match any files" block
* below before enabling new magic.
@@ -468,6 +461,35 @@ int cmd_add(int argc, const char **argv, const char *prefix)
PATHSPEC_SYMLINK_LEADING_PATH,
prefix, argv);
+ if (pathspec_from_file) {
+ if (pathspec.nr)
+ die(_("--pathspec-from-file is incompatible with pathspec arguments"));
+
+ parse_pathspec_file(&pathspec, PATHSPEC_ATTR,
+ PATHSPEC_PREFER_FULL |
+ PATHSPEC_SYMLINK_LEADING_PATH,
+ prefix, pathspec_from_file, pathspec_file_nul);
+ } else if (pathspec_file_nul) {
+ die(_("--pathspec-file-nul requires --pathspec-from-file"));
+ }
+
+ if (require_pathspec && pathspec.nr == 0) {
+ fprintf(stderr, _("Nothing specified, nothing added.\n"));
+ fprintf(stderr, _("Maybe you wanted to say 'git add .'?\n"));
+ return 0;
+ }
+
+ if (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr)
+ /* Turn "git add pathspec..." to "git add -A pathspec..." */
+ addremove = 1;
+
+ flags = ((verbose ? ADD_CACHE_VERBOSE : 0) |
+ (show_only ? ADD_CACHE_PRETEND : 0) |
+ (intent_to_add ? ADD_CACHE_INTENT : 0) |
+ (ignore_add_errors ? ADD_CACHE_IGNORE_ERRORS : 0) |
+ (!(addremove || take_worktree_changes)
+ ? ADD_CACHE_IGNORE_REMOVAL : 0));
+
if (read_cache_preload(&pathspec) < 0)
die(_("index file corrupt"));
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 3634a3d..b52c490 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -70,6 +70,8 @@ struct checkout_opts {
int checkout_worktree;
const char *ignore_unmerged_opt;
int ignore_unmerged;
+ int pathspec_file_nul;
+ const char *pathspec_from_file;
const char *new_branch;
const char *new_branch_force;
@@ -1480,6 +1482,8 @@ static struct option *add_checkout_path_options(struct checkout_opts *opts,
OPT_BOOL('p', "patch", &opts->patch_mode, N_("select hunks interactively")),
OPT_BOOL(0, "ignore-skip-worktree-bits", &opts->ignore_skipworktree,
N_("do not limit pathspecs to sparse entries only")),
+ OPT_PATHSPEC_FROM_FILE(&opts->pathspec_from_file),
+ OPT_PATHSPEC_FILE_NUL(&opts->pathspec_file_nul),
OPT_END()
};
struct option *newopts = parse_options_concat(prevopts, options);
@@ -1618,10 +1622,6 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
die(_("reference is not a tree: %s"), opts->from_treeish);
}
- if (opts->accept_pathspec && !opts->empty_pathspec_ok && !argc &&
- !opts->patch_mode) /* patch mode is special */
- die(_("you must specify path(s) to restore"));
-
if (argc) {
parse_pathspec(&opts->pathspec, 0,
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
@@ -1641,10 +1641,33 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
if (opts->force_detach)
die(_("git checkout: --detach does not take a path argument '%s'"),
argv[0]);
+ }
+
+ if (opts->pathspec_from_file) {
+ if (opts->pathspec.nr)
+ die(_("--pathspec-from-file is incompatible with pathspec arguments"));
+
+ if (opts->force_detach)
+ die(_("--pathspec-from-file is incompatible with --detach"));
+ if (opts->patch_mode)
+ die(_("--pathspec-from-file is incompatible with --patch"));
+
+ parse_pathspec_file(&opts->pathspec, 0,
+ 0,
+ prefix, opts->pathspec_from_file, opts->pathspec_file_nul);
+ } else if (opts->pathspec_file_nul) {
+ die(_("--pathspec-file-nul requires --pathspec-from-file"));
+ }
+
+ if (opts->pathspec.nr) {
if (1 < !!opts->writeout_stage + !!opts->force + !!opts->merge)
die(_("git checkout: --ours/--theirs, --force and --merge are incompatible when\n"
"checking out of the index."));
+ } else {
+ if (opts->accept_pathspec && !opts->empty_pathspec_ok &&
+ !opts->patch_mode) /* patch mode is special */
+ die(_("you must specify path(s) to restore"));
}
if (opts->new_branch) {
diff --git a/t/t2026-checkout-pathspec-file.sh b/t/t2026-checkout-pathspec-file.sh
new file mode 100755
index 0000000..f62fd27
--- /dev/null
+++ b/t/t2026-checkout-pathspec-file.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+test_description='checkout --pathspec-from-file'
+
+. ./test-lib.sh
+
+test_tick
+
+test_expect_success setup '
+ test_commit file0 &&
+
+ echo 1 >fileA.t &&
+ echo 1 >fileB.t &&
+ echo 1 >fileC.t &&
+ echo 1 >fileD.t &&
+ git add fileA.t fileB.t fileC.t fileD.t &&
+ git commit -m "files 1" &&
+
+ echo 2 >fileA.t &&
+ echo 2 >fileB.t &&
+ echo 2 >fileC.t &&
+ echo 2 >fileD.t &&
+ git add fileA.t fileB.t fileC.t fileD.t &&
+ git commit -m "files 2" &&
+
+ git tag checkpoint
+'
+
+restore_checkpoint () {
+ git reset --hard checkpoint
+}
+
+verify_expect () {
+ git status --porcelain --untracked-files=no -- fileA.t fileB.t fileC.t fileD.t >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success '--pathspec-from-file from stdin' '
+ restore_checkpoint &&
+
+ echo fileA.t | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success '--pathspec-from-file from file' '
+ restore_checkpoint &&
+
+ echo fileA.t >list &&
+ git checkout --pathspec-from-file=list HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'NUL delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\0fileB.t\0" | git checkout --pathspec-from-file=- --pathspec-file-nul HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'LF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t\n" | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'no trailing delimiter' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t" | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'CRLF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\r\nfileB.t\r\n" | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes not compatible with --pathspec-file-nul' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" >list &&
+ test_must_fail git checkout --pathspec-from-file=list --pathspec-file-nul HEAD^1
+'
+
+test_expect_success 'only touches what was listed' '
+ restore_checkpoint &&
+
+ printf "fileB.t\nfileC.t\n" | git checkout --pathspec-from-file=- HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileB.t
+ M fileC.t
+ EOF
+ verify_expect
+'
+
+test_done
diff --git a/t/t2072-restore-pathspec-file.sh b/t/t2072-restore-pathspec-file.sh
new file mode 100755
index 0000000..db58e83
--- /dev/null
+++ b/t/t2072-restore-pathspec-file.sh
@@ -0,0 +1,139 @@
+#!/bin/sh
+
+test_description='restore --pathspec-from-file'
+
+. ./test-lib.sh
+
+test_tick
+
+test_expect_success setup '
+ test_commit file0 &&
+
+ echo 1 >fileA.t &&
+ echo 1 >fileB.t &&
+ echo 1 >fileC.t &&
+ echo 1 >fileD.t &&
+ git add fileA.t fileB.t fileC.t fileD.t &&
+ git commit -m "files 1" &&
+
+ echo 2 >fileA.t &&
+ echo 2 >fileB.t &&
+ echo 2 >fileC.t &&
+ echo 2 >fileD.t &&
+ git add fileA.t fileB.t fileC.t fileD.t &&
+ git commit -m "files 2" &&
+
+ git tag checkpoint
+'
+
+restore_checkpoint () {
+ git reset --hard checkpoint
+}
+
+verify_expect () {
+ git status --porcelain --untracked-files=no -- fileA.t fileB.t fileC.t fileD.t >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success '--pathspec-from-file from stdin' '
+ restore_checkpoint &&
+
+ echo fileA.t | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success '--pathspec-from-file from file' '
+ restore_checkpoint &&
+
+ echo fileA.t >list &&
+ git restore --pathspec-from-file=list --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'NUL delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\0fileB.t\0" | git restore --pathspec-from-file=- --pathspec-file-nul --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'LF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t\n" | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'no trailing delimiter' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t" | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'CRLF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\r\nfileB.t\r\n" | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ M fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes not compatible with --pathspec-file-nul' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" >list &&
+ test_must_fail git restore --pathspec-from-file=list --pathspec-file-nul --source=HEAD^1
+'
+
+test_expect_success 'only touches what was listed' '
+ restore_checkpoint &&
+
+ printf "fileB.t\nfileC.t\n" | git restore --pathspec-from-file=- --source=HEAD^1 &&
+
+ cat >expect <<-\EOF &&
+ M fileB.t
+ M fileC.t
+ EOF
+ verify_expect
+'
+
+test_done
diff --git a/t/t3704-add-pathspec-file.sh b/t/t3704-add-pathspec-file.sh
new file mode 100755
index 0000000..3cfdb66
--- /dev/null
+++ b/t/t3704-add-pathspec-file.sh
@@ -0,0 +1,127 @@
+#!/bin/sh
+
+test_description='add --pathspec-from-file'
+
+. ./test-lib.sh
+
+test_tick
+
+test_expect_success setup '
+ test_commit file0 &&
+ echo A >fileA.t &&
+ echo B >fileB.t &&
+ echo C >fileC.t &&
+ echo D >fileD.t
+'
+
+restore_checkpoint () {
+ git reset
+}
+
+verify_expect () {
+ git status --porcelain --untracked-files=no -- fileA.t fileB.t fileC.t fileD.t >actual &&
+ test_cmp expect actual
+}
+
+test_expect_success '--pathspec-from-file from stdin' '
+ restore_checkpoint &&
+
+ echo fileA.t | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success '--pathspec-from-file from file' '
+ restore_checkpoint &&
+
+ echo fileA.t >list &&
+ git add --pathspec-from-file=list &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'NUL delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\0fileB.t\0" | git add --pathspec-from-file=- --pathspec-file-nul &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ A fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'LF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t\n" | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ A fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'no trailing delimiter' '
+ restore_checkpoint &&
+
+ printf "fileA.t\nfileB.t" | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ A fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'CRLF delimiters' '
+ restore_checkpoint &&
+
+ printf "fileA.t\r\nfileB.t\r\n" | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ A fileB.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileA.t
+ EOF
+ verify_expect
+'
+
+test_expect_success 'quotes not compatible with --pathspec-file-nul' '
+ restore_checkpoint &&
+
+ printf "\"file\\101.t\"" >list &&
+ test_must_fail git add --pathspec-from-file=list --pathspec-file-nul
+'
+
+test_expect_success 'only touches what was listed' '
+ restore_checkpoint &&
+
+ printf "fileB.t\nfileC.t\n" | git add --pathspec-from-file=- &&
+
+ cat >expect <<-\EOF &&
+ A fileB.t
+ A fileC.t
+ EOF
+ verify_expect
+'
+
+test_done
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index ec3eccf..93877ba 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -1438,6 +1438,8 @@ test_expect_success 'double dash "git checkout"' '
--no-guess Z
--no-... Z
--overlay Z
+ --pathspec-file-nul Z
+ --pathspec-from-file=Z
EOF
'