open atlas
↑ Back to track
Git, from zero to senior GIT · 03 · 03

Merging and fast-forward

git merge folds another branch into the current one. If the current branch has not moved, git fast-forwards the pointer with no merge commit. If both diverged, a three-way merge from their common-ancestor merge base creates a commit with two parents. --no-ff forces it.

GIT Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

You split work onto a branch; now you need it back on main. git merge is the verb that folds one branch into another — but it does the job in two very different ways depending on the shape of the history, and they produce different commit graphs. One leaves a perfectly straight line; the other adds a commit with two parents. Choosing between them (and knowing which you got) is the difference between a history you can read and one you cannot.

By the end of this lesson you will be able to predict whether a merge fast-forwards or creates a merge commit, explain the three-way merge and its merge base, and control the outcome with --no-ff and --ff-only.

Goal

After this lesson you can run git merge, predict whether it fast-forwards or produces a three-way merge commit, explain how git uses the merge base (common ancestor) to combine diverged branches, and use --no-ff and --ff-only to force or forbid a merge commit.

1

git merge <branch> integrates another branch into the one you are on. You check out the destination, then name the source:

git switch main          # the branch you want to receive the work
git merge feature        # fold 'feature' into 'main'

Merge is always into the current branch. After it succeeds, main contains the work from feature, and feature is unchanged — merging does not move or consume the source branch, it only updates the branch HEAD is on. How main gets there is the interesting part, and it depends entirely on whether main itself moved since the branch split off.

2

Fast-forward: if the current branch has no commits of its own, git just slides its pointer forward. Suppose main is at commit A, you branched feature, and added B then C — but main got no new commits in the meantime. main is an ancestor of feature, so there is nothing to combine; merging only needs to advance main from A to C:

git merge feature
# Updating A..C
# Fast-forward

No merge commit is created. The history stays a single straight line, and afterward main and feature point at the same commit C. This is the common case when you are the only one who touched main.

3

Three-way merge: if both branches advanced, git builds a new merge commit with two parents. Now suppose after branching, feature got B and C, and main got its own commit D. The branches have diverged — neither is an ancestor of the other — so a fast-forward is impossible. Git performs a three-way merge: it finds the merge base (the latest commit both share, here A), then combines three snapshots — the base A, your side D, and their side C — into a new commit:

git merge feature
# Merge made by the 'recursive' strategy.

That new merge commit has two parents (D and C), tying the two lines of history back together. It is the only kind of commit with more than one parent, and it is how the branch graph forks and rejoins.

Why this works

Why three snapshots, not two? Comparing only the two tips cannot tell, for a changed line, which side changed it — maybe both, maybe one side changed it and the other left the original. The merge base is the reference: for each file, git looks at base-vs-yours and base-vs-theirs. If only one side changed a region, take that change. If both sides changed the same region differently, git cannot decide and reports a conflict (next lesson). Without the common ancestor, every differing line would look like a conflict — the merge base is what makes automatic merging possible.

4

--no-ff forces a merge commit; --ff-only forbids one. When a fast-forward is possible, you can still choose to record a merge commit, or insist on a clean fast-forward:

git merge --no-ff feature    # always create a merge commit, even if FF was possible
git merge --ff-only feature  # fast-forward if possible, otherwise abort (no merge commit)

--no-ff is common team policy: it preserves an explicit marker that “these commits arrived together as one feature,” keeping the branch shape visible in history. --ff-only is the safety setting for pulling shared branches — if your local branch has diverged, it refuses rather than silently creating an unexpected merge commit. Many teams set git config merge.ff only or use it on git pull.

Worked example

The same merge, two histories.

History so far: main and feature both descend from commit A. feature added B and C.

Case 1 — main untouched. main is still at A.

git switch main
git merge feature      # Fast-forward
git log --oneline --graph
# * C
# * B
# * A

A straight line; main and feature now both point at C. No merge commit, because A was an ancestor of C — there was nothing to reconcile.

Case 2 — main also moved. While feature had B and C, a teammate landed D on main.

git switch main        # main is at D
git merge feature
git log --oneline --graph
# *   M  (merge commit: parents D and C)
# |\
# | * C
# | * B
# * | D
# |/
# * A

Now there is a merge commit M with two parents. Same command, but because both branches advanced past the merge base A, git had to reconcile D and C and record the result. The graph forks at A and rejoins at M — that diamond shape is the visual signature of a three-way merge.

Common mistake

A frequent surprise: “I merged but there is no merge commit — did it work?” Yes; you got a fast-forward, which is a successful merge that simply did not need a new commit. The opposite trap is assuming every git pull fast-forwards: if the remote and your local branch both advanced, pull does a three-way merge and creates a merge commit you may not have wanted. Teams that prefer linear history use --ff-only (or rebase) on pull to avoid these surprise merge commits.

Check yourself
Quiz

When does git create a merge commit (with two parents) instead of fast-forwarding?

Recap

git merge <branch> folds another branch into the current one. If the current branch is an ancestor of the source — it gained no commits of its own — git fast-forwards, sliding the pointer with no new commit and keeping history linear. If both branches diverged, git does a three-way merge: it finds the merge base (common ancestor), reconciles each side’s changes against it, and records a merge commit with two parents, forking-and-rejoining the graph. --no-ff forces a merge commit even when a fast-forward was possible; --ff-only refuses anything but a fast-forward. When both sides change the same lines, git cannot auto-reconcile — and that is the merge conflict you will learn to resolve next.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.