Bisecting across merges with --first-parent Jump to heading
In a repository where pull requests land as merge commits, a plain git bisect wanders into feature branches. It tests commits that were never on main, many of which were work in progress that did not build or pass tests, so you spend the session marking commits as skip. Often what you actually want is coarser: which pull request introduced the regression? git bisect --first-parent (Git 2.29+) answers exactly that by following only the first-parent chain — the sequence of states main actually had. Each step tests a merge commit, every one of which passed CI when it landed. Once you know the culprit pull request, you can bisect inside it if you need the exact commit. This page shows both stages, within bisect and history forensics.
When to use this approach Jump to heading
- Pull requests land on main as merge commits, and their internal commits are not always buildable.
- A regression appeared on main between two known points, and you want the responsible pull request.
- Previous bisect sessions were full of
skipbecause intermediate commits did not build. - If your history is squash-merged, every commit on main already corresponds to a pull request, and plain bisect is effectively first-parent already — see tracing a regression through a squashed history.
Step 1 — Understand the first-parent chain Jump to heading
Each merge on main has main’s previous state as its first parent and the pull request’s head as its second. Following first parents from the tip walks back through every state main has been in, never entering a feature branch.
git log --first-parent --oneline v2.4.0..main | head
# e91a2c0 Merge pull request #851 from acme/export-retries
# 77b0d14 Merge pull request #848 from acme/search-tuning Step 2 — Bisect the merges Jump to heading
Start with --first-parent, mark a known-bad and a known-good state, and test as usual.
git bisect start --first-parent
git bisect bad main
git bisect good v2.4.0
# Bisecting: 15 revisions left to test after this (roughly 4 steps)
make test-regression && git bisect good || git bisect bad Automate it with run when you have a script that exits 0 for good and non-zero for bad.
git bisect run ./scripts/check-export-latency.sh
git bisect log | tail -3
# first bad commit: [77b0d14] Merge pull request #848 from acme/search-tuning
git bisect reset Step 3 — Bisect inside the culprit pull request Jump to heading
The first stage tells you which merge introduced the regression. If you need the specific commit, bisect the pull request’s own commits: the merge’s first parent is good, and its second parent is bad.
m=77b0d14
git bisect start "$m^2" "$m^1" # bad: PR head, good: main just before the merge
git bisect run ./scripts/check-export-latency.sh
git bisect reset Inside a pull request, some commits may not build. Mark those with git bisect skip, or have the run script exit 125, which bisect treats as “cannot test”.
#!/bin/sh
# scripts/check-export-latency.sh — exit 125 when the commit cannot be built
make build >/dev/null 2>&1 || exit 125
./bin/bench-export --rows 50000 | awk '{exit ($1 > 2.5)}' # bad if > 2.5 s Step 4 — Handle a regression caused by the merge itself Jump to heading
Sometimes neither side of a merge is bad alone: main before the merge passes, the pull request’s head passes on its own base, but the merge fails. That is a semantic conflict between the pull request and whatever main gained while it was open. Bisecting inside the pull request will find nothing, because every commit is good relative to its own base.
# Test the PR head on its own base and on the base main had at merge time
git checkout "$m^2" && ./scripts/check-export-latency.sh; echo "PR alone: $?"
git checkout "$m" && ./scripts/check-export-latency.sh; echo "merge: $?"
git checkout - The interaction case is covered in catching semantic conflicts in CI, and inspecting a merge’s own edits in reviewing conflict resolutions with remerge-diff.
Step 5 — Record the result where the next person will find it Jump to heading
A bisect result is valuable beyond the immediate fix. Attach the log to the issue, so anyone checking later can see how the culprit was found.
git bisect log > bisect-INC-4471.log
gh issue comment 4471 --body "First bad merge: 77b0d14 (#848). Bisect log attached in the incident doc." Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What Git version do I need? Jump to heading
git bisect start --first-parent was added in Git 2.29. On older versions, you can approximate it by marking every commit not on the first-parent chain as skipped, which is slower and clumsier.
Does first-parent bisect work with rebase-merged history? Jump to heading
Rebase merging produces a linear history with no merge commits, so the first-parent chain includes every rebased commit. It works, but it no longer groups commits by pull request.
Can bisect run my test in a clean worktree? Jump to heading
Bisect checks out commits in the current working tree. To keep your own checkout untouched, create a dedicated worktree for bisecting, as in running long builds in a separate worktree.
Related Jump to heading
- Bisect & History Forensics — the parent topic.
- Automating git bisect with a Test Script — writing the run script.
- Reading History with --first-parent — the same view for everyday log reading.
- Testing Every Commit with rebase --exec — making every commit bisectable in future.