Reading history with --first-parent Jump to heading
The usual objection to merge commits is that they make history unreadable: git log interleaves every commit from every branch in date order, and git log --graph turns into a tangle of lines. The history is not actually messy — it is a precise record of what happened — but the default view shows all of it at once. One option fixes the view: --first-parent. Every merge commit records main’s previous state as its first parent, so following only first parents from main’s tip walks through exactly the states main has been in, one entry per merged pull request. It turns a merge-based history into something as readable as a linear one, without giving up the detail underneath. This page shows how to use it for log, blame, bisect and release notes, within merge vs rebase decision matrix.
When to use this approach Jump to heading
- Your team merges pull requests with merge commits and finds
git loghard to read. - You are deciding between merge and rebase and want to know how readable merge history can be.
- You need a clean list of what changed on main between two releases.
- Bisect sessions wander into feature branches; see also bisecting across merges with --first-parent.
Step 1 — See the difference in log Jump to heading
Compare the default log with the first-parent log of the same range.
git log --oneline v2.4.0..main | wc -l # every commit, from every branch
git log --oneline --first-parent v2.4.0..main | wc -l # one per merge to main
git log --oneline --first-parent v2.4.0..main | head
# e91a2c0 Merge pull request #851 from acme/export-retries
# 77b0d14 Merge pull request #848 from acme/search-tuning
# 3c5a1f9 Merge pull request #846 from acme/credit-note-fix Step 2 — Make merge commits informative Jump to heading
The first-parent view shows merge commit subjects, so make them useful. “Merge pull request #851 from acme/export-retries” says little. Forges let you set the merge commit message to the pull request title and description.
gh api -X PATCH "repos/$OWNER/$REPO" -f merge_commit_title=PR_TITLE -f merge_commit_message=PR_BODY
git log --first-parent --format='%h %s' -3 main
# e91a2c0 Add retry with backoff to export client (#851) With informative merge commits, the first-parent log reads like a changelog.
Step 3 — Use first-parent everywhere you read main Jump to heading
The same option, or an equivalent, works in most history commands.
# What changed on main between two releases, one line per PR
git log --first-parent --format='- %s' v2.4.0..v2.5.0
# Diff each merge against main's previous state (what the PR brought in)
git log --first-parent -p --diff-merges=first-parent -1 e91a2c0
# Blame showing the merge that brought each line into main
git blame --first-parent src/export/client.py | head
# Graph of main only
git log --first-parent --graph --oneline -20 --diff-merges=first-parent is particularly useful: it shows a merge commit’s change as a single diff against main’s previous state, which is exactly what the pull request contributed — the same view a squash commit would have given.
Step 4 — Drill into a pull request when you need detail Jump to heading
The advantage over squash merging is that the detail is still there. From any merge in the first-parent log, list the pull request’s own commits with the range between its two parents.
m=e91a2c0
git log --oneline "$m^1..$m^2" # the PR's commits, in order
git log -p "$m^1..$m^2" -- src/export/ # their individual changes in one area This answers questions a squash commit cannot: which step introduced a line, why a change was made in two parts, what a reviewer asked for. It is the main argument for keeping merge commits, weighed in squash vs merge vs rebase decision matrix.
Step 5 — Keep the first-parent chain clean Jump to heading
The view relies on main’s first parents being main’s own history. Two habits break it: merging main into a branch and then fast-forwarding main to that branch (which makes the branch’s first parent chain become main’s), and pushing directly to main. Require pull requests and disable fast-forward merges into main.
# Main should consist only of merge commits (and the occasional revert)
git log --first-parent --no-merges --oneline v2.4.0..main Step 6 — Teach it with aliases Jump to heading
Make the first-parent view the easy one with aliases the whole team shares.
# team.gitconfig
[alias]
lm = log --first-parent --format='%h %ad %s' --date=short
lmd = log --first-parent -p --diff-merges=first-parent
prc = "!f() { git log --oneline \"$1^1..$1^2\"; }; f" git lm v2.4.0..main # main's history, one line per PR
git prc e91a2c0 # the commits inside one PR Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does --first-parent work with squash-merged history? Jump to heading
It works, but makes no difference: every commit on main is already one per pull request. The option matters only when main contains merge commits.
Why do some merges show the branch as the first parent? Jump to heading
Someone merged main into their branch and then fast-forwarded main to it, or a forge merged that way. The chain then follows the branch. Requiring merge commits for pull requests prevents it.
Can GUI tools show the first-parent view? Jump to heading
Many history viewers have a “first parent only” toggle. On forges, the commit list for a branch typically shows merges; the pull request list is the equivalent view.
Related Jump to heading
- Merge vs Rebase Decision Matrix — the parent topic.
- The Real Cost of Requiring Linear History — what a linear rule gives up.
- Finding Who Changed a Line with log and blame — blame techniques beyond first-parent.
- Cherry-Picking a Merge Commit with --mainline — using parent numbers in backports.