How does git bisect work and how do you automate it with a test script to find the exact commit that introduced a regression?
Quick Answer
git bisect performs a binary search through commit history, halving the candidate range at each step. You mark a known good and bad commit, and Git checks out the midpoint for testing. With git bisect run, you provide an automated test script that returns exit 0 for good and non-zero for bad, enabling fully automated regression hunting across thousands of commits.
Detailed Answer
Think of git bisect like a technician troubleshooting a 100-floor building where the lights stopped working somewhere between floors 1 and 100. Instead of testing every floor sequentially, the technician goes to floor 50. If the lights work there, the problem is between 50 and 100, so they test floor 75. If lights are broken at 75, they test floor 62. Each test eliminates half the remaining floors. In just 7 tests, they pinpoint the exact floor. Git bisect applies this same binary search to your commit history.
Git bisect starts with git bisect start, then you mark the current HEAD as bad (git bisect bad) and a known-good historical commit as good (git bisect good v2.3.0). Git calculates the midpoint between these commits in the DAG and checks it out in a detached HEAD state. You test the application, mark the commit as good or bad, and Git checks out the next midpoint. This continues until Git narrows down to the exact first-bad commit. For a history of 1024 commits, bisect finds the culprit in at most 10 steps (log2 of 1024).
Internally, bisect does not simply pick the chronological midpoint. It uses the commit DAG topology to find the commit that most effectively bisects the remaining candidate range, accounting for merge commits and branch structure. The bisect state is stored in .git/BISECT_LOG, .git/BISECT_EXPECTED_REV, and related files. When you mark a commit as good or bad, Git updates the candidate range and selects the next optimal test point. The git bisect visualize command shows the remaining candidates in gitk or log format. If a commit cannot be tested (does not compile or has an unrelated failure), git bisect skip tells Git to try a nearby commit instead. Multiple commits can be skipped, though if too many adjacent commits are skippable, bisect may not be able to isolate the exact commit.
The real power of bisect lies in automation with git bisect run. You provide a script or command that returns exit code 0 for good, 1-124 or 126-127 for bad, and 125 for skip (cannot test). For example, git bisect run pytest tests/billing/test_invoice.py automatically tests each midpoint commit without human intervention. This can find a regression across 10,000 commits in about 14 automated steps, often completing in minutes. In production, teams create dedicated bisect scripts that set up the environment, run the relevant test, and clean up. Some organizations maintain a library of bisect scripts for common regression categories: performance regressions (comparing benchmark output), API compatibility (running contract tests), and build regressions (checking compilation).
A critical gotcha: bisect checks out commits in detached HEAD state, which means you are not on any branch. Do not make commits during a bisect session unless you know what you are doing. Always end with git bisect reset to return to your original branch. Another trap: if your test script has side effects that persist between runs (database state, generated files), you may get false results. Ensure each test run starts from a clean state. Also, bisect works best with small, atomic commits because a single large commit that touches 50 files is harder to diagnose even after bisect identifies it. This is yet another argument for the discipline of making focused, single-purpose commits.