11. Debugging With git bisect#

You can use

git bisect

to find out which commit caused a bug.

An example repository#

In a nice open source example, I found an arbitrary exemplar on github

import os

try:
    from google.colab import drive  # type: ignore

    drive.mount("/content/drive")
    drive_dir = "/content/drive/MyDrive"
except ImportError:
    print("Not running on colab")
    drive_dir = os.path.join(os.getcwd(), "drive", "MyDrive")
    os.makedirs(drive_dir, exist_ok=True)

print(f"Drive dir: {drive_dir}")

git_dir = os.path.join(drive_dir, "learning_git")
working_dir = os.path.join(git_dir, "git_example")

if os.path.exists(working_dir):
    print(f"Git example directory: {working_dir}")
    os.chdir(git_dir)
else:
    print("Start from the beginning")
Not running on colab
Drive dir: /mnt/nvme1n1p2/home/yj.lee/workspace/projects/lecture/book/lectures/softeng/vcs/drive/MyDrive
Git example directory: /mnt/nvme1n1p2/home/yj.lee/workspace/projects/lecture/book/lectures/softeng/vcs/drive/MyDrive/learning_git/git_example
%%bash
rm -rf bisectdemo
git clone https://github.com/chu-aie/bisectdemo.git
Cloning into 'bisectdemo'...
bisect_dir = os.path.join(git_dir, "bisectdemo")
os.chdir(bisect_dir)
%%bash
python squares.py 2 # 4
4

This has been set up to break itself at a random commit, and leave you to use bisect to work out where it has broken:

%%bash
./breakme.sh > break_output
error: branch 'buggy' not found.
Switched to a new branch 'buggy'
fatal: bad revision 'origin/broken'

Which will make a bunch of commits, of which one is broken, and leave you in the broken final state

%%bash
python squares.py 2 # Error message
4

Bisecting manually#

%%bash
git bisect start
git bisect bad # We know the current state is broken
git checkout master
git bisect good # We know the master branch state is OK
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Bisecting: 499 revisions left to test after this (roughly 9 steps)
[a93619043f8d34fc902c9968898a2690b4e504d1] Comment 500

Bisect needs one known good and one known bad commit to get started

Solving Manually#

python squares.py 2 # 4
git bisect good
python squares.py 2 # 4
git bisect good
python squares.py 2 # 4
git bisect good
python squares.py 2 # Crash
git bisect bad
python squares.py 2 # Crash
git bisect bad
python squares.py 2 # Crash
git bisect bad
python squares.py 2 #Crash
git bisect bad
python squares.py 2 # 4
git bisect good
python squares.py 2 # 4
git bisect good
python squares.py 2 # 4
git bisect good

And eventually:

git bisect good
    Bisecting: 0 revisions left to test after this (roughly 0 steps)

python squares.py 2
    4

git bisect good
2777975a2334c2396ccb9faf98ab149824ec465b is the first bad commit
commit 2777975a2334c2396ccb9faf98ab149824ec465b
Author: Shawn Siefkas <shawn.siefkas@meredith.com>
Date:   Thu Nov 14 09:23:55 2013 -0600

    Breaking argument type
git bisect end

Solving automatically#

If we have an appropriate unit test, we can do all this automatically:

%%bash
git bisect start
git bisect bad HEAD # We know the current state is broken
git bisect good master # We know master is good
git bisect run python squares.py 2
Previous HEAD position was a936190 Comment 500
Switched to branch 'buggy'
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Bisecting: 499 revisions left to test after this (roughly 9 steps)
[a93619043f8d34fc902c9968898a2690b4e504d1] Comment 500
running 'python' 'squares.py' '2'
4
Bisecting: 249 revisions left to test after this (roughly 8 steps)
[c608fbcd461a99d00c2d71d26c64bd6fb26c6c71] Comment 750
running 'python' 'squares.py' '2'
4
Bisecting: 124 revisions left to test after this (roughly 7 steps)
[f400fc7fc75fc9594a1731f6791aa0215fb1ead5] Comment 875
running 'python' 'squares.py' '2'
4
Bisecting: 62 revisions left to test after this (roughly 6 steps)
[acd2b4820c0b62bd7294ddb900c498fdab6ba657] Comment 937
running 'python' 'squares.py' '2'
4
Bisecting: 31 revisions left to test after this (roughly 5 steps)
[f9070a12dba861b399012944f966f08d40205281] Comment 968
running 'python' 'squares.py' '2'
4
Bisecting: 15 revisions left to test after this (roughly 4 steps)
[03b8b76ad005950d86fc5494546cd5435a47cbbd] Comment 984
running 'python' 'squares.py' '2'
4
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[5d99c3aa2cd1b79db65583bd456136cd9fbbcc4e] Comment 992
running 'python' 'squares.py' '2'
4
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[a57fa0037aa12ddd2821b0233a9d917cf821e415] Comment 996
running 'python' 'squares.py' '2'
4
Bisecting: 1 revision left to test after this (roughly 1 step)
[d6ed38f9b76acdb2c76ae773f15031c42b9fbdbe] Comment 998
running 'python' 'squares.py' '2'
4
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[69c99c1f20b21cfc214b07dc630afcc52a23a3a4] Comment 999
running 'python' 'squares.py' '2'
4
b86f514977d51ee81aaea6b10573b8fd9bfd0a38 is the first bad commit
commit b86f514977d51ee81aaea6b10573b8fd9bfd0a38
Author: Young Joon Lee <entelecheia@hotmail.com>
Date:   Mon Sep 18 04:50:46 2023 +0900

    Comment 1000

 squares.py | 1 +
 1 file changed, 1 insertion(+)
bisect found first bad commit

Boom!