131

I am trying to recover my work. I stupidly did git reset --hard, but before that I've done only get add . and didn't do git commit. Please help! Here is my log:

MacBookPro:api user$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)

#   modified:   .gitignore
...


MacBookPro:api user$ git reset --hard
HEAD is now at ff546fa added new strucuture for api

Is it possible to undo git reset --hard in this situation?

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
eistrati
  • 2,314
  • 6
  • 26
  • 35
  • @MarkLongair awesome man! You just got my work back! I wrote a Python script to create files of all the output! I'll add the script as an answer – Boy Jan 08 '14 at 13:47
  • 6
    Not 'stupidly'.. but 'naively'... because I just did THE SAME! – Rosdi Kasim Mar 07 '14 at 08:34
  • Could still be stupidly ;-) – Duncan McGregor Jan 11 '16 at 19:22
  • Here's a great [article](https://medium.com/@CarrieGuss/how-to-recover-from-a-git-hard-reset-b830b5e3f60c#.353djvmkb) on how to reverse some of this. It is going to take some manual work. – Jan Swart Oct 22 '16 at 11:05
  • 1
    @MarkLongair ``` find .git/objects/ -type f -printf '%TY-%Tm-%Td %TT %p\n' | sort ``` worked for me. dates also appear, start checking blobs from the end. – ZAky Apr 08 '18 at 17:57
  • How do I recover text after `git show 907b308...`? This gives me a diff, not the file. – Danijel Jan 18 '19 at 15:16

13 Answers13

203

You should be able to recover any files back that you added to the index (e.g, as in your situation, with git add .) although it might be a bit of work. In order to add a file to the index, git adds it to the object database, which means it can be recovered so long as garbage collection hasn't happened yet. There's an example of how to do this given in Jakub Narębski's answer here:

However, I tried that out on a test repository, and there were a couple of problems - --cached should be --cache, and I found that it didn't actually create the .git/lost-found directory. However, the following steps worked for me:

git fsck --cache --unreachable $(git for-each-ref --format="%(objectname)")

That should output all objects in the object database that aren't reachable by any ref, in the index, or via the reflog. The output will look something like this:

unreachable blob 907b308167f0880fb2a5c0e1614bb0c7620f9dc3
unreachable blob 72663d3adcf67548b9e0f0b2eeef62bce3d53e03

... and for each of those blobs, you can do:

git show 907b308

To output the contents of the file.


Too much output?

Update in response to sehe's comment below:

If you find that you have many commits and trees listed in the output from that command, you may want to remove from the output any objects which are referenced from unreferenced commits. (Typically you can get back to these commits via the reflog anyway - we're just interested in objects that have been added to the index but can never be found via a commit.)

First, save the output of the command, with:

git fsck --cache --unreachable $(git for-each-ref --format="%(objectname)") > all

Now the object names of those unreachable commits can be found with:

egrep commit all | cut -d ' ' -f 3

So you can find just the trees and objects that have been added to the index, but not committed at any point, with:

git fsck --cache --unreachable $(git for-each-ref --format="%(objectname)") \
  $(egrep commit all | cut -d ' ' -f 3)

That enormously cuts down the number of objects you'll have to consider.


Update: Philip Oakley below suggests another way of cutting down the number of objects to consider, which is to just consider the most recently modified files under .git/objects. You can find these with:

find .git/objects/ -type f -printf '%TY-%Tm-%Td %TT %p\n' | sort

(I found that find invocation here.) The end of that list might look like:

2011-08-22 11:43:43.0234896770 .git/objects/b2/1700b09c0bc0fc848f67dd751a9e4ea5b4133b
2011-09-13 07:36:37.5868133260 .git/objects/de/629830603289ef159268f443da79968360913a

In which case you can see those objects with:

git show b21700b09c0bc0fc848f67dd751a9e4ea5b4133b
git show de629830603289ef159268f443da79968360913a

(Note that you have to remove the / at the end of the path to get the object name.)

Community
  • 1
  • 1
Mark Longair
  • 446,582
  • 72
  • 411
  • 327
  • @PhilipOakley I've got timestamps with `%tT` instead of `%TT` – Steven Pribilinskiy Jul 31 '14 at 06:44
  • 6
    not to steal anyones lime light here. But just as a note since I just ran into the same problem and thought I lost all my work. **For those using an IDE**, the IDEs usually have a one click recovery solution. In case of PHPstorm I just had to right click the directory and select show local history and then revert to the last valid state. – Shalom Sam Sep 22 '14 at 04:44
  • If you did the changes on the same day and had some sort of keyword to search through all blobs: in the ./git/objects folder, I did `find -maxdepth 2 -type f -mtime -1 | sed -r 's/[\.\/]//g' | xargs -n 1 -I {} -i bash -c 'git show {} | grep -q YOUR_KEYWORD && echo {}'` – dojuba Jun 30 '15 at 13:33
  • Great approach @MarkLongair, i got my backup in blob and converted to .txt file. – Sunil kumar May 31 '16 at 08:56
  • My version of find didn't support that syntax (and I was too lazy to figure out how to modify it), but `ls -ltr ~/.git/objects/*/*` worked fine for me. – user1071847 Jun 27 '16 at 22:56
  • As suggested by @PhilipOakley, I find that looking into `.git/objects` and searching for recently modified files is by far the best way to recover your work. This means that `git fsck` didn't work for me, however, `find .git/objects/ ...` worked very nicely. – Danijel Jan 21 '19 at 09:46
64

I just did a git reset --hard and lost one commit. But I knew the commit hash, so I was able to do git cherry-pick COMMIT_HASH to restore it.

I did this within a few minutes of losing the commit, so it may work for some of you.

wjandrea
  • 28,235
  • 9
  • 60
  • 81
  • 5
    Thank you thank you thank you Managed to recover a days work thanks to this answer – Dace Nov 11 '13 at 16:24
  • 24
    Probably is worth mentioning you can see those hashes using `git reflog`, e.g. `git reset --hard` -> `git reflog` (looking at the HEAD@{1} hash) and finally `git cherry-pick COMMIT_HASH` – Javier López Nov 16 '13 at 14:11
  • This is absolutely the easiest and fastest way to fix this. Awesome! – Lotus May 28 '14 at 23:58
  • great answer! short and simple! – Technotronic Jun 03 '14 at 12:31
  • 2
    Guys reading this, beware that this only works for a single commit hash, if there was more than a single commit, it won't solve the problem at once! – Ain Tohvri Jun 05 '14 at 00:39
  • Almost lost 5 days of work. Using git reflog I was able to do a hard reset back to one of the hashes from there and get everything back! – Senica Gonzalez Jun 18 '14 at 18:59
  • 4
    You can actually reset "forward". So if you have reset passed multiple commits, doing another 'git reset --hard ' should restore the whole commit chain up to that point. (given that they're not GC'ed yet) – akimsko Sep 16 '14 at 13:49
  • 7
    The one-liner for recovering commits lost with `git reset --hard` (assuming you haven't done anything else after that command) is `git reset --hard @{1}` – Ajedi32 Jan 15 '15 at 18:40
18

Thanks to Mark Longair I got my stuff back!

First I saved all the hashes into a file:

git fsck --cache --unreachable $(git for-each-ref --format="%(objectname)") > allhashes

next I put them all (removing the 'unreachable blob' thing) in a list and put the data all in new files...you have to pick your files and rename them again which you need...but I only needed a few files..hope this helps someone...

commits = ["c2520e04839c05505ef17f985a49ffd42809f",
    "41901be74651829d97f29934f190055ae4e93",
    "50f078c937f07b508a1a73d3566a822927a57",
    "51077d43a3ed6333c8a3616412c9b3b0fb6d4",
    "56e290dc0aaa20e64702357b340d397213cb",
    "5b731d988cfb24500842ec5df84d3e1950c87",
    "9c438e09cf759bf84e109a2f0c18520",
    ...
    ]

from subprocess import call
filename = "file"
i = 1
for c in commits:
    f = open(filename + str(i),"wb")
    call(["git", "show", c],stdout=f)
    i+=1
Boy
  • 7,010
  • 4
  • 54
  • 68
  • 1
    Yes! This is exactly what I needed. I like the Python script for recreating all the files too. The other answers made me nervous about losing my data with garbage collection, so dumping the files is a win for me :) – Eric Olson Nov 18 '14 at 23:26
  • 1
    I wrote this script based on this answer. Works out of the box: https://github.com/pendashteh/git-recover-index – Alexar Jan 31 '16 at 07:17
  • 2
    I find it easier to automate it like this: `mkdir lost; git fsck --cache --unreachable $(git for-each-ref --format="%(objectname)") | grep -Po '\s\S{40}$' | xargs -i echo "git show {} > lost/{}.blob" | sh`. Files will end up in `lost/*.blob` – Matija Nalis Jan 25 '18 at 17:22
7

@Ajedi32's solution in the comments worked for me in exactly this situation.

git reset --hard @{1}

Note that all these solutions rely on there being no git gc, and some of them might cause one, so I'd zip up the contents of your .git directory before trying anything so that you have a snapshot to go back to if one doesn't work for you.

Duncan McGregor
  • 17,665
  • 12
  • 64
  • 118
  • I had a number of uncommited files. Then I commited 1 file, and did a `git reset --hard`, which messed my repo in any unknown fashion. @Duncan, what does the @{1} do? and which comment are you referring to? Is it reseting a `git reset`? – alpha_989 Mar 21 '18 at 16:28
  • I think that the @{1} is a relative reference to the last but one commit, but I'm no expert, just reporting what worked for me – Duncan McGregor Mar 21 '18 at 17:21
5

Ran into the same issue, but had not added the changes to the index. So all commands above didn't bring me back the desired changes.

After all the above elaborate answers, this is a naive hint, but may be it'll save someone who didn't thought about it first, as I did.

In despair, I tried to press CTRL-Z in my editor (LightTable), once in every open tab — this luckily recovered the file in that tab, to its latest state before the git reset --hard. HTH.

olange
  • 165
  • 2
  • 7
  • 2
    Exactly same situation, missed to add in index and did hard reset and none of the solution worked all above. This one was a SMART move , thanks I did Ctrl+Z and saved my day. My editor : SublimeText. One up from my side ! – Vivek Dec 22 '17 at 10:19
3

Goodness, I pulled my hair until I ran into this question and its answers. I believe the correct and succinct answer to the question asked is only available if you pull two of the comments above together so here it is all in one place:

  1. As mentioned by chilicuil, run git reflog to identify in there the commit hash that you want to get back to

  2. As mentioned by akimsko, you will likely NOT want to cherry pick unless you only lost one commit, so you should then run git reset --hard <hash-commit-you-want>

Note for egit Eclipse users: I couldn't find a way to do these steps within Eclipse with egit. Closing Eclipse, running the commands above from a terminal window, and then reopening Eclipse worked just fine for me.

Saikat
  • 14,222
  • 20
  • 104
  • 125
Lolo
  • 3,935
  • 5
  • 40
  • 50
2

If you are editing your files with an IDE, it may also be keeping its own history independent of Git. In some circumstances it is much simpler to bypass Git entirely and use the IDE. For example, IntelliJ IDEA and Eclipse both have this kind of automated local version control, which they refer to as "local history". In IntelliJ it's straightforward to retrieve a whole batch of lost changes across many files: in the Project pane, you can right-click an entire project or directory tree and select Show History in the Local History submenu. The git reset --hard should appear in the list as an "external change" (i.e. triggered from outside the IDE), and you can use the revert button or context menu item to revert everything to the state right before that external change happened.

abyrd
  • 611
  • 6
  • 8
1

This is probably obvious to the git professionals out there, but I wanted to put it up since in my frantic searching I didn't see this brought up.

I staged some files, and did a git reset --hard, freaked out a little, and then noticed that my status showed all my files still staged as well as all their deletions unstaged.

At this point you can commit those staged changes, as long as you don't stage their deletions. After this, you only have to work up the courage to do git reset --hard one more time, which will bring you back to those changes you had staged and now just committed.

Again, this is probably nothing new to most, but I'm hoping that since it helped me and I didn't find anything suggesting this, it might help someone else.

Saikat
  • 14,222
  • 20
  • 104
  • 125
ddoty
  • 33
  • 5
1

If you've recently reviewed a git diff then there's another way to recover from incidents like this, even when you haven't staged the changes yet: if the output of git diff is still in your console buffer, you can just scroll up, copy-paste the diff into a file and use the patch tool to apply the diff to your tree: patch -p0 < file. This approach saved me a few times.

Confusion
  • 16,256
  • 8
  • 46
  • 71
0

The above solutions may work, however, there are simpler ways to recover from this instead of going through git-s complex undo. I would guess that most git-resets happen on a small number of files, and if you already use VIM, this might be the most time-efficient solution. The caveat is that you already must be using ViM's persistent-undo, which you should be using either way, because it provides you the ability to undo unlimited number of changes.

Here are the steps:

  1. In vim press : and type the command set undodir. If you have persistent undo turned on in your .vimrc, it will show a result similar to undodir=~/.vim_runtime/temp_dirs/undodir.

  2. In your repo use git log to find out the last date/time you made the last commit

  3. In your shell navigate to your undodir using cd ~/.vim_runtime/temp_dirs/undodir.

  4. In this directory use this command to find all the files that you have changed since the last commit

    find . -newermt "2018-03-20 11:24:44" \! -newermt "2018-03-23" \( -type f -regextype posix-extended -regex '.*' \) \-not -path "*/env/*" -not -path "*/.git/*"

    Here "2018-03-20 11:24:44" is the date and time of the last commit. If the date on which you did the git reset --hard is "2018-03-22", then use "2018-03-22", then use "2018-03-23". This is because of a quirk of find, where the lower boundary is inclusive, and the upper bound is exclusive. https://unix.stackexchange.com/a/70404/242983

  5. Next go to each of the files open them up in vim, and do a "earlier 20m". You can find more details about "earlier" by using "h earlier". Here earlier 20m mean go back to the state of the file 20 minutes back, assuming that you did the git hard --reset, 20 mins back. Repeat this over all the files that was spitted out from the find command. I am sure somebody can write a script that will combine these things.

alpha_989
  • 4,882
  • 2
  • 37
  • 48
0

I'm using IntelliJ and was able to simply go through each file and do:

Edit -> reload from disk

Luckily, I had just done a git status right before I wiped out my working changes, so I knew exactly what I had to reload.

Forrest
  • 2,968
  • 1
  • 27
  • 18
0

Remembering your file hierarchy and using the technique of Mark Longair with Phil Oakley modification yields dramatic results.

Essentially if you at least added the files to the repo but not committed it, you can restore by interactively using git show, inspect the log and use shell redirection to create each file (remembering your path of interest).

HTH!

Saikat
  • 14,222
  • 20
  • 104
  • 125
ainthu
  • 1
0

I was not able to recover the file using git commands. In my case, it was a CPP file. I knew a unique string in it and this helped. The command is:

sudo grep -a -B[number of rows before the text being searched] -A[number of rows after the text being searched] '[some unique text in the lost file]' /dev/sda3 > test.cpp

Searches the whole disk but in the end, it found the content of the lost file.

AntonDudov
  • 21
  • 2