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 skip because 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.

The commits a first-parent bisect will testMain has merge commits M1 to M4, each bringing in a pull request whose own commits sit on the second parent side. A first-parent bisect tests only M1 to M4, the states main actually had, and never checks out the pull requests' intermediate commits.only the top row is testedmain (first parent)M1M2M3M4PR commits (skipped)a1 a2b1c1 c2 c3d1each tested state passed CI when it landed — no skip marks needed The commits a first-parent bisect will testMain has merge commits M1 to M4, each bringing in a pull request whose own commits sit on the second parent side. A first-parent bisect tests only M1 to M4, the states main actually had, and never checks out the pull requests' intermediate commits.only the top row is testedmain (first parent)M1M2M3M4PR commits (skipped)a1 a2b1c1 c2 c3d1each tested state passed CI when it landed — no skip marks needed
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
Bisect steps and skips with and without --first-parentAn illustrative regression hunt across two hundred commits on main and in its merged pull requests. A plain bisect took more steps and hit many unbuildable work-in-progress commits that had to be skipped. A first-parent bisect over forty merges took fewer steps and needed no skips.one regression hunt (illustrative)plain: steps8plain: skips6first-parent: steps6first-parent: skips0fewer steps matters less than zero skips — skips are where bisect sessions stall Bisect steps and skips with and without --first-parentAn illustrative regression hunt across two hundred commits on main and in its merged pull requests. A plain bisect took more steps and hit many unbuildable work-in-progress commits that had to be skipped. A first-parent bisect over forty merges took fewer steps and needed no skips.one regression hunt (illustrative)plain: steps8plain: skips6first-parent: steps6first-parent: skips0fewer steps matters less than zero skips — skips are where bisect sessions stall

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 -
Where the regression lives after a first-parent bisectIf the pull request's head is already bad, bisect inside it to find the commit. If the pull request is good alone but the merge is bad, the cause is an interaction with changes main gained meanwhile. If the merge commit itself contains extra changes, inspect the conflict resolution.First bad merge found — what is bad?PR head already badBisect inside PRm^1..m^2only the mergeSemantic conflictPR + newer mainmerge has extra editsCheck resolution--remerge-diffthe second case is why merge-result testing matters Where the regression lives after a first-parent bisectIf the pull request's head is already bad, bisect inside it to find the commit. If the pull request is good alone but the merge is bad, the cause is an interaction with changes main gained meanwhile. If the merge commit itself contains extra changes, inspect the conflict resolution.First bad merge found — what is bad?PR head already badBisect inside PRm^1..m^2only the mergeSemantic conflictPR + newer mainmerge has extra editsCheck resolution--remerge-diffthe second case is why merge-result testing matters

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.