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 log hard 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
Following only first parentsMain's merge commits M1 to M3 each have main's previous state as their first parent and a pull request's head as their second. Following first parents from the tip visits only M3, M2 and M1, giving one entry per merged pull request, while the pull requests' own commits stay one step aside.the top row is what --first-parent showsmain (first parents)M1M2M3PR #846a1a2PR #848b1b2PR #851c1c2nothing is hidden — the PR commits are one --first-parent away from being shown Following only first parentsMain's merge commits M1 to M3 each have main's previous state as their first parent and a pull request's head as their second. Following first parents from the tip visits only M3, M2 and M1, giving one entry per merged pull request, while the pull requests' own commits stay one step aside.the top row is what --first-parent showsmain (first parents)M1M2M3PR #846a1a2PR #848b1b2PR #851c1c2nothing is hidden — the PR commits are one --first-parent away from being shown

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
Commands that accept the first-parent viewgit log lists one entry per merge. log with --diff-merges=first-parent shows each merge's full contribution as a diff. git blame attributes lines to the merge that brought them into main. git bisect tests only main's states. Release notes come from the same first-parent range.logone line per PR--diff-merges=first-parentPR as one diffblame--first-parentbisectstart --first-parentrelease notestag..tag rangeone habit, five tools — merge history becomes as readable as squash history Commands that accept the first-parent viewgit log lists one entry per merge. log with --diff-merges=first-parent shows each merge's full contribution as a diff. git blame attributes lines to the merge that brought them into main. git bisect tests only main's states. Release notes come from the same first-parent range.logone line per PR--diff-merges=first-parentPR as one diffblame--first-parentbisectstart --first-parentrelease notestag..tag rangeone habit, five tools — merge history becomes as readable as squash history

--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
What keeps the first-parent chain meaningfulWhen every change reaches main through a merge commit created on main, the first-parent chain is exactly main's history. Fast-forward merges and direct pushes splice branch commits into the chain, so the first-parent view starts showing individual commits again.Preserves the chainBreaks the chainmerge methodmerge commit on mainfast-forwardhow changes arrivepull requests onlydirect pushesmain merged into branchfine, branch sidethen fast-forward maina check that main's first parents are all merges keeps the view honest What keeps the first-parent chain meaningfulWhen every change reaches main through a merge commit created on main, the first-parent chain is exactly main's history. Fast-forward merges and direct pushes splice branch commits into the chain, so the first-parent view starts showing individual commits again.Preserves the chainBreaks the chainmerge methodmerge commit on mainfast-forwardhow changes arrivepull requests onlydirect pushesmain merged into branchfine, branch sidethen fast-forward maina check that main's first parents are all merges keeps the view honest

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.