Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Fast-forward: when merge only moves a pointer

Module 2: Branching and merging · 10 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • predict from the graph when a merge will be a fast-forward understand
  • merge a finished branch into main with git merge while you are on main apply
  • choose between a fast-forward and --no-ff, and explain the difference in the graph evaluate
  • delete a fully merged branch with git branch -d apply

Understand: The pointer slides forward

You have worked on feature and made two commits. main has not received anything new in the meantime. It still points to A, where feature started.

Follow the arrows back from C: first B, then A. The commit of main is an ancestor of feature. Everything on main is already in feature.

Git does not need to make anything new. git merge feature just slides main forward to C. That is called a fast-forward. No new commit, and the history is still a straight line.

Now both names point to the same commit. git branch -d feature deletes the name, but not the commits. They are part of the history of main. If the work is not merged, -d refuses and tells you.

Some teams would rather see in the history that a feature was merged. We go back to the starting point: main at A and feature at C.

git merge --no-ff feature now makes a merge commit M with two parents, A and C, even though a fast-forward was possible. main moves to M. It is a choice the team makes, not a requirement.

Watch: A fast-forward in real Git

We are on feature, which has two new commits. main is still at the first one. The graph is a straight line: main sits right behind feature.

$ git log --oneline --graph --all

A merge brings work into the branch you are on. We want main to receive the work, so we switch there first.

$ git switch main

Now we merge feature. The terminal says Fast-forward, and in the graph main slides up to the same commit as feature.

$ git merge feature

feature has done its job. We delete the name. Watch the graph: the label disappears, but the commits stay, because main reaches them.

$ git branch -d feature

Tasks

  1. Show the graph with all branches.
  2. Go to the branch that should receive the work.
  3. Bring the work from feature into main without making a new commit.
  4. Delete the branch that is now fully merged.

Practise: Transfer: choose the shape of the history

You are on docs. Two branches have one commit each after main: docs is finished, spike is an unfinished experiment.

Tasks

  1. Your team wants the history of main to show where each finished feature was merged in. Bring docs into main following that rule.
  2. Tidy up: delete the branches that are fully merged into main, and leave the rest.

Check yourself: Recall it

The history is a straight line A, B, C, D. main points to B and feature to D. You are on main and run git merge feature. How many new commits does Git make?

  • None
  • One merge commit
  • As many as feature has, as copies

You want main to receive the work from feature. Where must you be when you run git merge?

  • On feature
  • On main
  • It does not matter

What disappears when you run git branch -d feature after a fast-forward?

  • The commits that were made on feature
  • Only the name feature
  • The files that were added on feature

The team wants every feature to show as one merge commit on main. After git merge docs the history is a straight line. What is the right assessment?

  • The rule is followed, because the work from docs is now on main
  • The rule is broken: it was a fast-forward. git merge --no-ff docs would have made the merge commit
  • The merge failed, so docs must be merged again

Review: Before you merge, git status --short shows an M in the second column for README.md. What does that mean?

  • README.md is modified and ready in the staging area
  • README.md is modified in the working tree, but the change is not in the staging area
  • README.md is already merged

Common misconceptions

  • “A merge always makes a new commit.” When main has not moved, the pointer just slides forward.
  • “It does not matter which branch I am on when I merge.” A merge only ever changes the branch HEAD is on.
  • “git branch -d deletes the work.” It deletes only the name. The commits stay in the history.

Read more

When is a merge a fast-forward?

git merge feature only ever changes the branch you are on. Git first asks one question: is the commit of the current branch an ancestor of feature? If yes, everything on main is already in feature, there is nothing to combine, and Git just moves the branch forward. You can ask the same question yourself:

$ git merge-base --is-ancestor main feature && echo "fast-forward possible"
fast-forward possible

A fast-forward prints the old and new position and a summary of what arrived in your working tree:

$ git merge feature
Updating df6c7db..2379bbb
Fast-forward
 login.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 login.txt

No commit was made, and no commit was copied. main now names the same commit as feature.

If it is the other way round and feature is already contained in main, Git says Already up to date. and does nothing.

Three ways to control the shape

Option Behaviour
--ff (default) fast-forward when possible, otherwise a merge commit
--ff-only fast-forward or nothing: refuses when the lines have diverged
--no-ff a merge commit even when a fast-forward was possible

--ff-only: update, never combine

Use it when you only expect to catch up, for example when updating your main from the team's main. If both lines have new commits, Git stops without changing anything and exits with status 128:

$ git merge --ff-only feature
hint: Diverging branches can't be fast-forwarded, you need to either:
...
fatal: Not possible to fast-forward, aborting.

That is not an error: a real integration choice is needed, a three-way merge (next lesson) or a rebase. To make this the default, run git config merge.ff only.

--no-ff: keep the feature visible

$ git merge --no-ff feature
Merge made by the 'ort' strategy.
 login.txt | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 login.txt
$ git log --oneline --graph
*   4f3483e Merge branch 'feature'
|\
| * 2379bbb Style login form
| * 27a6985 Add login form
|/
* df6c7db Initial snapshot

Benefits: the commits of one feature stay grouped, git log --first-parent shows each merged feature as a single merge commit, and the whole feature can later be undone with a single git revert -m 1 of the merge commit. Costs: more commits and a busier graph. It is a team decision, not a right or wrong.

Squash merges, briefly

git merge --squash feature puts all changes from feature into the staging area, and you make one ordinary commit with git commit. It has only one parent, so Git does not know feature was merged, and git branch -d feature later refuses. Check with git diff main feature that nothing is missing, then delete with -D.

Before you merge

  • Be on the receiving branch. git branch --show-current or git status tells you. A merge on the wrong branch moves the wrong name.
  • Start clean. If uncommitted changes would be overwritten, Git refuses and changes nothing:
error: Your local changes to the following files would be overwritten by merge:
    README.md
Please commit your changes or stash them before you merge.
Aborting
  • Know what is coming. git log --oneline main..feature lists the commits the merge will bring in.

If you merged into the wrong branch, Git has saved the previous position in ORIG_HEAD. The reset lesson shows how to move back to it safely.

Cleaning up merged branches

$ git branch --merged
  feature
* main
$ git branch -d feature
Deleted branch feature (was 2379bbb).

git branch --merged lists branches whose commits are all contained in the current branch, so -d can delete them safely. git branch --no-merged lists the ones that still hold unique work, and on those -d refuses:

error: the branch 'spike' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D spike'

git log main..spike lists the work only spike has. Use -D only when you know it is not needed. Deleting a name never deletes commits that another branch still reaches.

Official documentation