Understanding Git Merge Types: Fast-Forward vs Three-Way Merge

Master the two types of Git merges - fast-forward and three-way (merge commit). Learn when Git uses each type, how to read merge output, and control merge behavior with practical examples.

9 min read
PreviousNext

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

AspectFast-ForwardThree-Way Merge
When UsedLinear historyDivergent branches
Creates CommitNoYes (merge commit)
HistoryLinearShows branch structure
Revert FeatureCommit by commitSingle revert possible
OutputFast-forwardMerge 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!

Thank you for reading!

Published on December 25, 2025

Owais

Written by Owais

I'm an AIOps Engineer with a passion for AI, Operating Systems, Cloud, and Security—sharing insights that matter in today's tech world.

I completed the UK's Eduqual Level 6 Diploma in AIOps from Al Nafi International College, a globally recognized program that's changing careers worldwide. This diploma is:

  • ✅ Available online in 17+ languages
  • ✅ Includes free student visa guidance for Master's programs in Computer Science fields across the UK, USA, Canada, and more
  • ✅ Comes with job placement support and a 90-day success plan once you land a role
  • ✅ Offers a 1-year internship experience letter while you study—all with no hidden costs

It's not just a diploma—it's a career accelerator.

👉 Start your journey today with a 7-day free trial

Related Articles

Continue exploring with these handpicked articles that complement what you just read

9 min read

Git Rebase: Creating a Clean Linear History

Master git rebase to create clean, linear commit histories. Learn how rebase works, when to use it instead of merge, and follow best practices to avoid common pitfalls with practical examples.

#git#version-control+4 more
Read article

More Reading

One more article you might find interesting