Skip to main content
RunBook Academy

Git, CI/CD & GitOpsIX · MergingMerging

Fast-forward merges — when a merge is just a pointer move

Intermediate⏱ ~20 mingit

What you'll learn

  • Explain what fast-forward means in terms of the branch pointer and the commit DAG
  • Identify the topological condition that allows a fast-forward from the commit graph alone
  • Recognise the output of `git merge` when it has performed a fast-forward
  • Use `--ff-only` to refuse anything other than a fast-forward merge
  • Predict whether a merge will fast-forward before running it

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A fast-forward merge is the simplest case of git merge: the operation does not create a commit, does not compute a merge base, and does not run a merge strategy. It moves the current branch pointer forward to the tip of the branch being merged in. The mechanism is identical to the pointer motion described in Part V; the only thing that changed is the verb. Understanding when this optimisation is allowed — and what you lose when it happens — is what separates “I ran git merge” from “I understand what git merge did to my history”.

When fast-forward is possible

The rule is a topological condition on the commit graph: a merge from branch B into the current branch can be a fast-forward if and only if B’s tip is a descendant of the current branch’s tip. In reachability terms, the merge base of the two tips is the current branch’s tip, and there is nothing to combine that the current branch does not already contain.

git checkout main
git merge --ff-only feature/iam-rotation
# Fast-forwarding main to feature/iam-rotation.
# Updating 8a3f9d2..a1b2c3d
# terraform/main.tf | 2 +-
# 1 file changed, 1 insertion(+), 1 deletion(-)

Two visualisations of the same operation:

flowchart LR
    subgraph BEFORE_FF["before merge"]
        A1["6f4e5a6 merge base = main"] --> B1["8a3f9d2 main"]
        B1 --> C1["9f3c1d7 feature"]
    end
    subgraph AFTER_FF["after git merge (fast-forward)"]
        A2["6f4e5a6"] --> B2["8a3f9d2"] --> C2["9f3c1d7 main = feature"]
    end

In the “before” diagram, main is at 8a3f9d2 and feature is at 9f3c1d7. The merge base is 8a3f9d2 — it is main’s tip. After the merge, main points at the same commit as feature. No new commit appears. The branch pointer has simply moved.

The “after” diagram is the operational point: a fast-forward produces no new object in the object store, no merge commit, and no second parent. The audit trail of the merge is the absence of a merge event in the log.

Why fast-forward happens

Git’s default behaviour is to fast-forward whenever possible, then fall back to a true merge. The reasoning is that a fast-forward preserves a linear history: the commit graph contains no merge nodes for the operation, every commit has exactly one parent, and git log reads as a straight line from the new tip back to the root. For a team that treats main as a flat timeline of changes, fast-forward is the desirable outcome.

The flip side is that fast-forward erases the topology of the branch: there is no commit that records “this work happened on a branch called feature/iam-rotation”, because the merge did not happen — the commits simply walked onto main. A reader of the log sees the commits in order, but cannot tell from main’s history alone that they ever lived on a separate branch.

git log --oneline main
# a1b2c3d feat(iam): add role assumption policy
# 9f3c1d7 feat(iam): add terraform module
# 8a3f9d2 chore: bump provider versions
# 6f4e5a6 feat(network): initial vpc layout

This log looks identical whether the work was done on a feature branch and fast-forwarded, or done directly on main. The branch lifecycle is not in the graph; it is only in the reflog, in upstream tracking, and in the team’s pull-request records.

The --ff-only flag

git merge --ff-only <branch> refuses to do anything except fast-forward. If the topology would require a true merge, Git exits with a non-zero status and prints a message that begins Not possible to fast-forward, aborting.. The flag is a guard rail: it converts the silent optimisation into an explicit decision that fails loud.

The operational cases:

# Allowed: feature is a descendant of main, fast-forward proceeds
git merge --ff-only feature/iam-rotation
# Fast-forwarding main to feature/iam-rotation.

# Refused: feature has diverged, fast-forward would lose history
git merge --ff-only feature/iam-rotation
# fatal: Not possible to fast-forward, aborting.

--ff-only is the right flag for a release-branching workflow where the team wants main to be a linear superset of accepted features, but wants to refuse any merge that would create a non-linear history. It pairs naturally with a CI policy that runs git merge --ff-only --verify-signatures <branch> against the release candidate: if the branch has diverged, the release cannot proceed without an explicit reconciliation step.

The --ff flag is the default behaviour made explicit: prefer fast-forward, fall back to a true merge. The three flags --ff, --ff-only, and --no-ff form the fast-forward policy triangle; the difference between them is the fallback behaviour when fast-forward is not possible.

Production discipline

Three rules for fast-forward in a production-grade workflow:

  1. Treat fast-forward as the default; document when it is not. Linear history is the easier history to reason about. If the team needs merge commits, configure --no-ff for that branch (see IX-03), but do not disable fast-forward globally without a reason. The commit graph is read more often than it is written.
  2. Use --ff-only for release-branching. When a release branch is supposed to be a linear superset of accepted features, --ff-only turns a silent optimisation into an explicit guard. A refused fast-forward is a prompt to reconcile, not an error to ignore.
  3. Audit the reflog, not just the log, for fast-forwarded work. A fast-forwarded feature branch leaves no merge commit in main, but its reflog records the branch creation, the commits added, and the pointer motion. The reflog is the audit trail when the graph is not.

Cross-course references

  • Linux for Production Sysadmins - Parts XII (RepoSecurity) covers apt repository layout; the linear-history preference is shared with package-manager overlay chains.
  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers role and playbook branching; fast-forward versus true merge is the same trade-off at the Ansible repository level.
  • Terraform for Production Sysadmins - Parts IX-XII (State) cover Terraform state; a fast-forwarded feature branch must still leave the state file consistent with the new main tip.

Quiz

Knowledge check · 4 questions

  1. Q1. Which topological condition is necessary and sufficient for `git merge <branch>` to fast-forward by default?

  2. Q2. A fast-forward merge creates a new merge commit whose first parent is the current branch and second parent is the merged-in branch.

  3. Q3. Name the flag that makes `git merge` refuse to do anything except a fast-forward, and describe the exit behaviour when fast-forward is not possible.

  4. Q4. Decide which merge flag to use for a release-branching workflow and justify the choice.

    Your team maintains a `release/2026-q3` branch that is supposed to be a linear superset of features accepted for the Q3 release. The release manager runs `git merge --ff-only <feature>` to land each accepted feature. On Wednesday, a feature branch has been rebased against `release/2026-q3` and now fast-forwards cleanly. On Friday, a feature branch has not been rebased and has diverged; running `git merge --ff-only <feature>` prints 'Not possible to fast-forward, aborting.'

Passing score: 75%. Answers are checked in this browser.