Recovering changes with git reflog

I ran into a situation recently where I accidentally merged an unfinished feature branch directly into master. I had been working on the feature and got an urgent hotfix request. Without thinking, I branched from the feature branch to perform the hotfix changes, then merged that directly into master once I was finished.

Whoops.

Luckily enough, I noticed the vast number of changes in master and realized what I had done before tagging and releasing.

My first thought was to revert the merge commit, but since it was a fast forward it wasn’t that simple. The feature branch had about a month’s worth of work in it and it would have been a pain to wade through all of those commits.

What is a developer to do?

Reflog to the rescue!

Reflog is basically a list of every single action performed on the repository. Specifically, the man pages say:

Reflog is a mechanism to record when the tip of branches are updated.

So anytime you commit, checkout or merge, an entry is entered into the reflog. This is important to remember, because it means that basically nothing is ever lost.

Here’s some sample output from the reflog command:

D:/Projects/reflog [develop]> git reflog
    38ca8c4 HEAD@{0}: checkout: moving from feature/foo to develop
    512e62c HEAD@{1}: commit: Now with 50% more foos!:
    38ca8c4 HEAD@{2}: checkout: moving from develop to feature/foo```

Some things:

  • The results here are listed in descending order – newest action is first
  • The first alphanumeric string is the commit hash of the result of the action – if the action is a commit it’s the new commit hash, if the action is a checkout it’s the commit hash of the new branch head, etc
  • The next column is the history of HEAD. So the first line (HEAD@{0}) is where HEAD is now, the second line is where head was before that, the third line is where head was before that, etc
  • The final column is the action along with any additional information – if the action is a commit, it’s the commit message, if the action is a checkout, it includes information about the to and from branches

Using that output, you can easily trace my footsteps (remember, descending order so we’re starting at the bottom):

  1. First, I checked out a feature branch
  2. I then committed 50% more foos
  3. Finally, I checked out the develop branch

So how do I get my data back?

It’s relatively easy – in most cases you can perform a checkout on the commit you want to get back, and branch from there.

Let’s pretend that, while in the develop branch, I somehow deleted my unmerged feature/foo branch. I can run git reflog to see the history of HEAD, and see that the last time I was on the feature/foo branch was on commit 512e62c. I can run git checkout 512e62c, then git branch branch-name:

D:/Projects/reflog [master]> git checkout 512e62c
    Note: checking out '512e62c'.

    You are in 'detached HEAD' state. You can look around, make experimental
    changes and commit them, and you can discard any commits you make in this
    state without impacting any branches by performing another checkout.

    If you want to create a new branch to retain commits you create, you may
    do so (now or later) by using -b with the checkout command again. Example:

        git checkout -b new_branch_name

    HEAD is now at 512e62c... First commit
D:/Projects/reflog [(512e62c...)]> git branch feature/foo
D:/Projects/reflog [(512e62c...)]> git checkout feature/foo
    Switched to branch 'feature/foo'
D:/Projects/reflog [feature/foo]>```

Notice how it said that I’m in a detached HEAD state. What that means is that HEAD is not pointing to the tip of a branch – we’re down the line of commits somewhere. However, at this point the files are checked out in my working copy and I am able to recover them. I can run git branch to create a branch from this commit, and continue working where I left off like nothing happened.