Skip to content

Merge Commits in Git: Parents, Topology and Trade-offs

Lesson 3 of 7Beginner → Intermediate8 min readModern Git Workflows · MergingVerified: Git 2.43.0 on Ubuntu 24.04; every command and output in this lesson was run

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.

An ordinary commit has one parent line:

Terminal window
git cat-file -p HEAD
tree 06f3e565236176c7633ec5bb2471844d53aafa24
parent c86ff2b9962c64f6e2d5361b57f4f6d7d875d90b
author Dev <dev@example.com> 1787405411 +0000
committer Dev <dev@example.com> 1787405411 +0000
Add validation

A merge commit has two or more:

tree ae1af2b170d94d7b8d833dbf091d42ef7c9f9b18
parent 5f49aabec7815dc0cb237a91e678d27aa6b55606
parent abe666692988d8a2bed78d6a092fb9f1da4bd72c
author Dev <dev@example.com> 1787403598 +0000
committer 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.

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.

First parent continues the mainline; second parent is the branch

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.

ABCMXYmainfeatureHEAD^1 is C. HEAD^2 is Y. The distinction drives log, revert and bisect behaviour.
Terminal window
git rev-parse --short HEAD^1 HEAD^2

Three things depend on this ordering, and all three matter in practice:

Depends on parent orderWhy
git log --first-parentFollows only parent 1, giving one entry per integration
git revert -m NSelects which parent’s line of development to keep
git bisect --first-parentRestricts 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.

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:

Terminal window
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.

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.

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:

Terminal window
git revert HEAD
error: commit f1b6bc006e03a46fffcd9fb9e4cb61d2682c369a is a merge but no -m option was given.
fatal: revert failed
Terminal window
git revert -m 1 HEAD

What 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.

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.

Nothing limits a merge commit to two parents. Merging several branches at once produces an octopus merge:

Terminal window
git merge feat-a feat-b feat-c -m "Octopus merge"
Fast-forwarding to: feat-a
Trying simple merge with feat-b
Trying simple merge with feat-c
Terminal window
git cat-file -p HEAD | grep -c parent
3
*-. 76ceafe Octopus merge
|\ | | * 7e66983 add c
| * | cae3854 add b
| |/
* / 2a25d93 add a

Octopus 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.

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.

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 bisect heavily and want every commit on main to 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.

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.

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.

  • 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-parent reads 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 1 reverts a merge; re-merging afterwards restores nothing without extra steps.
  • Platform merge buttons use --no-ff, so they always produce a merge commit.
  1. Create a repository, commit, branch to feature, and add a file there.
  2. Back on main, commit a different change so the branches diverge.
  3. Merge with --no-ff and inspect the object: git cat-file -p HEAD | grep parent — expect two lines.
  4. Confirm git rev-parse --short HEAD^1 HEAD^2 names the two tips.
  5. Revert it: git revert -m 1 HEAD --no-edit. Confirm the feature’s file is gone.
  6. Predict: what happens if you now run git merge feature again?
  7. Run it. Read the output carefully.
  8. 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.

Squash merging takes the opposite approach: no merge commit, and the branch’s individual commits are discarded in favour of one.