Something’s broken. It worked last week. Somewhere in the 47 commits since then, someone introduced a bug. You could check each commit manually, or you could let git do the work.
git bisect performs a binary search through your commit history to find the exact commit that introduced a problem. Instead of checking 47 commits, you check about 6.
The Basic Workflow#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # Start bisecting
git bisect start
# Mark current commit as bad (has the bug)
git bisect bad
# Mark a known good commit (before the bug existed)
git bisect good v1.2.0
# or
git bisect good abc123
# Git checks out a commit halfway between good and bad
# Test it, then tell git the result:
git bisect good # This commit doesn't have the bug
# or
git bisect bad # This commit has the bug
# Git narrows down and checks out another commit
# Repeat until git finds the first bad commit
# When done, return to original state
git bisect reset
|
Example Session#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| $ git bisect start
$ git bisect bad # HEAD is broken
$ git bisect good v2.0.0 # v2.0.0 worked fine
Bisecting: 23 revisions left to test after this (roughly 5 steps)
[abc123...] Add caching layer
$ ./run-tests.sh
Tests pass!
$ git bisect good
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[def456...] Refactor auth module
$ ./run-tests.sh
FAIL!
$ git bisect bad
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[ghi789...] Update dependencies
# ... continue until:
abc123def456789 is the first bad commit
commit abc123def456789
Author: Someone <someone@example.com>
Date: Mon Feb 20 14:30:00 2024
Fix edge case in login flow
This commit introduced the bug!
$ git bisect reset
|
Automated Bisecting#
If you have a script that can test for the bug:
1
2
3
4
5
6
| git bisect start
git bisect bad HEAD
git bisect good v2.0.0
# Run automatically with a test script
git bisect run ./test-for-bug.sh
|
The script should:
- Exit 0 if the commit is good
- Exit 1-127 (except 125) if the commit is bad
- Exit 125 to skip (can’t test this commit)
Example Test Script#
1
2
3
4
5
6
7
8
9
| #!/bin/bash
# test-for-bug.sh
# Build the project (skip if build fails)
make || exit 125
# Run the specific test that catches the bug
./run-specific-test.sh
exit $?
|
Testing for Specific Behavior#
1
2
3
4
5
| #!/bin/bash
# Check if a specific string appears in output
./my-program | grep -q "expected output"
# Exit 0 (good) if found, 1 (bad) if not
|
Handling Untestable Commits#
Sometimes a commit can’t be tested (broken build, unrelated failure):
1
2
3
4
| # Skip this commit
git bisect skip
# Git will try a nearby commit instead
|
If too many commits need skipping, git might not find the exact culprit but will narrow it down to a range.
Bisecting Across Branches#
1
2
3
4
5
6
| # Start from current (broken) branch
git bisect start
git bisect bad
# Good commit can be on another branch or tag
git bisect good origin/stable
|
Viewing Bisect Progress#
1
2
3
4
5
6
7
| # See the current bisect log
git bisect log
# Visualize the bisect range
git bisect visualize
# or
gitk
|
Replaying a Bisect#
If you need to redo or share a bisect:
1
2
3
4
5
| # Save bisect log
git bisect log > bisect.log
# Replay it later
git bisect replay bisect.log
|
Common Patterns#
1
2
3
4
5
6
7
8
9
10
11
12
| #!/bin/bash
# test-performance.sh
# Run benchmark
time_ms=$(./benchmark.sh)
# Fail if slower than threshold
if [ "$time_ms" -gt 1000 ]; then
exit 1 # Bad - too slow
else
exit 0 # Good - fast enough
fi
|
Finding When a Test Started Failing#
1
2
3
4
| git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run pytest tests/test_auth.py::test_login
|
Finding When a File Changed#
1
2
3
| #!/bin/bash
# Did this file exist with specific content?
grep -q "specific pattern" ./path/to/file.py
|
Finding a Build Breakage#
1
2
3
4
| git bisect start
git bisect bad HEAD
git bisect good last-known-good
git bisect run make
|
Tips and Tricks#
Start with Terms You Understand#
If “good” and “bad” feel backwards for your use case:
1
2
3
4
5
| # Define custom terms
git bisect start --term-old=working --term-new=broken
git bisect broken # Current state
git bisect working v1.0.0
|
Or for features:
1
| git bisect start --term-old=without --term-new=with
|
Bisect from a Script’s Perspective#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #!/bin/bash
# Full automated bisect
git bisect start
git bisect bad HEAD
git bisect good $LAST_GOOD_SHA
git bisect run ./test.sh
# Get the bad commit
BAD_COMMIT=$(git bisect view --oneline | head -1 | cut -d' ' -f1)
git bisect reset
echo "Bug introduced in: $BAD_COMMIT"
git show $BAD_COMMIT
|
Handle Flaky Tests#
1
2
3
4
5
6
7
8
9
| #!/bin/bash
# Run test multiple times to handle flakiness
for i in {1..3}; do
if ./flaky-test.sh; then
exit 0 # Passed at least once = good
fi
done
exit 1 # Failed all attempts = bad
|
When Bisect Doesn’t Help#
Bisect assumes the bug was introduced in a single commit. It doesn’t work well for:
- Merge conflicts: The bug might only appear after a merge
- Multiple interacting changes: Two commits together cause the bug
- Environment-dependent bugs: Works on some machines, not others
- Data-dependent bugs: Only fails with specific data
For these, you might need to:
- Bisect to narrow the range
- Manually inspect commits in that range
- Use
git log -p to review changes
Quick Reference#
| Command | Purpose |
|---|
git bisect start | Begin bisecting |
git bisect bad [commit] | Mark commit as having the bug |
git bisect good [commit] | Mark commit as not having the bug |
git bisect skip | Skip untestable commit |
git bisect reset | End bisect, return to original HEAD |
git bisect run <script> | Automate with test script |
git bisect log | Show bisect history |
git bisect visualize | Open gitk to visualize |
Next time you’re staring at a bug that “wasn’t there before,” reach for git bisect. In a few minutes of binary search, you’ll have the exact commit—and usually, the fix becomes obvious once you know where to look.