Git Bisect Assistant
Use git bisect to efficiently find the exact commit that introduced a bug, regression, or unwanted behavioral change. This skill covers manual and automated bisect workflows, handling complex repository histories, writing bisect test scripts, skip strategies for untestable commits, and techniques for bisecting across merge-heavy histories.
When to Use This Skill
Use this skill when you need to:
- Find the commit that introduced a bug or regression
- Identify when a performance degradation was introduced
- Determine which change broke a previously passing test
- Track down when a configuration change caused unexpected behavior
- Find when a visual regression was introduced
- Identify the commit that changed an API response format
- Locate the source of a memory leak or resource exhaustion
- Determine when a flaky test started failing
Trigger phrases: "git bisect", "find regression", "find the commit that broke", "binary search commits", "when did this break", "bisect bug", "track down regression", "find introducing commit"
What This Skill Does
Core Capabilities
- Binary Search Guidance: Structured approach to bisecting across any commit range
- Automated Bisect Scripts: Write test scripts that automate the good/bad classification
- Skip Strategy: Handle commits that cannot be tested (build failures, incomplete work)
- Complex History Navigation: Bisect across merge commits, rebased branches, and non-linear history
- Bisect Visualization: Understand the bisect state and remaining search space
- Results Interpretation: Analyze the identified commit and understand the root cause
How Git Bisect Works
Git bisect performs a binary search through commit history. Given a known "good" commit and a known "bad" commit, it repeatedly selects the midpoint, asks you to test it, and narrows the range based on your answer.
Known Good Known Bad
| |
v v
G---G---G---?---?---?---?---?---?---?---B---B---B---B---B
^
First test (midpoint)
If midpoint is GOOD:
G---G---G---G---G---G---G---G---?---?---B---B---B---B---B
^
Next test
If midpoint is BAD:
G---G---G---?---?---B---B---B---B---B---B---B---B---B---B
^
Next test
For N commits, bisect finds the introducing commit in at most log2(N) steps. For 1000 commits, that is approximately 10 tests.
Instructions
Phase 1: Prepare for Bisect
Step 1.1: Identify the good and bad commits
Before starting bisect, you need two reference points:
# The bad commit: usually HEAD (current broken state)
BAD_COMMIT="HEAD"
# The good commit: the last known working state
# Option 1: Use a known release tag
GOOD_COMMIT="v2.3.0"
# Option 2: Find the last good commit by testing recent tags/releases
git tag --sort=-v:refname | head -10
# Test each until you find one where the bug does not exist
# Option 3: Use a date-based approach
git log --oneline --after="2025-01-01" --before="2025-02-01" | tail -1
# Test the oldest commit in the range where the bug might have been introduced
# Option 4: If you have no idea when it broke, start with a wide range
git log --oneline | tail -1 # First commit in the repo
Step 1.2: Define the test condition
Write a clear, repeatable test that determines whether a commit is "good" or "bad":
## Test Condition
- **Bug description**: The /api/users endpoint returns 500 instead of 200
- **Test command**: `curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/users`
- **Good result**: HTTP 200
- **Bad result**: HTTP 500
- **Prerequisites**: Application must be running (`npm start`)
Step 1.3: Ensure the test is reliable
# Verify the test identifies the bad commit correctly
git checkout $BAD_COMMIT
npm install && npm start &
sleep 5
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/users
# Should return 500
kill %1
# Verify the test identifies the good commit correctly
git checkout $GOOD_COMMIT
npm install && npm start &
sleep 5
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/users
# Should return 200
kill %1
Phase 2: Manual Bisect
Step 2.1: Start the bisect session
# Start bisect
git bisect start
# Mark the current commit as bad
git bisect bad HEAD
# Mark the known good commit
git bisect good v2.3.0
# Git checks out the midpoint commit
# Output: Bisecting: 42 revisions left to test after this (roughly 6 steps)
Step 2.2: Test each commit
At each step, Git checks out a commit for you to test:
# Build and test the current commit
npm install
npm run build
npm test
# If the bug is present at this commit:
git bisect bad
# If the bug is NOT present at this commit:
git bisect good
# If you cannot test this commit (build failure, etc.):
git bisect skip
Step 2.3: Interpret the result
When bisect narrows down to a single commit:
# Git outputs the first bad commit:
# abc1234 is the first bad commit
# commit abc1234
# Author: Jane Developer <jane@example.com>
# Date: Mon Jan 15 10:30:00 2025 +0000
#
# Add user caching middleware
#
# path/to/file.ts | 45 ++++++++++++++++++++++++++-------------------
# Examine the commit in detail
git show abc1234
git diff abc1234^ abc1234
# End the bisect session and return to original branch
git bisect reset
Phase 3: Automated Bisect
Automated bisect is the preferred approach for repeatable tests. You provide a script that exits with code 0 (good), 1-124/126-127 (bad), or 125 (skip).
Step 3.1: Write a bisect test script
#!/bin/bash
# bisect-test.sh - Automated bisect test script
set -e
# Clean state
npm ci 2>/dev/null || { echo "npm install failed"; exit 125; }
npm run build 2>/dev/null || { echo "build failed"; exit 125; }
# Run the specific test
npm test -- --filter="users endpoint" 2>/dev/null
TEST_RESULT=$?
# Exit code 0 = good (test passes), 1 = bad (test fails), 125 = skip
if [ $TEST_RESULT -eq 0 ]; then
exit 0 # Good: bug not present
else
exit 1 # Bad: bug present
fi
Step 3.2: Run automated bisect
# Start bisect with known range
git bisect start HEAD v2.3.0
# Run automated bisect with the test script
git bisect run bash bisect-test.sh
# Git will:
# 1. Check out a commit
# 2. Run your script
# 3. Mark as good/bad/skip based on exit code
# 4. Repeat until the first bad commit is found
# 5. Output the result
# When done, reset
git bisect reset
Step 3.3: Advanced test script patterns
Test script with server startup:
#!/bin/bash
# bisect-test-server.sh - Test that requires a running server
cleanup() {
# Kill background processes
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null
wait "$SERVER_PID" 2>/dev/null
fi
}
trap cleanup EXIT
# Build
npm ci 2>/dev/null || exit 125
npm run build 2>/dev/null || exit 125
# Start server in background
npm start &
SERVER_PID=$!
# Wait for server to be ready
for i in $(seq 1 30); do
if curl -s http://localhost:3000/health > /dev/null 2>&1; then
break
fi
sleep 1
done
# Test
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/users)
if [ "$HTTP_CODE" = "200" ]; then
exit 0 # Good
else
exit 1 # Bad
fi
Test script for performance regression:
#!/bin/bash
# bisect-test-perf.sh - Find when performance regressed
npm ci 2>/dev/null || exit 125
npm run build 2>/dev/null || exit 125
# Run benchmark and capture the result
RESULT=$(npm run benchmark -- --json 2>/dev/null | jq '.results[0].mean')
if [ -z "$RESULT" ]; then
exit 125 # Could not run benchmark, skip
fi
# Threshold: if mean response time exceeds 200ms, it's "bad"
THRESHOLD=200
EXCEEDED=$(echo "$RESULT > $THRESHOLD" | bc -l)
if [ "$EXCEEDED" = "1" ]; then
exit 1 # Bad: performance regression present
else
exit 0 # Good: performance acceptable
fi
Test script for Python projects:
#!/bin/bash
# bisect-test-python.sh
# Set up virtual environment
python3 -m venv .venv 2>/dev/null || exit 125
source .venv/bin/activate
pip install -e ".[dev]" 2>/dev/null || exit 125
# Run specific test
python -m pytest tests/test_users.py::test_list_users -x --tb=short 2>/dev/null
exit $?
Phase 4: Handle Complex Scenarios
Scenario 4.1: Bisecting across merge commits
When the history contains many merge commits, bisect may check out merge commits that are difficult to test:
# Option 1: Use --first-parent to only bisect the mainline commits
git bisect start --first-parent HEAD v2.3.0
# Option 2: Skip merge commits in the test script
#!/bin/bash
# Check if the current commit is a merge commit
PARENT_COUNT=$(git cat-file -p HEAD | grep -c "^parent")
if [ "$PARENT_COUNT" -gt 1 ]; then
exit 125 # Skip merge commits
fi
# ... rest of test
Scenario 4.2: Bisecting when some commits do not build
#!/bin/bash
# bisect-test-with-skip.sh
# Attempt to build; skip if it fails
make build 2>/dev/null
if [ $? -ne 0 ]; then
echo "Build failed at $(git rev-parse --short HEAD), skipping"
exit 125
fi
# Run test
make test-specific 2>/dev/null
exit $?
When too many consecutive commits fail to build, bisect may report "unable to determine first bad commit" due to insufficient good/bad data points. In that case:
# Manually test commits around the skip range
git bisect visualize --oneline
# Manually mark specific commits
git bisect good abc1234
git bisect bad def5678
Scenario 4.3: Bisecting a specific file or path
When you know the bug is in a specific area:
# Start bisect, limiting to commits that touch specific paths
git bisect start HEAD v2.3.0 -- src/services/user.ts src/middleware/cache.ts
# This restricts bisect to only consider commits that modified these paths
Scenario 4.4: Bisecting with submodules
#!/bin/bash
# bisect-test-submodules.sh
# Update submodules for the current commit
git submodule update --init --recursive 2>/dev/null || exit 125
# Build and test
npm ci 2>/dev/null || exit 125
npm test -- --filter="integration" 2>/dev/null
exit $?
Scenario 4.5: Narrowing the range before bisecting
For very large commit ranges, pre-narrow before bisecting:
# Find the approximate range using a coarse search
# Test every 100th commit
git log --oneline HEAD...v2.0.0 | awk 'NR % 100 == 0 {print $1}' | while read commit; do
echo "Testing $commit..."
git checkout "$commit" 2>/dev/null
npm ci --silent 2>/dev/null && npm test --silent 2>/dev/null
if [ $? -eq 0 ]; then
echo "GOOD: $commit"
break
else
echo "BAD: $commit"
fi
done
# Once you have a narrower range, bisect within it
git bisect start BAD_COMMIT GOOD_COMMIT
Phase 5: Analyze the Result
Step 5.1: Examine the introducing commit
# Show the full commit details
git show <first-bad-commit>
# Show only the files changed
git show --stat <first-bad-commit>
# Show the diff
git diff <first-bad-commit>^..<first-bad-commit>
# Check if this commit was part of a PR
git log --oneline --merges --ancestry-path <first-bad-commit>..HEAD | head -5
Step 5.2: Verify the finding
# Confirm: the parent of the first bad commit should be good
git checkout <first-bad-commit>^
# Run test... should pass (good)
git checkout <first-bad-commit>
# Run test... should fail (bad)
Step 5.3: Document the finding
## Bisect Result
- **First bad commit**: abc1234 ("Add user caching middleware")
- **Author**: Jane Developer
- **Date**: 2025-01-15
- **Files changed**: src/middleware/cache.ts, src/routes/users.ts
- **Root cause**: The caching middleware returns stale data for the /api/users endpoint because it caches responses by URL without considering query parameters.
- **Verification**: Parent commit (abc1233) passes the test; the bad commit fails.
- **Fix**: Add query parameter awareness to the cache key generation in cache.ts.
Bisect Log and Replay
You can save and replay a bisect session:
# Save the current bisect log
git bisect log > bisect-log.txt
# View the log
cat bisect-log.txt
# git bisect start
# # good: [abc1234] Initial release
# git bisect good abc1234
# # bad: [def5678] HEAD
# git bisect bad def5678
# # good: [111aaaa] Add logging
# git bisect good 111aaaa
# ...
# Replay a previous bisect session
git bisect replay bisect-log.txt
Bisect Visualization
# Visualize the remaining bisect range
git bisect visualize --oneline
# Show the bisect state in a graph
git bisect visualize --graph --oneline
# Show how many steps remain
git bisect log | tail -1
# "Bisecting: 5 revisions left to test after this (roughly 3 steps)"
Best Practices
- Always write a repeatable, automated test script for bisect rather than testing manually; manual testing is error-prone and slow across many commits
- Use exit code 125 (skip) generously in your test script; it is better to skip a commit than to incorrectly classify it
- Start with a narrow commit range when possible; bisecting across 10,000 commits is feasible but slow if each test takes minutes
- Clean the working directory between tests (use
git clean -fdxin the test script if needed) to avoid artifacts from previous commits affecting results - Use
--first-parentwhen bisecting on a branch with many merges; it keeps the bisect on the mainline and avoids testing intermediate merge states - Save the bisect log (
git bisect log) before resetting, especially for long sessions that you might need to replay - Verify the result by testing the first bad commit and its parent independently; false results can occur if the test is non-deterministic
- For flaky tests, run the test multiple times in the bisect script and only mark as "bad" if it fails consistently
- Document the bisect process and result in the bug fix commit message or PR description for future reference
- Consider using
git bisect runwith a CI-like script that handles dependency installation, environment setup, and teardown
Common Pitfalls
- Non-deterministic tests: If the test is flaky (sometimes passes, sometimes fails), bisect will produce incorrect results. Run the test multiple times in your script and use majority voting.
- Forgetting to reset: If you forget
git bisect resetafter finishing, you remain in a detached HEAD state. Always reset before continuing other work. - Incorrect good/bad labels: Swapping good and bad is easy to do, and bisect will silently search in the wrong direction. Double-check your initial labels.
- State leaking between tests: Build artifacts, caches, or database state from a previous commit can affect the test at the current commit. Clean thoroughly between tests.
- Skipping too many commits: If too many commits are skipped (exit 125), bisect may not have enough data points to converge. Investigate why commits are being skipped and fix the build issues.
- Testing the wrong thing: Ensure your test script is specific to the bug you are investigating. A test that fails for unrelated reasons will mislead the bisect.
- Ignoring bisect with path limiting: When you know the bug is in a specific area, using
git bisect start -- path/to/dirdramatically reduces the number of commits to test. - Not handling dependency changes: Commits that change
package.json,requirements.txt, or build configuration need dependency reinstallation. Always includenpm cior equivalent in your test script. - Running bisect on uncommitted changes: Bisect checks out different commits, which will fail or produce confusing results if you have uncommitted changes. Stash or commit before bisecting.
- Giving up too early: If bisect seems stuck or produces a suspicious result, check the bisect log, verify the test, and consider restarting with adjusted good/bad boundaries. Bisect is reliable when the test is reliable.