Thursday, September 10, 2026

Fast-Forwarding Merge vs. Non-Fast-Forwarding Merge in Git

See All Posts on GitHub    « Previously    Next »

Fast-Forwarding Merge vs. Non-Fast-Forwarding Merge in Git

When you merge branches in Git, Git has to decide how to combine the histories of those branches. Sometimes it can simply move a branch pointer forward. Other times, it must create a new commit to record the merge.

The key difference: A fast-forward merge does not create a new merge commit. A non-fast-forward merge creates a new merge commit that explicitly joins two lines of development.

First, Understand What a Git Branch Really Is

Before understanding the two types of merges, it helps to remember that a Git branch is essentially a movable pointer to a commit.

Suppose you start with a main branch:

A --- B --- C
              ^
             main

You then create a branch called feature from commit C and make two commits:

A --- B --- C --- D --- E
              ^           ^
             main       feature

Notice something important: main is still pointing to C, while feature is ahead of it.

This particular situation is what makes a fast-forward merge possible.

What Is a Fast-Forward Merge?

A fast-forward merge happens when the branch you are merging into has not moved forward since the feature branch was created. In other words, the target branch is an ancestor of the branch being merged.

Git does not need to create a new commit because there are no competing changes to combine. It can simply move the target branch pointer forward.

Before the Merge

A --- B --- C --- D --- E
              ^           ^
             main       feature

If you run:

git switch main
git merge feature

Git can simply move main from C to E.

After the Merge

A --- B --- C --- D --- E
                          ^
                    main, feature

No new commit was created. The history is still a straight line.

Think of it this way: Git is saying, "There is nothing to merge. The target branch is simply behind. I can move its pointer forward to the latest commit."

Why Is It Called "Fast-Forward"?

The term comes from what Git does to the branch pointer. It does not perform a complicated three-way merge or create a new commit. It simply fast-forwards the branch reference to a later commit.

Imagine that main is pointing at page 3 of a book and feature has continued the same story through page 5. There is no second story to reconcile. Git can simply move the bookmark from page 3 to page 5.

What Is a Non-Fast-Forwarding Merge?

A non-fast-forward merge is required when both branches have developed independently.

For example, suppose main contains commit D, while the feature branch was created earlier from commit C and contains commits F and G:

              D --- E
             /
A --- B --- C
             \
              F --- G
                    ^
                  feature

Here, main and feature have diverged. The feature branch cannot simply replace main's pointer, because main contains commits that are not part of the feature branch.

Git therefore needs to combine the two histories.

The Merge Commit

When you run:

git switch main
git merge feature

Git can create a new merge commit:

              D --- E
             /         \
A --- B --- C           M
             \         /
              F --- G
                    ^
                  feature

The new commit M has two parents: one from the existing main history and one from the feature history. It records the point at which the two lines of development were brought together.

Think of it this way: Git is saying, "These branches have both moved forward independently, so I need a new commit that joins their histories."

A Simple Real-World Example

Imagine you are working on a website.

You create a login-page branch from main and work on the login page:

git switch main
git switch -c login-page

You make several commits:

git commit -m "Add login form"
git commit -m "Add login validation"

Meanwhile, nobody makes any new commits to main.

The history looks like this:

main
  |
  v
A --- B --- C --- D
              ^
          login-page

Merging login-page into main can be a fast-forward:

A --- B --- C --- D
                  ^
             main, login-page

Now consider a different situation. While you were working on the login page, another developer added commits to main:

              X --- Y
             /
A --- B --- C
             \
              D --- E
                    ^
                login-page

Now the branches have diverged. Git cannot simply move main forward to E, because that would leave commits X and Y out of the resulting history.

A merge commit can join both histories:

              X --- Y
             /         \
A --- B --- C           M
             \         /
              D --- E
                    ^
                login-page

Fast-Forward vs. Non-Fast-Forward

Characteristic Fast-Forward Merge Non-Fast-Forward Merge
Branches diverged? No Yes
Creates a merge commit? No Usually yes
History shape Straight line Branches join together
What Git does Moves the branch pointer forward Creates a commit combining the histories
Useful for Simple linear development Preserving explicit branch/merge history

Forcing a Non-Fast-Forward Merge

There is an important Git option that lets you create a merge commit even when a fast-forward merge would be possible.

git merge --no-ff feature

The --no-ff option means "do not fast-forward."

Suppose the history is:

A --- B --- C --- D
              ^       ^
             main   feature

A normal merge could simply move main to D. But with:

git merge --no-ff feature

Git creates a merge commit:

A --- B --- C --- D
              \       /
                --- M
                    ^
                   main

The exact visual layout of the graph depends on the history, but the important point is that M is an explicit merge commit.

Why Would You Use --no-ff?

At first, creating an extra commit may seem unnecessary. However, it can make the history easier to understand.

Suppose every feature branch is merged with --no-ff. The history can show clearly that a group of commits represented one feature:

--- A --- B ----------- M1 --- N -------- M2 --->
          \             /              /
           C --- D --- /              /
                                      /
                       E --- F -------

The merge commits provide visible boundaries between pieces of work. This can be particularly useful in team environments where understanding the development history matters.

Important: --no-ff does not mean that Git will ignore the actual changes from the branch. It simply tells Git to preserve the merge as an explicit commit instead of reducing it to a pointer movement.

Fast-Forward Is Not the Same as "No Merge"

This is a common source of confusion.

When Git performs a fast-forward, people sometimes say that "nothing was merged." Technically, the changes from the feature branch become part of the target branch, but Git does not need to create a merge commit.

The important distinction is between integrating the changes and creating a merge commit.

A fast-forward integrates the branch by moving the target branch pointer. A non-fast-forward merge integrates the histories by creating a new commit with both histories as parents.

How Git Decides Which One to Use

When you run:

git merge feature

Git examines the relationship between the current branch and feature.

  • If the current branch is an ancestor of feature, Git can fast-forward.
  • If the branches have diverged, Git needs to perform a real merge and create a merge commit, assuming the merge is successful.
  • If you use --no-ff, Git creates a merge commit even when fast-forwarding would otherwise be possible.

What About Conflicts?

A non-fast-forward merge may result in merge conflicts when Git cannot automatically determine how to combine changes made on the two branches.

For example, if both branches modify the same lines of the same file, Git may stop and ask you to resolve the conflict.

Fast-forward merges generally do not encounter this type of merge conflict because there is no divergent history to reconcile. Git is simply advancing the branch pointer along an existing line of commits.

A Useful Mental Model

Fast-forward

Think of two people reading the same book. One person stopped at chapter 3, while the other continued to chapter 5. To catch up, the first person's bookmark simply moves from chapter 3 to chapter 5.

Non-fast-forward

Now imagine two people started from chapter 3 and wrote different endings. Their work has diverged. Someone has to combine the two versions and record that combination. The merge commit represents that joining point.

Common Commands

Allow Fast-Forward When Possible

git merge feature

This is the normal merge command. Git will fast-forward if possible; otherwise it performs a non-fast-forward merge.

Require a Merge Commit

git merge --no-ff feature

This prevents Git from fast-forwarding and creates a merge commit.

Allow Only Fast-Forwarding

git merge --ff-only feature

This tells Git that the merge must be a fast-forward. If the branches have diverged, Git refuses to perform the merge rather than creating a merge commit.

Why This Matters in Team Development

The choice between fast-forward and non-fast-forward merging is not just about Git mechanics. It affects how your project's history looks.

A project that favors fast-forward merges tends to maintain a cleaner, linear history. A project that favors explicit merge commits preserves more information about when separate branches were integrated.

Neither approach is universally better. Teams often choose a strategy based on how they want their Git history to communicate the development process.

In Summary

The easiest way to remember the difference is:

  • Fast-forward merge: the target branch is behind the feature branch, so Git can simply move the target pointer forward.
  • Non-fast-forward merge: the branches have diverged, so Git needs a new merge commit to join their histories.
  • --no-ff: forces Git to create a merge commit even when a fast-forward would be possible.
  • --ff-only: allows the merge only when Git can fast-forward.

In short: fast-forwarding moves a pointer; non-fast-forwarding creates a new commit to join two histories.


See All Posts on GitHub    « Previously    Next »

No comments:

Post a Comment