Skip to content

lab 43 Finding Commits That Introduced a Bug

Goals

Bugs happen, and it can be necessary to find the cause of a bug without knowing what code specifically is involved. Git allows you to efficiently search a series of commits to find the commit that introduced the bug.

Git bisect asks you to declare a known good commit and a known bad commit. Then git bisect will pick a commit in the middle of that set and ask you test that commit. You’ll test your project at that point either manually or via an automated test, and then you’ll mark the commit as good or bad. Git bisect will continue to pick the midpoint between the latest good and bad commits until you land on the exact commit that introduced the bug.

In this lab we will be considering it a bug to allow users to specify their name and finding that commit that introduced that behavior. While you could find it manually in this project, when there are hundreds or thousands of commits this becomes impractical. git bisect will make this task trivial. We will not be changing any code, only finding the commit that introduced the behavior.

The Test

The first thing we need is a test. git bisect works by checking out different commits and seeing if the test succeeds or fails. The test can be anything. It can be a manual test you perform by running the project, a project test suite, or another command or script. For the bug we’re searching for we’ll need to run our program and inspect the output. If the output includes the name we specified then we know it still has the bug.

Execute:

ruby -Ilib lib/hello.rb BisectString

Output:

$ ruby -Ilib lib/hello.rb BisectString
Hello, BisectString.

This includes the name we specified, so it’s still buggy. This is of course because we didn’t start the bisect yet but it is good practice to confirm your test works as expected.

Using git bisect

Once you have your command using git bisect is fairly straightforward. You need a known location that doesn’t exhibit the bug and a known location that does. git bisect calls these “bad” and “good” or “new” and “old” commits respectively. For our purposes, the bad commit is our HEAD because it includes bug we are not looking for, and the good commit is the initial commit because it does not include the bug. Let’s get started.

Execute:

git bisect start
git bisect good <hash>
git bisect bad HEAD

Output:

$ git bisect start
status: waiting for both good and bad commits
$ git bisect good 91b926e
status: waiting for bad commit, 1 good commit known
$ git bisect bad HEAD
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[1dee7f9aea43849aaa80edbc7bc43e63eb6f5315] Tell user how many names they have

You’ll see an estimate of how many steps are left. Run the test and based on its output run either git bisect good or git bisect bad. Below is an example of what this might look like. One thing to note is that git starts you halfway between the two commits you gave it. This could mean your test process needs to change slightly to accommodate unrelated changes. In this instance the hello.rb file changed locations, so the command to execute it needs to be modified. As you move through the bisect keep this in mind and use whichever command is appropriate to the current repository state. Also notice how the command output changes as you move through the bisect.

Execute:

ruby hello.rb BisectString
git bisect bad

Output:

$ ruby hello.rb BisectString
Hello, BisectString!
You have 1 names!
You have 1 names!
$ git bisect bad
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[28fe3964845c16ff89ac6793c4f9c91fde44f109] Added a comment

Now, we repeat the process until the bisect is finished.

Execute:

ruby hello.rb BisectString
git bisect bad
ruby hello.rb BisectString
git bisect bad
ruby hello.rb BisectString
git bisect bad

Output:

$ ruby hello.rb BisectString
Hello, BisectString!

$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 1 step)
[15c7573dd7a7e91fca1db3a72da1ca4757c90137] Added a default value

$ ruby hello.rb BisectString
Hello, BisectString!

$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7d55044c68b2827a88297c1c44afc61792577352] Using ARGV

$ ruby hello.rb BisectString
Hello, BisectString!

$ git bisect bad
7d55044c68b2827a88297c1c44afc61792577352 is the first bad commit
commit 7d55044c68b2827a88297c1c44afc61792577352
Author: Jim Weirich <jim (at) edgecase.com>
Date:   Mon Oct 24 19:02:00 2022 +0000

    Using ARGV

 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

git bisect will leave your working directory checked out on the first bad commit. You can confirm that it is the commit that introduced the bug by checking out its parent and running the bisect script again.

Execute:

git checkout HEAD^
ruby hello.rb BisectString

Output:

$ git checkout HEAD^
Previous HEAD position was 7d55044 Using ARGV
HEAD is now at 91b926e First Commit
$ ruby hello.rb BisectString
Hello, World

You should not see your specified name in the output. Now that we’ve found the commit run the following command to get back to your starting point.

Execute:

git bisect reset

Automating the Bisect

The bisect process can be automated by providing a script to execute at each bisect point. When it runs it should exit with a 0 code if the commit has the bug and exit with 1 if the commit does not have the bug. Below is an example script you can use to automate this bisect process.

bisect.sh

#!/usr/bin/env bash

if [ -e lib/hello.rb ]
then
    if ruby -Ilib lib/hello.rb BisectString | grep BisectString
    then
        exit 1
    else
        exit 0
    fi
elif [ -e hello.rb ]
then
    if ruby hello.rb BisectString | grep BisectString
    then
        exit 1
    else
        exit 0
    fi
fi

Make it executable.

Execute:

chmod +x bisect.sh

Now to automate the bisect process we use the git bisect run command.

Execute:

git bisect start
git bisect bad HEAD
git bisect good <hash>
git bisect run "./bisect.sh"

In your output you’ll see that the bisect proceeded automatically and finished on the same commit as when we did it manually.

Output:

$ git bisect start
status: waiting for both good and bad commits
$ git bisect bad HEAD
status: waiting for good commit(s), bad commit known
$ git bisect good 91b926e
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[1dee7f9aea43849aaa80edbc7bc43e63eb6f5315] Tell user how many names they have
$ git bisect run "./bisect.sh"
running  './bisect.sh'
Hello, BisectString!
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[28fe3964845c16ff89ac6793c4f9c91fde44f109] Added a comment
running  './bisect.sh'
Hello, BisectString!
Bisecting: 0 revisions left to test after this (roughly 1 step)
[15c7573dd7a7e91fca1db3a72da1ca4757c90137] Added a default value
running  './bisect.sh'
Hello, BisectString!
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7d55044c68b2827a88297c1c44afc61792577352] Using ARGV
running  './bisect.sh'
Hello, BisectString!
7d55044c68b2827a88297c1c44afc61792577352 is the first bad commit
commit 7d55044c68b2827a88297c1c44afc61792577352
Author: Jim Weirich <jim (at) edgecase.com>
Date:   Mon Oct 24 19:02:00 2022 +0000

    Using ARGV

 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit

Bisect and the Importance of Atomic Commits

Bisecting is a powerful technique for doing fault isolation in a codebase, but it can be difficult to use when the codebase is not created from atomic commits. An atomic commit is one that only incorporates a single change in functionality or code structure. That is, all of the changes are syntactically or functionally related to each other. When commits are not atomic, the commit that the bisect points to could be full of unrelated changes making it difficult to determine which one is the one you are looking for.

Additionally, when a feature is partially implemented over the course of several commits and is not working until the last one then the commit that bisect points to may not have any of the changes you are looking for at all. There are a number of other reasons to prefer atomic commits but keep these in mind as you continue your work.