3

I feel a little confused about these two option when using "git checkout"

I always used git checkout -- . to clear my working directory.

but today when I type git checkout - . by mistake. I found no error alert from git.

I have read the git documentation, and also do not know what this - option does. And this is hard to google.

So please anyone have any idea about this?

torek
  • 448,244
  • 59
  • 642
  • 775
da_d
  • 33
  • 4
  • 2
    One possibility is that it didn't do anything and was just ignored. Did something happen to cause you a concern? – Tim Biegeleisen May 16 '17 at 12:12
  • When dealing with `checkout`, a good diagnostic is to run `git status` (because it shows you the types of thing a checkout would change). Beyond that, the definitive answer is Marth's below. – Mark Adelsberger May 16 '17 at 12:59

3 Answers3

7

When using git checkout, you can use - as a shorthand for @{-1}.
From man git-checkout (emphasis on the second paragraph):

<branch>
    Branch to checkout; if it refers to a branch (i.e., a name that, when
    prepended with "refs/heads/", is a valid ref), then that branch is checked
    out. Otherwise, if it refers to a valid commit, your HEAD becomes
    "detached" and you are no longer on any branch (see below for details).

    As a special case, the "@{-N}" syntax for the N-th last branch/commit
    checks out branches (instead of detaching). You may also specify - which is
    synonymous with "@{-1}".

Trying this out in an empty repo by creating a file in branch master, modifying it in another branch, and using checkout - from master:

$ git init
Initialized empty Git repository in workspace/git-test/.git/

git:(master)  $ echo a > a
git:(master*) $ git add a 
git:(master*) $ git commit
[master (root-commit) 8433343] Add a to master
 1 file changed, 1 insertion(+)
 create mode 100644 a

git:(master)  $ git checkout -b other
Switched to a new branch 'other'

git:(other)   $ echo b > a
git:(other*)  $ git add a 
git:(other*)  $ git commit
[other be2298f] Replace a by b
 1 file changed, 1 insertion(+), 1 deletion(-)

git:(other)   $ git checkout -
Switched to branch 'master'

git:(master)  $ git checkout -- . # no changes (no * next to master in the line below)
git:(master)  $ git checkout - . # checking out files from alternate branch 'other'
git:(master*) $ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   a

git:(master*) $ cat a
b
Marth
  • 23,920
  • 3
  • 60
  • 72
  • 1
    Amending my previous comment after more testing: A strict reading of the syntax says this *shouldn't* work, but under suitable circumstances it does. (My previous tests I couldn't get this syntax to work at all, but my test repo didn't have enough reflog information to make it work at that time and the error message was misleading.) – Mark Adelsberger May 16 '17 at 12:50
  • @MarkAdelsberger: You're right, I hadn't noticed that the `` argument in the man page is not available with ``. But according to [this answer](https://stackoverflow.com/questions/23303549/what-are-commit-ish-and-tree-ish-in-git), `@{-1}` is "tree-ish", so I think it should work? Works here with git 2.10.1 – Marth May 16 '17 at 12:50
  • Oh, I see... you're right. `@{-1}` is a more general syntax, and what the `git checkout` doc is saying is that it's handled in a slightly special way when it's used as a `` argument. It works when used as a `` argument (i.e. with a pathspec), but that special handling doesn't apply. So this is almost certainly the behavior OP encountered; nice catch – Mark Adelsberger May 16 '17 at 12:53
4

UPDATE - I can reproduce the behavior now, but that's because I followed the information Marth provided in their answer.

The main thing I would add is: for diagnostic purposes if you're not sure the effect of a git command like checkout, use git status and see what's what. Since checkout mostly affects the index and working tree, you should be able to at least see what the command changed that way.

Summary is you've stumbled on a less common syntax that almost certainly didn't do what you intended (and certainly can't be counted on to do what you were meaning to do).


Original answer

I am unable to reproduce the behavior you describe. Specifically, if I say git checkout - . I get an error, a non-0 return code, and no checkout occurs.

The - should be interpreted as a path spec (and the error I get reflects this); the other possibility would be a "tree-ish" (which means a branch, tag, commit ID, etc.); but - isn't a valid branch name.

So if you happen to have a tracked file named - in your working directory, git will just refresh that file and report no warning/error.

I'm not sure what you mean about "hard to google"; if you need help on a git command, just google the command (e.g. 'git checkout`) and, at least in my experience, this should lead to the definitive git documentation for that command (which includes the command syntax, which should tell you how a given argument will be interpreted).

In this case, when I google git checkout the first result is https://git-scm.com/docs/git-checkout

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • thanks firstly. I feel sorry about that I did not post what I saw when I type "git checkout - ." . because firstly my workspace is clear, then after I type the command, I have lots of files not to commit in index. I didn't know what happened and I didn't know how to describe it. – da_d May 17 '17 at 08:31
3

As in Marth's answer (which is correct, and I've upvoted it), we start with the notion that git checkout - is a synonym for git checkout @{-1}, which tells Git: Look at my HEAD reflog and find which branch I was on before I ran git checkout branchname, and check that branch out again. The general idea behind this syntax is that you can do:

$ git checkout feature/X
... hack for a while ...
$ git commit -m "save some temporary work"
$ git checkout bug1234
... do a quick fix or look for something ...
$ git checkout -
... now back on feature/X ...

Unfortunately, this combines poorly with the general git checkout [ tree-ish ] [ -- ] path [path ...] command format, which, as I think you know, means:

  • If tree-ish is specified, look it up as a tree (i.e., if it's a commit, convert it to the commit's source tree; if it's a tag, peel the tag until we arrive at a tree). Otherwise (no tree-ish specified), extract files from the index.
  • --: This part is optional and serves to separate the tree-ish, if present, from any path arguments. We need this if we have, e.g., a file named zorg that we want to extract from the index, yet also have a branch named zorg. If we wrote git checkout zorg instead of git checkout -- zorg, Git would try to check out the branch.
  • path: At least one must be specified; these are the specific files to extract (from the index, or from the tree-ish). When extracting files from the index, Git writes them to the work-tree (and not the index, because they are already in the index). When extracting files from a tree-ish, however, Git writes them to the index first, and only then on into the work-tree.

Thus, when we write:

git checkout -- f1 f2 f3

Git treats the -- as meaning "no branch and thus no tree-ish, so use the index"; the various file names are therefore files to extract from the index, to overwrite what's in the work-tree.

One reason we might do this is that we had edited f1, f2, and f3 and thought they were good. We then git added them to copy those versions into the index. Then we edited them some more ... but decided that the work-tree versions are bad, and that we want the index versions back in the work-tree. These index versions are not in any commit yet: they are stored only in the index.

(Another reason we might use git checkout -- f1 f2 f3 is that we had edited them but not git added them. In this case, the index versions were identical to the HEAD versions ... and hence in this case we're fine: we can just re-extract the HEAD versions.)

The problem

Unfortunately, if we typo this as:

git checkout - f1 f2 f3

then Git treats this as if we had typed in:

git checkout @{-1} f1 f2 f3

which means Find the previous branch via the reflogs, then extract files from that branch's tip commit into the index and then on into the work-tree. This overwrites the index versions we intended to restore (unless, by whatever chance, those "intended to restore" versions match the previous-branch versions).

Recovering

There is a way to get the only-in-index versions back, but it is quite painful: run git fsck --lost-found and then search through the hash-named files Git writes into .git/lost-found/other. There are probably many (the actual number depends on the number of dangling blobs lying around in your repository). Three of them—but there is no telling which three without careful examination of every recovered blob-file—are our three versions of files f1, f2, and f3 that we wanted back.

Musings

I have often thought that git checkout should be at least two, and probably three or more, separate commands:

  • One to check out a branch or a commit: this one would have the standard safety checks. It would always be safe to run git checkout foo since that would never overwrite any files; only git checkout --force foo would be dangerous.
  • One to check out files from the index. (Actually, there is such a command, namely git checkout-index. You can use this instead of git checkout. It's a plumbing command, though, so it is not as user-oriented and more of a pain to use.) This could perhaps also be git checkout, although it's a bit dangerous as it can overwrite work-tree work, which plain git checkout does and would not. A front-end porcelain git co-index might be the right name for this.
  • One to check out specific files from specific commits. Since this would be spelled differently (perhaps git co-tree or some such), then even though it's "dangerous" (overwrites existing files and/or index entries), it would be clear that we're not using the "safe" variety of git checkout. The tree-ish here would not be optional, so it would be harder to make the - vs -- mistake (and this is why it probably should be a separate command from git co-index).

But this is not what we have.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • Maybe a bit obvious, but imo still worth pointing out that in the special (but in my experience common) case that the files to be checked out from index would have been unchanged from the prior commit, recovery is much simpler (`git checkout HEAD -- f1 f2 f3`) – Mark Adelsberger May 16 '17 at 14:22