Git Bisect: Find the Commit That Introduced a Bug
Learn how to use git bisect to perform binary search through your commit history and pinpoint the exact commit that introduced a bug in your codebase.
What you'll learn
- ✓How git bisect performs binary search through commits
- ✓Running bisect manually with good and bad markers
- ✓Automating bisect with a test script
- ✓Real-world debugging workflows with bisect
Prerequisites
- •Basic Git commands (commit, log, checkout)
- •Familiarity with the command line
Why Finding the Right Commit Matters
You are working on a project and suddenly a feature that worked last week is broken. The test suite passes on an older tag but fails on main. Somewhere in between, a commit introduced the regression. Scrolling through git log and checking commits one by one is painfully slow, especially when there are hundreds of commits in the range.
git bisect solves this by applying binary search to your commit history. Instead of checking every commit linearly, it halves the search space with each step. For 1,000 commits, you only need about 10 steps instead of 1,000.
How Git Bisect Works
The algorithm is straightforward:
- You tell Git a “bad” commit (where the bug exists) and a “good” commit (where everything worked).
- Git checks out the commit in the middle of that range.
- You test that commit and tell Git whether it is “good” or “bad.”
- Git narrows the range and checks out the next midpoint.
- This repeats until Git identifies the single commit that introduced the problem.
Mathematically, for n commits between good and bad, you need at most log2(n) steps.
Starting a Basic Bisect Session
Let’s walk through a hands-on example. Suppose your test suite fails on HEAD but worked fine at tag v2.1.0.
# Start the bisect session
git bisect start
# Mark the current commit as bad
git bisect bad
# Mark the known good commit
git bisect good v2.1.0
Git responds with something like:
Bisecting: 47 revisions left to test after this (roughly 6 steps)
[a3b8c9d] Refactor user authentication module
Git has checked out a commit halfway between v2.1.0 and HEAD. Now you test this commit.
Testing Each Step
At each midpoint, run whatever test reveals the bug. This could be a unit test, a build command, or a manual check.
# Run your test
npm test
# If the test passes, this commit is good
git bisect good
# If the test fails, this commit is bad
git bisect bad
After each response, Git checks out the next midpoint and reports how many steps remain:
Bisecting: 23 revisions left to test after this (roughly 5 steps)
[f7e2a1b] Add caching layer for API responses
Continue marking commits as good or bad until Git finds the culprit:
b4d5c6e7 is the first bad commit
commit b4d5c6e7
Author: Jane Developer <jane@example.com>
Date: Mon Jun 15 14:30:00 2026 -0500
Update database query for user profiles
src/db/queries.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
Now you know exactly which commit broke things and can inspect the diff.
Ending the Bisect Session
When you are done, reset your working tree back to where you started:
git bisect reset
This checks out the branch you were on before bisecting. Always remember to do this, or you will be left in a detached HEAD state.
Viewing the Bisect Log
You can review the steps you took during a bisect session:
git bisect log
This outputs something like:
git bisect start
# bad: [abc1234] HEAD commit message
git bisect bad abc1234
# good: [def5678] v2.1.0
git bisect good def5678
# good: [a3b8c9d] Refactor user authentication module
git bisect good a3b8c9d
# bad: [f7e2a1b] Add caching layer for API responses
git bisect bad f7e2a1b
You can save this log to replay a session later:
git bisect log > bisect.log
# Replay the session
git bisect replay bisect.log
Automating Bisect with a Script
The real power of git bisect comes from automation. Instead of manually testing each commit, you provide a script that returns exit code 0 for good and non-zero for bad.
git bisect start HEAD v2.1.0
git bisect run npm test
Git will automatically run npm test at each midpoint and mark commits based on the exit code. The entire process completes without any manual intervention.
Writing a Custom Test Script
Sometimes you need a more specific test. Create a script that checks for the exact condition:
#!/bin/bash
# bisect-test.sh
# Build the project first
make build 2>/dev/null
if [ $? -ne 0 ]; then
# Build failure - skip this commit (exit code 125)
exit 125
fi
# Run the specific failing test
python -m pytest tests/test_user_profile.py::test_query_returns_results
The exit code 125 is special. It tells git bisect to skip this commit because it cannot be tested (for example, it does not compile). Git will try a neighboring commit instead.
Make the script executable and run it:
chmod +x bisect-test.sh
git bisect start HEAD v2.1.0
git bisect run ./bisect-test.sh
Using Inline Commands
For simple checks, you can pass the command directly:
# Find when a file was deleted
git bisect start HEAD v2.1.0
git bisect run test -f src/utils/helper.py
# Find when a string disappeared from a file
git bisect start HEAD v2.1.0
git bisect run grep -q "def calculate_tax" src/billing.py
Handling Untestable Commits
Sometimes a commit in the middle of your range cannot be tested. Maybe it does not compile or the test infrastructure is broken at that point. You have two options.
Skip a Single Commit
git bisect skip
Git picks a nearby commit to test instead.
Skip a Range of Commits
If you know an entire range is untestable:
git bisect skip v2.1.5..v2.1.8
Be aware that skipping too many commits can prevent Git from narrowing down to a single commit. It may report a range of suspects instead of one definitive answer.
Using Bisect with Terms
By default, bisect uses “good” and “bad,” but sometimes those labels do not make sense. If you are looking for when a performance improvement was introduced (not a bug), the commit you want is “good” in the conventional sense. Git lets you define custom terms:
git bisect start --term-old=slow --term-new=fast
git bisect fast HEAD
git bisect slow v2.0.0
# At each step
git bisect slow # this commit is still slow
git bisect fast # this commit is fast
This avoids the mental confusion of calling a “good” change “bad.”
Real-World Example: Finding a Performance Regression
Suppose your API response time jumped from 50ms to 300ms. You can automate bisect with a performance check:
#!/bin/bash
# perf-bisect.sh
# Start the application in the background
python app.py &
APP_PID=$!
sleep 2
# Measure response time
RESPONSE_TIME=$(curl -o /dev/null -s -w '%{time_total}' http://localhost:8000/api/users)
# Kill the app
kill $APP_PID 2>/dev/null
# Convert to milliseconds and compare
MS=$(echo "$RESPONSE_TIME * 1000" | bc | cut -d. -f1)
if [ "$MS" -lt 100 ]; then
exit 0 # Good - fast response
else
exit 1 # Bad - slow response
fi
git bisect start HEAD v2.0.0
git bisect run ./perf-bisect.sh
Bisect in a CI/CD Context
You can integrate bisect into your CI pipeline for automated regression detection:
# .github/workflows/bisect-regression.yml
name: Bisect Regression
on:
workflow_dispatch:
inputs:
bad_commit:
description: 'Bad commit SHA'
required: true
good_commit:
description: 'Good commit SHA'
required: true
test_command:
description: 'Test command to run'
required: true
default: 'npm test'
jobs:
bisect:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run git bisect
run: |
git bisect start ${{ inputs.bad_commit }} ${{ inputs.good_commit }}
git bisect run ${{ inputs.test_command }}
Tips for Effective Bisecting
Keep commits small and atomic. If each commit changes one thing, bisect pinpoints the exact change. Massive commits that touch 50 files make the result less useful even when you find the right commit.
Use descriptive commit messages. When bisect finds the offending commit, a message like “fix stuff” gives you nothing. A message like “Update SQL query to use LEFT JOIN for user profiles” tells you exactly what changed.
Fetch the full history. Bisect needs access to all commits in the range. If you did a shallow clone, deepen it first:
git fetch --unshallow
Stash or commit local changes before starting. Bisect checks out different commits, which will conflict with uncommitted work:
git stash
git bisect start HEAD v2.1.0
# ... do bisect ...
git bisect reset
git stash pop
Combine with git show and git diff. Once bisect identifies the commit, inspect it thoroughly:
# Show the full commit with diff
git show b4d5c6e7
# Compare the commit with its parent
git diff b4d5c6e7^..b4d5c6e7
Common Pitfalls
Forgetting git bisect reset. This leaves you in a detached HEAD state. If you accidentally close the terminal, just run git bisect reset in a new session.
Flaky tests. If your test sometimes passes and sometimes fails on the same commit, bisect will give wrong results. Make sure the test is deterministic before automating.
Merge commits. Bisect follows the first-parent history by default. If your history has many merge commits, bisect may check out states that never existed on any branch. Use git bisect start --first-parent to follow only the mainline.
git bisect start --first-parent HEAD v2.1.0
Wrapping Up
git bisect turns the problem of finding a regression from a linear search into a logarithmic one. For manual sessions, you mark commits as good or bad while Git narrows the range. For automated sessions, you hand Git a test script and let it do everything.
The key steps are: start a session with git bisect start, mark known endpoints with good and bad, test each midpoint or automate with git bisect run, and finish with git bisect reset. Combined with atomic commits and good test coverage, bisect becomes one of the most powerful debugging tools in your Git workflow.
Related articles
- Git Debugging with Git Bisect
Learn to use git bisect to find the exact commit that introduced a bug. Covers manual bisect, automated bisect with scripts, and practical strategies.
- Git Git Bisect Tutorial: Find the Bad Commit Fast
Use git bisect to binary-search through history and pinpoint the commit that introduced a regression, with manual and automated examples.
- Git Git Cherry-pick and Revert Tutorial
Learn how to copy specific commits across branches with cherry-pick and how to safely undo merged changes with revert, including conflict handling and recovery.
- Git Git Large File Storage (LFS) Tutorial
Set up Git LFS to version large binaries like images, models, and datasets without bloating your repository, including tracking, migration, and CI tips.