Git, CI/CD & GitOpsXXI · Fetch vs PullFetchVsPull
git pull --rebase versus git pull --merge — linear history versus merge commits
What you'll learn
- Decompose git pull --rebase into git fetch followed by git rebase
- Decompose git pull --no-rebase (the default) into git fetch followed by git merge
- Predict the resulting local history in each mode when the local branch has diverged from the remote
- Identify the trade-off between a clean linear history (rebase) and an explicit merge node (merge)
- Choose between the two modes for local feature branches, shared branches, and CI clones
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 has exactly two modes: it either merges the fetched
upstream into the local branch (the default) or rebases the
local branch onto the fetched upstream. The choice between the
two is one of the most consequential defaults a team can set,
because it determines the shape of every local branch’s
history. A team that pulls with rebase sees linear history; a
team that pulls with merge sees merge nodes at every
integration point. Both are valid; neither is universally
correct.
Pull —rebase: linear history
When git pull --rebase runs, it performs git fetch and
then git rebase FETCH_HEAD (or the upstream of the current
branch). The local commits are replayed on top of the fetched
upstream, one by one, producing a linear history where the
local branch tip is the last replayed commit and the fetched
upstream is its sole parent.
flowchart LR
subgraph Before["Before pull --rebase"]
A1["origin/main"] --> B1["upstream commit A"]
A1 --> C1["upstream commit B"]
D1["local main"] --> E1["local commit 1"]
D1 --> F1["local commit 2"]
G1["common ancestor"] --> A1
G1 --> D1
end
subgraph After["After pull --rebase"]
A2["origin/main"] --> B2["upstream A"]
A2 --> C2["upstream B"]
D2["local main (rewritten)"] --> E2["local 1 (new SHA)"]
D2 --> F2["local 2 (new SHA)"]
E2 --> C2
end
The replayed commits get new SHAs because their parents changed; the commit objects’ content (author, message, diff) is preserved, but their position in the DAG is different. If the upstream contains changes that conflict with a local commit, the rebase pauses at that commit and lets the engineer resolve the conflict before continuing.
# Rebase-mode pull
git pull --rebase origin main
# From github.com:acme/infra
# 4d2c8e0..9e1f2a3 main -> origin/main
# Rebasing (2/2)
# Successfully rebased and updated refs/heads/main.
# The result: linear history
git log --oneline --graph main
# * 9f1c2a3 local commit 2 (rebased)
# * 7d8e9f0 local commit 1 (rebased)
# * 9e1f2a3 upstream commit B
# * 4d2c8e0 upstream commit A
The local commits now sit on top of the upstream commits with
no merge commit in between. git log --graph shows a straight
line, which is what most readers of the history find easier to
follow than a tree with merge nodes.
Pull —merge (the default): merge commits
When git pull runs without --rebase, it performs git fetch and then git merge FETCH_HEAD (or the upstream). The
local commits stay where they are; the fetched upstream is
integrated via a three-way merge that produces a merge commit
with both the previous local tip and the fetched upstream tip
as parents.
# Merge-mode pull (the default)
git pull origin main
# From github.com:acme/infra
# 4d2c8e0..9e1f2a3 main -> origin/main
# Merge made by the 'ort' strategy.
# infra/networking.tf | 4 ++++
# 1 file changed, 4 insertions(+)
# The result: non-linear history with a merge node
git log --oneline --graph main
# * 8a3f9d2 Merge branch 'origin/main' into main
# |\
# | * 9e1f2a3 upstream commit B
# | * 4d2c8e0 upstream commit A
# * | 7d8e9f0 local commit 2
# * | 3c4d5e6 local commit 1
The merge commit (8a3f9d2) records that two lines of
development were integrated at this point. Engineers reading
the history later can see exactly which commits came from
upstream and which were local, and they can git revert -m 1
the merge commit to undo the integration if needed. The cost
is the merge node itself, which adds noise to git log --graph
output.
When to use which mode
The trade-off is not abstract; it has concrete operational consequences:
- Use
--rebaseon local branches that have not been pushed. The local commits get rewritten, but no one else has the old SHAs, so no one notices. The benefit is a clean linear history that is easy to read and easy to fast-forward when the branch is eventually merged. - Use
--rebaseon CI clones. CI clones never push (typically), so the SHA rewrite is harmless and the linear history makes log parsing easier. - Use
--merge(the default) on shared branches. When multiple engineers pull from the same branch and each has local commits ahead of the upstream, a merge-mode pull preserves everyone’s SHAs and produces a merge commit that documents the integration. Rebasing in this case would rewrite SHAs that other engineers’ local clones might have, producing confusing “your branch and origin/main have diverged” messages. - Use
--mergewhen the local commit has been pushed. A rebase of a pushed commit changes its SHA; the next push becomes a force-push. On a shared branch, a force-push is a coordination event that should never happen by accident.
# Equivalent sequences, side by side
git pull --rebase origin main
# == git fetch origin
# == git rebase origin/main
git pull origin main # default
# == git fetch origin
# == git merge origin/main
# Override the global pull.rebase for one pull only
git pull --no-rebase origin main
Pull —rebase and the cost of a clean history
Teams that value a strictly linear history (no merge commits)
often configure pull.rebase = true globally so that every
pull on every branch defaults to rebase mode. The benefit is
that the shared branch history is always linear; the cost is
that every engineer must remember not to pull with rebase on
shared branches they have already pushed to. For an
infrastructure team with a single main branch and many
short-lived feature branches, rebase-mode pulls are
appropriate. For a team with long-lived shared branches where
multiple engineers commit concurrently, merge-mode pulls are
safer.
Production discipline
- Pull with rebase on local branches; pull with merge on shared branches. This is the simplest rule and the one that most teams converge on.
- Never rebase commits that have been pushed and pulled by someone else. A rebase rewrites SHAs; rewriting SHAs that other engineers depend on is a coordination event.
- Configure the default at the repo level, not the global
level.
pull.rebase = trueat the global level means every pull on every repository behaves the same way; a repo-level setting lets shared branches opt out without requiring every engineer to remember--no-rebase. - Treat merge-mode pull as the safer default. A merge
commit can always be reverted with
-m 1; a rebase can only be reverted by identifying the original SHAs in the reflog. When in doubt, merge.
Cross-course references
- Git, CI/CD & GitOps - Part XXII (RebaseDeeper) covers rebase in detail; the pull —rebase command is a thin wrapper over the same machinery.
- GitOps with Argo CD - Part IV (SyncPolicies) describes how an Argo CD repo-server prefers merge-mode reconciliation so the GitOps history is auditable per sync wave; rebase-mode reconciliation would obscure which commits were applied when.
- Ansible for Production Sysadmins - Part XL (BranchPolicy) recommends rebase-mode pulls on local feature branches but merge-mode pulls on the team’s shared integration branch.
Quiz
Knowledge check · 4 questions
Q1. What does `git pull --rebase origin main` do that `git pull origin main` (the default) does not?
Q2. Running `git pull --rebase` on a branch whose commits have already been pushed to a shared remote is not always safe because rebase is a local operation.
Q3. Explain the trade-off between rebase-mode and merge-mode pulls, and identify one scenario where each is the right choice.
Q4. A team has configured `pull.rebase = true` globally. An engineer on a shared release branch runs `git pull` and accidentally force-pushes rebased commits. Diagnose and recommend a fix.
A team uses `release/v2.0` as a long-lived shared branch for stabilisation work before a release. An engineer working on the branch has pulled (with rebase, because of the global config) several times and pushed their rebased commits. Another engineer pulled the same branch and has based work on the old SHAs. The first engineer's latest push is a force-push; the second engineer's `git status` now says 'your branch and origin/release-v2.0 have diverged' even though they have not committed anything new.
Passing score: 75%. Answers are checked in this browser.