You are running git bisect to find when a performance regression was introduced. The binary search narrowed down to a 15-commit window, but every commit in that window is a build-broken state — the test suite does not compile at those commits because of a large refactor. You keep typing git bisect skip and Git responds “There are only ‘skip’-ped commits left to test. The first bad commit could be any of…” followed by a list of 10 commits. The bisect is effectively stuck: it cannot identify one culprit because the testable commits are exhausted. This guide shows how to escape this state, skip entire commit ranges efficiently, and use automated bisect scripts to work around untestable commits.
Common causes
Ordered by hit rate, highest first.
1. Large refactoring commits are in the bisect range
A “big-bang” refactor that does not compile mid-way is common. The middle of the bisect window contains 5-10 commits that all fail to build, making every candidate a skip.
How to spot it: git bisect log shows a long run of skip entries before the stuck message.
2. Test infrastructure changed in the middle of the range
The bisect range spans commits before and after a test framework migration (e.g., Mocha to Vitest). The test command you are using does not exist in the older commits.
How to spot it: Run git show <older-candidate>:package.json | grep "scripts" — if the test script is different or missing, the old test command will not work.
3. The regression was introduced by a merge commit, not a regular commit
Merge commits appear in the bisect range. The regression may have been introduced by the merge itself (conflict resolution that introduced a bug), but bisect cannot test the intermediate state.
How to spot it: git bisect log | grep merge — if merge commits appear frequently in the skipped range, a merge-induced bug is likely.
4. Database or environment state is not reproducible at historical commits
The test depends on a database schema that did not exist at older commits. Checking out old code and running the test produces false negatives (looks like “good” because the test cannot run, not because the regression is absent).
How to spot it: Test output at older commits shows “table does not exist” or “missing migration” errors, not an actual pass or fail of the feature under test.
5. Commits in the range are only accessible on a different branch
The bisect range was started with incorrect good and bad SHAs that span a merge from another branch, placing some commits in an unreachable state for the current checkout strategy.
How to spot it: git log --graph --oneline good..bad shows a non-linear history with branches that were never part of the feature’s linear history.
Shortest path to fix
Step 1: Review the current bisect state
git bisect log
git bisect visualize --oneline
git bisect visualize (or git bisect view) shows the remaining candidate commits. Count how many are truly untestable.
Step 2: Skip entire commit ranges at once
Instead of skipping one by one, skip all known-bad-build commits in a range:
# Skip all commits between SHA_A and SHA_B (exclusive start, inclusive end)
git bisect skip SHA_A..SHA_B
Or skip specific individual SHAs:
git bisect skip abc1234 def5678 gh90123
Step 3: Use a bisect run script that distinguishes “untestable” from “bad”
Create a script that exits 125 (the magic “skip” exit code) for untestable states, 0 for “good,” and 1 for “bad”:
#!/bin/sh
# bisect-test.sh
set -e
# Try to build — exit 125 (skip) if build fails
npm run build 2>/dev/null || exit 125
# Try to run the specific test — exit 125 if test infrastructure missing
npm test -- --testNamePattern="performance regression" 2>/dev/null || {
code=$?
[ $code -eq 127 ] && exit 125 # command not found = skip
exit $code
}
chmod +x bisect-test.sh
git bisect run ./bisect-test.sh
Step 4: Use custom terms for three-state bisect
If you need more nuance than good/bad/skip (e.g., a feature existed but was slow vs. broken vs. missing):
git bisect start --term-old fast --term-new slow
git bisect fast <known-fast-sha>
git bisect slow HEAD
Step 5: Accept the range result and narrow manually
When Git reports a range of possible first-bad commits, examine each with git show:
# From the "first bad commit could be any of:" list
for sha in abc1234 def5678 ghi9012; do
echo "=== $sha ==="
git show --stat "$sha"
done
Look for the commit that changed behavior-critical code — often a configuration change or a subtle logic inversion.
Step 6: Clean up after bisect
git bisect reset # returns HEAD to the branch you were on before bisect
Prevention
- Before starting a bisect, identify compile-breaking ranges:
git log --oneline good..bad | wc -landgit log --graph --oneline good..bad— if there are merge commits or refactors, plan to skip them upfront. - Always write a
bisect-test.shscript with proper exit codes (125 for skip) rather than interactive good/bad answers — it runs faster and handles untestable commits automatically. - Keep CI builds green at every commit, including work-in-progress commits on feature branches. A “WIP: do not merge” commit that builds is still bisectable.
- Tag known-good releases:
git tag good/v2.3.0— this makes starting bisect trivial:git bisect start HEAD good/v2.3.0. - Avoid large “big-bang” refactors in a single commit — break them into independent, bisectable steps.
- Record the bisect log when you find the culprit:
git bisect log > bisect-finding.txt && git commit --allow-empty -m "bisect: found regression in $(git bisect log | tail -1)"for team knowledge.
FAQ
Q: git bisect reports a range of possible bad commits instead of a single SHA. Is this normal? A: Yes, when the range contains only skipped commits, Git cannot definitively identify one SHA. It gives you the set of candidates. You must manually inspect each one.
Q: Can I run git bisect across multiple branches or across repositories?
A: git bisect operates on a single linear history. For multi-branch bisects, use git log --graph to identify the merge points and bisect within each branch individually, then combine findings.
Q: I found the bad commit but it is a dependency version bump in package.json. How do I find the actual regression?
A: The dependency’s changelog is your next step. Look for breaking changes between the old and new version. You can also npm install <dep>@<old-version> locally and run the failing test to confirm the rollback fixes it.
Q: Does git bisect work with submodules?
A: Partially. Bisect checks out different commits of the parent repo, which changes the submodule pointer. If the test requires the submodule to also be at the correct version, add git submodule update --init --recursive to your bisect-test.sh script.