Git Bisect — Find the Regression-Introducing Commit in Under 10 Minutes
Something worked last month. It doesn’t work now. Between then and now there are four hundred commits, three of them yours, and nobody remembers touching that code. The instinct is to start reading diffs. Don’t. Git has a binary search built in, and it will find the exact commit that broke things in roughly log₂(n) steps — about nine checkouts for four hundred commits.
The Mental Model
git bisect is binary search over your commit history. You tell it one commit where the behaviour was good and one where it’s bad. It checks out the midpoint. You test. You tell it good or bad. It halves the range and repeats.
The only thing you supply is a yes/no answer at each step. Git handles the arithmetic. Four hundred commits becomes nine tests. Ten thousand commits becomes fourteen. The history size barely matters, which is the whole point — this is the one debugging technique that gets relatively faster as the codebase gets older.
The Manual Flow
Setup:
git bisect start
git bisect bad # current HEAD is broken
git bisect good v2.4.0 # this tag worked
Git responds with something like Bisecting: 203 revisions left to test after this (roughly 8 steps) and drops you on a detached HEAD at the midpoint.
Now test. Run the spec, hit the endpoint, click the button — whatever proves the behaviour. Then report back:
Example:
git bisect good # this commit is fine, bug came later
# or
git bisect bad # this commit is broken, bug came earlier
Repeat until git prints the verdict:
a3f8c21b9d4e5f60718293a4b5c6d7e8f9012345 is the first bad commit
commit a3f8c21b9d4e5f60718293a4b5c6d7e8f9012345
Author: someone <[email protected]>
Date: Tue Mar 3 14:22:09 2026 +0545
Refactor order total calculation
When you’re done, always:
git bisect reset
That returns you to the branch you started on. Forgetting this leaves you stranded on a detached HEAD, which is how people end up committing work that vanishes.
Automate It With git bisect run
Manual bisect is fine for bugs you have to eyeball. If you can express the failure as a command that exits non-zero, hand the whole thing to git and walk away.
Example:
git bisect start HEAD v2.4.0
git bisect run bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"
Git runs the command at each step, reads the exit code, and marks the commit automatically. Exit 0 means good, exit 1–124 means bad. It will chew through hundreds of commits unattended and print the culprit.
For anything more involved than a single command, write a script:
Example:
#!/usr/bin/env bash
# bisect_check.sh
set -e
bundle install --quiet
bin/rails db:migrate RAILS_ENV=test 2>/dev/null
bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"
chmod +x bisect_check.sh
git bisect run ./bisect_check.sh
Keep the script out of version control, or at least out of the commits being bisected — otherwise git checks out a revision where your script doesn’t exist yet.
The Exit Code 125 Escape Hatch
Some commits can’t be tested. The migration is mid-rename, a dependency doesn’t install, the app won’t boot for reasons unrelated to your bug. Marking those good or bad poisons the search.
Exit code 125 tells git “skip this one, I can’t judge it.”
Example:
#!/usr/bin/env bash
bundle install --quiet || exit 125
bin/rails runner 'exit 0' >/dev/null 2>&1 || exit 125
bundle exec rspec spec/models/order_spec.rb -e "applies bulk discount"
Manually, the equivalent is git bisect skip. Skip too many adjacent commits and git will tell you it can’t narrow further than a range — that’s honest, and still far better than reading four hundred diffs.
Narrowing the Search Before You Start
Bisect gets faster when you feed it a smaller range and a tighter test.
1. Find a real known-good point
Don’t guess v2.4.0 because it feels old. Check out the tag, run the test, confirm it passes. A wrong “good” endpoint sends the search down the wrong half and produces a confident, incorrect answer.
2. Scope by path
If you know the bug lives in the billing code, tell git to only consider commits that touched it:
git bisect start -- app/models/billing app/services/billing
Commits outside those paths get skipped entirely, which can cut a nine-step search to four.
3. Write the smallest possible reproduction
A full suite run at each step turns a ten-minute bisect into an hour. One focused spec, or a bin/rails runner one-liner, keeps each step under a few seconds.
When the Culprit Is a Merge Commit
Bisect sometimes lands on a merge. That’s not a dead end — it means the bug came in with a branch, and the individual commits on that branch may each have been fine in isolation while their combination is not.
Re-bisect inside the branch:
git bisect start <merge-commit> <merge-commit>^1
That searches the second parent’s lineage — the commits the branch contributed — against the mainline state before the merge.
Pro-Tip: The most common reason bisect gives a wrong answer is a flaky test used as the oracle. Binary search assumes the property is monotonic — good up to a point, bad after it. A test that fails one run in five turns that assumption into noise, and git will confidently name an innocent commit. Before bisecting, run your check three times on a known-bad commit and three times on a known-good one. If it isn’t deterministic, fix the oracle first or bisect on something else entirely — a log line, an HTTP status, a checksum. Ten minutes spent making the check reliable saves an afternoon chasing a phantom.
Recording What You Learned
git bisect log prints the full sequence of good/bad decisions. Paste it into the issue — it’s a compact record of exactly which commits were ruled out and why.
git bisect log > /tmp/bisect.log
# ... later, or on another machine
git bisect replay /tmp/bisect.log
replay reconstructs the entire search state, which is useful when you have to stop halfway through and come back, or when a colleague wants to verify your conclusion without redoing the work.
Conclusion
Bisect converts “which of these four hundred commits broke it” from an archaeology problem into nine yes/no questions. The technique costs nothing to learn and pays off every time a regression sneaks past review. The two things that make it work in practice are a genuinely verified good endpoint and a deterministic test — get those right and git bisect run will hand you the commit hash while you’re getting coffee. On a large, long-lived codebase, that is the difference between fixing a regression this morning and fixing it this week.
FAQs
Q1: Does bisect work if the history has merge commits?
Yes. Git traverses the full commit graph, not just the first-parent line. If it lands on a merge, re-bisect within that branch using the merge commit and its first parent as endpoints.
Q2: What happens to my uncommitted changes when I start bisecting?
Bisect checks out different commits, so a dirty working tree will block it or get in the way. Stash or commit your work first, then git bisect start.
Q3: Can I bisect something that isn’t a test failure?
Any command with a meaningful exit code works — a curl against a local server piped to grep, a build step, a benchmark compared against a threshold. Exit 0 for good, 1–124 for bad, 125 to skip.
Q4: How do I get back to my branch if I forget git bisect reset?
Run git bisect reset at any point; it restores the original HEAD. If you’ve already switched branches manually, git checkout <your-branch> works too, but reset is the clean exit.
Q5: Is there a limit on how far back I can bisect?
Only your history depth. Shallow clones (--depth) are the real constraint — bisect needs the full history in the range, so run git fetch --unshallow on a CI checkout before bisecting there.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀