Merge Commits in Git: Parents, Topology and Trade-offs
A merge commit is an ordinary commit object with one unusual property: it records more than one parent. Everything else about it — a tree, an author, a committer, a message — is identical to any other commit.
That single structural difference is what makes history a graph rather than a list, and it is the source of every behaviour in this lesson: how logs read, how reverting works, and why some teams avoid merge commits entirely.
What makes it different
Section titled “What makes it different”An ordinary commit has one parent line:
git cat-file -p HEADtree 06f3e565236176c7633ec5bb2471844d53aafa24parent c86ff2b9962c64f6e2d5361b57f4f6d7d875d90bauthor Dev <dev@example.com> 1787405411 +0000committer Dev <dev@example.com> 1787405411 +0000
Add validationA merge commit has two or more:
tree ae1af2b170d94d7b8d833dbf091d42ef7c9f9b18parent 5f49aabec7815dc0cb237a91e678d27aa6b55606parent abe666692988d8a2bed78d6a092fb9f1da4bd72cauthor Dev <dev@example.com> 1787403598 +0000committer Dev <dev@example.com> 1787403598 +0000
Merge branch 'topic'Nothing else is special. The tree is the merged result — a complete snapshot, exactly like any commit.
There is no stored “diff” and no record of how conflicts were resolved beyond the resulting tree itself.
Parent order
Section titled “Parent order”The order of parents is meaningful and fixed at merge time.
First parent is the branch you were on. Second parent is what you merged in.
A trunk lane labelled main with commits A, B, C and a merge commit M. A branch lane labelled feature leaves main after commit B with commits X and Y, merging into M. M's first parent is C on the mainline; its second parent is Y on the feature branch.
git rev-parse --short HEAD^1 HEAD^2Three things depend on this ordering, and all three matter in practice:
| Depends on parent order | Why |
|---|---|
git log --first-parent | Follows only parent 1, giving one entry per integration |
git revert -m N | Selects which parent’s line of development to keep |
git bisect --first-parent | Restricts the search to the mainline |
Because merges normally happen onto the mainline, the first parent is reliably “the branch that
continues” — which is what makes --first-parent such a useful way to read a busy history.
What merge commits give you
Section titled “What merge commits give you”A record of integration. The commit says when two lines of development were combined, by whom, and which branch was involved (from the default message). Squashing and rebasing both discard this.
Both histories stay reachable. Every commit from the branch remains in the graph with its original ID,
author and message. git log can still show the individual steps.
Whole-branch revert. One command undoes the entire integration:
git revert -m 1 <merge-commit>With a rebased or squashed integration you would revert several commits or one large one respectively.
Accurate --merged detection. git branch --merged works by reachability, so a properly merged branch
is recognised as merged. Squash and rebase integrations are not.
Conflict resolution is recorded once. Whatever you decided during resolution lives in the merge commit’s tree, in one place.
What they cost
Section titled “What they cost”Log noise. On a busy repository, merge commits can outnumber real changes. A main that receives
thirty branches a week gains thirty commits whose only content is “these joined here”.
Non-linear history. git log interleaves commits from every branch by date unless you ask otherwise.
Understanding “what happened in what order” takes more effort.
Bisect traverses branch internals. Intermediate commits inside a branch may not build. Mitigated with
--first-parent or git bisect skip.
Reverting takes an extra step. You must specify -m, and the consequences of reverting a merge are
subtle — see below.
Merge bubbles. When people repeatedly merge main into their feature branch, the branch fills with
merge commits, and the eventual integration produces a shape that is genuinely hard to read.
Reverting a merge commit
Section titled “Reverting a merge commit”This is where merge commits behave least like ordinary commits.
git revert normally creates a commit that reverses another commit’s changes. For a merge, “its changes”
is ambiguous — changes relative to which parent? Git refuses to guess:
git revert HEADerror: commit f1b6bc006e03a46fffcd9fb9e4cb61d2682c369a is a merge but no -m option was given.fatal: revert failedgit revert -m 1 HEADWhat it doesCreates a new commit that undoes the changes the merge introduced, relative to the parent number you specify as the mainline.
Why we run itGit cannot infer which side you intend to keep, so you state it. -m 1 keeps the first parent's line — the branch you were on.
Expected resultA new commit named Revert "Merge …". Files the merged branch introduced are removed again.
[main 04a2fa1] Revert "Merge feature" 1 file changed, 1 deletion(-)-m 1 almost always. -m 2 would keep the merged branch’s changes and discard the mainline’s, which is
rarely what anyone wants.
The consequence people get caught by
Section titled “The consequence people get caught by”After reverting a merge, the branch’s commits are still reachable — the merge commit is still in history — but their effect has been undone.
If you later merge that branch again, Git computes the merge base, sees the branch has nothing new since it was already merged, and merges cleanly without restoring anything. The work does not come back.
More than two parents
Section titled “More than two parents”Nothing limits a merge commit to two parents. Merging several branches at once produces an octopus merge:
git merge feat-a feat-b feat-c -m "Octopus merge"Fast-forwarding to: feat-aTrying simple merge with feat-bTrying simple merge with feat-cgit cat-file -p HEAD | grep -c parent3*-. 76ceafe Octopus merge|\ | | * 7e66983 add c| * | cae3854 add b| |/* / 2a25d93 add aOctopus merges are rare in application development and mostly appear in integration branches that combine many topic branches at once. The important limitation: the octopus strategy refuses to run if any of the branches conflict. It handles only the case where every side merges cleanly, so it is a convenience for batching trivial integrations rather than a general-purpose tool.
If you need to combine several conflicting branches, merge them one at a time and resolve each. Advanced Merge Strategies covers the octopus strategy in more detail.
Merge commits from a hosting platform
Section titled “Merge commits from a hosting platform”When you press a merge button, the platform performs a Git merge on the server. GitHub’s “Create a merge
commit” option uses --no-ff, so it produces a merge commit even when the branch could have
fast-forwarded.
Two differences from merging locally are worth knowing:
The committer is the platform, not you. The merge commit’s committer is typically a platform identity rather than the person who clicked. The author fields of the branch’s own commits are untouched.
The message is generated. Something like Merge pull request #142 from example/fix-parser, which is
more useful than the local default because it links back to the review.
Repositories can enable or disable each merge method. If your main has no merge commits at all, the
repository is probably configured for squash or rebase only — that is a settings question, not a Git one.
When to prefer merge commits
Section titled “When to prefer merge commits”Use them when:
- Branches represent meaningful units and you want integration points visible.
- You need to know when work was integrated, for audit or release-note purposes.
- Branch commits are individually well-formed and worth preserving.
- You want whole-branch revert to be one operation.
- The branch was shared, so rewriting it was never an option.
Avoid them when:
- Branches are tiny and the merge commit outweighs the change.
- Commits within branches are working notes rather than meaningful steps.
- Your team strongly values a linear
main. - You use
git bisectheavily and want every commit onmainto be buildable.
Many teams split the difference: --no-ff merges for substantial feature branches, squash for small
fixes. That is a per-pull-request decision if the platform allows more than one method.
Common mistakes
Section titled “Common mistakes”Reverting a merge and expecting to re-merge later. Covered above; it is a no-op without further work.
Using -m 2 because the branch you merged is “the one you want”. -m names the parent to keep, not
the one to undo. Keep the mainline: -m 1.
Treating a merge commit as a place to make changes. A merge commit whose tree differs from both parents in ways unrelated to resolution — an “evil merge” — hides a change from every normal review path. Resolve conflicts in a merge; make other changes in their own commit.
Assuming a merge commit means conflicts occurred. Most merge commits are conflict-free. The commit records integration, not difficulty.
Reading git log on a merge-heavy history without --first-parent. The default interleaving is
rarely what you want.
Mental Model
Section titled “Mental Model”A merge commit is a join in the graph.
Ordinary commits form a chain: each points at what came before. A merge commit points at two, which is what turns the chain into a graph and makes it possible to ask “where did these two lines of work come together?”
The first parent is the road you were on. The second is the road that joined.
What You Learned
Section titled “What You Learned”- A merge commit is an ordinary commit with two or more parent lines; nothing else is special about it.
- Parent order is meaningful: first parent is the branch you were on.
--first-parentreads a merge-heavy history as one entry per integration.- Merge commits preserve both histories, record integration, and make whole-branch revert one command.
- They cost log noise, non-linear history and extra care when bisecting or reverting.
git revert -m 1reverts a merge; re-merging afterwards restores nothing without extra steps.- Platform merge buttons use
--no-ff, so they always produce a merge commit.
Try It Yourself
Section titled “Try It Yourself”- Create a repository, commit, branch to
feature, and add a file there. - Back on
main, commit a different change so the branches diverge. - Merge with
--no-ffand inspect the object:git cat-file -p HEAD | grep parent— expect two lines. - Confirm
git rev-parse --short HEAD^1 HEAD^2names the two tips. - Revert it:
git revert -m 1 HEAD --no-edit. Confirm the feature’s file is gone. - Predict: what happens if you now run
git merge featureagain? - Run it. Read the output carefully.
- Restore the work by reverting the revert, and confirm the file returns.
Steps 6 and 7 are the point of the exercise. Seeing “Already up to date” while the file is missing is memorable in a way that reading about it is not.
Next Lesson
Section titled “Next Lesson”Squash merging takes the opposite approach: no merge commit, and the branch’s individual commits are discarded in favour of one.