Honey I Squashed the Commits

Use git long enough and you will come upon a point where you have squashed multiple commits into a single commit, and then you discover that you need to recover or break out one or more of those commits. This article walks you through the steps in recovering commits that have been squashed.

Creating the Git Repo State

First lets create a repo and get it into a state where we have commits and then squashed commits.

That creates the base git repo. Now lets create some activity in the git logs.

Now we have some activity in our logs. Do a git log command to show the log, it should look something like this:

commit 5b7370912ad1a3e293c4f87cb8d7511d09b7dbe3
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:27:00 2012 -0500

    third commit

commit 48ace67607e8c75068f59ec16529914c8431e6ca
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    second commit

commit 5e99d35336a87a2ebbe604817ed2191ed0ff97f9
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    first commit

commit 2d374fc6ce48285a0f586f8ec4fcb453c2806309
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    zero commit, creates the master branch

Now let’s do a rebase of the last two commits. We will leave the one commit in place.

Squash the second and third commits.

pick 48ace67 second commit
squash 5b73709 third commit
 
# Rebase 5e99d35..5b73709 onto 5e99d35
#
# 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
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

And leave a new comment.

# This is a combination of 2 commits.
# The first commit's message is:

second and third commits squashed

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   three
#   new file:   two
#

Your repo is now in a state where you had individual commits; then those commits were squashed. Let’s recover the individual commits you squashed.

Recovering Squashed Commits

Even after squashing, the individual commits still exist inside the git repo, though they don’t point to anything. They will continue to exist until the repo is repacked. Then if not pointing to anything they will be removed.

If you do a git log you won’t see them.

commit 24cde82e14aea833a6b253de00c4f0981ff5426d
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    second and third commits squashed

commit 5e99d35336a87a2ebbe604817ed2191ed0ff97f9
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    first commit

commit 2d374fc6ce48285a0f586f8ec4fcb453c2806309
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    zero commit, creates the master branch

To recover the commits go through these steps:

  1. Checkout your master branch – You were working on a separate branch weren’t you?
  2. Create another branch from master
  3. Find the commits in the .git/logs/HEAD file or using git reflog
  4. Use git show to ensure they are the commits you want.
  5. Use git cherry-pick to add the commits into your new branch.
  6. Continue coding merrily on your new branch.

First checkout your master branch and from it create your recovery branch.

There are two ways to get to the old commits; Using git reflog and looking in the logs/HEAD file.

Using git reflog

The first way is using the git reflog command.

That will show output like the following.

5c9a0a8 HEAD@{0}: checkout: moving from master to newbranch
5c9a0a8 HEAD@{1}: checkout: moving from another to master
2983347 HEAD@{2}: rebase -i (squash): second and third commits
c96a39a HEAD@{3}: rebase -i (squash): updating HEAD
b3aca5e HEAD@{4}: checkout: moving from another to b3aca5e
daff876 HEAD@{5}: commit: third commit
b3aca5e HEAD@{6}: commit: second commit
c96a39a HEAD@{7}: commit: first commit
5c9a0a8 HEAD@{8}: checkout: moving from master to another
5c9a0a8 HEAD@{9}: commit (initial): zero commit, creates the master branch

Notice the lines with the commit: messages. Choose the hashes that corresponds to the commits you want to recover.

Using the .git/logs/HEAD file

The second way to find hashes is to look in a file in the .git foler. In the base directory of your git repo you will find the .git folder, under that is a logs directory that contains a HEAD file. Full path is .git/logs/HEAD file from the base of your git repository.

Here is what the .git/logs/HEAD file looks like.

hash0 hash1 Name <email> 1345591615 -0500      commit (initial): zero commit, creates the master branch
hash1 hash1 Name <email> 1345591615 -0500      checkout: moving from master to another
hash1 hash2 Name <email> 1345591615 -0500      commit: first commit
hash2 hash3 Name <email> 1345591615 -0500      commit: second commit
hash3 hash4 Name <email> 1345591620 -0500      commit: third commit
...

Notice the lines like commit: message. On those lines the second hash from the left are the ones you want. Find the individual commit(s) you want to recover and record the hashes.

Confirming commits

Once you have the hash or hashes you can use git show to ensure they are the correct commits. Here I am checking the second commit.

As we can see this is the the commit we want.

commit 48ace67607e8c75068f59ec16529914c8431e6ca
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    second commit

diff --git a/two b/two
new file mode 100644
index 0000000..0cfbf08
--- /dev/null
+++ b/two
@@ -0,0 +1 @@
+2

Cherry picking commits

Now let’s cherry-pick that commit into our current recovery branch. We are already in our newbranch so the command is just cherry-pick.

Now do a git log and you can see we have added the previously squashed commit into our recovery branch.

commit 144f92762e12c8d473267c0e2deb2600c45c81b4
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    second commit

commit 2d374fc6ce48285a0f586f8ec4fcb453c2806309
Author: Dennis Kubes <dennis@denniskubes.com>
Date:   Tue Aug 21 18:26:55 2012 -0500

    zero commit, creates the master branch

That is it. You can cherry-pick as many commits as you want, and then you can continue coding away merrily on your new branch.

What if you want to continue working on your original branch where you had squashed the commits but you want to reset it’s state to only some of the commits? Simple you do a git reset –hard HEAD~n to get it back to a certain state. Then you can use git cherry-pick to reapply some of the commits but not others from the squash.

Conclusion

This same process of checking the reflog or the HEAD file will also work if you accidentaly delete a branch and want to recover commits. Commits are very hard to actually lose in git.

I hope this quick tutorial helps you if you ever need to recover squashed commits. As always I am open to comments and suggestions.

Update 1:Thanks to Patrick for pointing out git reflog. I have updated the article to flesh out the use of reflog in addition to the HEAD file more.

4 Responses to “Honey I Squashed the Commits”

  1. Patrick says:

    This is a really great step by step explanation of how to recover from a bad rebase.

    Just to point it out as an alternative, instead of viewing the raw commit data on the file system you could also use `git reflog` to see some of “lost” history of the repo. In this case you could `git reset –hard ` to the desired commit in the reflog.

  2. Steve says:

    If I do the reset –hard without cherry-picking on a recovery branch, will they still be there?

    • Dennis Kubes says:

      Yes, any commits will still be in git. Reset hard doesn’t remove the commits, t just resets the HEAD pointer for the current branch. Any changes to tracked files in the working tree since are lost.

  3. Loic Nageleisen says:

    You can do:
    git checkout -b revovery
    git reset –hard HEAD@{5}

    or even faster:
    git branch recovery -f HEAD@{4}

    in the case of existing commits *after* the squash, you woudl instead do:
    git checkout -b recovery
    git rebase –onto last_sha1_before_squash squashed_commit_sha1 recovery