Merging
Merging is how Git combines two lines of work into one. The result either fast-forwards a pointer or creates a new commit with two parents that ties the histories together. Understanding which case you are in, and why, removes most of the mystery around git merge.
The mental model
A merge takes two commits, almost always the tips of two branches, and produces a result that contains both histories. The result is one of two shapes: either Git slides the target branch's pointer forward along an existing chain, or it builds a new commit whose two parents are the tips you merged.
Which shape you get depends entirely on whether the histories have diverged. If the branch you are merging is purely "ahead" of where you stand, there is nothing to reconcile and Git can fast-forward. If both sides have moved since they last agreed, Git has to combine real changes and records a merge commit that points back to both of them.
The everyday command is short:
git switch main
git merge feature
Read it as "while standing on main, pull feature in." The current branch is always the destination. The named branch is the source. After the command, the current branch reflects both histories; the source branch is untouched.
Fast-forward merge
A fast-forward is the simplest case. The target branch's tip is already an ancestor of the source branch's tip, meaning the target has done nothing new since they diverged. Git does not need to build a new commit; it just moves the target's branch pointer forward.
Git tells you when this happens:
$ git merge feature
Updating f42c576..3a0874c
Fast-forward
src/app.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
"Fast-forward" is the giveaway. No merge commit was created, the history stays linear, and the only thing that changed is which commit main points to.
True (three-way) merge
When both branches have moved since they parted ways, the histories have diverged. Git cannot just slide a pointer; it has to combine two sets of changes and record that combination as a commit. This is called a three-way merge because three commits are involved: the two branch tips and their common ancestor.
The most recent commit that is an ancestor of both branches being merged. Git uses the merge base as the reference point: it computes the diff from base to each tip, then attempts to apply both sets of changes to produce a result. git merge-base <A> <B> prints this commit explicitly.
If the two diffs touch different lines, Git combines them automatically and writes the result as a merge commit. If they touch the same lines differently, Git stops and asks you to resolve the conflict by hand. The mechanics of conflict resolution are the subject of the next topic.
Git announces a three-way merge differently:
$ git merge feature
Merge made by the 'ort' strategy.
src/parser.js | 24 ++++++++++++++++++++----
src/util.js | 3 +++
2 files changed, 23 insertions(+), 4 deletions(-)
The new merge commit has two parents (the previous tip of main and the tip of feature), an author and committer (usually you), and a message. By default Git writes "Merge branch 'feature' into main"; you can edit it before saving.
Controlling fast-forward: --ff, --no-ff, --ff-only
Three flags decide whether fast-forwarding is allowed when a fast-forward is technically possible. The flag matters only for cases that could fast-forward; when histories have diverged, only a three-way merge can succeed.
| Flag | If FF possible | If FF not possible | Result |
|---|---|---|---|
--ff (default) | Fast-forward | Three-way merge commit | No merge commit unless needed |
--no-ff | Three-way merge commit anyway | Three-way merge commit | Always a merge commit |
--ff-only | Fast-forward | Refuses; exits with error | History stays linear or nothing happens |
git merge feature # default: fast-forward if possible
git merge --no-ff feature # always record a merge commit
git merge --ff-only feature # fast-forward or fail
Many teams pass --no-ff when merging a feature branch into a long-lived branch like main. The resulting merge commit keeps the topological record of "this group of commits was a feature," which is visible in git log --graph and survives even after the feature branch is deleted. Without it, a fast-forward erases the grouping.
Merge strategies
When Git has to combine real diffs, it chooses a strategy: an algorithm for computing the merged tree. You almost never pick one explicitly, but knowing what is there helps when you read merge output or a defaults discussion.
| Strategy | Used for | Notes |
|---|---|---|
ort |
Two-head merges (default since Git 2.34, 2021) | "Ostensibly Recursive's Twin." Reimplementation of recursive with the same semantics, much faster and free of several long-standing bugs. |
recursive |
Was the default before 2.34 | Now a synonym for ort in modern Git. Handles renames and multiple common ancestors recursively. |
resolve |
Two-head merges, older & simpler | Plain three-way merge; no recursive handling of criss-cross histories. Occasionally useful as a fallback. |
octopus |
Three or more heads at once | Default when you pass multiple branches to git merge. Refuses to do conflict resolution -- octopus merges only work when no human intervention is needed. |
ours |
Discarding the other side entirely | Records a merge commit whose tree is exactly the current branch's. The other branch's changes are formally ancestors but contribute nothing to the result. Useful for "we are abandoning that work but want history to show it was considered." |
Pass a strategy with -s:
git merge -s ours obsolete-experiment # keep current tree, record the merge
git merge -s octopus topic-a topic-b # merge three histories at once
Note: there is no top-level theirs strategy. The opposite of -s ours -- "use the other side's content" -- is available as a strategy option, covered next.
Strategy options (-X)
Where -s picks the algorithm, -X tunes it. The two most useful options resolve conflicts automatically by preferring one side:
git merge -X ours feature # on conflict, keep our version
git merge -X theirs feature # on conflict, keep their version
The distinction between -s ours and -X ours matters. -s ours throws away the other branch's content entirely, regardless of conflicts. -X ours still performs a normal merge -- non-conflicting changes from both sides land in the result -- and only falls back to our version when Git would otherwise stop on a conflict.
A few other -X options that come up:
-X ignore-all-space,-X ignore-space-change-- treat whitespace-only differences as non-conflicting. Useful when one branch reformatted a file.-X renormalize-- run line-ending normalization on both sides before comparing. Helpful when CRLF/LF settings have changed.-X diff-algorithm=patience(orhistogram) -- swap the diff algorithm. Occasionally produces a cleaner result on heavily reorganized files.
Squash merges
A squash merge is technically not a merge at all -- it just borrows the verb. git merge --squash <branch> stages the combined changes from the named branch but does not create a merge commit and does not record the branch's commits as part of history. You then commit those staged changes yourself, producing a single ordinary commit on the current branch.
git switch main
git merge --squash feature
git commit -m "Add CSV import"
The result on main is one commit whose tree contains everything feature built, but whose history shows no link to feature at all. This is the model GitHub uses for "Squash and merge" pull requests: many small WIP commits on the branch collapse into one tidy commit on the target.
The squashed branch's individual commits never become part of the target's history. You cannot bisect across them, blame will point at the squash commit, and the feature branch is now safe to delete only because all of its content has been copied -- not linked. If you later need to know how the feature was built piece by piece, you must keep the original branch around or live with what survives in the pull request page.
Aborting and undoing
A merge in progress -- one Git started but stopped on a conflict, or one you started and want to back out of -- can be rolled back with:
git merge --abort
This restores the working tree and index to their state before the merge started. It works only while the merge is mid-flight. git reset --merge is a related, slightly older command useful in some edge cases involving uncommitted changes; --abort is the modern recommendation.
Once a merge has been committed, undoing it is a different problem. If you have not pushed the merge anywhere, git reset --hard HEAD~1 drops it and rewinds to the previous tip. If you have already published it, you should revert the merge with git revert -m 1 <merge-commit>, which records a new commit that undoes the merge's effects without rewriting history. Revert is covered in a later chapter on undoing work.
Visualizing merges
Reading the merge structure of a project is much easier once you train yourself to look at the graph rather than the linear log. The everyday incantation:
git log --oneline --graph --all --decorate
You will see ASCII branches splitting and joining at each merge commit. A fast-forward shows up as a straight line; a true merge shows up as a diamond. --decorate annotates each commit with the branches and tags pointing at it, which makes "where am I and where is the merge base" much clearer.
Merge vs rebase
Git offers two ways to integrate one branch's work into another: merge and rebase. Merge preserves the actual history -- the two lines of development really happened in parallel, and the merge commit records exactly that. Rebase rewrites the source branch's commits on top of the target so the result looks linear, as if the work had always happened sequentially.
Both are useful; they answer different questions. Merge answers "what is the real shape of this project's history?" Rebase answers "what is the cleanest story to tell about this work?" Rebasing gets its own topic later in this chapter.
Common pitfalls
Merging in the wrong direction. git merge feature while standing on main brings feature into main. Many people start with the source branch checked out and get a result they did not intend. Always confirm which branch you are on first: git status shows it on line one.
Assuming a fast-forward will always happen. The default tries to fast-forward, but the moment the target branch has new commits the source does not contain, Git creates a merge commit instead. If you assumed otherwise, you may end up surprised by a "Merge branch" commit in your linear history. Use --ff-only when you want a hard guarantee.
Confusing -s ours with -X ours. The first throws away the other branch's content; the second is just a conflict-resolution preference applied on top of a real merge. Reach for -s ours only when you genuinely want to discard the work.
Squash-merging and then merging the branch again. A squashed branch shares no commit identity with the target, so a later git merge of the same branch will try to bring all its original commits back in -- and Git, not knowing they were already squashed, may report odd conflicts on changes that "look like they should already be there." Delete the branch after squashing, or rebase it onto the target before any further integration.
Worked examples
Example 1: A fast-forward, start to finish
You branch from main, add two commits, and merge back while main has not moved. The merge fast-forwards.
git switch -c hotfix
# edit, commit, edit, commit
git switch main
git merge hotfix
# Updating a1b2c3d..e4f5g6h
# Fast-forward
git log --oneline --graph
# * e4f5g6h (HEAD -> main, hotfix) Fix typo in error message
# * d9e8c7b Defensive check on null input
# * a1b2c3d Earlier commit on main
No merge commit appears; main and hotfix now point at the same commit.
Example 2: A true merge with --no-ff
Same setup, but you want the merge commit to survive in the log so a future reader can see "this group of commits was the hotfix."
git switch main
git merge --no-ff hotfix
# Merge made by the 'ort' strategy.
git log --oneline --graph
# * 3f2c8a1 (HEAD -> main) Merge branch 'hotfix'
# |\
# | * e4f5g6h (hotfix) Fix typo in error message
# | * d9e8c7b Defensive check on null input
# |/
# * a1b2c3d Earlier commit on main
The graph splits and rejoins at the merge commit. Even if you delete hotfix, the shape stays.
Example 3: Finding the merge base directly
Before integrating, you want to see exactly which commit the two branches share as their most recent common ancestor.
git merge-base main feature
# 7a8b9c0d1e2f...
git log --oneline 7a8b9c0..main
# commits on main since the branch point
git log --oneline 7a8b9c0..feature
# commits on feature since the branch point
The two log commands list what each branch contributes to the merge. If one is empty, the merge will fast-forward.
Example 4: A squash merge for a small PR
The feature branch has six small WIP commits ("wip", "fix typo", "address review"). You want one tidy commit on main.
git switch main
git merge --squash feature
# Squash commit -- not updating HEAD
# Automatic merge went well; stopped before committing as requested
git status
# Changes to be committed:
# modified: src/import.js
# new file: tests/import.test.js
git commit -m "Add CSV import"
git branch -D feature
The result on main is a single commit. feature is gone, and its six commits are gone with it -- they were never part of main's history.
Sources & further reading
-
The canonical narrative explanation of fast-forward and three-way merges, with a worked example through a hotfix and a feature branch. The right place to read this story end-to-end.
-
Official command reference. Definitive source for every flag, strategy, and strategy option, plus exact behavior of
--abortand--squash. -
Goes deeper on strategies,
-Xoptions, manual conflict resolution, and undoing merges. Read after you have done a few real merges and want the next layer of control. -
Reference for the command that computes the common ancestor used as the reference point in a three-way merge. Useful when reasoning about exactly what Git is comparing.
-
Introduces the
ortmerge strategy, the rewrite ofrecursivethat became the default in Git 2.34. Explains the motivation -- speed and correctness fixes -- and the road to becoming default. -
A friendlier, diagram-heavy walkthrough of the same material. Useful as a second pass if Pro Git's prose feels dense.