Git Bisect Run: Bug Hunting On Steroids

Written on February 19, 2022

When you have a newly introduced bug and you don’t know which git commit introduced it, what’s the best way to find that commit? Typically it’s easy enough to find an example of a commit for which the bug is not present (e.g. the last release) but finding the problematic commit between then and now is the hard part. If you have a set of 100 commits, and any commit could have introduced the bug, you could go through one by one, testing to see if the bug is present or not, but that’s inefficient! You’re better off picking the commit in the middle, seeing if it has the bug, and then narrowing down your search by a factor of two! So you’ll have 100 candidate commits, then 50, then 25, etc all the way down to one. Rather than checking 100 commits individually, you only need to check log₂(100) i.e. 7 commits.

But keeping track of which commits you’ve already checked can be laborious. That’s where git bisect comes in: you simply tell git bisect which commit you know has the bug and which commit you know doesn’t have the bug, and then it recursively checks out the middle commit for you to evaluate. The process looks like this:

(HEAD has the bug, 77bbfccc does not)
▶ git bisect start HEAD 77bbfccc
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[67b89c80893275fea001d2f332790ee62270afc0] add menu panel

(git bisect has checked out 67b89c808, I've tested it for the bug, and haven't found it)
▶ git bisect good
Bisecting: 2 revisions left to test after this (roughly 1 step)
[bdcc1c296abb5435a131aa5cbda45f386e3a9939] fix some tests

(repeat the process)
▶ git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[877f7bd76218d9d9220f99080d5aaea4fbf9c540] mother forgive me for this hack

▶ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[992fbbbd61680cf0e5e4cacf0dbe63089bdc74a9] this definitely _won't_ introduce a bug

▶ git bisect bad
992fbbbd61680cf0e5e4cacf0dbe63089bdc74a9 is the first bad commit
commit 992fbbbd61680cf0e5e4cacf0dbe63089bdc74a9
Author: Jesse Duffield <jessedduffield@gmail.com>
Date:   Tue Jan 18 20:57:24 2022 +1100

    this definitely _won't_ introduce a bug
...

(putting myself back on the original branch)
▶ git bisect reset

Pretty cool!

git bisect run

Git bisect takes care of checking out commits and keeping track of which commits we’ve already considered. But we, the human, still need to go in and actually test for the bug at each commit. The name of this blog is ‘Pursuit of Laziness’ for a reason, so we should find a lazier way to do this!

Enter git bisect run. This command lets you specify a command to run for each commit. If the command returns an exit code of zero, we consider that a pass. If the command returns a non-zero exit code, we consider it a fail. The exception is the special exit code 125 which tells git bisect to skip the commit because we can’t know if the bug was present (for example if the code doesn’t compile).

In the Lazygit codebase I’ve built myself an integration test system that lets you quickly test functionality from the perspective of a user. It goes like this:

  1. You create a setup shell script to build a sandbox repo from scratch (e.g. git init and add a couple files)
  2. You open lazygit in that sandbox and make some changes.
  3. A recording of the session and the resultant repo snapshot is saved and used for future test runs.

We’ve also got a script that takes a good ref, a bad ref and an integration test name and uses git bisect run to find the problematic commit:

# ./scripts/bisect.sh
git bisect start $1 $2
# if `go build` fails that means the code wouldn't compile, so we tell git bisect we can't know whether it had the bug.
git bisect run sh -c "(go build -o /dev/null || exit 125) && go test ./pkg/gui -run /$3"
git bisect reset

So, say you know that the bug was added after release v0.31. You check out that release, record an integration test, name it ‘myTest’, then run:

./scripts/bisect.sh master v0.31 myTest

And whallah! Sit back and wait a few moments for the bug to be found.

I find integration tests are a good fit for the git bisect run use case because they are immune to internal refactorings, and if you’re going to be manually testing for the bug anyway, you may as well record it once and let your computer take it from there.

So next time you find yourself hunting down a bug manually, think about whether it’s possible to automate the whole process and save yourself some time.

Also, shameless plug: I wrote this piece back in Jan and figured I should probably add a bisect feature to Lazygit so I did just that in version 0.33: give it a go and tell me your thoughts!

Till next time.