Lesson 31 of 6820 min read

git merge: Fast-Forward Merge vs Three-Way Merge

Understand how git merge combines branches, and the crucial difference between a simple fast-forward merge and a true three-way merge commit.

Author: CodersNexus

git merge: Fast-Forward Merge vs Three-Way Merge

Once work on a branch is complete, `git merge` brings it back together with another branch, most commonly merging a feature branch into `main`. Git actually performs one of two fundamentally different operations depending on the situation — a simple fast-forward, or a genuine three-way merge that creates a new commit — and understanding the distinction is essential for correctly interpreting your project's history.

Learning Objectives

  • Perform a basic merge using git merge <branch>.
  • Explain what a fast-forward merge is and when Git performs one.
  • Explain what a three-way merge is, and why it creates a new merge commit.
  • Use --no-ff to force a merge commit even when a fast-forward would be possible.

Key Terms to Know Before Learning git merge

  • git merge <branch>: Integrates the specified branch's changes into your currently checked-out branch.
  • Fast-forward merge: A merge where the target branch's pointer simply moves forward to match the branch being merged in, because no divergent work exists on the target branch.
  • Three-way merge: A merge where Git creates a new commit that combines changes from two diverged branches, using their common ancestor as a reference point.
  • Merge commit: A special commit created by a three-way merge that has two (or more) parent commits, representing the point where two lines of history reunite.
  • Common ancestor: The most recent commit that both branches being merged share in their history, used by Git to determine what changed on each side.

How git merge Actually Works

**Fast-forward merge** happens in the simplest possible scenario: your current branch (say, `main`) has had no new commits since the branch you're merging (say, `feature/login`) was created. In this case, `feature/login`'s commits are simply a straight continuation of `main`'s history — there's no divergence to reconcile. Git handles this by just moving `main`'s pointer forward to match `feature/login`'s latest commit. No new commit is created; it's literally just updating where the `main` pointer points to. This is why it's called 'fast-forward' — Git is just fast-forwarding the branch pointer along an already-existing, linear sequence of commits.

```
git switch main
git merge feature/login
# Fast-forward
```

**Three-way merge** happens when both branches have diverged — meaning `main` has received its own new commits *after* `feature/login` was created, so the two branches now have genuinely different, non-linear histories. In this case, there's no way to just 'move a pointer forward', since neither branch's history is a simple continuation of the other. Instead, Git performs a three-way merge: it looks at three points — the tip of `main`, the tip of `feature/login`, and their most recent **common ancestor** commit — and combines the changes made on each side since that ancestor. The result is a brand-new **merge commit**, which is special in that it has *two* parent commits (one from each branch being merged), explicitly recording that these two lines of history have been reunited.

```
git switch main
git merge feature/login
# Merge made by the 'ort' strategy.
# (creates a new merge commit)
```

This merge commit will show up distinctly in `git log --graph` (Module 2), visually appearing as the point where two separate lines converge back into one.

An important nuance: even in a situation where a fast-forward *would* be technically possible, you can force Git to create an explicit merge commit anyway using the `--no-ff` flag:

```
git merge --no-ff feature/login
```

Many teams prefer this deliberately, because it preserves a clear, permanent record in history that a feature branch existed and was merged as a distinct unit of work — even if the underlying commits *could* have been fast-forwarded — making the project's history easier to read and understand later, especially when reviewing what specific set of commits belonged to one feature.

Understanding this distinction also directly explains why merge conflicts (the next lesson) can only occur during a three-way merge, never a fast-forward: a fast-forward involves no actual combination of divergent changes at all, so there's nothing that could possibly conflict.

git merge: Visual Walkthrough

Draw two side-by-side scenarios. LEFT labeled 'Fast-Forward Merge': main at commit C2, feature branch continuing straight from C2 to C3-C4 with no new commits on main in between. Arrow labeled 'git merge feature (fast-forward)' shows main's pointer simply sliding forward to C4 — no new commit created. RIGHT labeled 'Three-Way Merge': main has commit C2 then diverges with its own new commit C3-main, while feature branch also diverged from C2 with C3-feature-C4-feature. Arrow labeled 'git merge feature (three-way)' shows a NEW commit C5 created with two parent arrows pointing back to both C3-main and C4-feature, captioned 'Merge commit — has two parents.'

Fast-Forward Merge vs Three-Way Merge: Side-by-Side Comparison

AspectFast-Forward MergeThree-Way Merge
When it occursTarget branch has no new commits since the source branch divergedBoth branches have diverged with their own separate new commits
New commit created?No — pointer just moves forwardYes — a new merge commit is created
Number of parents on resultN/A (no new commit)Two (or more) parent commits
Can conflicts occur?No — there's no divergent content to reconcileYes — divergent changes to the same lines may conflict
Forcing a merge commit anywayPossible with --no-ffN/A — already produces a merge commit

git merge: Command Syntax and Examples

# Scenario: fast-forward merge (main has no new commits since feature/login diverged)
git switch main
git merge feature/login
# Output: Fast-forward

# Scenario: three-way merge (main HAS new commits since feature/login diverged)
git switch main
git merge feature/dark-mode
# Output: Merge made by the 'ort' strategy.
#  (a new merge commit is created with two parents)

# Force a merge commit even when a fast-forward would otherwise be possible
git merge --no-ff feature/login

# Visualize the resulting history, including merge commits
git log --oneline --graph --decorate --all

Breaking Down the git merge Example

The first merge succeeds as a fast-forward because `main` had no independent new commits — Git simply moves its pointer forward, and the output explicitly says 'Fast-forward' with no new commit created. The second scenario shows a three-way merge, since `main` had diverged with its own commits — Git's output confirms a new merge commit was created via its merge strategy ('ort' being Git's modern default strategy). The `--no-ff` example demonstrates deliberately forcing a merge commit for record-keeping purposes, even in a fast-forward-eligible situation. Finally, reviewing history with `git log --graph` visually reveals the branching and (if applicable) converging structure these merges create.

How git merge Is Used on Real Engineering Teams

  • Many teams enforce a --no-ff policy (or equivalent settings on GitHub/GitLab pull requests) specifically so that every feature's history remains visually grouped and identifiable as a distinct merge, rather than blending invisibly into a single flat line via fast-forward.
  • GitHub's 'Squash and merge' and 'Rebase and merge' pull request options are alternative strategies to a plain three-way merge, each producing different history shapes, but the underlying three-way merge concept remains foundational to understanding all of them.
  • Release engineering teams often specifically look for merge commits in git log --graph to identify exactly which set of commits belonged to a particular feature or bug fix when investigating a regression.
  • Solo developers working on personal projects often merge via fast-forward without noticing, since their main branch frequently has no independent commits between when a feature branch was created and later merged.

git merge Interview Questions and Answers

Q1. What is the difference between a fast-forward merge and a three-way merge?

A fast-forward merge occurs when the target branch has had no new commits since the branch being merged diverged from it — Git simply moves the target branch's pointer forward, with no new commit created. A three-way merge occurs when both branches have diverged with their own separate commits, requiring Git to create a new merge commit that combines changes from both, using their common ancestor as a reference point.

Q2. Why can merge conflicts only happen during a three-way merge, never a fast-forward?

A fast-forward merge involves no actual combination of divergent changes — it's simply advancing a pointer along an already-linear history. A three-way merge, by contrast, reconciles genuinely different changes made independently on two branches since their common ancestor, and conflicts arise specifically when both branches changed the same lines in incompatible ways.

Q3. What does the --no-ff flag do, and why might a team choose to always use it?

It forces Git to create an explicit merge commit even in situations where a fast-forward would otherwise be possible. Teams often prefer this because it preserves a clear, permanent record in history showing that a feature branch existed and was merged as a distinct, identifiable unit of work.

git merge Quiz: Test Your Understanding

1. When does a fast-forward merge occur?

  1. Whenever git merge is run, regardless of history
  2. When the target branch has had no new commits since the source branch diverged
  3. Only when using the --no-ff flag
  4. Only for the very first merge in a repository's history

Answer: B. When the target branch has had no new commits since the source branch diverged

Explanation: A fast-forward is possible specifically when the target branch's history is unchanged since the branch being merged split off, allowing Git to simply move the pointer forward with no new commit.

2. What makes a merge commit special compared to a regular commit?

  1. It cannot be viewed with git show
  2. It has two or more parent commits
  3. It automatically deletes the merged branch
  4. It cannot contain any code changes

Answer: B. It has two or more parent commits

Explanation: A merge commit created by a three-way merge uniquely has multiple parents, explicitly recording that two (or more) diverged lines of history have been reunited at that point.

3. What does the --no-ff flag do when merging?

  1. Prevents the merge from completing
  2. Forces a merge commit to be created even if a fast-forward would otherwise be possible
  3. Automatically resolves any conflicts
  4. Deletes the source branch after merging

Answer: B. Forces a merge commit to be created even if a fast-forward would otherwise be possible

Explanation: --no-ff (no fast-forward) instructs Git to always create an explicit merge commit, preserving a clear record of the merge even when the history could have simply been fast-forwarded instead.

git merge: Common Mistakes Beginners Make

  • Being surprised that a merge sometimes creates a new commit and sometimes doesn't, without understanding the fast-forward vs. three-way distinction.
  • Assuming merge conflicts can happen during any merge, rather than understanding they're only possible during an actual three-way merge.
  • Not using --no-ff when a team convention calls for it, resulting in feature history disappearing invisibly into a flat, fast-forwarded main branch.
  • Confusing the common ancestor concept with simply 'the oldest commit in the repository' — it specifically refers to the most recent commit shared by the two branches being merged.

git merge: Exam-Ready Quick Notes

  • Fast-forward merge: no divergence, pointer just moves forward, no new commit created.
  • Three-way merge: both branches diverged, Git creates a new merge commit with two parents, using the common ancestor as reference.
  • Conflicts can only occur during a three-way merge, never a fast-forward.
  • --no-ff forces a merge commit even when a fast-forward would otherwise be possible.

git merge: Key Takeaways

  • Git chooses between a fast-forward and a three-way merge automatically, based entirely on whether the target branch has diverged since the source branch split off.
  • A three-way merge produces a distinctive merge commit with two parents, explicitly marking where two lines of history reunite.
  • --no-ff is a valuable tool for teams that want feature history to remain clearly visible in the project's log, rather than disappearing via a silent fast-forward.

Frequently Asked Questions About git merge

Q1. What does git merge do?

It integrates the changes from a specified branch into your currently checked-out branch, either by simply moving your branch's pointer forward (a fast-forward merge) or by creating a new merge commit that combines diverged changes (a three-way merge), depending on the situation.

Q2. What is a fast-forward merge?

It's the simplest type of merge, occurring when your current branch has had no new commits since the branch you're merging in was created. Git just moves your branch's pointer forward to match, without creating any new commit.

Q3. What is a three-way merge and why does it create a new commit?

A three-way merge happens when both branches have diverged with their own independent commits. Git creates a new merge commit that combines changes from both branches, using their common ancestor as a reference point — this new commit has two parent commits, one from each branch.

Q4. Why do merge conflicts only happen sometimes, not on every merge?

Conflicts can only occur during a three-way merge, when both branches have made changes that need to be reconciled. A fast-forward merge involves no actual combination of divergent changes, so there's nothing that could possibly conflict.

Q5. What does the --no-ff flag do?

It forces Git to create an explicit merge commit even in a situation where a fast-forward merge would otherwise be possible, which some teams prefer because it keeps a clearer, more visible record of when a feature branch was merged.

Summary

`git merge <branch>` integrates one branch's changes into your current branch, and Git automatically performs one of two distinct operations depending on the situation. A fast-forward merge occurs when the target branch has had no new commits since the source branch diverged — Git simply moves the target's pointer forward, creating no new commit. A three-way merge occurs when both branches have diverged independently, requiring Git to create a new merge commit with two parent commits, combining changes from both sides relative to their most recent common ancestor. Merge conflicts can only arise during a three-way merge, since a fast-forward involves no actual reconciliation of divergent changes. The `--no-ff` flag lets teams force an explicit merge commit even when a fast-forward would otherwise be possible, preserving a clearer historical record of feature branches.

Frequently Asked Questions

It integrates the changes from a specified branch into your currently checked-out branch, either by simply moving your branch's pointer forward (a fast-forward merge) or by creating a new merge commit that combines diverged changes (a three-way merge), depending on the situation.

It's the simplest type of merge, occurring when your current branch has had no new commits since the branch you're merging in was created. Git just moves your branch's pointer forward to match, without creating any new commit.

A three-way merge happens when both branches have diverged with their own independent commits. Git creates a new merge commit that combines changes from both branches, using their common ancestor as a reference point — this new commit has two parent commits, one from each branch.

Conflicts can only occur during a three-way merge, when both branches have made changes that need to be reconciled. A fast-forward merge involves no actual combination of divergent changes, so there's nothing that could possibly conflict.

It forces Git to create an explicit merge commit even in a situation where a fast-forward merge would otherwise be possible, which some teams prefer because it keeps a clearer, more visible record of when a feature branch was merged.