In Git, what does `--` (dash dash) mean?

This question asks for a conceptual understanding of the double-dash across all git commands.

The double-dash, which signals the end of options, has been recognized as "not enough" for Git.

With Git 2.24 (Q3 2019), the command line parser learned the "--end-of-options" notation:

The standard convention for scripters to have hardcoded set of options first on the command line, and force the command to treat end-user input as non-options, has been to use "--" as the delimiter, but that would not work for commands that use "--" as a delimiter between revs and pathspec.

See commit 67feca3, commit 51b4594, commit 19e8789 (06 Aug 2019) by Jeff King (peff).
(Merged by Junio C Hamano -- gitster -- in commit 4a12f89, 09 Sep 2019)

revision: allow --end-of-options to end option parsing

There's currently no robust way to tell Git that a particular option is meant to be a revision, and not an option.
So if you have a branch "refs/heads/--foo", you cannot just say:

git rev-list --foo

You can say:

git rev-list refs/heads/--foo

But that breaks down if you don't know the refname, and in particular if you're a script passing along a value from elsewhere.
In most programs, you can use "--" to end option parsing, like this:

some-prog -- "$revision"

But that doesn't work for the revision parser, because "--" is already meaningful there: it separates revisions from pathspecs.
So we need some other marker to separate options from revisions.

This patch introduces "--end-of-options", which serves that purpose:

git rev-list --oneline --end-of-options "$revision"

will work regardless of what's in "$revision" (well, if you say "--" it may fail, but it won't do something dangerous, like triggering an unexpected option).

The name is verbose, but that's probably a good thing; this is meant to be used for scripted invocations where readability is more important than terseness.

One alternative would be to introduce an explicit option to mark a revision, like:

git rev-list --oneline --revision="$revision"

That's slightly more informative than this commit (because it makes even something silly like "--" unambiguous). But the pattern of using a separator like "--" is well established in git and in other commands, and it makes some scripting tasks simpler like:

git rev-list --end-of-options "$@"

parse-options: allow --end-of-options as a synonym for "--"

The revision option parser recently learned about --end-of-options, but that's not quite enough for all callers.
Some of them, like git-log, pick out some options using parse_options(), and then feed the remainder to setup_revisions().
For those cases we need to stop parse_options() from finding more options when it sees --end-of-options, and to retain that option in argv so that setup_revisions() can see it as well.

Let's handle this the same as we do "--". We can even piggy-back on the handling of PARSE_OPT_KEEP_DASHDASH, because any caller that wants to retain one will want to retain the other.

Example:

git update-ref refs/heads/--source HEAD &&\
git log --end-of-options --source

With Git 2.30 (Q1 2021), "git rev-parse"(man) learned the "--end-of-options" to help scripts to safely take a parameter that is supposed to be a revision, e.g. "git rev-parse --verify -q --end-of-options $rev(man)".

See commit 3a1f91c, commit 9033add, commit e05e2ae (10 Nov 2020) by Jeff King (peff).
(Merged by Junio C Hamano -- gitster -- in commit 0dd171f, 21 Nov 2020)

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

Signed-off-by: Jeff King

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, Git v2.24.0-rc0 -- merge listed in batch #2), 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 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, as it would otherwise see just "--foo".

git rev-parse now includes in its man page:

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.

$ git rev-parse --verify --end-of-options $REV^{commit}
$ git rev-parse --default master --verify --end-of-options $REV

With Git 2.31 (Q1 2021), "git mktag"(man) validates its input using its own rules before writing a tag object---it has been updated to share the logic with git fsck".

That means it also supports --end-of-options.

See commit 06ce791 (06 Jan 2021), commit 2aa9425, commit 3f390a3, commit 9a1a3a4, commit acfc013, commit 1f3299f, commit acf9de4, commit 40ef015, commit dfe3948, commit 0c43911, commit 692654d, commit 30f882c, commit ca9a1ed, commit 47c95e7, commit 3b9e4dd, commit 5c2303e, commit 317c176, commit 0d35ccb, commit b5ca549, commit aba5377, commit 18430ed (05 Jan 2021), and commit 9ce0fc3, commit f59b61d (23 Dec 2020) by Ævar Arnfjörð Bjarmason (avar).
(Merged by Junio C Hamano -- gitster -- in commit c7d6d41, 25 Jan 2021)

mktag: convert to parse-options

Signed-off-by: Ævar Arnfjörð Bjarmason

Convert the "mktag" command to use parse-options.h instead of its own ad-hoc argc handling.
This doesn't matter much in practice since it doesn't support any options, but removes another special-case in our codebase, and makes it easier to add options to it in the future.

It does marginally improve the situation for programs that want to execute git commands in a consistent manner and e.g. always use --end-of-options.
E.g.
"gitaly" does that, and has a blacklist of built-ins that don't support --end-of-options.
This is one less special case for it and other similar programs to support.


The double dash -- in git means different things to different commands, but in general it separates options from parameters.

In git specifically, the meaning of -- depends on which subcommand you are using it with. It usually separates subcommand arguments (like the branch name in git checkout) from revisions or filenames. Sometimes it is completely optional, and used only to prevent an unusual filename being interpreted as program options.

For Example

  • git checkout. To check out a "commit" (referred to as "tree-ish" in the manual, because you can actually specify a range of object types) you use

    git checkout <commit>

    To refine the checkout to just a file or two, use -- to separate the "tree-ish" parameters from the "filenames" you wish to check out.

  • git commit. To commit whatever is in the "index" (ie, what you have staged via git add, simply issue the git commit command.

    git commit [-m message]

    To ignore whatever you have added via git add and commit the changes in a specific file, use git commit -- <filename>

  • git add. To commit a file whose name begins with a - or a --, you must tell git add to stop reading parameters, and start reading filenames; -- does that.

    git add -- -sample.txt

  • git log. To see the commit history restricted to only commits affecting a file use

    git log -- filename

You need to check the man pages for any git command you use if you need to understand its specific meaning.

Tags:

Git