Tuesday, June 2, 2015

Recovering A Lost Commit With Git

Ever have one of those days where you make a bad decision?

No, I mean with writing code.

You do?

How about with source control? Things like deleting a commit on your work branch and then realizing you didn't mean to delete it?

Well, guess what? You're in luck! Git doesn't throw away your commits! And if, like me, you've accidentally.....okay, INTENTIONALLY deleted some work and then realized you want to recover it, you can!

A little background: I'm playing around with a tool to let me create and post a series of entries on the blog for my comic book podcast (The Comic Book Update). What I do now is manually schedule hourly posts of comic previews when the publishers send them to me. Which results in about two hours throughout the week (usually while drinking coffee and watching the news) of creating 60 posts (10 posts per day) and setting the scheduled time.

Tedious, I know. So I want to create a tool to do this.

Long story short, I started playing with a Ruby solution, a Python and a Java solution. But after deleting Ruby, I realized I in fact DO want to use it. And it was the one I had explored the most, but dammit I already deleted it!

Except, in my terminal buffer, I could still get to the commit hashes for the Ruby version!

So what I did was checkout a clean branch in my project repo. I then scrolled up and, one by one, did a git cherry pick of each of those hashes.

Voila! Commits were recovered!

The reason why is that, even when you delete a commit with "git reset --hard HEAD~1" or doing an interactive rebase and deleting commits from the list to be included, git does NOT actually throw the commit away. Instead it leaves it in the set of objects in your .git directory until such a time as you do garbage collection.

If you go into a repo and peek in the .git/objects directory you'll see a series of directories name 00, 01, ..., fe and ff (or some subset of them). Each of those is composed of multiple files, each representing a different commit. The parent directory is from the first two characters of the commit hash, and the filename is from the remainder of the hash. So if your hash id was f43fe07d9df08fdb6440c562639eb4ad4ce4c49e then you'll find that specific commit itself in .git/objects/f4/3fe07d9df08fdb6440c562639eb4ad4ce4c49e.

Such a nice turn of luck not having to rewrite that initial bit of code....thank you git!