Skip to content

Commit a9aecc7

Browse files
SyntevoAlexgitster
authored andcommitted
checkout, restore: support the --pathspec-from-file option
Decisions taken for simplicity: 1) For now, `--pathspec-from-file` is declared incompatible with `--patch`, even when <file> is not `stdin`. Such use case it not really expected. 2) It is not allowed to pass pathspec in both args and file. `you must specify path(s) to restore` block was moved down to be able to test for `pathspec.nr` instead, because testing for `argc` is no longer correct. `git switch` does not support the new options because it doesn't expect `<pathspec>` arguments. Signed-off-by: Alexandr Miloslavskiy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent cfd9376 commit a9aecc7

6 files changed

+336
-4
lines changed

Documentation/git-checkout.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SYNOPSIS
1313
'git checkout' [-q] [-f] [-m] [--detach] <commit>
1414
'git checkout' [-q] [-f] [-m] [[-b|-B|--orphan] <new_branch>] [<start_point>]
1515
'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...
16+
'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]
1617
'git checkout' (-p|--patch) [<tree-ish>] [--] [<pathspec>...]
1718

1819
DESCRIPTION
@@ -79,6 +80,7 @@ be used to detach `HEAD` at the tip of the branch (`git checkout
7980
Omitting `<branch>` detaches `HEAD` at the tip of the current branch.
8081

8182
'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...::
83+
'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] --pathspec-from-file=<file> [--pathspec-file-nul]::
8284

8385
Overwrite the contents of the files that match the pathspec.
8486
When the `<tree-ish>` (most often a commit) is not given,
@@ -306,6 +308,19 @@ Note that this option uses the no overlay mode by default (see also
306308
working tree, but not in `<tree-ish>` are removed, to make them
307309
match `<tree-ish>` exactly.
308310

311+
--pathspec-from-file=<file>::
312+
Pathspec is passed in `<file>` instead of commandline args. If
313+
`<file>` is exactly `-` then standard input is used. Pathspec
314+
elements are separated by LF or CR/LF. Pathspec elements can be
315+
quoted as explained for the configuration variable `core.quotePath`
316+
(see linkgit:git-config[1]). See also `--pathspec-file-nul` and
317+
global `--literal-pathspecs`.
318+
319+
--pathspec-file-nul::
320+
Only meaningful with `--pathspec-from-file`. Pathspec elements are
321+
separated with NUL character and all other characters are taken
322+
literally (including newlines and quotes).
323+
309324
<branch>::
310325
Branch to checkout; if it refers to a branch (i.e., a name that,
311326
when prepended with "refs/heads/", is a valid ref), then that

Documentation/git-restore.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SYNOPSIS
99
--------
1010
[verse]
1111
'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] [--] <pathspec>...
12+
'git restore' [<options>] [--source=<tree>] [--staged] [--worktree] --pathspec-from-file=<file> [--pathspec-file-nul]
1213
'git restore' (-p|--patch) [<options>] [--source=<tree>] [--staged] [--worktree] [--] [<pathspec>...]
1314

1415
DESCRIPTION
@@ -113,6 +114,19 @@ in linkgit:git-checkout[1] for details.
113114
appear in the `--source` tree are removed, to make them match
114115
`<tree>` exactly. The default is no-overlay mode.
115116

117+
--pathspec-from-file=<file>::
118+
Pathspec is passed in `<file>` instead of commandline args. If
119+
`<file>` is exactly `-` then standard input is used. Pathspec
120+
elements are separated by LF or CR/LF. Pathspec elements can be
121+
quoted as explained for the configuration variable `core.quotePath`
122+
(see linkgit:git-config[1]). See also `--pathspec-file-nul` and
123+
global `--literal-pathspecs`.
124+
125+
--pathspec-file-nul::
126+
Only meaningful with `--pathspec-from-file`. Pathspec elements are
127+
separated with NUL character and all other characters are taken
128+
literally (including newlines and quotes).
129+
116130
\--::
117131
Do not interpret any more arguments as options.
118132

builtin/checkout.c

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ struct checkout_opts {
7070
int checkout_worktree;
7171
const char *ignore_unmerged_opt;
7272
int ignore_unmerged;
73+
int pathspec_file_nul;
74+
const char *pathspec_from_file;
7375

7476
const char *new_branch;
7577
const char *new_branch_force;
@@ -1480,6 +1482,8 @@ static struct option *add_checkout_path_options(struct checkout_opts *opts,
14801482
OPT_BOOL('p', "patch", &opts->patch_mode, N_("select hunks interactively")),
14811483
OPT_BOOL(0, "ignore-skip-worktree-bits", &opts->ignore_skipworktree,
14821484
N_("do not limit pathspecs to sparse entries only")),
1485+
OPT_PATHSPEC_FROM_FILE(&opts->pathspec_from_file),
1486+
OPT_PATHSPEC_FILE_NUL(&opts->pathspec_file_nul),
14831487
OPT_END()
14841488
};
14851489
struct option *newopts = parse_options_concat(prevopts, options);
@@ -1618,10 +1622,6 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
16181622
die(_("reference is not a tree: %s"), opts->from_treeish);
16191623
}
16201624

1621-
if (opts->accept_pathspec && !opts->empty_pathspec_ok && !argc &&
1622-
!opts->patch_mode) /* patch mode is special */
1623-
die(_("you must specify path(s) to restore"));
1624-
16251625
if (argc) {
16261626
parse_pathspec(&opts->pathspec, 0,
16271627
opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0,
@@ -1641,10 +1641,33 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
16411641
if (opts->force_detach)
16421642
die(_("git checkout: --detach does not take a path argument '%s'"),
16431643
argv[0]);
1644+
}
1645+
1646+
if (opts->pathspec_from_file) {
1647+
if (opts->pathspec.nr)
1648+
die(_("--pathspec-from-file is incompatible with pathspec arguments"));
1649+
1650+
if (opts->force_detach)
1651+
die(_("--pathspec-from-file is incompatible with --detach"));
16441652

1653+
if (opts->patch_mode)
1654+
die(_("--pathspec-from-file is incompatible with --patch"));
1655+
1656+
parse_pathspec_file(&opts->pathspec, 0,
1657+
0,
1658+
prefix, opts->pathspec_from_file, opts->pathspec_file_nul);
1659+
} else if (opts->pathspec_file_nul) {
1660+
die(_("--pathspec-file-nul requires --pathspec-from-file"));
1661+
}
1662+
1663+
if (opts->pathspec.nr) {
16451664
if (1 < !!opts->writeout_stage + !!opts->force + !!opts->merge)
16461665
die(_("git checkout: --ours/--theirs, --force and --merge are incompatible when\n"
16471666
"checking out of the index."));
1667+
} else {
1668+
if (opts->accept_pathspec && !opts->empty_pathspec_ok &&
1669+
!opts->patch_mode) /* patch mode is special */
1670+
die(_("you must specify path(s) to restore"));
16481671
}
16491672

16501673
if (opts->new_branch) {

t/t2026-checkout-pathspec-file.sh

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/bin/sh
2+
3+
test_description='checkout --pathspec-from-file'
4+
5+
. ./test-lib.sh
6+
7+
test_tick
8+
9+
test_expect_success setup '
10+
test_commit file0 &&
11+
12+
echo 1 >fileA.t &&
13+
echo 1 >fileB.t &&
14+
echo 1 >fileC.t &&
15+
echo 1 >fileD.t &&
16+
git add fileA.t fileB.t fileC.t fileD.t &&
17+
git commit -m "files 1" &&
18+
19+
echo 2 >fileA.t &&
20+
echo 2 >fileB.t &&
21+
echo 2 >fileC.t &&
22+
echo 2 >fileD.t &&
23+
git add fileA.t fileB.t fileC.t fileD.t &&
24+
git commit -m "files 2" &&
25+
26+
git tag checkpoint
27+
'
28+
29+
restore_checkpoint () {
30+
git reset --hard checkpoint
31+
}
32+
33+
verify_expect () {
34+
git status --porcelain --untracked-files=no -- fileA.t fileB.t fileC.t fileD.t >actual &&
35+
test_cmp expect actual
36+
}
37+
38+
test_expect_success '--pathspec-from-file from stdin' '
39+
restore_checkpoint &&
40+
41+
echo fileA.t | git checkout --pathspec-from-file=- HEAD^1 &&
42+
43+
cat >expect <<-\EOF &&
44+
M fileA.t
45+
EOF
46+
verify_expect
47+
'
48+
49+
test_expect_success '--pathspec-from-file from file' '
50+
restore_checkpoint &&
51+
52+
echo fileA.t >list &&
53+
git checkout --pathspec-from-file=list HEAD^1 &&
54+
55+
cat >expect <<-\EOF &&
56+
M fileA.t
57+
EOF
58+
verify_expect
59+
'
60+
61+
test_expect_success 'NUL delimiters' '
62+
restore_checkpoint &&
63+
64+
printf "fileA.t\0fileB.t\0" | git checkout --pathspec-from-file=- --pathspec-file-nul HEAD^1 &&
65+
66+
cat >expect <<-\EOF &&
67+
M fileA.t
68+
M fileB.t
69+
EOF
70+
verify_expect
71+
'
72+
73+
test_expect_success 'LF delimiters' '
74+
restore_checkpoint &&
75+
76+
printf "fileA.t\nfileB.t\n" | git checkout --pathspec-from-file=- HEAD^1 &&
77+
78+
cat >expect <<-\EOF &&
79+
M fileA.t
80+
M fileB.t
81+
EOF
82+
verify_expect
83+
'
84+
85+
test_expect_success 'no trailing delimiter' '
86+
restore_checkpoint &&
87+
88+
printf "fileA.t\nfileB.t" | git checkout --pathspec-from-file=- HEAD^1 &&
89+
90+
cat >expect <<-\EOF &&
91+
M fileA.t
92+
M fileB.t
93+
EOF
94+
verify_expect
95+
'
96+
97+
test_expect_success 'CRLF delimiters' '
98+
restore_checkpoint &&
99+
100+
printf "fileA.t\r\nfileB.t\r\n" | git checkout --pathspec-from-file=- HEAD^1 &&
101+
102+
cat >expect <<-\EOF &&
103+
M fileA.t
104+
M fileB.t
105+
EOF
106+
verify_expect
107+
'
108+
109+
test_expect_success 'quotes' '
110+
restore_checkpoint &&
111+
112+
printf "\"file\\101.t\"" | git checkout --pathspec-from-file=- HEAD^1 &&
113+
114+
cat >expect <<-\EOF &&
115+
M fileA.t
116+
EOF
117+
verify_expect
118+
'
119+
120+
test_expect_success 'quotes not compatible with --pathspec-file-nul' '
121+
restore_checkpoint &&
122+
123+
printf "\"file\\101.t\"" >list &&
124+
test_must_fail git checkout --pathspec-from-file=list --pathspec-file-nul HEAD^1
125+
'
126+
127+
test_expect_success 'only touches what was listed' '
128+
restore_checkpoint &&
129+
130+
printf "fileB.t\nfileC.t\n" | git checkout --pathspec-from-file=- HEAD^1 &&
131+
132+
cat >expect <<-\EOF &&
133+
M fileB.t
134+
M fileC.t
135+
EOF
136+
verify_expect
137+
'
138+
139+
test_done

0 commit comments

Comments
 (0)