Git, CI/CD & GitOpsIV · Commit Graph and HistoryCommit Graph and History
Parent references — first parent versus all parents
What you'll learn
- Read the parent lines of a commit object and identify which parent is the first parent by Git convention
- Explain what `git log --first-parent` walks and what it deliberately skips
- Use the first-parent chain as the trunk of a history for release notes, change logs, and deployment tracking
- Identify when a non-first parent represents a merged branch tip and when it represents a rebase or cherry-pick
- Trace an octopus merge's parent list and reason about which parent is the first parent
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The parent lines of a commit object are not just bookkeeping - they
are the edges of the DAG and the contract every traversal command
relies on. The first parent is special: it is the branch that was
checked out at the moment the commit was constructed, and every
Git command that follows a “trunk” of history - git log --first-parent, git log --first-parent main, git rev-list --first-parent - walks only that edge and ignores the rest.
Parent lines are edges
When git cat-file -p <oid> prints a commit, the parent lines
appear in a fixed order. The first parent is the branch that was
checked out; the second and subsequent parents are the branches that
were merged in. This ordering is part of the commit’s on-disk format
and is preserved by git commit-tree -p - the first -p argument
becomes the first parent.
git cat-file -p "$MERGE_OID"
# tree 4d2c8e01...
# parent 9f3c1d72... <-- main, the branch that was checked out
# parent 7a1b2c3a... <-- feature/iam-rotation, the merged-in branch
# author Ops <ops@example.com> 1730000000 +0000
# committer Ops <ops@example.com> 1730000000 +0000
#
# Merge branch 'feature/iam-rotation'
flowchart LR
M["merge commit\nparents: A, B\nfirst parent = A"] --> A["A (main)"]
M --> B["B (feature)"]
A --> P["ancestors of A"]
Reading the diagram: the merge commit has two outgoing edges, one
to A and one to B. The first edge, to A, is the first parent
and is the edge git log --first-parent follows. The edge to B
is the second parent and is the edge a merge brought into the
trunk.
Why first parent is a convention, not a property
Git could have ordered the parents differently - by OID, by timestamp, by ref name. It chose “first parent = branch that was checked out” because that is what every downstream tool needs. The trunk of history is “where HEAD was” at the moment of each commit; following that edge reproduces what a deployment would have deployed. The other parents are detail: the feature branches, hotfix branches, and topic branches that converged into the trunk at some specific moment.
This convention is what makes git log --first-parent useful. If
the first parent were arbitrary, “follow the trunk” would be an
ill-defined operation; if it were “lowest OID”, the trunk would be
random and tools that track deployments would break.
What —first-parent shows and what it hides
git log --first-parent follows the first-parent edge from the
starting commit back to the root. Merge commits appear on the
trunk exactly once (as a single commit with a multi-parent edge
the walk skips), and the feature branches they merged are not
enumerated.
# Trunk view: every commit on main's first-parent chain
git log --oneline --first-parent main
# 8a3f9d2 bump terraform module to v1.4.0
# 4d2c8e0 Merge branch 'feature/iam-rotation' into main
# 6f4e5a6 initial commit
# Full view: every commit reachable from any ref on main
git log --oneline main
# 8a3f9d2 bump terraform module to v1.4.0
# 4d2c8e0 Merge branch 'feature/iam-rotation' into main
# 9f3c1d7 (feature/iam-rotation) rotate iam keys
# 7a1b2c3 add rotation cron
# 6f4e5a6 initial commit
The three extra lines in the full view are the feature-branch
commits, which git log --first-parent deliberately skips. The
trunk view has three commits; the full view has five. The
difference is exactly the work that happened on the side branch.
Counting with —first-parent
git rev-list --first-parent --count main returns the number of
first-parent commits on main - that is, the number of “releases”
or “deploy steps” if the trunk commits map to deployments. This
counter is what changelogs, deployment dashboards, and “commits
since last release” queries should use, because it excludes
feature-branch commits that may not be merged yet.
git rev-list --first-parent --count main
# 47
git rev-list --count main
# 312
The difference between 47 and 312 is the work that happened on side branches and was merged in. For an infrastructure team, the first number is the deployment counter; the second is the total work counter.
Octopus parents
When git merge resolves more than two tips into one commit, the
result is an octopus merge: the merge commit has three or more
parents. The first parent is still the branch that was checked out
when the merge ran; the remaining parents are the merged-in tips,
in the order they were passed on the command line.
# An octopus merge of three branches into main
git checkout main
git merge feature-a feature-b feature-c
# Merge made by the 'octopus' strategy.
# 4d2c8e0 (main) Merge branches 'feature-a', 'feature-b', and 'feature-c'
The resulting commit object has four parents:
git cat-file -p 4d2c8e0
# tree <merged-tree-oid>
# parent <main-oid> <-- first parent (the checked-out branch)
# parent <feature-a-oid> <-- second parent (first merged-in branch)
# parent <feature-b-oid> <-- third parent
# parent <feature-c-oid> <-- fourth parent
git log --first-parent still follows only the first edge, to
main’s previous tip. The three feature tips are reachable through
the merge commit, but not enumerated as part of the trunk walk.
Cherry-picks and rebases: parent lines rewritten
A cherry-pick creates a new commit whose parent is the current HEAD, not the original commit’s parent. The original commit is unaffected; the new commit has the same tree change but a different parent, and therefore a different OID. A rebase rewrites every rebased commit with a new parent chain anchored at the rebase target.
# Cherry-pick: original parent (X) is replaced by current HEAD (Y)
git checkout feature-branch
git cherry-pick 9f3c1d72
# Creates new commit with parent = current HEAD, tree = X's change
# Rebase: every rebased commit gets new parents, anchored at the rebase target
git checkout feature-branch
git rebase main
# Each commit's first parent becomes the previous rebased commit
# The last rebased commit's first parent becomes main's tip
Both operations produce new commits with new OIDs. The DAG is extended forward, never rewritten - this is what people mean by “rebasing creates new history” rather than “rebasing modifies history”.
Production discipline
- Use
git log --first-parentfor the deployment view. A release-notes script, a deployment tracker, or a “commits since last tag” query should follow the first-parent chain. Anything else includes unreleased feature-branch work. - Inspect parent lines before merging.
git cat-file -p <oid>on a candidate merge commit reveals the parent order, which is the order Git will record and every subsequent first-parent walk will assume. - Be cautious with
git log --merges. It walks every merge commit reachable from HEAD. In a busy repository that may be thousands of commits. Combine it with--first-parentto limit to the trunk’s merges.
Cross-course references
- GitOps with Argo CD - Part IV (SyncPhases) uses first-parent history to determine which manifests were deployed on the trunk and which came from a feature branch, because GitOps controllers should sync to the trunk, not to merged feature branches.
- Terraform for Production Sysadmins - Part IX (State) notes that Terraform state version numbers are append-only and monotonically increasing on the trunk; the analogue of first-parent is the “main state series” versus feature-branch state experiments.
- Docker for Production Sysadmins - Part VII (Tagging)
describes
--first-parentas a way to enumerate release commits on a branch, which is also the canonical way to enumerate container image tags that correspond to releases.
Quiz
Knowledge check · 4 questions
Q1. What does `git log --first-parent` walk, and what does it deliberately skip?
Q2. In an octopus merge, the first parent of the resulting commit is arbitrary - Git picks the lowest OID.
Q3. Which Git command counts the number of trunk commits on a branch (first-parent only), and what does the returned number represent for an infrastructure team?
Q4. Generate a release-notes block from a trunk and identify which commits should be listed and which should be excluded.
An infrastructure team is cutting release notes for the upcoming v1.4.0 deploy. The trunk is `main`. A feature branch `feature/iam-rotation` was merged in via a merge commit earlier in the cycle. The team wants the release notes to enumerate only commits on the trunk - not feature-branch commits - and to include the merge commit itself as a single entry. Two scripts are candidates: one uses `git log --oneline v1.3.0..main`, the other uses `git log --oneline --first-parent v1.3.0..main`.
Passing score: 75%. Answers are checked in this browser.