Git Bisect: Find the Bug-Introducing Commit in Minutes
A silent bug slipped in somewhere across 200 commits. Git bisect uses binary search to pinpoint the culprit in O(log n) steps, not O(n).
Imagine this: a user reports that a feature is silently broken. No test caught it, no alert fired. You check git log and count 200 commits since the last known-good version. Questions stack up: which commit introduced it? Why does this code even exist, and what was it trying to do? Checking each one manually would take hours.
Git bisect solves this with binary search: a strategy where you repeatedly check the midpoint of a range and eliminate half the remaining possibilities each step. Applied to commits, 200 commits becomes 8 steps. 1,000 commits becomes 10 steps. You find the exact commit that introduced the bug.
What you will have at the end
By the end, you will find the exact commit that broke a function using git bisect, first manually and then with a fully automated script. Tested on Git 2.39+ on macOS 14 and Ubuntu 22.04, June 2026.
Final output:
b3f91a2 is the first bad commit
commit b3f91a2e7d5c1a0f3b8e9d4c2a1f6e7b8c9d0e1
Author: You <you@example.com>
Date: Mon Jan 15 14:32:01 2026
refactor: extract validation logicThat is the commit you want. Name, date, and a diff you can read and revert.
Why not just read git log?
With a handful of commits, reading the log is fine. But bugs may surface late. By the time a test fails in CI, the culprit could be dozens of commits back, buried among refactors, dependency bumps, and formatting fixes.
Binary search removes the guesswork. You need only two things: a bad commit (current HEAD) and a good commit (some point before the bug appeared). Git bisect narrows the gap from there.
Prerequisites
- Git 2.x (
git --versionto confirm) - A repository with at least a few commits of history
- A way to test whether a given commit is broken: a test command, a script, or a quick manual check
How bisect works
The midpoint is the commit that sits exactly halfway between your known-good and known-bad boundaries. Git calculates it automatically, checks it out, and waits for you to label it. When you do, one boundary moves inward and Git picks a new midpoint. The loop repeats until the two boundaries are adjacent and only one commit remains.
Each loop cuts the remaining commits in half. The midpoint moves each round. Here is what that looks like on a history of 8 commits, where commit 5 introduced the bug:
Start: [1 2 3 4 5 6 7 8] bad=8, good=1, midpoint=4
Round 1: test commit 4 → PASS move good boundary to 4
[ 5 6 7 8] new midpoint=6
Round 2: test commit 6 → FAIL move bad boundary to 6
[ 5 6] new midpoint=5
Round 3: test commit 5 → FAIL boundaries adjacent, range collapsed
Result: commit 5 is the first bad commitThree tests for eight commits. That ratio holds at any scale: 1,000 commits takes at most 10 tests.
Walking through a real session
The goal here is to simulate the exact scenario from the intro: a bug slipped in silently, we know a good starting point, and we want git bisect to surface the exact commit. We will build a small repository where we control which commit breaks things, then run bisect as if we did not know the answer.
Step 1: Create a demo repository
Set up a small repo that simulates a real regression. A JavaScript add function gets silently broken in one commit.
mkdir bisect-demo && cd bisect-demo
git initAdd a function and build some history around it:
cat > math.js << 'EOF'
function add(a, b) { return a + b; }
module.exports = { add };
EOF
git add . && git commit -m "initial: add function"
git commit --allow-empty -m "chore: update readme"
git commit --allow-empty -m "feat: add multiply"
git commit --allow-empty -m "refactor: clean up imports"
# Introduce the bug
cat > math.js << 'EOF'
function add(a, b) { return a - b; }
module.exports = { add };
EOF
git add . && git commit -m "refactor: extract validation logic"
git commit --allow-empty -m "feat: add divide"
git commit --allow-empty -m "fix: unrelated typo"
git commit --allow-empty -m "chore: bump dependencies"Check the full history:
git log --onelineExpected output (your hashes will differ):
a1b2c3d (HEAD) chore: bump dependencies
e4f5a6b fix: unrelated typo
c7d8e9f feat: add divide
b3f91a2 refactor: extract validation logic <- the bug lives here
8a9b0c1 refactor: clean up imports
7d8e9f0 feat: add multiply
6c7d8e9 chore: update readme
5b6c7d8 initial: add functionStep 2: Start the bisect session
A bisect session is the active search process managed by Git. It tracks which commits are good, which are bad, and which midpoint to test next.
git bisect startExpected output:
status: waiting for both good and bad commitsStep 3: Mark the current commit as bad
Tell Git that HEAD is broken. In practice this is the commit your CI just flagged.
git bisect badExpected output:
status: waiting for good commit(s), bad commit knownStep 4: Mark a known-good commit
Pick the last commit you know was working. The older it is, the more commits Git searches, but the step count is still O(log n). Use the SHA from your git log --oneline output.
git bisect good 5b6c7d8 # replace with your "initial: add function" SHAExpected output:
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[c7d8e9f] feat: add divideGit checked out the midpoint commit. You are no longer on HEAD. Do not worry about that; git bisect reset will restore your branch at the end.
Step 5: Test and mark each midpoint
At each step, Git waits for you to decide whether the current commit is good or bad. Test it:
node -e "const {add} = require('./math.js'); console.log(add(2, 3) === 5 ? 'PASS' : 'FAIL')"If the output is PASS, mark it good:
git bisect goodIf the output is FAIL, mark it bad:
git bisect badRepeat until Git prints the culprit:
b3f91a2 is the first bad commit
commit b3f91a2...
Author: You <you@example.com>
Date: ...
refactor: extract validation logicCheckpoint: You should see "is the first bad commit" and a single SHA. If you still see "Bisecting:", there are more rounds to complete.
Step 6: Inspect the culprit
git show b3f91a2The diff shows exactly what changed. From here you can understand the bug, file an issue, or revert the commit:
git bisect reset # always do this first to restore HEAD
git revert b3f91a2 # optional: undo the commitStep 7: Reset the session
Git modifies your working tree during bisect. Resetting restores your branch.
git bisect resetExpected output:
Previous HEAD position was b3f91a2 refactor: extract validation logic
Switched to branch 'main'Skipping this step leaves your repository in a detached HEAD state, meaning you are no longer on any branch. Always reset before continuing normal work.
Automate with git bisect run
Pro tip: Once you have a reliable test command,
git bisect runremoves you from the loop entirely. Useful for large histories and CI pipelines where you want zero human interaction.
Manual marking works for quick investigations. For large histories or CI pipelines, git bisect run removes the human entirely. You provide a script that exits 0 for good and non-zero for bad, and Git drives every step.
Create the test script:
cat > test.sh << 'EOF'
#!/bin/sh
node -e "const {add} = require('./math.js'); process.exit(add(2, 3) === 5 ? 0 : 1)"
EOF
chmod +x test.shRun the full automated bisect:
git bisect start
git bisect bad HEAD
git bisect good 5b6c7d8
git bisect run ./test.shExpected output:
running ./test.sh
Bisecting: 3 revisions left to test after this (roughly 2 steps)
running ./test.sh
Bisecting: 1 revision left to test after this (roughly 1 step)
running ./test.sh
b3f91a2 is the first bad commit
bisect run successNo interaction required. The script ran on every midpoint and stopped at the first bad commit.
Edge cases worth knowing
The commit does not build
Sometimes a midpoint cannot be tested because the code does not compile, or a dependency from that era is missing.
What to do:
git bisect skipWhat to expect: Git picks an adjacent commit and continues. If several consecutive commits are skipped, the final answer may be a small range ("the bug is somewhere between commit A and commit B") rather than a single SHA. That range is still narrow enough to inspect manually.
Flaky tests
A test that randomly passes or fails on the same code will misclassify commits. One wrong good/bad label shifts the entire search in the wrong direction, and bisect will point at an innocent commit.
What to do: fix the flakiness before using git bisect run. You can verify a test is stable by running it ten times on the same commit and checking for consistent output. Manual bisect is safer with flaky tests because you can re-run before marking.
Multiple regressions
Bisect finds the first bad commit in the range you defined. If two separate bugs were introduced in the same window, bisect stops at the earlier one.
What to do: after fixing the first bug, run a fresh bisect session with updated bounds. Mark the commit just after the first fix as your new "good" starting point and repeat.
The one thing to remember
Start with a good commit as far back as you need. A wider initial range adds at most one or two extra steps, and every step you save is a minute you keep. When in doubt, start at the last release tag.
References
- "git bisect documentation." git-scm.com
- "git bisect run scripting guide." git-scm.com