Git Bisect: Find Breaking Commits
Use git bisect to find breaking commits — Git Tutorial.
Focus: use git bisect to find breaking commits
You've shipped a feature, merged it, and moved on. Days later, a test starts failing, or the app crashes in production. The error is real but the cause is buried somewhere in a sea of commits — maybe 50, maybe 500. Scrolling through git log line by line is soul-crushing. This is exactly the pain point that git bisect was built to solve: a binary-search-driven tool that pinpoints the exact commit that introduced a regression, turning hours of manual hunting into a few minutes of automated detective work.
The problem this lesson solves
When a regression appears, your first instinct might be to git log --oneline and stare at every commit message, hoping one triggers a memory. That approach breaks down fast when the change set is large or when the commit messages are vague like "fix stuff" or "refactor". You need a methodical way to identify the guilty commit — and git bisect gives you exactly that.
Without bisect, debugging a regression typically looks like this:
- You manually pick a commit from the middle of the history.
- You check out that commit (or run tests against it) and see if the bug exists.
- You repeat, narrowing the range by guesswork.
That's neither efficient nor reliable. git bisect automates the search by treating your commit history as a binary search space: each check eliminates half of the remaining commits. If you have 100 commits, you only need about 7 checks to find the culprit. With 1,000 commits, it's about 10. This is a massive time saver compared to linear inspection.
Beyond saving time, bisect gives you certainty — you don't just find a commit that could be the problem, you find the commit that turned a working state into a broken one. That knowledge is invaluable when you need to write a precise revert or craft an informed fix.
Core concept / mental model
Think of your commit history as a timeline. At one end — an older commit — everything works. At the other end — your current HEAD — the bug exists. Somewhere in between, a single commit flipped the world from good to bad.
Binary search is the logic that powers bisect. Instead of checking commits one by one, you start in the middle: if the middle commit is bad, the breaking change must be in the earlier half; if it's good, the breaking change is in the later half. You repeat this halving until only one commit remains — that's your culprit.
In Git terms, you label commits as good (no bug) or bad (bug present). Bisect maintains a bisect state — a range of candidate commits — and checks out the midpoint for you. You run your test, mark the result, and Git narrows the range. This is pure binary search applied to your project's history.
A useful analogy: it's the "guess the number" game. If someone thinks of a number between 1 and 100, you don't guess 1, 2, 3... You guess 50, then 25 or 75, and so on. Each guess halves the possibilities. git bisect does the same with commits.
Key terms to remember: - Good commit: a revision you know does not have the bug. - Bad commit: a revision you know does have the bug. - Bisect range: the set of commits between (and including) the first bad and the last good. - Bisect state: Git's internal tracking of the current midpoint and progress.
How it works step by step
Here's the typical workflow for using git bisect to find a breaking commit:
- Start bisect: Run
git bisect start. This puts Git into bisect mode. - Mark a known bad commit:
git bisect bad(orgit bisect bad <commit-ish>) tells Git that the current HEAD (or a specified commit) exhibits the bug. - Mark a known good commit:
git bisect good <commit-ish>tells Git which commit was the last known good state. This must be an ancestor of the bad commit. - Run your test: Git checks out the midpoint of the range. You need to test this specific revision — run your test suite, compile the app, or run the specific failing scenario.
- Mark the result: If the bug is present, run
git bisect bad; if absent, rungit bisect good. Git then halves the range and checks out the next midpoint. - Repeat steps 4–5 until Git reports the first bad commit.
- Clean up: Once done, run
git bisect resetto exit bisect mode and return to your original branch and HEAD.
If you can automate your test, you can run the entire process hands-free using git bisect run, which scripted the marking steps for you.
Hands-on walkthrough
Let's see this in action with a simple Python project. We'll create a repo with a function that's been broken by a later commit.
# Set up a demo repo
mkdir bisect-demo && cd bisect-demo
git init
echo "print('hello')" > app.py
git add app.py && git commit -m "initial commit"
# Simulate a history of 10 commits
for i in $(seq 1 10); do
echo "print('change $i')" >> app.py
git commit -am "change $i"
done
# Introduce a bug in commit 'change 7'
# (We'll edit app.py to raise an error)
sed -i "s/print('change 7')/raise Exception('bug introduced')/" app.py
git commit -am "change 7 (bug)"
# Add a few more commits after the bug
for i in $(seq 8 10); do
echo "print('change $i')" >> app.py
git commit -am "change $i"
done
Now, let's find the commit that introduced the bug using bisect.
# Start bisect
git bisect start
# Mark current HEAD (which has the bug) as bad
git bisect bad
# Mark the initial commit as good (we know it works)
git bisect good <initial-commit-sha>
Git checks out a middle commit and says something like "Bisecting: 5 revisions left to test". Now run your test:
# Test the current checkout
python app.py
If it fails (you see the exception), mark as bad:
git bisect bad
If it passes, mark as good:
git bisect good
Repeat until Git says:
<commit-sha> is the first bad commit
commit <sha> ...
change 7 (bug)
Finally, exit bisect mode:
git bisect reset
For a fully automated search, write a test script and run it:
# test.sh
#!/bin/bash
python app.py > /dev/null 2>&1
chmod +x test.sh
git bisect start
git bisect bad
git bisect good <initial-sha>
git bisect run ./test.sh
git bisect reset
Git will automatically mark each commit based on the script's exit code (0 = good, non-zero = bad). This is a huge win in CI or when the test is expensive to run manually.
Compare options / when to choose what
You have a few ways to find a breaking commit. Here’s a quick comparison:
| Method | Pros | Cons | Best for |
|---|---|---|---|
git log eyeballing |
No setup, works with small histories | Slow, error-prone with many commits | A handful of commits |
git bisect manual |
Precise, controls the test | Requires manual testing each step | Medium histories, where tests can't be automated |
git bisect run |
Fully automated, fast | Needs a script/test that returns exit code | Automated test suites, CI environments |
git blame |
Finds the commit that changed a specific line | Only shows last change, not when a bug was introduced | If you already know the line causing the issue |
When to choose what:
- Small history (fewer than ~10 commits):
git logmight be fine, but bisect is still faster and more reliable. - Larger history (dozens or hundreds of commits): Always use
git bisect— manual or run. - Automated tests available:
git bisect runis your best pick. - If you know the exact line:
git blamecan show you the commit, but it might not be the one that introduced the bug (a later refactor could have changed the line).
Variation: using a good/bad branch instead of commit SHAs — You can also do git bisect start <bad-branch> <good-branch> to set the range, which is handy when you have long-running branches.
Another variation is using git bisect skip — if a commit can’t be tested (e.g., it won’t compile), you skip it and Git picks another midpoint.
Troubleshooting & edge cases
- Error: "You need to start by "git bisect start"" — You forgot to run
git bisect start. Run it first. - Error: "Bad rev input" — You passed a bad commit reference. Check that the SHA or branch name exists.
- Error: "Cannot bisect (no good commits known)" — You marked a bad commit but didn't mark a good one. Start with a good commit that's an ancestor of the bad one.
- Error: "external diff died, stopping at ..." — This can happen if you have a custom diff tool. Try setting
GIT_EXTERNAL_DIFFto empty or rungit bisect resetand redo. - Merge commits complicate the picture — Bisect treats the history as a linear sequence. When working with a non-linear history (merges), bisect may not work as expected. In such cases, use
git bisect start <bad> <good>with a straight line of commits, or use--no-checkoutif you need to keep the working tree intact. - Your test is flaky — Inconsistent test results can mislead the bisect. Ensure you have a deterministic test. If a commit is genuinely hard to test (e.g., it doesn't compile), use
git bisect skip. - You inadvertently broke the working tree — Checkouts during bisect can change files. Run
git bisect resetto restore your branch to its original state.
What you learned & what's next
You've now mastered the core skill of using git bisect to find breaking commits. To recap:
- You understand the binary-search mental model behind bisect.
- You can run a manual bisect session, marking commits as good or bad.
- You can automate the search using
git bisect runwith a test script. - You know how to handle common pitfalls and edge cases.
This skill is a massive time-saver in real-world debugging, especially when regressions surface after many commits. Whether you're hunting a sandbox-breaking bug in a small script or a regression in a large codebase, git bisect puts a precise tool in your hands.
Next in the Git Tutorial track, you'll learn how to use git reflog to recover lost commits — the perfect companion for those moments when you've accidentally reset or rebased and lost work. With bisect and reflog, you'll have both a surgical debugger and a safety net for your Git history.
Pro tip: When you find the breaking commit, consider writing a regression test for the bug before you fix it. This ensures the exact issue is covered and prevents future regressions from slipping through.
Practice recap
Set up a small repo with 20–30 commits, deliberately introduce a bug in one of them, and then use git bisect to find it. First, do it manually. Then, write a simple test script (even python -c "...") and try git bisect run. Finally, try skipping a commit that you 'can't test' to see how Git handles it.
Common mistakes
- Forgetting to run
git bisect resetafter the search — this leaves your repo in a detached HEAD state and can confuse future commits. - Marking a commit as good when you didn't actually test it — always run your test on the checked-out commit before labeling it.
- Starting bisect without a known good commit that's an ancestor of the bad one — this can lead to an invalid range.
- Relying on
git bisect runwithout ensuring your script returns a correct exit code (0 for good, non-zero for bad). - Ignoring merge commits — bisect works best on linear history; you may need to flatten the graph first.
Variations
- Use
git bisect runwith a test script for fully automated searching — ideal in CI pipelines. - Use
git bisect skipto jump over commits that can't be tested (e.g., won't compile). - You can pass branch names instead of SHAs to
git bisect start <bad-branch> <good-branch>for easier setup.
Real-world use cases
- A CI pipeline fails a unit test after merging a feature branch; you use
git bisect runagainst the test to find the exact commit that broke it. - A production bug that only manifests on a specific dataset is traced back to a commit from a month ago; manual bisect narrows it down to one commit.
- A code refactor introduces a subtle performance regression; you use
git bisectwith a benchmark script to pinpoint the change that slowed things down.
Key takeaways
git bisectuses a binary search over your commit history to find the first bad commit efficiently.- Always start with
git bisect start, mark a bad commit, and provide a known good ancestor. - Automate the search with
git bisect runby providing a test script that returns an exit code. - Handle untestable commits with
git bisect skipto keep the search moving. - Always run
git bisect resetwhen you're done to restore your repo to a normal state. - Combine bisect with regression tests — after finding the culprit, write a test to prevent future regressions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.