Red Green Repeat Adventures of a Spec Driven Junkie

git: recover squashed commit

I’m working on a lunch & learn presentation on git for work and have been digging around git’s internals.

Fragmentary Face of King Khafre

source

Currently, I am comfortable enough with squashing commits in my workflow that I can’t think of an immediate need.

When first working with squashing commits, I definitely did the wrong thing and had to “redo” work lost to git rebase -i

After doing research into git, I realize it is possible to recover squashed commits!

If you know the squashed commit you want to recover:

  1. run: $ git show <squashed commit's SHA>, which will display the commit details, with individual blobs (that’s the git term for a file)

  2. from step 1, extract out the SHA for the change you want to recover and run: $ git cat-file -p <file change SHA> > new_file.txt, which will put the contents of the file change into new_file.txt

Example

Let’s say a git repository (that has no downstreams!) has the following log:

vagrant@ubuntu-xenial:~/git_recover_squash$ git log
commit bc6cbe67f9e142d077f1f27c6d74c0bd44369d3e (HEAD -> master)
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:26:33 2019 +0000

	FIX: update contents

commit 0b7047ea011ad53533d6dd62eff5254e2fd7f31f
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:25:44 2019 +0000

	FIX: update contents

commit ad9295675ecf44e808f79f31bc64264f52af3340
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:22:30 2019 +0000

	FEATURE: second file

commit b0880368ff63068b4054278d9006deae21f228ae
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:21:54 2019 +0000

	FEATURE: first file

commit a35f41e8831edd47183081c1f1dee17528b6cc24
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:21:12 2019 +0000

	start of the universe

And to keep the repository history clean, I squash the last two commits, bc6c & 0b70 into the feature change: ad92.

I would run the following command:

$ git rebase -i HEAD~3
...

and then the log would look like:

vagrant@ubuntu-xenial:~/git_recover_squash$ git log
commit f736917fc30c853b096f1417eceb85d00feefd01 (HEAD -> master)
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:22:30 2019 +0000

	FEATURE: second file

commit b0880368ff63068b4054278d9006deae21f228ae
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:21:54 2019 +0000

	FEATURE: first file

commit a35f41e8831edd47183081c1f1dee17528b6cc24
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:21:12 2019 +0000

	start of the universe

Sweet, nice and clean, ready for PR, right?

Pushing up the branch and the continuous integration server throws an error!

Looks like there’s important work in a squashed commit!

Normally, I’d just redo the work and swear I would never rebase again and force everyone to accept all my commits, even my WIP. If anyone complains about my commits, I’d just stand my ground and not rebase, ever.

I didn’t do that and doubled down on squashing my commits.

And today, there’s a way to recover the squashed commit.

Let’s say the important work is in the 0b70 commit. It’s squashed, so it’s lost to the ether, right?

Well, no. git maintains all the commits, even when squashing. The blob is still in your commit history, accessing it is just trickier.

To see all the commits, run: $ find .git/objects -type f

vagrant@ubuntu-xenial:~/git_recover_squash$ find .git/objects/ -type f
.git/objects/c7/068a966d283167b96a5d651dffa60695560adb
.git/objects/ac/1bd6c4a88b3483ec16d73651138b6c741dd5a3
.git/objects/0b/7047ea011ad53533d6dd62eff5254e2fd7f31f
.git/objects/f7/36917fc30c853b096f1417eceb85d00feefd01
.git/objects/ad/9295675ecf44e808f79f31bc64264f52af3340
.git/objects/ee/2ae2f16aef8dac88d5021702c35f38b752c4d1
.git/objects/b0/880368ff63068b4054278d9006deae21f228ae
.git/objects/25/d28f069ab4504af450c2070c4383d7368cb95f
.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
.git/objects/20/77dd08466c2d544aa981d4e6d579d4b1a9aeb0
.git/objects/a3/5f41e8831edd47183081c1f1dee17528b6cc24
.git/objects/bc/6cbe67f9e142d077f1f27c6d74c0bd44369d3e
.git/objects/85/519348cc2825306675f39de4539ed6d1efb3e5
.git/objects/8b/fe55b7c90f27ba6739e0ba5b1bb5067204db63
.git/objects/59/1654ce7549e3ddfa389700e41a2329d56565c1
.git/objects/5b/efeb31ceb7ac81be0df1bebc28972d7db48286

See, the 0b70 is still there, even when we told git to squash!

The object, 0b70 is a commit object in git, to check, run: $ git cat-file -t <SHA>

vagrant@ubuntu-xenial:~/git_recover_squash$ git cat-file -t 0b70
commit

You can’t recover a commit, but you can recover a blob… so using $ git show <commit sha> reveals:

vagrant@ubuntu-xenial:~/git_recover_squash$ git show 0b70
commit 0b7047ea011ad53533d6dd62eff5254e2fd7f31f
Author: Andrew Leung <andrew@redgreenrepeat.com>
Date:   Sat May 4 00:25:44 2019 +0000

	FIX: update contents

diff --git a/second_file.txt b/second_file.txt
index ac1bd6c..2077dd0 100644
--- a/second_file.txt
+++ b/second_file.txt
@@ -1 +1,2 @@
 second file contents
+more contents to second file

And tada, the commit shows the file changes involved. The important part is locating what changes are on the second_file.txt, which is the only change in the commit.

The change to second_file.txt in this commit has SHA of: 2077dd0. Checking the git object type using $ git cat-file -t <SHA> reveals:

vagrant@ubuntu-xenial:~/git_recover_squash$ git cat-file -t 2077dd0
blob

So, to recover the change, use: $ git cat-file -p <SHA> > recover_file.txt, which will recover the file from the commit into recover_file.txt

vagrant@ubuntu-xenial:~/git_recover_squash$ git cat-file -p 2077dd0 > recover_file.txt
vagrant@ubuntu-xenial:~/git_recover_squash$ cat recover_file.txt
second file contents
more contents to second file

Tada! We have recovered the squashed commit’s file!

Conclusion

Even when a commit is squashed, it’s still recoverable as the data is in the commit history.

Once you know the SHA of the blob you want to recover, use: $ git cat-file -p <SHA> > recover_file.txt.

Whew, I wish I knew this when I started squashing commits. It would have saved me rewriting work twice!