Table of Contents

Introduction

Sometimes you run git merge and the output says "Fast-forward" instead of "Merge made by the 'ort' strategy", and there's no merge commit anywhere. People occasionally think something went wrong. Nothing did. Git noticed there was nothing to combine and took the shortcut.

In this article, we'll:

  1. Watch a fast-forward merge on a small repository
  2. See why Git didn't need to create a commit
  3. Cover when this happens, why some teams turn it off, and how to force a merge commit

What is a fast-forward merge?

When you merge <branch> into your current branch and your current branch has no commits that <branch> lacks, the tip of <branch> already contains everything. There is nothing to combine, so Git just moves your branch's pointer to that commit. That's a fast-forward. The history stays a straight line and no new commit is made.

It's the same thing that happens on a git pull when you have no local commits: your branch catches up to the remote by moving forward.

Watch it happen

Our sample repo has a release branch two commits behind main with nothing of its own: release sits at 96c4fc2, and main has two commits after that. Here's git merge main with release checked out:

  1. Before: release and HEAD on 96c4fc2, two commits behind main at 8c02d5b on the same history.
  2. Git fast-forwards release two commits to 8c02d5b. HEAD moves with it because it's attached to release.
  3. Git creates no commit. The history has the same commits as before.

Before and after

release and main now point at the same commit. The only thing that changed in .git is the hash inside .git/refs/heads/release. Git's output says so directly:

The raw git output, if you want to read along in text

git log --oneline --graph --all

before

* 1117a34 (feature) Add search tests
* e5869f0 Fix typo in search box
* fc19889 Add search box
| * 8c02d5b (main) Update dependencies
| * a0b2db3 Add user settings page
|/  
* 96c4fc2 (HEAD -> release) Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

after

* 1117a34 (feature) Add search tests
* e5869f0 Fix typo in search box
* fc19889 Add search box
| * 8c02d5b (HEAD -> release, main) Update dependencies
| * a0b2db3 Add user settings page
|/  
* 96c4fc2 Fix header layout
* ae65976 Add login page
* 3e1ffe4 Add project skeleton
* 4114b2c Initial commit

what git printed

Updating 96c4fc2..8c02d5b
Fast-forward
 requirements.txt | 1 +
 settings.html    | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 requirements.txt
 create mode 100644 settings.html

"Updating 96c4fc2..8c02d5b" followed by "Fast-forward" is Git telling you it moved the pointer and did nothing else.

Why some teams disable it

A fast-forward leaves no trace that a branch ever existed. If your team likes to see a merge commit for every feature, so git log --first-parent main reads as a list of features, they'll set merge.ff = false or use --no-ff when merging, which has its own page. GitHub's "Create a merge commit" button does the same thing.

The opposite preference exists too: git merge --ff-only refuses to merge unless it can fast-forward, which guarantees a straight-line history and is a common setting for pull (the git merge documentation describes both --ff-only and merge.ff).

For my own repositories I let fast-forwards happen. A one-commit fix doesn't need a merge commit wrapped around it. On a team repo I've come to prefer --no-ff for feature branches, mostly because reverting a whole feature is one command when it came in as a single merge commit.

Is it safe?

Safe git-sim pre-flight

Brings in 2 commit(s) from main (fast-forward).

The way back

  • A fast-forward only moves the branch: git reset --hard ORIG_HEAD puts it back on 96c4fc2

A fast-forward changes one ref. No commits are created or rewritten, and your working directory updates to the new tip the same way a checkout would.

How to undo it

Move the branch back:

git reset --hard 96c4fc2

or git reset --hard ORIG_HEAD, which Git sets to the pre-merge position for exactly this purpose.

Try it on your repository

pip install git-sim
git-sim merge main

git-sim tells you before the merge whether it will fast-forward or create a merge commit, and lists the commits your branch would gain.

Common questions

What does fast-forward mean in Git?

Your branch had no commits the other branch lacked, so Git moved your branch pointer to the other branch's tip instead of creating a merge commit.

Why did git merge not create a merge commit?

Because it fast-forwarded. Use git merge --no-ff if you want a merge commit regardless.

What is git merge --ff-only?

A merge that refuses to run unless it can fast-forward. It's a way to guarantee your branch's history stays linear, and a common choice for git pull.

How do I undo a fast-forward merge?

git reset --hard ORIG_HEAD moves the branch back to where it was before the merge.

Summary

In this article, we watched git merge fast-forward the release branch two commits without creating a merge commit, saw why Git could take that shortcut, and covered how to prevent or require it.

Next steps

git merge --no-ff shows the same merge with a merge commit forced. git merge covers the case where both sides have moved.