You are in luck because of this bit you put in a comment:
The thing is that I did not commit those changes. I first did git add .
and then ...
First, let's note that git add .
stages work-tree contents. That is, suppose that .
, your current directory, contains files foo.txt
and bar.xml
that are both in the current (HEAD) commit,1 and you have modified one or both of these. The add
step copies the new contents into the underlying repository, and then makes the index/staging-area refer to the new contents.
This answer is in the linked question, so this is technically a duplicate, but I think it's worth expanding on a bit, especially since that one answer is buried relatively far down, below the more common case of having used git reset
to discard a commit.
... I used git reset --hard
Note that this form of reset
does not discard any commits, but it does clear out work-tree files that have been staged. That is, it not only unstages files—i.e., undoes git add
and/or git rm
—just like git reset --mixed
, it also overwrites the work-tree version of the file with the committed version.2
This makes it difficult to get the staged version back, but not impossible.
The key here is that the file's contents are in the repository, stored under their "hash name"—one of those big ugly SHA-1 hashes like fa49b077972391ad58037050f2a75f74e3671e92
.
If you run:
$ git fsck
Git will find any hash-names that have nothing referring to them:
Checking object directories: 100% (256/256), done.
dangling blob fa49b077972391ad58037050f2a75f74e3671e92
The index/staging-area used to refer to this file, but git reset --hard
cleared that out. So now nothing refers to this fa49b...
file.
You can extract the file with git plumbing commands, but if there are a bunch of these and you want to refer to them more easily, you can add the --lost-found
option to git fsck
, then look in the lost-and-found directory, where these "dangling blob" files have been extracted:
$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling blob fa49b077972391ad58037050f2a75f74e3671e92
$ cd .git/lost-found/other
$ ls
fa49b077972391ad58037050f2a75f74e3671e92
$ cat fa49b077972391ad58037050f2a75f74e3671e92
new file
$
(I only added, then reset away, the one file; if there were many files you would have to sift through them all to find any good ones.)
When you are done rooting through the lost-found bin contents, it is probably a good idea to remove them (cd .git/lost-found && rm -rf *
), although they are largely harmless to leave there—they just occupy space. You can run git fsck --lost-found
again later to repopulate it (with any then-dangling commits and blobs).
Note that this method will fail if you wait too long—these dangling blob objects are not semi-protected (by reflog entries) the way discarded commits are. By default, though, leftover objects are not pruned for 14 days anyway. This means you have two weeks, instead of about a month as for commits.
There is one more trap here, not that it will affect people in practice. The file I reset away had one line reading new file
(plus a newline). Suppose that, for whatever reason, some other file, in some existing (not discarded) commit, has the exact same contents. When I git add
ed the file, Git would have computed this same "true name" hash—the true name of any file containing nothing but the one line new file
is always fa49b077972391ad58037050f2a75f74e3671e92
, in any repository anywhere on the planet3—and would have just referred to the existing copy of that file. Resetting it away and running git fsck
, Git will not notice a dangling blob, because at this point it is not dangling: some other commit refers to file fa49b077972391ad58037050f2a75f74e3671e92
, perhaps under the same path name, or perhaps under some other path.
1It also adds files that are not in the HEAD
commit, but I did not want to get into all the side issues of worrying about .gitignore
directives. See footnote 2. :-)
2If the file is newly added, git reset --hard
still removes the file from the work-tree. This is where the .gitignore
complication comes in: if the file was ignored, it wasn't added, but then git reset --hard
did not remove it either. If it was added, it's in the repository and we are back to the "file existed in HEAD
" case. So this all comes out in the wash anyway.
3This suggests a method of attack on Git repositories, consisting of using Google as a massive indexing engine: search blob hashes until you find some hash H that repeats, but refers to two different contents C1 and C2. Then, knowing that someone will want to use repository R, which has neither version of that blob, to hold content C1 in the future, make a commit in R holding content C2 instead. No new commit can now contain content C1 since Git will hash it to H and decide that it is already in the repository.
Practical, this attack is not. :-)