Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull
git pull --ff-only — refusing a pull that would require a merge
What you'll learn
- Explain what git pull --ff-only refuses to do, and why the refusal is the safety property
- Distinguish a fast-forward pull from a divergent pull in terms of the resulting local history
- Predict the failure message when --ff-only is used on a branch that has diverged
- Configure pull.ff = only at the repo or branch level so every pull defaults to ff-only
- Identify the production scenarios — shared branches, CI clones, GitOps controllers — where ff-only is the right default
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
git pull --ff-only is the strictest of the three pull modes
(default, --rebase, and --ff-only). It succeeds only when
the local branch tip is already an ancestor of the fetched
upstream tip — that is, only when the pull can be a pure
fast-forward with no merge commit and no rebase. Any other
state causes the pull to refuse and exit non-zero. The refusal
is the safety property: it turns a silent divergence into a
loud failure that the engineer (or the CI pipeline) cannot
ignore.
What ff-only allows and what it refuses
git pull --ff-only runs git fetch and then attempts a
fast-forward merge of the fetched upstream into the current
branch. The fast-forward succeeds when the local branch has
not moved since the last fetch (or has only moved to commits
the upstream already has). It refuses when:
- The local branch has commits the upstream does not have (divergence — a true merge or rebase would be required).
- The local branch tip is not an ancestor of the fetched upstream tip (which is the same condition expressed more formally).
# Fast-forward case: ff-only succeeds
git pull --ff-only origin main
# From github.com:acme/infra
# 4d2c8e0..9e1f2a3 main -> origin/main
# Updating 4d2c8e0..9e1f2a3
# Fast-forward
# modules/iam/main.tf | 12 ++++++------
# 1 file changed, 6 insertions(+), 6 deletions(-)
The output is the same as a successful fast-forward pull
without --ff-only. The interesting case is the failure:
# Divergent case: ff-only refuses
git pull --ff-only origin main
# From github.com:acme/infra
# 4d2c8e0..9e1f2a3 main -> origin/main
# fatal: Not possible to fast-forward, aborting.
The exit code is non-zero. The local branch has not moved; the fetched refs have been updated, but the merge did not happen. The engineer is forced to make an explicit decision: rebase the local branch onto the fetched upstream, merge the fetched upstream into the local branch, or investigate why the divergence happened before doing anything.
flowchart LR
A["git pull --ff-only origin main"] --> B["git fetch origin"]
B --> C["refs/remotes/origin/main advances"]
C --> D{"local tip ancestor\nof origin/main?"}
D -->|yes| E["fast-forward merge\nlocal tip advances"]
D -->|no| F["fatal: Not possible to fast-forward, aborting"]
E --> G["working tree updated\nno merge commit"]
F --> H["exit non-zero\nlocal branch unchanged"]
The fatal: Not possible to fast-forward, aborting line is
the safety net firing. It is not a bug; it is the design.
Why ff-only is the right default for shared branches
On a shared infrastructure branch (the team’s main, a
release stabilisation branch, a long-running production
branch), a merge commit produced by git pull is noise: it
documents an integration that the team did not author and did
not review. A pull that cannot be a pure fast-forward is a
pull that the engineer should not run at all — the divergence
needs to be resolved by a deliberate merge or rebase, with a
meaningful commit message, and ideally via a reviewed pull
request.
git pull --ff-only enforces this by refusing anything that
is not a clean fast-forward. The engineer is forced to stop
and think before producing a merge commit. The cost is
occasional friction when the local branch has fallen behind;
the benefit is that the shared branch history is always a
clean fast-forward lineage of the upstream.
# CI: refuse any pull that would require a merge
git fetch origin
git pull --ff-only origin main
# exit 0 if local is a fast-forward of origin/main
# exit non-zero otherwise — fail the CI job
# Or, equivalently, without --ff-only:
git pull origin main
# then assert the tip is origin/main
git rev-parse main
git rev-parse origin/main
# if they differ, a merge commit was produced — fail the job
The CI version that asserts after the fact works but is
fragile: a merge commit can be produced and then the job can
move on. The git pull --ff-only version is cleaner because
the failure surfaces inside the pull itself, with a clear
error message, before any merge commit lands.
Configuring ff-only as the default
The behaviour can be made the default at the repo level
(pull.ff = only) or the global level (pull.ff = only).
The configuration accepts three values:
true(default) — allow fast-forward merges; fall back to a merge commit if fast-forward is not possible.false— always produce a merge commit, even when a fast-forward is possible.only— refuse anything that is not a fast-forward.
# Set ff-only at the repo level (this clone only)
git config pull.ff only
# Set ff-only at the global level (every clone on this machine)
git config --global pull.ff only
# Verify
git config --get pull.ff
# only
# Override for one pull
git pull --no-ff origin main # force a merge commit
git pull --rebase origin main # force a rebase
A repo-level setting is preferable to a global one because it
encodes the policy in the repository’s .git/config and
travels with the clone. A new engineer cloning the repository
gets the policy for free; a global setting requires every
engineer to configure their own machine the same way.
When ff-only is the wrong default
There are scenarios where ff-only is too strict:
- Local feature branches with WIP commits. A feature branch that has unpushed local commits cannot fast-forward from origin/main by definition. ff-only on every pull would refuse every sync with the upstream. Merge or rebase is the right default here.
- Repositories with a true long-running branch. A team
that maintains
mainandproductionas parallel branches and pulls between them regularly needs merge-mode pulls, not ff-only. The long-running branch is structurally divergent from the upstream. - Workflows that integrate many sources. A monorepo with many feature branches landing into a single integration branch daily needs merge-mode pulls to integrate them. The integration branch is not a fast-forward of any single source.
For these scenarios, --rebase or the default merge mode is
appropriate. ff-only is right when the local branch is meant
to be a pure mirror of a single upstream.
Production discipline
- Use
--ff-onlyon CI clones and GitOps controllers. A CI job that pulls the main branch should refuse anything that is not a pure fast-forward; the failure surfaces before any deploy step runs. - Set
pull.ff = onlyat the repo level, not globally. A repo-level setting encodes the policy in the repository’s.git/configand travels with the clone. A global setting requires every engineer to remember to set it. - Treat a refused ff-only as an investigation, not a retry.
The failure is information: the local branch has diverged
from the upstream, or the local branch tip is wrong, or the
remote is not what the engineer thinks it is. Retrying with
--no-ffor--rebaseto make the pull “go through” hides the information. - Pair ff-only with a deterministic checkout. A CI job that pulls and then checks out a specific SHA is more deterministic than a CI job that pulls and runs whatever tip the upstream currently advertises. Pin the SHA when reproducibility matters.
Cross-course references
- Terraform for Production Sysadmins - Part XVI
(ModuleVers) uses
git pull --ff-onlyin CI to ensure the local Terraform module cache always tracks the upstream as a pure fast-forward; a merge commit in the local cache is a signal that something is wrong upstream. - GitOps with Argo CD - Part V (DriftDetection) describes how Argo CD’s repo-server uses a ff-only-equivalent policy internally: it refuses to advance the local clone to a commit that is not a fast-forward of the fetched upstream, surfacing the divergence as a sync error.
- Ansible for Production Sysadmins - Part XXXIX
(MirrorRefresh) recommends
git pull --ff-onlyin mirror scripts so a mirror never diverges from the upstream by accident; the mirror’s job is to reflect, not to integrate.
Quiz
Knowledge check · 4 questions
Q1. What happens when `git pull --ff-only origin main` is run on a local branch that has diverged from origin/main?
Q2. Setting `pull.ff = only` in `.git/config` means every `git pull` in that clone will refuse to do anything other than a pure fast-forward.
Q3. Explain why `git pull --ff-only` is the right default for a CI clone and the wrong default for a local feature branch with WIP commits.
Q4. A CI job that runs `git pull --ff-only origin main` starts failing with `Not possible to fast-forward` after a teammate force-pushes the shared branch. Diagnose and recommend a fix.
A CI pipeline for an infrastructure repo has been green for weeks. The pipeline runs `git fetch origin && git pull --ff-only origin main` and then deploys. This morning the pipeline fails with `fatal: Not possible to fast-forward, aborting`. The on-call engineer checks the team's main branch and sees the latest commit SHA is different from what the CI was previously tracking. A teammate admits they force-pushed earlier today.
Passing score: 75%. Answers are checked in this browser.