When you run git merge, Git doesn't always do the same thing. Depending on your branch history, Git chooses between two merge strategies: fast-forward or three-way merge. Understanding the difference is crucial for maintaining a clean, readable Git history.
What You'll Learn: In this comprehensive guide, you'll master:
- The difference between fast-forward and three-way merges
- How Git decides which merge type to use
- Reading and understanding merge output
- When merge commits are created
- Controlling merge behavior with flags
- Best practices for different workflows
The Two Types of Merges
FAST-FORWARD MERGE THREE-WAY MERGE
================== ================
Before: Before:
master: A---B master: A---B---E
\ \
dev: C---D dev: C---D
After: After:
master: A---B---C---D master: A---B---E-------M
(HEAD) \ /
dev: C---D
Just moves pointer!
Creates merge commit M!
Fast-Forward Merge Explained
A fast-forward merge happens when there's a linear path from your current branch to the target branch. Git simply moves the branch pointer forward.
Setting Up the Scenario
Let's create a scenario for a fast-forward merge:
# Start on master
git checkout master
git log --oneline
Output:
a2464fe (HEAD -> master, hello/master) added fourth.txt
ce0024b Revert "added fourth.txt"
479cd2a updated second.txt
Now create a branch and make commits:
# Create and switch to dev branch
git branch dev
git checkout dev
# Make some commits on dev
echo "working to update fast forward" >> first.txt
git add .
git commit -m "updated first.txt"
echo "working to update fast forward" >> third.txt
git add .
git commit -m "updated third.txt"
The Branch State Before Merge
git log --oneline
Output:
d4a741d (HEAD -> dev) updated third.txt
73cdf0c updated first.txt
a2464fe (hello/master, master) added fourth.txt
Visualized:
master
|
a2464fe -----------> (no new commits on master)
|
+----> 73cdf0c ----> d4a741d
|
dev (HEAD)
Notice: master hasn't moved. There's a direct linear path from master to dev.
Performing the Fast-Forward Merge
# Switch to master
git checkout master
# Merge dev
git merge dev
Output:
Updating a2464fe..d4a741d
Fast-forward
first.txt | 1 +
third.txt | 1 +
2 files changed, 2 insertions(+)
Key Output: Fast-forward - This tells you Git didn't create a merge commit. It just moved the master pointer!
After Fast-Forward Merge
git log --oneline
Output:
d4a741d (HEAD -> master, dev) updated third.txt
73cdf0c updated first.txt
a2464fe (hello/master) added fourth.txt
Visualized:
Before merge: After merge:
a2464fe (master) a2464fe
| |
+----> 73cdf0c ----> d4a741d +----> 73cdf0c ----> d4a741d
| |
dev dev, master (HEAD)
Both master and dev now point to the same commit. No new commit was created!
Three-Way Merge (Merge Commit) Explained
A three-way merge happens when both branches have new commits. Git must create a new "merge commit" to combine them.
Setting Up the Scenario
First, let's create divergent branches:
# Create a new branch from current master
git branch devtest
git checkout devtest
# Make a commit on devtest
vi Fourth.txt # Add: "updated for merge commit"
git add .
git commit -m "updated fourth.txt for merge commit"
Now go back to master and make a different commit:
# Switch to master
git checkout master
# Make a commit on master
vi second.txt # Add: "updated for merge commit"
git add .
git commit -m "updated second.txt for merge commit"
The Branch State Before Merge
master log:
03439a0 (HEAD -> master) updated second.txt for merge commit
d4a741d (dev) updated third.txt
devtest log:
41083cf (devtest) updated fourth.txt for merge commit
d4a741d (dev) updated third.txt
Visualized:
03439a0 (master, HEAD)
/ "updated second.txt"
d4a741d ---------------+
| \
"updated 41083cf (devtest)
third.txt" "updated fourth.txt"
Both branches have diverged from their common ancestor (d4a741d).
Performing the Three-Way Merge
# On master, merge devtest
git merge devtest
Git opens your editor for a merge commit message (default is fine):
Merge branch 'devtest'
Output:
Merge made by the 'ort' strategy.
Fourth.txt | 2 ++
1 file changed, 2 insertions(+)
Key Output: Merge made by the 'ort' strategy - This tells you a merge commit was created. The 'ort' strategy is Git's default merge algorithm.
After Three-Way Merge
git log --oneline
Output:
bdc2211 (HEAD -> master) Merge branch 'devtest'
03439a0 updated second.txt for merge commit
41083cf (devtest) updated fourth.txt for merge commit
d4a741d (hello/master, dev) updated third.txt
73cdf0c updated first.txt
The full log shows the merge commit's parents:
git log
Output (partial):
commit bdc2211595e50ac397fea2cb8f4ce652a410224f (HEAD -> master)
Merge: 03439a0 41083cf
Author: Owais Abbasi <owais.abbasi9@gmail.com>
Date: Mon Jan 26 22:52:42 2026 +0500
Merge branch 'devtest'
Notice: Merge: 03439a0 41083cf - The merge commit has TWO parents! This is what makes it a merge commit.
Visualizing the Merge Commit
Before merge: After merge:
03439a0 (master) 03439a0 --------+
/ / \
d4a741d d4a741d bdc2211 (master)
\ \ /
41083cf (devtest) 41083cf ---------+
(devtest)
bdc2211 is the merge commit
It has two parents: 03439a0 and 41083cf
How Git Decides Which Merge Type
git merge feature-branch
|
Can master reach feature-branch
by moving forward only?
|
+-------------+-------------+
| |
YES NO
| |
FAST-FORWARD THREE-WAY MERGE
(just move pointer) (create merge commit)
The Key Question
Git asks: "Is there a direct path from the current branch tip to the target branch tip?"
- Yes → Fast-forward (no divergence)
- No → Three-way merge (branches diverged)
Controlling Merge Behavior
Force a Merge Commit (No Fast-Forward)
Even when fast-forward is possible, you can force a merge commit:
git merge --no-ff feature-branch
This creates a merge commit even for linear history:
Without --no-ff: With --no-ff:
master: A---B---C---D master: A---B-------M
(moved) \ /
feature: C---D
Why use --no-ff?
- Preserves feature branch history
- Makes it clear when features were merged
- Easier to revert entire features
Force Fast-Forward Only
If you want to ensure no merge commit:
git merge --ff-only feature-branch
This fails if fast-forward isn't possible, rather than creating a merge commit.
Reading Git Merge Output
Fast-Forward Output
Updating a2464fe..d4a741d
Fast-forward <-- Type of merge
first.txt | 1 + <-- Files changed
third.txt | 1 +
2 files changed, 2 insertions(+)
Three-Way Merge Output
Merge made by the 'ort' strategy. <-- Type of merge + strategy
Fourth.txt | 2 ++ <-- Files changed
1 file changed, 2 insertions(+)
After Merging: Syncing Feature Branch
After merging devtest into master, the branches point to different commits:
# master has the merge commit
git log --oneline master
# bdc2211 (HEAD -> master) Merge branch 'devtest'
# 03439a0 updated second.txt for merge commit
# devtest doesn't have master's changes
git log --oneline devtest
# 41083cf (devtest) updated fourth.txt for merge commit
To update devtest with the merge:
git checkout devtest
git merge master
Output:
Updating 41083cf..bdc2211
Fast-forward
second.txt | 2 ++
1 file changed, 2 insertions(+)
Now both branches point to the merge commit!
Comparison Summary
| Aspect | Fast-Forward | Three-Way Merge |
|---|---|---|
| When Used | Linear history | Divergent branches |
| Creates Commit | No | Yes (merge commit) |
| History | Linear | Shows branch structure |
| Revert Feature | Commit by commit | Single revert possible |
| Output | Fast-forward | Merge made by... |
Visual Summary
FAST-FORWARD (Linear Path Exists)
=================================
master dev master, dev
| | |
v v v
A--B + B--C--D ==> A--B--C--D
No merge commit needed!
THREE-WAY MERGE (Divergent Paths)
=================================
master dev master
| | |
v v v
A--B--E C--D ==> A--B--E--M
\ / \ |
\ / dev \ |
\ / | \|
(diverged) C-----D
Merge commit M has two parents!
Best Practices
1. Use --no-ff for Feature Branches
git merge --no-ff feature-x
This makes your history show when features were integrated.
2. Use Fast-Forward for Sync Operations
git merge --ff-only main
When pulling updates from main into your feature branch.
3. Clean Up After Merge
# Delete the merged feature branch
git branch -d feature-x
4. Write Good Merge Commit Messages
Instead of just "Merge branch 'feature'":
Merge branch 'feature-user-auth'
Adds user authentication with JWT tokens.
Includes login, logout, and session management.
Quick Reference
# Regular merge (Git chooses type)
git merge branch-name
# Force merge commit
git merge --no-ff branch-name
# Force fast-forward only (fails if not possible)
git merge --ff-only branch-name
# Abort a merge in progress
git merge --abort
# See merge commits in log
git log --merges
# See graph of branches
git log --oneline --graph --all
Summary
Understanding merge types helps you:
- Read Git output: Know what happened during merge
- Control history: Choose between clean linear history or preserved branch structure
- Debug issues: Understand why merge commits appear (or don't)
- Work with teams: Use appropriate strategies for different workflows
Fast-forward keeps history simple; merge commits preserve branch context. Choose based on your team's needs and workflow preferences!

