rm: support the --pathspec-from-file option

Decisions taken for simplicity:
1) It is not allowed to pass pathspec in both args and file.

Adjustments were needed for `if (!argc)` block:

This code actually means "pathspec is not present". Previously, pathspec
could only come from commandline arguments, so testing for `argc` was a
valid way of testing for the presence of pathspec. But this is no longer
true with `--pathspec-from-file`.

During the entire `--pathspec-from-file` story, I tried to keep its
behavior very close to giving pathspec on commandline, so that switching
from one to another doesn't involve any surprises.

However, throwing usage at user in the case of empty
`--pathspec-from-file` would puzzle because there's nothing wrong with
"usage" (that is, argc/argv array).

On the other hand, throwing usage in the old case also feels bad to me.
While it's less of a puzzle, I (as user) never liked the experience of
comparing my commandline to "usage", trying to spot a difference. Since
it's already known what the error is, it feels a lot better to give that
specific error to user.

Judging from [1] it doesn't seem that showing usage in this case was
important (the patch was to avoid segfault), and it doesn't fit into how
other commands react to empty pathspec (see for example `git add` with a
custom message).

Therefore, I decided to show new error text in both cases. In order to
continue testing for error early, I moved `parse_pathspec()` higher. Now
it happens before `read_cache()` / `hold_locked_index()` /
`setup_work_tree()`, which shouldn't cause any issues.

[1] Commit 7612a1ef ("git-rm: honor -n flag" 2006-06-09)

Signed-off-by: Alexandr Miloslavskiy <alexandr.miloslavskiy@syntevo.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Alexandr Miloslavskiy 2020-02-17 17:25:16 +00:00 committed by Junio C Hamano
parent 6a7aca6f01
commit 5f393dc3aa
3 changed files with 117 additions and 7 deletions

View File

@ -8,7 +8,9 @@ git-rm - Remove files from the working tree and from the index
SYNOPSIS
--------
[verse]
'git rm' [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch] [--quiet] [--] <pathspec>...
'git rm' [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch]
[--quiet] [--pathspec-from-file=<file> [--pathspec-file-nul]]
[--] [<pathspec>...]
DESCRIPTION
-----------
@ -73,6 +75,19 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
`git rm` normally outputs one line (in the form of an `rm` command)
for each file removed. This option suppresses that output.
--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).
REMOVING FILES THAT HAVE DISAPPEARED FROM THE FILESYSTEM
--------------------------------------------------------

View File

@ -235,7 +235,8 @@ static int check_local_mod(struct object_id *head, int index_only)
}
static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0;
static int ignore_unmatch = 0;
static int ignore_unmatch = 0, pathspec_file_nul;
static char *pathspec_from_file;
static struct option builtin_rm_options[] = {
OPT__DRY_RUN(&show_only, N_("dry run")),
@ -245,6 +246,8 @@ static struct option builtin_rm_options[] = {
OPT_BOOL('r', NULL, &recursive, N_("allow recursive removal")),
OPT_BOOL( 0 , "ignore-unmatch", &ignore_unmatch,
N_("exit with a zero status even if nothing matched")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),
OPT_END(),
};
@ -259,8 +262,24 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, builtin_rm_options,
builtin_rm_usage, 0);
if (!argc)
usage_with_options(builtin_rm_usage, builtin_rm_options);
parse_pathspec(&pathspec, 0,
PATHSPEC_PREFER_CWD,
prefix, argv);
if (pathspec_from_file) {
if (pathspec.nr)
die(_("--pathspec-from-file is incompatible with pathspec arguments"));
parse_pathspec_file(&pathspec, 0,
PATHSPEC_PREFER_CWD,
prefix, pathspec_from_file, pathspec_file_nul);
} else if (pathspec_file_nul) {
die(_("--pathspec-file-nul requires --pathspec-from-file"));
}
if (!pathspec.nr)
die(_("No pathspec was given. Which files should I remove?"));
if (!index_only)
setup_work_tree();
@ -270,9 +289,6 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
if (read_cache() < 0)
die(_("index file corrupt"));
parse_pathspec(&pathspec, 0,
PATHSPEC_PREFER_CWD,
prefix, argv);
refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &pathspec, NULL, NULL);
seen = xcalloc(pathspec.nr, 1);

79
t/t3601-rm-pathspec-file.sh Executable file
View File

@ -0,0 +1,79 @@
#!/bin/sh
test_description='rm --pathspec-from-file'
. ./test-lib.sh
test_tick
test_expect_success setup '
echo A >fileA.t &&
echo B >fileB.t &&
echo C >fileC.t &&
echo D >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 status --porcelain --untracked-files=no -- fileA.t fileB.t fileC.t fileD.t >actual &&
test_cmp expect actual
}
test_expect_success 'simplest' '
restore_checkpoint &&
cat >expect <<-\EOF &&
D fileA.t
EOF
echo fileA.t | git rm --pathspec-from-file=- &&
verify_expect
'
test_expect_success '--pathspec-file-nul' '
restore_checkpoint &&
cat >expect <<-\EOF &&
D fileA.t
D fileB.t
EOF
printf "fileA.t\0fileB.t\0" | git rm --pathspec-from-file=- --pathspec-file-nul &&
verify_expect
'
test_expect_success 'only touches what was listed' '
restore_checkpoint &&
cat >expect <<-\EOF &&
D fileB.t
D fileC.t
EOF
printf "fileB.t\nfileC.t\n" | git rm --pathspec-from-file=- &&
verify_expect
'
test_expect_success 'error conditions' '
restore_checkpoint &&
echo fileA.t >list &&
test_must_fail git rm --pathspec-from-file=list -- fileA.t 2>err &&
test_i18ngrep -e "--pathspec-from-file is incompatible with pathspec arguments" err &&
test_must_fail git rm --pathspec-file-nul 2>err &&
test_i18ngrep -e "--pathspec-file-nul requires --pathspec-from-file" err &&
>empty_list &&
test_must_fail git rm --pathspec-from-file=empty_list 2>err &&
test_i18ngrep -e "No pathspec was given. Which files should I remove?" err
'
test_done