git unfuck-it-up with reflog and bisect

There are some git commands I use thousands of times a day. For example, git status, git commit, git push, git pull. There are some others I keep in the toolbox ready for the fuckups. This post will cover the latter category of commands. In particular, git reflog and git bisect.

Reflog

No matter how big of a mess, everything that was ever committed can be recovered in git. That’s one of the reasons why it’s so important to get in the habit of committing small, committing often.

git log shows commits in chronological order, git reflog (i.e. reference log) shows commits in the order they were last referenced. In other words, every time the HEAD reference is updated, a new entry in the reflog is added.

Given a repository with the following four commits

$ git log --oneline
eaf3a04 (HEAD -> master) fourth commit
c978275 third commit
bc63f50 second commit
e137e9b first commit

or visually

then the reflog would show what follows

$ git reflog
eaf3a04 (HEAD -> master) HEAD@{0}: commit: fourth commit
c978275 HEAD@{1}: commit: third commit
bc63f50 HEAD@{2}: commit: second commit
e137e9b HEAD@{3}: commit: first commit

where the first column is the hash referenced by HEAD after executing the command on the right (i.e. commit). In particular currently (i.e. HEAD@{0}) “fourth commit” is reference, the step before (i.e. HEAD@{1}) commit “third commit” and so on.
In this case the reflog is redundant. In fact, each commit knows its ancestor (see image above) and HEAD references the latest commit, therefore any commits can be reached just by navigating backwards the commit chain.

On the contrary, let’s say for some reasons a commit done in the past is not referenced any more by any refs. In that case, git log wouldn’t be able to show that commit. This is where reflog can unfuck it up.

For example, after a hard reset to “third commit”, there’s no references to “fourth commit” anymore.

$ git reset --hard HEAD~1
$ git log --oneline
c978275 (HEAD -> master) third commit
bc63f50 second commit
e137e9b first commit

or visually

On the other hand, it is true that “fourth commit” was committed once or, equivalently, HEAD at some point was referencing “fourth commit”. Thus, reflog will show it

$ git reflog
c978275 (HEAD -> master) HEAD@{0}: reset: moving to HEAD~1
eaf3a04 HEAD@{1}: commit: fourth commit
c978275 (HEAD -> master) HEAD@{2}: commit: third commit
bc63f50 HEAD@{3}: commit: second commit
e137e9b HEAD@{4}: commit: first commit

As a matter of fact, the commit is still there and can be reached at eaf3a04.

Bisect

Sometimes things go wrong and some previously working code is found broken. In the following example, commit 7534d74 (i.e. oldest) introduces a sum function. At some point, somebody breaks it in b256750. Unfortunately, the developer doesn’t realize it and the problem slips through. While working on 612ea6e, another developer realizes that sum is broken.

612ea6e (HEAD -> master) some stuff
30630c3 some stuff
0e0869a some stuff
9821498 some stuff
0cc2262 some stuff
2e4cb8d some stuff
c98969b some stuff
f45e1f4 some stuff
435aa89 some stuff
b256750 break sum in lib
1ddb50e some stuff
f308756 some stuff
98a1b8d some stuff
e8ff14e some stuff
91df566 some stuff
7534d74 sum for lib

A strategy to pinpoint where the function got broken and unfuck it up would be as follows:
1) find a GOOD_COMMIT in the past where the code was working
2) checkout a MIDDLE_COMMIT in the middle between HEAD and GOOD_COMMIT
3) check if the code works in MIDDLE_COMMIT
4a) if it works keep checking between MIDDLE_COMMIT and HEAD (code got broken after MIDDLE_COMMIT)
4b) if it doesn’t work keep checking between GOOD_COMMIT and MIDDLE_COMMIT (code got broken before or on MIDDLE_COMMIT)
in other words, a manual binary search must be performed until the commit which broke the code is found. This is what I call a pain in the ass.

Luckily, the folks behind git invented bisect, which performs the checkouts automatically. Therefore, now the steps are the following ones:
1) find a GOOD_COMMIT in the past where the code was working
2) git bisect HEAD GOOD_COMMIT (or equivalently git bisect bad HEAD; git bisect good GOOD_COMMIT)
3) check if the code works
4a) if it works git bisect good
4b) if it doesn’t work git bisect bad
and after some steps git will show you

b25675054e77a245dba7b167b637150cf046a134 is the first bad commit

This is already awesome. But if you are ready for some next level shit keep reading. In fact, if you prepare a test that covers the broken code (i.e. anything that returns 0 for success or otherwise for failure), then git does everything in your place.
1) find a GOOD_COMMIT in the past where the code was working
2) git bisect HEAD GOOD_COMMIT
3) git bisect run test

The following was run using the sum example from above (some noise from the tests was left out)

$ git bisect start HEAD 7534d74
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[435aa89fb818a9dcccc3d79c4789d64287e3338a] some stuff
$ git bisect run ruby test.rb
running ruby test.rb
F
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

Bisecting: 3 revisions left to test after this (roughly 2 steps)
[98a1b8dd99968a985520f590d8081bcc57b29c4d] some stuff
running ruby test.rb
.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Bisecting: 1 revision left to test after this (roughly 1 step)
[1ddb50e2c8a359bc7697b3c42e5cc91e6a4b4990] some stuff
running ruby test.rb
.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[b25675054e77a245dba7b167b637150cf046a134] break sum in lib
running ruby test.rb
F
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

b25675054e77a245dba7b167b637150cf046a134 is the first bad commit
commit b25675054e77a245dba7b167b637150cf046a134
Author: 3v0k4 <riccardo.odone@gmail.com>
Date: Sun Oct 29 20:15:01 2017 +0100
break sum in lib

Pointers


This post has been written as part of my 6-week experiment on blogging. In other words, I’ll publish one post a week trying to timebox as much as possible the time needed to write. Therefore, please let me know what I’ve fucked up completely and what else I’ve just fucked up slightly. I’d be grateful.