rev-parse: handle --end-of-options

We taught rev-list a new way to separate options from revisions in
19e8789b23 (revision: allow --end-of-options to end option parsing,
2019-08-06), but rev-parse uses its own parser. It should know about
--end-of-options not only for consistency, but because it may be
presented with similarly ambiguous cases. E.g., if a caller does:

  git rev-parse "$rev" -- "$path"

to parse an untrusted input, then it will get confused if $rev contains
an option-like string like "--local-env-vars". Or even "--not-real",
which we'd keep as an option to pass along to rev-list.

Or even more importantly:

  git rev-parse --verify "$rev"

can be confused by options, even though its purpose is safely parsing
untrusted input. On the plus side, it will always fail the --verify
part, as it will not have parsed a revision, so the caller will
generally "fail closed" rather than continue to use the untrusted
string. But it will still trigger whatever option was in "$rev"; this
should be mostly harmless, since rev-parse options are all read-only,
but I didn't carefully audit all paths.

This patch lets callers write:

  git rev-parse --end-of-options "$rev" -- "$path"

and:

  git rev-parse --verify --end-of-options "$rev"

which will both treat "$rev" always as a revision parameter. The latter
is a bit clunky. It would be nicer if we had defined "--verify" to
require that its next argument be the revision. But we have not
historically done so, and:

  git rev-parse --verify -q "$rev"

does currently work. I added a test here to confirm that we didn't break
that.

A few implementation notes:

 - We don't document --end-of-options explicitly in commands, but rather
   in gitcli(7). So I didn't give it its own section in git-rev-parse(1).
   But I did call it out specifically in the --verify section, and
   include it in the examples, which should show best practices.

 - We don't have to re-indent the main option-parsing block, because we
   can combine our "did we see end of options" check with "does it start
   with a dash". The exception is the pre-setup options, which need
   their own block.

 - We do however have to pull the "--" parsing out of the "does it start
   with dash" block, because we want to parse it even if we've seen
   --end-of-options.

 - We'll leave "--end-of-options" in the output. This is probably not
   technically necessary, as a careful caller will do:

     git rev-parse --end-of-options $revs -- $paths

   and anything in $revs will be resolved to an object id. However, it
   does help a slightly less careful caller like:

     git rev-parse --end-of-options $revs_or_paths

   where a path "--foo" will remain in the output as long as it also
   exists on disk. In that case, it's helpful to retain --end-of-options
   to get passed along to rev-list, s it would otherwise see just
   "--foo".

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Jeff King 2020-11-10 16:40:19 -05:00 коммит произвёл Junio C Hamano
Родитель 9033addfa6
Коммит 3a1f91cfd9
4 изменённых файлов: 68 добавлений и 25 удалений

Просмотреть файл

@ -109,6 +109,10 @@ names an existing object that is a commit-ish (i.e. a commit, or an
annotated tag that points at a commit). To make sure that `$VAR`
names an existing object of any type, `git rev-parse "$VAR^{object}"`
can be used.
+
Note that if you are verifying a name from an untrusted source, it is
wise to use `--end-of-options` so that the name argument is not mistaken
for another option.
-q::
--quiet::
@ -446,7 +450,7 @@ $ git rev-parse --verify HEAD
* Print the commit object name from the revision in the $REV shell variable:
+
------------
$ git rev-parse --verify $REV^{commit}
$ git rev-parse --verify --end-of-options $REV^{commit}
------------
+
This will error out if $REV is empty or not a valid revision.
@ -454,7 +458,7 @@ This will error out if $REV is empty or not a valid revision.
* Similar to above:
+
------------
$ git rev-parse --default master --verify $REV
$ git rev-parse --default master --verify --end-of-options $REV
------------
+
but if $REV is empty, the commit object name from master will be printed.

Просмотреть файл

@ -595,6 +595,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
struct object_context unused;
struct strbuf buf = STRBUF_INIT;
const int hexsz = the_hash_algo->hexsz;
int seen_end_of_options = 0;
if (argc > 1 && !strcmp("--parseopt", argv[1]))
return cmd_parseopt(argc - 1, argv + 1, prefix);
@ -628,21 +629,23 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
continue;
}
if (!strcmp(arg, "--local-env-vars")) {
int i;
for (i = 0; local_repo_env[i]; i++)
printf("%s\n", local_repo_env[i]);
continue;
}
if (!strcmp(arg, "--resolve-git-dir")) {
const char *gitdir = argv[++i];
if (!gitdir)
die("--resolve-git-dir requires an argument");
gitdir = resolve_gitdir(gitdir);
if (!gitdir)
die("not a gitdir '%s'", argv[i]);
puts(gitdir);
continue;
if (!seen_end_of_options) {
if (!strcmp(arg, "--local-env-vars")) {
int i;
for (i = 0; local_repo_env[i]; i++)
printf("%s\n", local_repo_env[i]);
continue;
}
if (!strcmp(arg, "--resolve-git-dir")) {
const char *gitdir = argv[++i];
if (!gitdir)
die("--resolve-git-dir requires an argument");
gitdir = resolve_gitdir(gitdir);
if (!gitdir)
die("not a gitdir '%s'", argv[i]);
puts(gitdir);
continue;
}
}
/* The rest of the options require a git repository. */
@ -652,14 +655,15 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
did_repo_setup = 1;
}
if (*arg == '-') {
if (!strcmp(arg, "--")) {
as_is = 2;
/* Pass on the "--" if we show anything but files.. */
if (filter & (DO_FLAGS | DO_REVS))
show_file(arg, 0);
continue;
}
if (!strcmp(arg, "--")) {
as_is = 2;
/* Pass on the "--" if we show anything but files.. */
if (filter & (DO_FLAGS | DO_REVS))
show_file(arg, 0);
continue;
}
if (!seen_end_of_options && *arg == '-') {
if (!strcmp(arg, "--git-path")) {
if (!argv[i + 1])
die("--git-path requires an argument");
@ -937,6 +941,12 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
puts(the_hash_algo->name);
continue;
}
if (!strcmp(arg, "--end-of-options")) {
seen_end_of_options = 1;
if (filter & (DO_FLAGS | DO_REVS))
show_file(arg, 0);
continue;
}
if (show_flag(arg) && verify)
die_no_single_rev(quiet);
continue;

Просмотреть файл

@ -144,4 +144,17 @@ test_expect_success SYMLINKS 'ref resolution not confused by broken symlinks' '
test_must_fail git rev-parse --verify broken
'
test_expect_success 'options can appear after --verify' '
git rev-parse --verify HEAD >expect &&
git rev-parse --verify -q HEAD >actual &&
test_cmp expect actual
'
test_expect_success 'verify respects --end-of-options' '
git update-ref refs/heads/-tricky HEAD &&
git rev-parse --verify HEAD >expect &&
git rev-parse --verify --end-of-options -tricky >actual &&
test_cmp expect actual
'
test_done

Просмотреть файл

@ -263,4 +263,20 @@ test_expect_success 'arg after dashdash not interpreted as option' '
test_cmp expect actual
'
test_expect_success 'arg after end-of-options not interpreted as option' '
test_must_fail git rev-parse --end-of-options --not-real -- 2>err &&
test_i18ngrep bad.revision.*--not-real err
'
test_expect_success 'end-of-options still allows --' '
cat >expect <<-EOF &&
--end-of-options
$(git rev-parse --verify HEAD)
--
path
EOF
git rev-parse --end-of-options HEAD -- path >actual &&
test_cmp expect actual
'
test_done