Git Bisect: Find the Bug-Introducing Commit in Minutes, Not Hours
Git bisect finds the exact commit that introduced a bug using binary search.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓Basic Git commands (commit, log, checkout)
- ✓Understanding of Git history and commits
- ✓Familiarity with shell scripting
Run git bisect start, mark a bad commit with git bisect bad, mark a good commit with git bisect good , then repeatedly test and mark git bisect good or git bisect bad until Git reports the first bad commit. Automate with git bisect run .
Imagine you're looking for a typo in a 1000-page book. Instead of reading every page, you open the book in the middle. If the typo is after page 500, you tear out the first half. Then you open the remaining half in the middle again. Each time you halve the search space. Git bisect does this with your commit history — it's the fastest way to find the needle in the haystack.
You just got paged at 2 AM. A critical bug is live — payments failing, users screaming. You know it worked last week. But between then and now, there are 200 commits. Manually checking each one? That's a night of your life you'll never get back. Git bisect cuts that to 8 steps. It's the single most underused debugging tool in Git.
The problem is simple: you have a regression. Something broke between a known good state and now. Without bisect, you're either guessing or wasting time. With bisect, you run a binary search through your commit history, and Git tells you exactly which commit caused the problem.
By the end of this, you'll be able to bisect any regression in under 10 minutes — even with thousands of commits. You'll know how to automate it, how to handle merge commits, and when bisect will lie to you.
Why Manual Searching Is a Trap
Before bisect, developers debugged regressions by scrolling through git log and checking commits that looked suspicious. That works if you're lucky. But most regressions come from innocent-looking commits — a refactor, a config change, a dependency bump. You can't spot them by reading diffs.
Bisect automates the search. It uses binary search: with N commits between good and bad, you only need log2(N) tests. For 1000 commits, that's 10 tests. For 100, it's 7. Manual checking would take hours; bisect takes minutes.
The real win? You don't need to understand the codebase. You just need a reliable way to tell if the bug exists at a given commit. That's it.
Running Bisect by Hand: The Step-by-Step
Here's the manual process. You'll do this when the bug is hard to automate — like a UI glitch that requires manual clicking.
First, find a good commit. Use git log --oneline --all to find a commit you know is clean. Then start bisect:
`` git bisect start git bisect bad # current HEAD is bad git bisect good <good-commit> # this commit is good ``
Git will checkout a commit halfway between good and bad. Now test. If the bug exists, run git bisect bad. If not, git bisect good. Git will checkout another commit. Repeat until Git says "X is the first bad commit".
When you're done, run git bisect reset to return to your original branch.
git bisect log to see the history and git bisect replay to re-run with corrections.Automating Bisect with a Test Script
Manual testing is fine for a few steps, but if you have many commits or the bug is reproducible with a script, automate it. git bisect run takes a script that returns 0 for good, any non-zero for bad.
Write a script that sets up the environment, runs the test, and exits with the right code. The script must be idempotent — running it on the same commit should give the same result.
Here's an example for a Python project with pytest. The script runs tests, and if a specific test fails, the bug is present.
git bisect reset if you abort.Handling Merge Commits and Skipping Broken Commits
Merge commits can confuse bisect because they have two parents. By default, bisect follows the first parent (the mainline). But if the bug was introduced in a merge, bisect might point to the merge commit itself rather than the actual commit inside the branch.
To avoid this, use git bisect --first-parent to only follow the mainline. This treats merge commits as single steps, ignoring the branch history. It's faster and more reliable for regressions that span merges.
Sometimes a commit doesn't build or the test can't run. Use git bisect skip to skip it. Git will choose another commit. If too many skips happen, bisect will warn you.
When Bisect Lies: False Positives and Non-Deterministic Bugs
Bisect assumes the bug is deterministic — same commit, same result every time. If the bug is intermittent (race condition, timing, flaky test), bisect will give you a random commit. You'll waste hours chasing ghosts.
To handle flaky tests, run the test multiple times per commit. If it passes 3 out of 5 times, mark it good. Or use a more reliable test. Another approach: bisect on a different metric, like a log message that always appears when the bug occurs.
Another lie: bisect assumes the bug was introduced in a single commit. If the bug is caused by a combination of commits (e.g., two changes that together break something), bisect will point to the first of those commits. That's correct — the first commit that made the bug visible. But the fix might require reverting both.
Bisect on Large Repos: Performance Tips
Bisect on a repo with 50,000 commits? No problem — it only needs ~16 steps. But each step requires a checkout, which can be slow if the repo is huge. Use git bisect --no-checkout to avoid checking out files — Git will only update the index. Then you can run your test against the index without touching the working tree.
Alternatively, use shallow clones or partial clones to reduce history. But for most repos, bisect is fast enough. The bottleneck is your test script, not Git.
When Not to Use Bisect
Also, bisect doesn't help if you don't have a good commit. If the bug has always been there, you need a different approach — like git blame or code review.
For simple cases, use git log --oneline --all --grep="bug" or git blame on the suspicious file. Bisect is a precision tool, not a sledgehammer.
The 3 AM Payment Gateway Meltdown
defaultPaymentMethod = null for new users in a migration script, thinking it was a default value. The commit was 3 days old, buried in 150 commits.git revert <hash>. Then ran git bisect to confirm no other commits were involved.- When a regression appears, don't guess — bisect first.
- It's faster than any hunch.
git bisect log to review marks. 2. Re-run bisect with git bisect replay after correcting marks. 3. Verify test script is deterministic.git bisect --first-parent to skip merge commits. 2. Ensure test script handles build failures with exit code 125 (skip). 3. Consider a different good commit.git bisect reset to clean up. 3. Rewrite script to be read-only.git bisect reset| File | Command / Code | Purpose |
|---|---|---|
| manual_search_vs_bisect.sh | for commit in $(git log --oneline --reverse good..bad | awk '{print $1}'); do | Why Manual Searching Is a Trap |
| manual_bisect.sh | git bisect start | Running Bisect by Hand |
| bisect_run.sh | make build 2>/dev/null || exit 125 # skip if build fails | Automating Bisect with a Test Script |
| handle_merges.sh | git bisect start --first-parent | Handling Merge Commits and Skipping Broken Commits |
| flaky_test_mitigation.sh | PASS=0 | When Bisect Lies |
| no_checkout_bisect.sh | git bisect start --no-checkout | Bisect on Large Repos |
Key takeaways
git bisect run and a deterministic test script--first-parent to avoid false positives from merge commits, and --no-checkout for large repos.Interview Questions on This Topic
How does git bisect handle merge commits, and how can you avoid false positives from merges?
--first-parent to only follow the mainline, treating merges as single steps. This avoids false positives from branch history.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Git. Mark it forged?
3 min read · try the examples if you haven't