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 add
ed 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 add
ed 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.