XClose

Research Software Engineering Summer School

Home
Menu

Debugging With Git Bisect

NOTE: using bash/git commands is not fully supported on jupyterlite yet (due to single thread/process restriction), and the cells below might error out on the browser (jupyterlite) version of this notebook

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

In [1]:
import os
top_dir = os.getcwd()
git_dir = os.path.join(top_dir, 'learning_git')
os.chdir(git_dir)
In [2]:
%%bash
rm -rf bisectdemo
git clone https://github.com/UCL-ARC-RSEing-with-Python/bisectdemo.git
Cloning into 'bisectdemo'...
In [3]:
bisect_dir=os.path.join(git_dir,'bisectdemo')
os.chdir(bisect_dir)
In [4]:
%%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:

In [5]:
%%bash
./breakme.sh > break_output
Switched to a new branch 'buggy'

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

In [6]:
%%bash
python squares.py 2 # Error message
Traceback (most recent call last):
  File "/home/runner/work/rsd-summerschool/rsd-summerschool/ch00g
it/learning_git/bisectdemo/squares.py", line 9, in <module>
    print(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand type(s) for ** or pow(): '
str' and 'int'
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[6], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'python squares.py 2 #\xa0Error message\n')

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2542, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2540 with self.builtin_trap:
   2541     args = (magic_arg_s, cell)
-> 2542     result = fn(*args, **kwargs)
   2544 # The code below prevents the output from being displayed
   2545 # when using magics with decorator @output_can_be_silenced
   2546 # when the last Python token in the expression is a ';'.
   2547 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:159, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    157 else:
    158     line = script
--> 159 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:336, in ScriptMagics.shebang(self, line, cell)
    331 if args.raise_error and p.returncode != 0:
    332     # If we get here and p.returncode is still None, we must have
    333     # killed it but not yet seen its return code. We don't wait for it,
    334     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    335     rc = p.returncode or -9
--> 336     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'python squares.py 2 #\xc2\xa0Error message\n'' returned non-zero exit status 1.

Bisecting manually

In [7]:
%%bash
git bisect start
git bisect bad # We know the current state is broken
git switch main
git bisect good # We know the main branch state is OK
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
warning: you are switching branch while bisecting
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Bisecting: 500 revisions left to test after this (roughly 9 steps)
[d2d7619ce1f0332ba9a0260e32d0844040215043] Comment 499

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

Stop the bisect process with:

git bisect reset

Solving automatically

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

(NOTE: You don't need to redirect the stderr and stdout (with &>) of git bisect run to a file when running these commands outside a jupyter notebook (i.e., on a shell). This is done here so the errors appears with the right commits)

In [8]:
%%bash
git bisect start
git bisect bad HEAD # We know the current state is broken
git bisect good main # We know main is good
git bisect run python squares.py 2 &> gitbisect.out
cat gitbisect.out
Previous HEAD position was d2d7619 Comment 499
Switched to branch 'buggy'
status: waiting for both good and bad commits
status: waiting for good commit(s), bad commit known
Bisecting: 500 revisions left to test after this (roughly 9 steps)
[d2d7619ce1f0332ba9a0260e32d0844040215043] Comment 499
running 'python' 'squares.py' '2'
Traceback (most recent call last):
  File "/home/runner/work/rsd-s
ummerschool/rsd-summerschool/ch00git/learning_git/bisectdemo/squares.py", line 9, in <module>
    pr
int(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand type(s) for ** or pow(): 'str' a
nd 'int'
Bisecting: 249 revisions left to test after this (roughly 8 steps)
[50b6453de38197ed13a5805
aaf97beb4234e8d10] Comment 249
running 'python' 'squares.py' '2'
Traceback (most recent call last):
  File "/home/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/bisectdemo/squares.
py", line 9, in <module>
    print(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand t
ype(s) for ** or pow(): 'str' and 'int'
Bisecting: 124 revisions left to test after this (roughly 7 
steps)
[c45d6e521f8097dd2f206570dc3e79a1d8b081a4] Comment 125
running 'python' 'squares.py' '2'
4
Bi
secting: 62 revisions left to test after this (roughly 6 steps)
[f794816584c5efa05c6a61903c20c3e53ea
076cb] Comment 187
running 'python' 'squares.py' '2'
4
Bisecting: 31 revisions left to test after th
is (roughly 5 steps)
[c4382320724f8bc16867c0c8baeab7e97904e0ef] Comment 218
running 'python' 'square
s.py' '2'
4
Bisecting: 15 revisions left to test after this (roughly 4 steps)
[5f81c4886f11341f6b45d
3cb8f8daf538d9e225f] Comment 233
running 'python' 'squares.py' '2'
Traceback (most recent call last)
:
  File "/home/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/bisectdemo/square
s.py", line 9, in <module>
    print(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand
 type(s) for ** or pow(): 'str' and 'int'
Bisecting: 7 revisions left to test after this (roughly 3 
steps)
[0df8456996e9acbda42b4414233c4740e352fa66] Comment 226
running 'python' 'squares.py' '2'
4
Bi
secting: 3 revisions left to test after this (roughly 2 steps)
[835942acec9def60627ea00a7e3e8f7248d0
12a1] Comment 229
running 'python' 'squares.py' '2'
Traceback (most recent call last):
  File "/home
/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/bisectdemo/squares.py", line 9, 
in <module>
    print(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand type(s) for **
 or pow(): 'str' and 'int'
Bisecting: 1 revision left to test after this (roughly 1 step)
[ca3b9ab38
5941cd8e0c413a1479193d405876a9e] Comment 228
running 'python' 'squares.py' '2'
4
Bisecting: 0 revisi
ons left to test after this (roughly 0 steps)
[38fa6242e920228b50a45e6a493e41864bee00d1] Breaking ar
gument type
running 'python' 'squares.py' '2'
Traceback (most recent call last):
  File "/home/runne
r/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/bisectdemo/squares.py", line 9, in <mo
dule>
    print(integer**2)
          ~~~~~~~^^~
TypeError: unsupported operand type(s) for ** or po
w(): 'str' and 'int'
38fa6242e920228b50a45e6a493e41864bee00d1 is the first bad commit
commit 38fa624
2e920228b50a45e6a493e41864bee00d1
Author: Shawn Siefkas <shawn.siefkas@meredith.com>
Date:   Thu Nov
 14 09:23:55 2013 -0600

    Breaking argument type

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

Boom!