Merge branch 'am/pathspec-f-f-more'

"git rm" and "git stash" learns the new "--pathspec-from-file"
option.

* am/pathspec-f-f-more:
  stash push: support the --pathspec-from-file option
  stash: eliminate crude option parsing
  doc: stash: synchronize <pathspec> description
  doc: stash: document more options
  doc: stash: split options from description (2)
  doc: stash: split options from description (1)
  rm: support the --pathspec-from-file option
  doc: rm: synchronize <pathspec> description
This commit is contained in:
Junio C Hamano 2020-03-09 11:21:19 -07:00
commit 9b7f726dfc
7 changed files with 381 additions and 115 deletions

View File

@ -8,16 +8,18 @@ git-rm - Remove files from the working tree and from the index
SYNOPSIS
--------
[verse]
'git rm' [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch] [--quiet] [--] <file>...
'git rm' [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch]
[--quiet] [--pathspec-from-file=<file> [--pathspec-file-nul]]
[--] [<pathspec>...]
DESCRIPTION
-----------
Remove files from the index, or from the working tree and the index.
`git rm` will not remove a file from just your working directory.
(There is no option to remove a file only from the working tree
and yet keep it in the index; use `/bin/rm` if you want to do that.)
The files being removed have to be identical to the tip of the branch,
and no updates to their contents can be staged in the index,
Remove files matching pathspec from the index, or from the working tree
and the index. `git rm` will not remove a file from just your working
directory. (There is no option to remove a file only from the working
tree and yet keep it in the index; use `/bin/rm` if you want to do
that.) The files being removed have to be identical to the tip of the
branch, and no updates to their contents can be staged in the index,
though that default behavior can be overridden with the `-f` option.
When `--cached` is given, the staged content has to
match either the tip of the branch or the file on disk,
@ -26,15 +28,20 @@ allowing the file to be removed from just the index.
OPTIONS
-------
<file>...::
Files to remove. Fileglobs (e.g. `*.c`) can be given to
remove all matching files. If you want Git to expand
file glob characters, you may need to shell-escape them.
A leading directory name
(e.g. `dir` to remove `dir/file1` and `dir/file2`) can be
given to remove all files in the directory, and recursively
all sub-directories,
but this requires the `-r` option to be explicitly given.
<pathspec>...::
Files to remove. A leading directory name (e.g. `dir` to remove
`dir/file1` and `dir/file2`) can be given to remove all files in
the directory, and recursively all sub-directories, but this
requires the `-r` option to be explicitly given.
+
The command removes only the paths that are known to Git.
+
File globbing matches across directory boundaries. Thus, given two
directories `d` and `d2`, there is a difference between using
`git rm 'd*'` and `git rm 'd/*'`, as the former will also remove all
of directory `d2`.
+
For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
-f::
--force::
@ -68,19 +75,19 @@ OPTIONS
`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`.
DISCUSSION
----------
--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).
The <file> list given to the command can be exact pathnames,
file glob patterns, or leading directory names. The command
removes only the paths that are known to Git. Giving the name of
a file that you have not told Git about does not remove that file.
File globbing matches across directory boundaries. Thus, given
two directories `d` and `d2`, there is a difference between
using `git rm 'd*'` and `git rm 'd/*'`, as the former will
also remove all of directory `d2`.
REMOVING FILES THAT HAVE DISAPPEARED FROM THE FILESYSTEM
--------------------------------------------------------

View 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>]
@ -43,10 +44,10 @@ created stash, `stash@{1}` is the one before it, `stash@{2.hours.ago}`
is also possible). Stashes may also be referenced by specifying just the
stash index (e.g. the integer `n` is equivalent to `stash@{n}`).
OPTIONS
-------
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).
@ -56,38 +57,13 @@ push [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q
For quickly making a snapshot, you can omit "push". In this mode,
non-option arguments are not allowed to prevent a misspelled
subcommand from making an unwanted stash entry. The two exceptions to this
are `stash -p` which acts as alias for `stash push -p` and pathspecs,
are `stash -p` which acts as alias for `stash push -p` and pathspec elements,
which are allowed after a double hyphen `--` for disambiguation.
+
When pathspec is given to 'git stash push', the new stash entry 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.
+
If the `--include-untracked` option is used, all untracked files are also
stashed and then cleaned up with `git clean`, leaving the working directory
in a very clean state. If the `--all` option is used instead then the
ignored files are stashed and cleaned in addition to the untracked files.
+
With `--patch`, you can interactively select hunks from the diff
between HEAD and the working tree to be stashed. The stash entry is
constructed such that its index state is the same as the index state
of your repository, and its worktree contains only the changes you
selected interactively. The selected changes are then rolled back
from your worktree. See the ``Interactive Mode'' section of
linkgit:git-add[1] to learn how to operate the `--patch` mode.
+
The `--patch` option implies `--keep-index`. You can use
`--no-keep-index` to override this.
save [-p|--patch] [-k|--[no-]keep-index] [-u|--include-untracked] [-a|--all] [-q|--quiet] [<message>]::
This option is deprecated in favour of 'git stash push'. It
differs from "stash push" in that it cannot take pathspecs.
differs from "stash push" in that it cannot take pathspec.
Instead, all non-option arguments are concatenated to form the stash
message.
@ -111,7 +87,7 @@ show [<options>] [<stash>]::
Show the changes recorded in the stash entry as a diff between the
stashed contents and the commit back when the stash entry was first
created. When no `<stash>` is given, it shows the latest one.
created.
By default, the command shows the diffstat, but it will accept any
format known to 'git diff' (e.g., `git stash show -p stash@{1}`
to view the second most recent entry in patch form).
@ -128,14 +104,6 @@ pop [--index] [-q|--quiet] [<stash>]::
Applying the state can fail with conflicts; in this case, it is not
removed from the stash list. You need to resolve the conflicts by hand
and call `git stash drop` manually afterwards.
+
If the `--index` option is used, then tries to reinstate not only the working
tree's changes, but also the index's ones. However, this can fail, when you
have conflicts (which are stored in the index, where you therefore can no
longer apply the changes as they were originally).
+
When no `<stash>` is given, `stash@{0}` is assumed, otherwise `<stash>` must
be a reference of the form `stash@{<revision>}`.
apply [--index] [-q|--quiet] [<stash>]::
@ -149,8 +117,7 @@ branch <branchname> [<stash>]::
the commit at which the `<stash>` was originally created, applies the
changes recorded in `<stash>` to the new working tree and index.
If that succeeds, and `<stash>` is a reference of the form
`stash@{<revision>}`, it then drops the `<stash>`. When no `<stash>`
is given, applies the latest one.
`stash@{<revision>}`, it then drops the `<stash>`.
+
This is useful if the branch on which you ran `git stash push` has
changed enough that `git stash apply` fails due to conflicts. Since
@ -166,9 +133,6 @@ clear::
drop [-q|--quiet] [<stash>]::
Remove a single stash entry from the list of stash entries.
When no `<stash>` is given, it removes the latest one.
i.e. `stash@{0}`, otherwise `<stash>` must be a valid stash
log reference of the form `stash@{<revision>}`.
create::
@ -185,6 +149,98 @@ store::
reflog. This is intended to be useful for scripts. It is
probably not the command you want to use; see "push" above.
OPTIONS
-------
-a::
--all::
This option is only valid for `push` and `save` commands.
+
All ignored and untracked files are also stashed and then cleaned
up with `git clean`.
-u::
--include-untracked::
This option is only valid for `push` and `save` commands.
+
All untracked files are also stashed and then cleaned up with
`git clean`.
--index::
This option is only valid for `pop` and `apply` commands.
+
Tries to reinstate not only the working tree's changes, but also
the index's ones. However, this can fail, when you have conflicts
(which are stored in the index, where you therefore can no longer
apply the changes as they were originally).
-k::
--keep-index::
--no-keep-index::
This option is only valid for `push` and `save` commands.
+
All changes already added to the index are left intact.
-p::
--patch::
This option is only valid for `push` and `save` commands.
+
Interactively select hunks from the diff between HEAD and the
working tree to be stashed. The stash entry is constructed such
that its index state is the same as the index state of your
repository, and its worktree contains only the changes you selected
interactively. The selected changes are then rolled back from your
worktree. See the ``Interactive Mode'' section of linkgit:git-add[1]
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`,
`save`, `store` commands.
+
Quiet, suppress feedback messages.
\--::
This option is only valid for `push` command.
+
Separates pathspec from options for disambiguation purposes.
<pathspec>...::
This option is only valid for `push` command.
+
The new stash entry 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.
+
For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
<stash>::
This option is only valid for `apply`, `branch`, `drop`, `pop`,
`show` commands.
+
A reference of the form `stash@{<revision>}`. When no `<stash>` is
given, the latest stash is assumed (that is, `stash@{0}`).
DISCUSSION
----------

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);

View 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>]"),
@ -1451,13 +1452,17 @@ done:
return ret;
}
static int push_stash(int argc, const char **argv, const char *prefix)
static int push_stash(int argc, const char **argv, const char *prefix,
int push_assumed)
{
int force_assume = 0;
int keep_index = -1;
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,
@ -1471,16 +1476,45 @@ 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()
};
if (argc)
if (argc) {
force_assume = !strcmp(argv[0], "-p");
argc = parse_options(argc, argv, prefix, options,
git_stash_push_usage,
0);
PARSE_OPT_KEEP_DASHDASH);
}
if (argc) {
if (!strcmp(argv[0], "--")) {
argc--;
argv++;
} else if (push_assumed && !force_assume) {
die("subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'",
argv[0]);
}
}
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);
}
@ -1550,7 +1584,6 @@ static int use_builtin_stash(void)
int cmd_stash(int argc, const char **argv, const char *prefix)
{
int i = -1;
pid_t pid = getpid();
const char *index_file;
struct argv_array args = ARGV_ARRAY_INIT;
@ -1583,7 +1616,7 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
(uintmax_t)pid);
if (!argc)
return !!push_stash(0, NULL, prefix);
return !!push_stash(0, NULL, prefix, 0);
else if (!strcmp(argv[0], "apply"))
return !!apply_stash(argc, argv, prefix);
else if (!strcmp(argv[0], "clear"))
@ -1603,45 +1636,15 @@ int cmd_stash(int argc, const char **argv, const char *prefix)
else if (!strcmp(argv[0], "create"))
return !!create_stash(argc, argv, prefix);
else if (!strcmp(argv[0], "push"))
return !!push_stash(argc, argv, prefix);
return !!push_stash(argc, argv, prefix, 0);
else if (!strcmp(argv[0], "save"))
return !!save_stash(argc, argv, prefix);
else if (*argv[0] != '-')
usage_msg_opt(xstrfmt(_("unknown subcommand: %s"), argv[0]),
git_stash_usage, options);
if (strcmp(argv[0], "-p")) {
while (++i < argc && strcmp(argv[i], "--")) {
/*
* `akpqu` is a string which contains all short options,
* except `-m` which is verified separately.
*/
if ((strlen(argv[i]) == 2) && *argv[i] == '-' &&
strchr("akpqu", argv[i][1]))
continue;
if (!strcmp(argv[i], "--all") ||
!strcmp(argv[i], "--keep-index") ||
!strcmp(argv[i], "--no-keep-index") ||
!strcmp(argv[i], "--patch") ||
!strcmp(argv[i], "--quiet") ||
!strcmp(argv[i], "--include-untracked"))
continue;
/*
* `-m` and `--message=` are verified separately because
* they need to be immediately followed by a string
* (i.e.`-m"foobar"` or `--message="foobar"`).
*/
if (starts_with(argv[i], "-m") ||
starts_with(argv[i], "--message="))
continue;
usage_with_options(git_stash_usage, options);
}
}
/* Assume 'stash push' */
argv_array_push(&args, "push");
argv_array_pushv(&args, argv);
return !!push_stash(args.argc, args.argv, prefix);
return !!push_stash(args.argc, args.argv, prefix, 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

View File

@ -285,6 +285,11 @@ test_expect_success 'stash --no-keep-index' '
test bar,bar2 = $(cat file),$(cat file2)
'
test_expect_success 'dont assume push with non-option args' '
test_must_fail git stash -q drop 2>err &&
test_i18ngrep -e "subcommand wasn'\''t specified; '\''push'\'' can'\''t be assumed due to unexpected token '\''drop'\''" err
'
test_expect_success 'stash --invalid-option' '
echo bar5 >file &&
echo bar6 >file2 &&

100
t/t3909-stash-pathspec-file.sh Executable file
View File

@ -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