XClose

MPHY0021: Research Software Engineering With Python

Home
Menu

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

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-RITS/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 "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)
/tmp/ipykernel_10362/1704875464.py in <module>
----> 1 get_ipython().run_cell_magic('bash', '', 'python squares.py 2 #\xa0Error message\n')

/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/site-packages/IPython/core/interactiveshell.py in run_cell_magic(self, magic_name, line, cell)
   2417             with self.builtin_trap:
   2418                 args = (magic_arg_s, cell)
-> 2419                 result = fn(*args, **kwargs)
   2420             return result
   2421 

/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/site-packages/IPython/core/magics/script.py in named_script_magic(line, cell)
    140             else:
    141                 line = script
--> 142             return self.shebang(line, cell)
    143 
    144         # write a basic docstring:

/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/site-packages/decorator.py in fun(*args, **kw)
    230             if not kwsyntax:
    231                 args, kw = fix(args, kw, sig)
--> 232             return caller(func, *(extras + args), **kw)
    233     fun.__name__ = func.__name__
    234     fun.__doc__ = func.__doc__

/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/site-packages/IPython/core/magic.py in <lambda>(f, *a, **k)
    185     # but it's overkill for just that one bit of state.
    186     def magic_deco(arg):
--> 187         call = lambda f, *a, **k: f(*a, **k)
    188 
    189         if callable(arg):

/opt/hostedtoolcache/Python/3.8.12/x64/lib/python3.8/site-packages/IPython/core/magics/script.py in shebang(self, line, cell)
    243             sys.stderr.flush()
    244         if args.raise_error and p.returncode!=0:
--> 245             raise CalledProcessError(p.returncode, cell, output=out, stderr=err)
    246 
    247     def _run_script(self, p, cell, to_close):

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 checkout master
git bisect good # We know the master branch state is OK
Your branch is up to date with 'origin/master'.
Bisecting: 500 revisions left to test after this (roughly 9 steps)
[3cf0f9f7b0c1e89f3fb6f69469b6252bf1ad2791] Comment 500
Switched to branch 'master'

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 master # We know master is good
git bisect run python squares.py 2 &> gitbisect.out
cat gitbisect.out
Bisecting: 500 revisions left to test after this (roughly 9 steps)
[3cf0f9f7b0c1e89f3fb6f69469b6252bf1ad2791] Comment 500
running  'python' 'squares.py' '2'
4
Bisecting: 250 revisions left to test after this (roughly 8 steps)
[7dfda4828315eb360bc7cc86037026265b9561a6] Comment 750
running  'python' 'squares.py' '2'
4
Bisecting: 125 revisions left to test after this (roughly 7 steps)
[be07d32fdb15eff4de59e6786fe7e8a1a2d493ec] Comment 875
running  'python' 'squares.py' '2'
4
Bisecting: 62 revisions left to test after this (roughly 6 steps)
[63f5bf021ede971f1fa4db0eae00fa2288ea8b73] Comment 938
running  'python' 'squares.py' '2'
4
Bisecting: 31 revisions left to test after this (roughly 5 steps)
[ab41ca85b182f7cf15c8bd2ba4900338ffa6841c] Comment 969
running  'python' 'squares.py' '2'
4
Bisecting: 15 revisions left to test after this (roughly 4 steps)
[74179e737b022f383dc54c3d93224139149f873f] Comment 985
running  'python' 'squares.py' '2'
4
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[c98beb947379d47138efcdf93b84c1fe8392db43] Comment 992
running  'python' 'squares.py' '2'
Traceback (most recent call last):
  File "squares.py", line 9, in <module>
    print(integer**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[b786b62e53d7a94552cb747e3b551f386b628347] Comment 989
running  'python' 'squares.py' '2'
4
Bisecting: 1 revision left to test after this (roughly 1 step)
[8f15b7ac29aced9d3a92f4cbfc82b391f146c65e] Comment 991
running  'python' 'squares.py' '2'
4
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[49dea67cb43b2a28d15981cceb3fe7eb799bbbfa] Breaking argument type
running  'python' 'squares.py' '2'
Traceback (most recent call last):
  File "squares.py", line 9, in <module>
    print(integer**2)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
49dea67cb43b2a28d15981cceb3fe7eb799bbbfa is the first bad commit
commit 49dea67cb43b2a28d15981cceb3fe7eb799bbbfa
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
Previous HEAD position was 3cf0f9f Comment 500
Switched to branch 'buggy'

Boom!