Occasionally, despite all our best intentions, a regression in functionality or user interface might creep into an app during the development cycle. Something that previously worked correctly will no longer function as intended. We’ll discover it later: when implementing another feature, or during developer testing, or once the app has made its way to our crack QA team. Finding that a problem exists in the first place is quite straightforward, but in order to fix it we need to be able to identify when the issue was introduced. If we can find the Git commit that introduced the issue, we can see exactly which lines of code changed, and revert the regression.

If you’d like to follow along with this article, I’ve created a small iPhone project which can be found on Github. It’s a simple app with one button and one label. Whenever the button is tapped, the number displayed on the label is incremented by 1. Or at least, that’s what should happen. At some point in the project’s commit history, the button has stopped working and the counter is no longer being incremented. We need to fix it! So how do we track down where it all went wrong?

We could attempt to find the bug manually. We could check out each commit in turn until we find one where the problem doesn’t exist. The example counter project only has 11 commits, but a real project would probably have tens of commits a day; it would take a lot of time and manual effort. Enter git bisect.

Git Bisect

The git bisect command performs a binary search on our commits to help us find a problem or regression. It’s very simple and fast to use, and it’s an invaluable git tool that not enough people know about.

To use git bisect, we need to know 2 things:

  1. The commit hash for a ‘good’ commit where everything is working as intended
  2. The commit hash for a ‘bad’ commit where our bug is present

The good commit could be a commit from yesterday, a week ago, a month ago, or even right at the start of the project (although this will take you longer to track down the rogue commit!). It could also be a tagged release; perhaps the last one that was QA verified. The bad commit is usually the most recent one.

Once git bisect knows about these two fixed points, it will begin to automatically check out intermediate commits for us to verify. We tell it whether each of those commits is ‘good’ or ‘bad’, and it uses that information to determine which which set of commits to disregard as part of the binary search. A binary search is extremely efficient for this kind of problem; even if there were 1000 commits between our known good and bad commits we would only have to test 10 of them (in the worst case) to find the offending one.

Using Git Bisect

In your project’s root directory, start bisecting:

> git bisect start

You won’t see any output at this stage, until we’ve told git about the good commit and the bad commit. Let’s do that now. First, the good commit:

> git bisect good c35d6ba2

Then tell git about the bad commit. If the bad commit is the current commit you’re on, you can omit the hash entirely:

> git bisect bad

Git will then begin bisecting by checking out a commit that is halfway between the known good commit and the known bad commit:

➜   Counter git:(master) git bisect start
➜   Counter git:(master) git bisect good d28a4555
➜   Counter git:(master) git bisect bad
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[8984c77817c720878e4eec14cce503ce858dbc05] Deleted unnecessary code from ViewController.

We now need to run this version of the project to see if it’s good or bad. Hit Run in Xcode and check in the iOS Simulator to see if the button correctly increments the counter. This version works just fine, so we’ll tell git that it’s a good commit:

> git bisect good

Git will now knows that everything between our original good commit and the commit we just tested was ‘good’, so it can discount them from our investigations. It’ll now check out a commit halfway between the previous good commit and the known bad commit:

Bisecting: 1 revision left to test after this (roughly 1 step)
[0303df871150dd77c4bf820c79f259f111aeb15a] Condensed incrementCount: to remove unnecessary variable.

It’s back to Xcode to build and run again. This commit is bad, so we’ll tell git:

> git bisect bad

Finally git does one more checkout for us to test:

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[fc09c51ec808d37a6b5a96cc201307fe391efdd8] Updated incrementing logic.

This commit is good. Once we’ve told git, it’ll let us know where everything went wrong: the first bad commit.

0303df871150dd77c4bf820c79f259f111aeb15a is the first bad commit
commit 0303df871150dd77c4bf820c79f259f111aeb15a
Author: James Frost
Date:   Sun Dec 8 12:38:29 2013 +0000

Condensed incrementCount: to remove unnecessary variable.

:040000 040000 be30ea7196e1287ef970a7a18506f6486fb527ef 61bcc7378d924bf44d52a741ed3084abbc28c55c M  Counter

The commit message here would seem to indicate that something might have gone wrong when I refactored the incrementCount: method. We can compare the bad commit to the commit immediately before it to see exactly what changed:

> git diff 0303df87^!

diff --git a/Counter/JFViewController.m b/Counter/JFViewController.m
index 59c0cf7..ca0be39 100644
--- a/Counter/JFViewController.m
+++ b/Counter/JFViewController.m
@@ -16,10 +16,8 @@
@implementation JFViewController

- (IBAction)incrementCounter:(id)sender
-    NSInteger counterValue = [self.counterLabel.text integerValue] + 1;
- self.counterLabel.text = [@(counterValue) stringValue];
+    self.counterLabel.text = [@([self.counterLabel.text integerValue]) stringValue];


It looks like I forgot to include the “+ 1” when condensing these two lines of code! In just a couple of minutes and with minimal effort, we’ve tracked down the exact location that the bug was introduced[1].

Finally, now we’re finished bisecting, we can reset back to HEAD:

> git bisect reset

Git bisect is an incredibly useful tool and doesn’t take long to learn[2]. Hopefully this brief introduction will encourage you to use it next time you find yourself wondering “where did it all go wrong?”

  1. As an aside, this example highlights the benefits of performing lots of small, dedicated commits for specific changes or features instead of one large commit that contains many changes.  ↩
  2. It’s even possible to automate this entire process if you can programmatically detect ‘good’ or ‘bad’ commits. For example if you have a set of tests you can run, you can tell git bisect to run them itself for each commit it checks out and to decide for itself whether a commit is good or bad.  ↩

By James Frost, Senior iOS developer

Join Our Mailing List

You have Successfully Subscribed!