Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull
What fetch does — downloading remote state into the local cache
What you'll learn
- Explain the three things git fetch does — contact the remote, negotiate objects, update remote-tracking refs
- Distinguish the refs it writes (refs/remotes/<name>/*) from the refs it leaves alone (refs/heads/*)
- Predict the output of git fetch, git fetch origin, and git fetch --all in terms of which refs move
- Use git fetch --prune to remove remote-tracking refs whose remote counterparts have been deleted
- Recognise why a fetch is always safe to run, even on a dirty working tree
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
Every operation that touches a remote eventually has to ask the
remote what it has and bring the answer back. git fetch is the
primitive that does exactly that, and nothing more. It contacts
the remote, negotiates the minimum set of objects the local clone
is missing, downloads them, and writes the result into the
remote-tracking namespace under refs/remotes/<name>/. The local
branches under refs/heads/, the index, and the working tree are
not touched. This separation is what makes git fetch safe to
run at any time — even on a dirty tree, even mid-rebase, even
mid-merge — without any risk to local work.
The three steps of a fetch
A git fetch <remote> performs exactly three steps:
- Contact the remote at the URL configured for
<remote>and negotiate the protocol (smart HTTP, SSH, or the unauthenticated Git protocol on port 9418). The remote advertises every ref it currently exposes. - Negotiate which objects to transfer. The local clone tells
the remote which OIDs it already has (the
haves); the remote replies with the minimum set of objects the local clone is missing to materialise every advertised ref (thewants). The result is a packfile transferred over the wire. - Update the remote-tracking refs using the refspec
configured under
[remote "<name>"]. The default refspec is+refs/heads/*:refs/remotes/<name>/*, which means “every branch on the remote becomes a remote-tracking ref underrefs/remotes/<name>/”.
flowchart LR
A["git fetch origin"] --> B["contact origin\nURL from .git/config"]
B --> C["remote advertises refs\nrefs/heads/main, refs/heads/feature/x"]
C --> D["negotiate have/want\ncompute minimum packfile"]
D --> E["download packfile\nobjects + loose refs"]
E --> F["apply refspec\nrefs/heads/* -> refs/remotes/origin/*"]
F --> G["refs/remotes/origin/main\nadvances to new OID"]
G -.->|untouched| H["local refs/heads/main\nworking tree\nindex"]
The + at the front of the default refspec is important: it
makes the update unconditional, so non-fast-forward updates to
the remote-tracking ref are accepted. Without it, a force-push on
the remote would leave the local cache unable to advance.
What fetch writes and what it leaves alone
A fetch writes only into refs/remotes/<name>/. It does not
touch:
- Local branches under
refs/heads/. Yourmain, yourfeature/iam-rotation, yourrelease/v1.4.0— all unchanged. - The index (staging area). No file is staged, no file is unstaged.
- The working tree. No file is modified, no file is deleted.
# Default fetch: every configured remote
git fetch
# fetch: nothing to fetch? means up to date
# Fetch one specific remote
git fetch origin
# remote: Enumerating objects: 7, done.
# remote: Counting objects: 100% (7/7), done.
# From github.com:acme/infra
# 8a3f9d2..4d2c8e0 main -> origin/main
# * [new branch] feature/oidc -> origin/feature/oidc
# Fetch every remote the clone knows about
git fetch --all
# Fetching origin
# From github.com:acme/infra
# ...
# Fetching upstream
# From github.com:upstream-org/infra
# ...
The -> arrows in the output are the line to read. Each one
says “the remote ref X advanced (or was created) and the
remote-tracking ref Y was written to match”. The local branch
is never mentioned, because it was never touched.
# Confirm the local branch did not move
git rev-parse main
# 8a3f9d2... (the OID from before the fetch)
# Confirm the remote-tracking ref did move
git rev-parse origin/main
# 4d2c8e0... (the new OID from the fetch)
Pruning stale remote-tracking refs
By default, git fetch is additive: it creates and updates
remote-tracking refs, but it does not remove the ones whose
remote counterparts have been deleted. Over time, the local
cache accumulates phantom refs — names that no longer exist on
the remote but still appear under refs/remotes/<name>/. The
cleanup flag is --prune:
# Fetch and prune in one step
git fetch --prune origin
# From github.com:acme/infra
# - [deleted] (none)
# * [new branch] feature/oidc -> origin/feature/oidc
# - [deleted] feature/stale -> origin/feature/stale
# Or prune a remote without performing a full fetch
git remote prune origin
# Pruning origin
# URL: git@github.com:acme/infra.git
# - [deleted] feature/stale
Pruning is safe by construction. It only touches refs under
refs/remotes/<name>/. It never deletes a local branch, never
deletes a commit, and never deletes a packfile. A pruned ref is
a cache entry that no longer has a counterpart on the remote;
the commits it pointed at remain in the object database and can
still be reached by SHA until git gc runs.
Fetch as the substrate for everything else
The other remote commands are layered on top of git fetch:
git pullisgit fetchplus a merge (or rebase) of the fetched refs into the current branch.git merge origin/mainis preceded by a fetch (usually implicit, sometimes explicit) so the local clone has the commits the merge needs.git rebase origin/mainis preceded by the same.
Understanding fetch as the primitive is what makes the behaviour of those higher-level commands predictable: every ref they move is first written by a fetch, then handed to a merge or rebase engine. If you understand fetch, you understand the first half of every other remote command.
Production discipline
- Fetch before you read the cache. Every CI pipeline and
every interactive check that depends on remote state should
begin with
git fetch. A status check on a stale cache is a status check that does not reflect the remote. - Prune on a schedule. Stale remote-tracking refs are
harmless but accumulate forever. Either pass
--pruneto the fetch or setfetch.prune = trueso the cleanup is automatic. - Never assume the cache is current.
git branch -rshows the cache, not the remote. If a decision depends on remote state, fetch first. - Treat fetch as free. The marginal cost of an extra fetch is small; the marginal cost of acting on a stale cache is large (wrong merge base, wrong deploy target, wrong fast-forward expectation). When in doubt, fetch.
Cross-course references
- GitOps with Argo CD - Part III (SyncWaves) describes how
the Argo CD repo-server polls the remote with
git fetchto detect new commits; an Argo CD installation that does not fetch is an Argo CD installation that does not see new desired state. - Ansible for Production Sysadmins - Part XXXIX
(MirrorRefresh) uses
git fetch --prunein mirror clones to keep the local cache aligned with the upstream repository. - Terraform for Production Sysadmins - Part XVI
(ModuleVers) runs
git fetchin CI before resolving Terraform module references, so the cache is current at the moment the version pin is validated.
Quiz
Knowledge check · 4 questions
Q1. What does `git fetch origin` do to the local branches under refs/heads/?
Q2. Running `git fetch --prune origin` can delete local branches whose remote counterparts have been removed.
Q3. Name the three steps git fetch performs, and explain why each step is necessary.
Q4. An engineer says 'I ran fetch and now my local main is ahead of origin/main by 2 commits, even though I never pushed.' Diagnose what actually happened and recommend a fix.
An engineer returns from lunch. Before lunch they ran `git fetch origin` and saw `main -> origin/main` advance. After lunch they ran `git status` and it reported 'Your branch is ahead of origin/main by 2 commits'. They did not commit anything between the two reads; they only edited files in the working tree.
Passing score: 75%. Answers are checked in this browser.