Chapter 7.3 — git bisect run — Automated Bug Hunting☕ 15 min read

git bisect run — Automated Bug Hunting

Computer test kare, computer good/bad bole, tu bas result dekh — yeh hai automated bisect.

01From Manual to Automated Bisect

Manual bisect requires you to test each commit yourself and run git bisect good or git bisect bad repeatedly. For 10 steps, that means 10 manual tests. Boring, error-prone, and slow.

If you have an automated test that fails for the bug, git bisect run does EVERYTHING automatically. You write a test script. Git runs it at each commit. The script returns an exit code. Git marks the commit good or bad automatically. No human interaction needed.

The power: 1000 commits, 10 automated steps, zero human interaction. Go get coffee while bisect works. Come back to "abc1234 is the first bad commit."

When to use bisect run: you have a failing test, a reproducible crash, or a build error that can be checked programmatically.

When NOT to use: visual bugs, subjective issues, non-deterministic bugs — things a script cannot reliably test.

# Manual bisect — you do the testing
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
# Git checks out commit → YOU run tests → YOU say good/bad
# Repeat 10 times... boring, error-prone, slow

# Automated bisect — script does the testing
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run ./test-script.sh
# Git checks out commit → SCRIPT runs tests → SCRIPT returns exit code
# Git automatically marks good/bad → moves to next commit
# ... all automatically ...
# "abc1234 is the first bad commit"
# You just watch. Coffee time.
The whole point of bisect run is removing humans from the loop. If you can write a script that answers "is the bug present?" with exit code 0 (no) or 1 (yes), then bisect run does the rest. The script is your automated QA — it never gets tired, never makes typos, never second-guesses itself.
02Exit Codes — The Language of bisect run

Your test script communicates with git bisect through EXIT CODES. This is the only language bisect understands — not text output, not print statements, not console logs. Just exit codes.

Exit code 0: "This commit is GOOD" — the bug is NOT present.

Exit code 1-124, 126-127: "This commit is BAD" — the bug IS present.

Exit code 125: "Cannot test this commit" — SKIP it. Use for broken builds, missing dependencies, migration failures.

Exit code 128+: ERROR — stop bisect entirely. Something is wrong with the script itself.

Most test frameworks return exit code 0 on pass, non-zero on fail. This maps perfectly to bisect:

  • npm test returns 0 if all tests pass, 1 if any fails. Perfect for bisect run!
  • pytest returns 0 on pass, 1 on fail. Perfect.
  • go test returns 0 on pass, 1 on fail. Perfect.
# Exit code meanings for git bisect run:
# 0          = good (bug not present)
# 1-124      = bad (bug present)
# 125        = skip (cannot test this commit)
# 126-127    = bad (bug present)
# 128+       = error (stop bisect, something wrong)

# Simple test script
cat > test-bug.sh << 'EOF'
#!/bin/bash
# Run the test suite
npm test
# npm test returns:
#   0 if all tests pass (good)
#   1 if any test fails (bad)
EOF
chmod +x test-bug.sh

# More explicit version
cat > test-bug-explicit.sh << 'EOF'
#!/bin/bash
if npm test; then
  exit 0   # good — tests pass, bug not present
else
  exit 1   # bad — tests fail, bug is present
fi
EOF
chmod +x test-bug-explicit.sh
💡 Pro Tip: Your test framework already returns the right exit codes! npm test, pytest, go test — they all return 0 on pass and 1 on fail. For many projects, git bisect run npm test just works. You only need a custom script when you need to install dependencies, build, or handle edge cases like exit code 125.
03Writing Effective Test Scripts for Bisect

Your script must be DETERMINISTIC: same commit + same script = same result every time. If your script returns different exit codes for the same commit, bisect will give wrong answers.

Your script must be FAST: it runs multiple times. 10 runs of a 5-minute test = 50 minutes. Keep it short. Target only the relevant test.

Your script must be SELF-CONTAINED: install dependencies, build, and test within the script if needed. Bisect checks out old commits where your current setup might not work.

Use exit code 125 for commits that cannot be tested. Build broken? Exit 125. Dependency missing? Exit 125. Git will skip those commits and try nearby ones.

Script pattern: build → test → return exit code.

Always make the script executable: chmod +x script.sh.

For compiled languages, your script should compile AND run. If compilation fails → exit 125 (skip).

# Complete bisect test script template
cat > bisect-test.sh << 'EOF'
#!/bin/bash
set -e

# 1. Install dependencies (skip if fails)
npm install --silent 2>/dev/null || exit 125

# 2. Build the project (skip if fails)
npm run build --silent 2>/dev/null || exit 125

# 3. Run the TARGETED test (not full suite)
npx jest tests/login.test.js --no-coverage 2>/dev/null
exit $?
EOF

chmod +x bisect-test.sh

# Test it manually first!
./bisect-test.sh; echo "Exit code: $?"
# Should return 0 or 1 for normal commits
# Should return 125 for commits that can't build

# Then use with bisect
git bisect run ./bisect-test.sh
💡 Pro Tip: Your test script should test ONLY the bug you're bisecting, not the entire test suite. If the bug is in the login module, write a script that tests only login. Running 5000 tests at each step when only 1 matters wastes enormous time. Target your bisect script like a laser.
04Real-World bisect run Examples

JavaScript/Node.js: git bisect run npm test — works directly if your test suite has a failing test for the bug.

Python: git bisect run pytest tests/test_login.py — target specific test file.

Go: git bisect run go test ./pkg/auth/... — test specific package.

Custom script: for bugs that don't have a test yet, write a quick reproduction script.

Build failures: git bisect run make — find which commit broke the build.

Performance regressions: script that measures execution time and returns 1 if too slow.

# Example 1: Node.js project — find which commit broke a test
git bisect start
git bisect bad HEAD
git bisect good v2.0.0
git bisect run npm test
# npm test runs, returns 0 or 1, bisect handles the rest

# Example 2: Custom reproduction script for a crash
cat > reproduce-crash.sh << 'EOF'
#!/bin/bash
# Build the project
npm run build || exit 125  # skip if build fails

# Run the app and check if it crashes
timeout 5 node dist/app.js --test-mode
if [ $? -eq 0 ]; then
  exit 0  # app ran successfully — good
else
  exit 1  # app crashed — bad
fi
EOF
chmod +x reproduce-crash.sh

git bisect run ./reproduce-crash.sh

# Example 3: Python — find which commit broke specific test
git bisect start
git bisect bad HEAD
git bisect good v1.5.0
git bisect run pytest tests/test_api.py::test_login

# Example 4: Go — find build failure
git bisect start
git bisect bad HEAD
git bisect good v3.0.0
git bisect run go build ./...
For bugs without existing tests, write a minimal reproduction script. The script should trigger the bug: run the app with specific input, check if it crashes, verify the output is wrong. Keep it simple — 5-10 lines. The goal is not a comprehensive test, just a reliable answer to "does this commit have the bug?"
05Handling Problems in bisect run

If bisect run gets stuck or gives wrong results, interrupt it with Ctrl+C and run git bisect reset.

Common problems and fixes:

  • Script not executablechmod +x script.sh
  • Script depends on files not in repo → add them or handle in script
  • Build fails at some commits → return exit code 125 to skip
  • Test is flaky → run test multiple times in script, return bad only if consistently fails
  • Missing dependencies → install in script or use exit 125

For large repos, bisect run can be slow because each step may require npm install or make.

Optimize: cache dependencies, install only what changed, use incremental builds.

# Handling flaky tests in bisect run
cat > robust-bisect.sh << 'EOF'
#!/bin/bash

# Install deps (skip if impossible)
npm ci --silent 2>/dev/null || exit 125

# Run test 3 times — only mark bad if consistently fails
FAIL_COUNT=0
for i in 1 2 3; do
  npx jest tests/login.test.js --no-coverage 2>/dev/null || FAIL_COUNT=$((FAIL_COUNT+1))
done

if [ $FAIL_COUNT -ge 2 ]; then
  exit 1   # consistently bad
else
  exit 0   # probably good (maybe 1 flaky failure)
fi
EOF

# Smart dependency caching
cat > smart-bisect.sh << 'EOF'
#!/bin/bash
# Only reinstall if package.json changed
if [ ! -d "node_modules" ] || [ package.json -nt node_modules ]; then
  npm install --silent 2>/dev/null || exit 125
fi
npx jest tests/login.test.js --no-coverage 2>/dev/null
exit $?
EOF
Pro tip: If your bisect run keeps failing because old commits can't build with new dependencies, use exit code 125 (skip) for those commits. Git will skip them and test nearby commits instead. The script: try to build → if fails, exit 125 → if succeeds, run test → exit 0 or 1.

Lo kar liya — Key Points:

  • ✅ git bisect run automates the testing — you provide a script, Git runs it at each commit
  • ✅ Exit code 0 = good (no bug), 1-124 and 126-127 = bad (bug exists), 125 = skip (cannot test)
  • ✅ Most test frameworks (npm test, pytest, go test) return 0 on pass and 1 on fail — perfect for bisect run
  • ✅ Your test script must be deterministic, fast, and self-contained — it runs multiple times automatically
  • ✅ Use exit code 125 for commits that cannot be tested (build failures, missing dependencies)
  • ✅ Target your test script to the specific bug — don't run the entire test suite if only one test fails
  • ✅ Always chmod +x your script before running bisect run — non-executable scripts cause immediate failure
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase