Git is an advanced tool. It features a philosophy that is dear to my heart: to treat developers as smart and responsible folks. This means that a lot of power is at your fingertips. The power to also shoot yourself in the foot – arguably with a titanium vest on – but shoot yourself nonetheless.

The topic of this post is to confirm what my colleague Charles O’Farrell said very well in the ever popular “Why Git?” article some time ago:

[…] Git is actually the safest of all the DVCS options. As we saw above,
Git never actually lets you change anything, it just creates new objects.

[…] Git actually keeps track of every change you make, storing them in
the reflog. Because every commit is unique and immutable, all the reflog has
to do is store a reference to them.

This means you’re safe from harm and your code is always preserved, but there are situations where you might need some conjuring to get it back.

Let me give you a few real world examples of how to recover from trouble, going from simple to advanced:

Table Of Contents

  1. How To Undo A (Soft) Reset And Recover A Deleted File
  2. If You Lose A Commit During An Interactive Rebase
  3. How To Undo reset –hard If You Only Staged Your Changes

How To Undo A (Soft) Reset And Recover A Deleted File

If you delete a file with git rm and immediately after you git reset your working directory (which is called a soft reset), you will find yourself with a missing file and a dirty working directory like the following:

# On branch master
# Changes not staged for commit:
#   (use "git add/rm file..." to update what will be committed)
#   (use "git checkout -- file..." to discard changes in working directory)
#
#   deleted:    test.txt
#

There are a couple of simple ways to go about restoring the file. One is to use a git reset –hard which will recreate the missing files but it will delete any local modifications you might have in your working directory.

The second is to just re-check out the file yourself with git checkout test.txt.

In a slightly different scenario, if the file was removed in an earlier commit, you can recover it by noting down the exact commit where the file was deleted and use the reference to pick the commit immediately before:

git checkout commit_id~ -- test.txt

Where the ~ (tilde) sign means the one before this one.

If You Lose A Commit During An Interactive Rebase

Interactive rebase is one of the most useful tools in the git arsenal; It allows to edit, squash and delete commits interactively.

Personally, I love to clean and streamline commits before sharing them with others. It’s nice when each commit is an understandable and logical chunk of work. In the heat of development, or in a local private branch, I might commit furiously for a bit, then when I am satisfied I spend time cleaning up the history.

But what if while in the interactive rebase session you accidentally remove a commit line without realizing it and save?

This situation is slightly annoying cause you have just removed a commit from your history!

If you created a throwaway branch before starting the rebase – which you should always do – you’re fine and you can restart the process all over. But what if you didn’t?

No worries, git has got you covered. Nothing is ever lost. This is a good opportunity to use the reflog, which is an automatic mechanism recording where the tip of all branches has been for the past 30 days.

Say at the start of the rebase I had 4 commits like the following:

$ git log
cfdf880 [2 seconds ago] (HEAD, master) wrote fifth change
ab446e6 [34 seconds ago] changed one line
6e1a130 [25 minutes ago] third change
6566977 [26 minutes ago] second change
5feeb33 [26 minutes ago] initial commit

I rebase the last 4 commits and accidentally remove one line:

$ git rebase -i HEAD~4

Editor opens on:

pick 6566977 second change
pick 6e1a130 third change
pick ab446e6 changed one line
pick cfdf880 wrote fifth change

# Rebase 5feeb33..cfdf880 onto 5feeb33
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

I removed one commit line (pick ab446e6 changed one line), I saved and got this:

Successfully rebased and updated refs/heads/master.

Now, if I check the list of commits I see I lost one:

$ git log
3dd6845 [6 minutes ago] (HEAD, master) wrote fifth change
6e1a130 [32 minutes ago] third change
6566977 [32 minutes ago] second change
5feeb33 [32 minutes ago] initial commit

But the reflog has our lost commit:

$ git reflog
3dd6845 HEAD@{0}: rebase -i (finish): returning to refs/heads/master
3dd6845 HEAD@{1}: rebase -i (pick): wrote fifth change
6e1a130 HEAD@{2}: checkout: moving from master to 6e1a130
cfdf880 HEAD@{3}: commit: wrote fifth change
----}} ab446e6 HEAD@{4}: commit: changed one line
[...]

Here ab446e6 is the sha-1 for the commit we lost. Armed with this id we can just cherry-pick it back in the code:

$ git cherry-pick ab446e6
[master 032c6a6] changed one line
1 file changed, 1 insertion(+), 1 deletion(-)

And as you can see the commit is now back in our history:

$ git log
032c6a6 [12 minutes ago] (HEAD, master) changed one line
3dd6845 [12 minutes ago] wrote fifth change
6e1a130 [37 minutes ago] third change
6566977 [38 minutes ago] second change
5feeb33 [38 minutes ago] initial commit

How To Undo reset –hard If You Only Staged Your Changes

I’ll save you the speech to commit frequently – whether in a private short lived branch or a shared one – to protect you from heaps of problems.

But let’s say you didn’t commit this time and you find yourself in the following scenario: you’ve done some work, haven’t committed it yet but have git added it to the staging area.

Then tragedy strikes: you type git reset –hard and immediately realize that you’ve zeroed your local changes(!!) and brought back (in it’s entirety) the previous commit.

This is a tough situation to be in. How do you recover your work? Is it even possible? The answer is yes! The solution involves some low level searching and can be approached in a couple of ways.

Since git creates new objects in the .git/objects folder as soon as something is added to the staging area we can look there for the most recently created objects:

In OSX (and BSD systems) you can do:

find .git/objects/ -type f | xargs stat -f "%Sm %N" | sort | tail -5

On Linux(with GNU tools) this should work:

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

With tail -n you can cut to the last n results if your repository is very big:

The result should be something like:

Apr  2 12:26:33 2013 .git/objects//pack/pack-4cf657357973915f6fb6f90a41f69a88c0b08bb7.pack
Apr  2 12:29:56 2013 .git/objects//3d/d6845a69cd59dac0851e1ae0bd69dde950c46b
Apr  2 12:29:56 2013 .git/objects//68/c983f0cefb4bee2f753516ab6352ee2f2b7d29
Apr  2 12:35:23 2013 .git/objects//03/2c6a653cc00c302020293e020d8d68326b112e
Apr  2 15:34:55 2013 .git/objects//df/485610889e98ff773b4440a032ea2ede1338b9

In my case my sample file was lost exactly around 15:34 so I can inspect and retrieve it with:

git show df485610889e98ff773b4440a032ea2ede1338b9

Remember that the folder is part of the sha-1 reference, so remove the / slash to compute the correct id.

Note that git has specific low level command to explore dangling objects and to verify their connectivity: git fsck. For an alternative solution to this problem using fsck, check out this very cool answer on Stack Overflow.

Conclusions

There are many more examples and scenarios to cover on this topic so I might come back later with more interesting cases.

One thing to take away from this should be the feeling that it’s very very hard to screw up and lose your data when using git. That’s all for now!

Follow me @durdn for more Git rocking.

Git Titanium Armor: Recovering from Various Disasters