Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull
What pull does — fetch plus merge (or fetch plus rebase)
What you'll learn
- Decompose git pull into git fetch plus git merge (the default) or git fetch plus git rebase
- Predict the output of git pull when the remote has fast-forwarded versus when it has diverged
- Explain why a fast-forward pull produces no merge commit, while a divergent pull always does (in merge mode)
- Identify the failure modes that pull hides — especially stale caches and uncommitted local changes
- Recognise when to use pull and when to substitute explicit fetch + merge or fetch + rebase
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 is the most-used remote command in interactive
workflows and the most-misunderstood. It feels atomic — one
command, one result — but it is actually two commands run in
sequence: git fetch followed by git merge (the default) or
git fetch followed by git rebase (if the local config says so).
The first step refreshes the remote-tracking refs; the second
step advances the local branch to include them. Understanding
that two distinct operations are happening is what makes the
behaviour of pull predictable.
Pull as fetch plus merge
The default mode of git pull runs git fetch and then runs
git merge FETCH_HEAD to integrate the fetched commits into
the current branch. The FETCH_HEAD ref is a small file Git
writes at fetch time recording which refs were just fetched
and what they pointed at; the merge reads it and resolves it to
the upstream of the current branch.
# The two steps pull actually runs
git pull origin main
# == git fetch origin
# == git merge origin/main (or FETCH_HEAD in some flows)
flowchart LR
A["git pull origin main"] --> B["git fetch origin"]
B --> C["update refs/remotes/origin/main"]
C --> D["git merge origin/main"]
D --> E{"local main diverged?"}
E -->|no, fast-forward| F["local main advances\nno merge commit"]
E -->|yes, divergent| G["three-way merge\nmerge commit produced"]
G --> H["working tree + index updated"]
F --> H
When the local branch has not moved since the last fetch, pulling is a fast-forward: the local branch tip simply advances to wherever the remote’s branch tip is, and no merge commit is produced. When the local branch has new commits the remote does not have, pulling is a three-way merge: Git finds the common ancestor, combines the two sets of changes, and (if there are no conflicts) writes a merge commit that has both sides as parents.
# Fast-forward case (no local commits ahead)
git pull
# Already up to date.
# or
# Updating 8a3f9d2..4d2c8e0
# Fast-forward
# modules/iam/main.tf | 12 ++++++------
# 1 file changed, 6 insertions(+), 6 deletions(-)
# Divergent case (local and remote have both moved)
git pull
# Merge made by the 'ort' strategy.
# infra/networking.tf | 4 ++++
# 1 file changed, 4 insertions(+)
The Merge made by the 'ort' strategy line is the signature of
the divergent case. It tells you a merge commit was created and
recorded in the local history; the engineer running the pull
should review the merge commit message to confirm what was
merged and from where.
What pull does not do
Three things git pull does not do, and which the convenience
hides from the engineer:
- It does not warn about a dirty working tree. If the
working tree has uncommitted changes,
git pullwill try to merge anyway; if the merge touches any of those files, Git will refuse to proceed (because the merge cannot overwrite unstaged changes). The engineer discovers this when the merge fails, not before. - It does not preview what will be merged. The engineer cannot see “these are the commits that will land” before the merge runs. They can only see them after the merge has either succeeded or failed.
- It does not record the rationale for the merge. A pull-generated merge commit has a default message (“Merge branch ‘origin/main’ into main”) that says nothing about why the merge was desired, only that it happened. A hand-crafted merge or rebase can carry a meaningful message.
Pull versus explicit fetch + merge
The reason to prefer the explicit sequence is that it gives the engineer a chance to inspect what will be merged before the merge runs:
# Explicit sequence: fetch, inspect, then merge
git fetch origin
# Look at what would be merged
git log --oneline main..origin/main
# Compare working tree to remote
git diff origin/main -- infra/networking.tf
# Decide and act
git merge origin/main # explicit merge
# or
git rebase origin/main # explicit rebase
The explicit sequence is what a team should standardise on for
shared infrastructure branches and for any merge that touches
production code paths. The convenience of git pull is
appropriate on local feature branches where the consequence of
a wrong merge is “delete the branch and start over” rather than
“investigate why a merge commit is in main”.
Pull with —autostash
One refinement that helps when the working tree is dirty:
git pull --autostash. The flag stashes any uncommitted
changes, runs the pull, then pops the stash back. It is a
convenience for the common case of “I have a small edit in
progress but I want to pull before continuing”:
# Pull even with uncommitted changes
git pull --autostash
# Created autostash: 7a8b9c0
# From github.com:acme/infra
# 4d2c8e0..9e1f2a3 main -> origin/main
# Updating 4d2c8e0..9e1f2a3
# Fast-forward
# Applied autostash.
The default behaviour (without --autostash) is to refuse the
pull if there are conflicting uncommitted changes. The flag
turns that refusal into a transparent stash/unstash cycle, but
it does not eliminate the underlying risk: if the merge commit
touches a file the stash contains, the stash pop will produce
conflicts that the engineer has to resolve after the pull.
Production discipline
- Pull on local branches; fetch-then-merge on shared
branches. The default
git pullis fine on feature branches where the worst case is a delete-and-restart. Shared branches deserve the explicit sequence. - Run
git statusbeforegit pull. If the tree is dirty, a pull can fail mid-merge and leave the tree in a half-merged state. Clean first, then pull. - Use
--autostashdeliberately. It is a convenience, not a magic wand. If the merge touches files the stash holds, the conflicts surface after the pull, not before. - Review the merge commit message. A pull-generated merge
commit has a default message that says nothing. For a
shared branch, replace the message with a meaningful one or
use
--no-ffdeliberately and document the merge in the PR. - Do not script
git pullblindly. CI should usegit fetch(or a checkout to a specific SHA), not pull, because pull can produce a merge commit when CI expects a clean fast-forward.
Cross-course references
- GitOps with Argo CD - Part IV (SyncPolicies) describes
how an Argo CD controller never runs
git pull; it explicitly fetches and reconciles the desired state from the fetched refs. The distinction matters because pull can produce merge commits that the controller cannot attribute cleanly. - Ansible for Production Sysadmins - Part XXXIX
(MirrorRefresh) prefers explicit
git fetchovergit pullin mirror scripts, because the mirror’s job is to refresh the cache, not to advance a local branch. - Terraform for Production Sysadmins - Part XVI
(ModuleVers) uses
git pull --ff-onlyin CI to refuse any pull that would require a merge commit, so the local CI clone always tracks the remote as a pure fast-forward.
Quiz
Knowledge check · 4 questions
Q1. In its default mode, what two operations does `git pull` perform?
Q2. `git pull` always produces a merge commit, even when the local branch has not diverged from the remote.
Q3. Explain the three things `git pull` does NOT do, and why each omission matters for a production workflow.
Q4. An engineer runs `git pull` on the team's main branch and a merge commit lands that no one was expecting. Diagnose and recommend a fix.
A team shares a `main` branch. An engineer runs `git pull` from their terminal. The pull succeeds; a merge commit with the default message 'Merge branch origin/main into main' lands in the shared history. CI triggers on the new tip and the team's Slack channel lights up asking 'who pulled and why?'. The engineer did not intend to coordinate; they intended to refresh their local copy.
Passing score: 75%. Answers are checked in this browser.