Git, CI/CD & GitOpsIX · MergingMerging
Fast-forward merges — when a merge is just a pointer move
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
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:
- 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-fffor 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. - Use
--ff-onlyfor release-branching. When a release branch is supposed to be a linear superset of accepted features,--ff-onlyturns a silent optimisation into an explicit guard. A refused fast-forward is a prompt to reconcile, not an error to ignore. - 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
maintip.
Quiz
Knowledge check · 4 questions
Q1. Which topological condition is necessary and sufficient for `git merge <branch>` to fast-forward by default?
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.
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.
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.