Beginner 3 min · March 30, 2026

Git Fetch vs Pull — 47 Merge Commits on Shared Branches

47 merge commits in 3 weeks from git pull on shared branches? Git fetch avoids them.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • git fetch = reconnaissance only — inspect before integrating
  • git pull = fetch + merge in one step — convenient but skips inspection
  • git pull --rebase = fetch + rebase — linear history, no merge commit
Plain-English First

git fetch downloads changes from the remote but doesn't touch your working directory — it's reconnaissance. git pull does the same download and then immediately merges or rebases them into your current branch. Fetch first, merge when ready. Pull is fetch + merge in one step. Knowing this distinction lets you inspect what's changed before applying it.

git fetch and git pull both download changes from a remote repository, but they differ in what happens next. Fetch stops after the download — your local branches and working directory are untouched. Pull continues by merging or rebasing the downloaded changes into your current branch immediately.

The distinction matters in production. Pulling on a shared branch creates unnecessary merge commits. Pulling without inspecting first means you merge blind — you do not see what is coming until it conflicts. Fetch gives you the inspection window. Pull removes it.

Common misconceptions: that fetch modifies your working directory (it does not), that pull is always safe (it can create merge commits and conflicts), and that origin/main updates automatically after fetch (your local main branch stays put — only the remote-tracking ref moves).

What Each Command Actually Does

Understanding what goes where in your local repo clears up the confusion immediately.

git fetch origin downloads objects and refs from the remote and updates your remote-tracking refs (origin/main, origin/feature/x). Your local branches and working directory are completely untouched. You can now inspect what changed, compare branches, or decide your integration strategy.

git pull runs git fetch then immediately runs git merge (or git rebase with --rebase) to integrate the fetched changes into your current branch. Convenient, but removes the inspection step.

The thing that trips people up: after git fetch, your local main branch hasn't moved. origin/main has. They're now two different things pointing at potentially different commits.

io/thecodeforge/git/FetchVsPull.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# io.thecodeforge — Fetch vs Pull

# ─────────────────────────────────────────────────────────────
# FETCH: download changes, update remote-tracking refs, touch NOTHING local
# ─────────────────────────────────────────────────────────────
git fetch origin
# Now origin/main is updated but your local main branch hasn't moved

# Inspect what changed before integrating
git log main..origin/main --oneline
# Shows commits on origin/main that aren't in your local main yet

git diff main origin/main
# Shows the actual file diff between your local and remote

# If you're happy with what you see, integrate manually
git merge origin/main     # or:
git rebase origin/main

# ─────────────────────────────────────────────────────────────
# PULL: fetch + merge in one step (less control)
# ─────────────────────────────────────────────────────────────
git pull origin main
# Equivalent to:
# git fetch origin main
# git merge origin/main

# PULL with rebase (cleaner history than merge)
git pull --rebase origin main
# Equivalent to:
# git fetch origin main
# git rebase origin/main

# ─────────────────────────────────────────────────────────────
# CONFIGURE pull to always rebase (recommended for feature branches)
# ─────────────────────────────────────────────────────────────
git config --global pull.rebase true
git config --global pull.rebase merges  # preserve merge commits when rebasing

# Verify configuration
git config --global pull.rebase
# Output: true
Output
# git fetch output:
From github.com:io/thecodeforge/payments-service.git
a3f9c2e..d4f8b3c main -> origin/main
# git log main..origin/main --oneline:
d4f8b3c feat(auth): Add JWT token refresh
9c3e8a2 fix(payment): Handle null payment reference
Fetch Moves origin/main. Pull Moves main.
  • git fetch: downloads objects, updates origin/main — your local main stays put
  • git pull: fetches then merges/rebases — your local main moves to match origin/main
  • After fetch: git log main..origin/main shows what you are missing
  • After pull: your local branch is synchronized — no gap to inspect
Production Insight
The fetch-then-inspect workflow prevents surprise merges. In teams with high commit velocity, pulling without fetching first means you merge blind. A developer runs git pull, gets a merge conflict, and does not know which commit caused it because they never inspected what was incoming. The safer workflow: git fetch origin, git log main..origin/main to see what is coming, review the commits, then git merge origin/main explicitly. This takes 30 seconds more and prevents the 'my code was working, I pulled, now it is broken' debugging sessions.
Key Takeaway
git fetch downloads and updates remote-tracking refs only — your local branches and working directory are untouched. git pull fetches then immediately merges or rebases. The distinction is the inspection window: fetch gives it to you, pull removes it.

When to Use Fetch Over Pull

Fetch is the safer professional habit for a few specific scenarios:

Before merging a PR locally: fetch first, inspect the diff against your branch, verify there are no conflicts, then merge with confidence.

On shared branches: before pushing to main or develop, always git fetch origin main first to check if the remote has moved. If it has, rebase or merge before pushing.

In CI/CD scripts: pipelines should git fetch and compare refs programmatically rather than blindly pulling, which might trigger merge commits that pollute the history being built.

After a long coding session: fetch before you push to understand what changed while you were working. git log HEAD..origin/main tells you exactly what to expect before you integrate.

io/thecodeforge/git/FetchWorkflow.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# io.thecodeforge — Fetch-First Workflow

# ─────────────────────────────────────────────────────────────
# PROFESSIONAL WORKFLOW: fetch first, then decide
# ─────────────────────────────────────────────────────────────
git fetch origin

# What's changed on main since I branched?
git log --oneline origin/main ^HEAD

# How many commits am I behind?
git rev-list HEAD..origin/main --count

# Am I ahead of remote? (commits I haven't pushed yet)
git rev-list origin/main..HEAD --count

# Full status view
git log --oneline --decorate --graph origin/main HEAD -10

# Only now integrate
git merge origin/main    # creates merge commit
# OR
git rebase origin/main   # linear history, no merge commit

# ─────────────────────────────────────────────────────────────
# CI/CD PIPELINE PATTERN: fetch and compare refs
# ─────────────────────────────────────────────────────────────
git fetch origin

# Get the latest commit on remote main
REMOTE_HEAD=$(git rev-parse origin/main)
LOCAL_HEAD=$(git rev-parse HEAD)

if [ "$REMOTE_HEAD" != "$LOCAL_HEAD" ]; then
  echo "Local is behind remote. Fetching and rebuilding."
  git checkout main
  git reset --hard origin/main
else
  echo "Local is up to date. No rebuild needed."
fi

# ─────────────────────────────────────────────────────────────
# POST-LONG-SESSION: check what changed while you were working
# ─────────────────────────────────────────────────────────────
git fetch origin

# Commits on remote that you don't have
git log HEAD..origin/main --oneline

# Files that changed on remote
git diff --name-only HEAD origin/main

# Decide: merge (creates merge commit) or rebase (linear history)
# For feature branches: rebase
# For shared branches: merge
Output
# git rev-list HEAD..origin/main --count:
3
# You are 3 commits behind origin/main
# git rev-list origin/main..HEAD --count:
2
# You have 2 local commits not yet on remote
Fetch Is Free. Pull Costs a Decision.
  • Fetch before every push to check if the remote has moved
  • Fetch before merging a PR to inspect the diff for conflicts
  • Fetch in CI/CD scripts to compare refs programmatically — never pull blindly
  • Pull only after you have fetched and inspected with git log
Production Insight
CI/CD pipelines that run git pull instead of git fetch create a specific failure mode: the pipeline merges remote changes into the build branch, creating a merge commit that is not on any developer's machine. This merge commit gets pushed as part of the pipeline, polluting the history with CI-generated merge commits. The correct pattern: git fetch origin, compare refs with git rev-parse, then git reset --hard origin/main to align without creating merge commits. This keeps the CI history clean and the build reproducible.
Key Takeaway
Fetch before every push, before merging PRs, and in CI/CD scripts. Pull only after you have inspected what changed. The five seconds of inspection with git log main..origin/main save five minutes of merge conflict archaeology.

Configuring Pull Behavior for Your Team

The default git pull behavior (fetch + merge) creates merge commits when your local branch has diverged from the remote. For feature branches and most development workflows, rebase is preferred because it produces linear history.

git config --global pull.rebase true makes every git pull rebase by default. This is the single most impactful git configuration change for team history cleanliness. Once set, git pull behaves like git fetch + git rebase instead of git fetch + git merge.

The trade-off: rebasing changes commit hashes. If you have local commits that have not been pushed, rebasing rewrites them with new hashes. This is fine for local-only commits but problematic if you have already shared those commits with others (they will see conflicts when they pull your rebased commits).

For teams that use merge commits intentionally (to mark integration points), pull.rebase merges preserves merge commits while rebasing regular commits. This is the middle ground.

io/thecodeforge/git/PullConfig.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# io.thecodeforge — Pull Configuration

# ─────────────────────────────────────────────────────────────
# RECOMMENDED: rebase by default on all pulls
# ─────────────────────────────────────────────────────────────
git config --global pull.rebase true
# Every git pull now does: git fetch + git rebase
# Result: linear history, no unnecessary merge commits

# ─────────────────────────────────────────────────────────────
# MIDDLE GROUND: rebase but preserve merge commits
# ─────────────────────────────────────────────────────────────
git config --global pull.rebase merges
# Rebases regular commits but keeps merge commits intact
# Useful when your workflow intentionally uses merge commits as markers

# ─────────────────────────────────────────────────────────────
# VERIFY CURRENT CONFIGURATION
# ─────────────────────────────────────────────────────────────
git config --global pull.rebase
# Output: true (or merges, or empty for default merge behavior)

# ─────────────────────────────────────────────────────────────
# PER-REPOSITORY OVERRIDE
# ─────────────────────────────────────────────────────────────
# Some repos may need merge behavior even if global is rebase
cd /path/to/specific-repo
git config pull.rebase false
# Overrides global setting for this repository only

# ─────────────────────────────────────────────────────────────
# TEAM ONBOARDING: add to setup script
# ─────────────────────────────────────────────────────────────
cat > io/thecodeforge/git/team-setup.sh << 'EOF'
#!/bin/bash
# io.thecodeforge — Team Git Setup

# Recommended git config for the team
git config --global pull.rebase true
git config --global fetch.prune true
git config --global rerere.enabled true
git config --global core.autocrlf input

echo "Git configured for team workflow."
EOF
chmod +x io/thecodeforge/git/team-setup.sh
Output
# After setting pull.rebase true:
git pull origin main
# Now runs: git fetch origin main + git rebase origin/main
# No merge commit created. Linear history preserved.
# After setting pull.rebase merges:
git pull origin main
# Rebases regular commits, preserves intentional merge commits
pull.rebase Is a Team Decision, Not an Individual One
  • pull.rebase true: linear history, no merge commits on pull
  • pull.rebase merges: rebase regular commits, keep intentional merge commits
  • Set in onboarding scripts so every developer has the same behavior
  • Per-repo override: git config pull.rebase false for repos that need merge behavior
Production Insight
The pull.rebase configuration is the single most impactful git setting for team history cleanliness. A team of 10 developers each pulling once daily on a shared branch creates 10 merge commits per day with default pull behavior — 50 per week, 200+ per month. Setting pull.rebase true eliminates all of them. The cost: developers must understand that rebasing changes commit hashes, which means they should not rebase commits that have already been pushed and shared with others. For local-only commits (the typical case during development), rebase is always safe.
Key Takeaway
Set git config --global pull.rebase true to eliminate unnecessary merge commits on pull. For teams that use intentional merge commits, use pull.rebase merges instead. Add to onboarding scripts so every developer has consistent behavior. This is the single most impactful git configuration change for history cleanliness.
● Production incidentPOST-MORTEMseverity: high

git pull on Shared Feature Branch: 47 Merge Commits in 3 Weeks

Symptom
The feature branch had 47 merge commits interspersed with 30 actual feature commits. git log --oneline showed a tangled graph of merge nodes. git bisect could not isolate bugs because merge commits changed the bisect path. Code review was painful — PRs showed merge commit diffs alongside actual code changes.
Assumption
The team assumed git pull was the correct way to stay in sync. Nobody questioned it because every tutorial recommends pull. They did not know that pull creates merge commits by default when the local branch has diverged from the remote.
Root cause
1. Six developers committed to the same feature branch daily. 2. Each developer ran git pull origin feature/payment-v2 every morning. 3. Because each developer had local commits that were not on the remote, git pull created a merge commit every time. 4. With 6 developers pulling once per day for 3 weeks: 6 x 1 x 21 = 126 potential merge commits. Some pulls were clean (no local commits yet), resulting in 47 actual merge commits. 5. The merge commits added no value — they were mechanical synchronization artifacts. 6. Nobody noticed until a git bisect session failed because the bisect landed on a merge commit that did not represent a logical code change.
Fix
1. Immediate: squash-merged the feature branch into a clean branch: git checkout -b feature/payment-v2-clean main && git merge --squash feature/payment-v2 && git commit. 2. Team-wide: configured git config --global pull.rebase true on every developer's machine. 3. Added to onboarding documentation: 'Always use git pull --rebase on shared branches.' 4. Added a pre-push hook that warns if the current branch has more than 2 merge commits: git log --merges --oneline origin/main..HEAD | wc -l. 5. Considered using short-lived feature branches with one developer per branch instead of shared long-lived branches.
Key lesson
  • git pull creates merge commits when your local branch has diverged from the remote. On shared branches with multiple committers, this multiplies rapidly.
  • git config --global pull.rebase true eliminates unnecessary merge commits by rebasing instead of merging during pull.
  • Shared feature branches with multiple developers are an anti-pattern when used long-term. Short-lived branches with one developer per branch avoid the problem entirely.
  • git bisect is useless on branches with merge commit pollution. Clean history is a debugging tool, not just aesthetics.
Production debug guideSystematic recovery paths for merge conflicts, diverged branches, and history pollution.5 entries
Symptom · 01
git pull creates unexpected merge commits on every pull
Fix
1. Your local branch has commits not on the remote. Each pull merges remote changes into your local, creating a merge commit. 2. Check: git log --oneline --merges origin/main..HEAD — shows merge commits on your branch. 3. Fix: configure git config --global pull.rebase true to rebase instead of merge. 4. Clean up existing merge commits: git rebase -i HEAD~N to squash them.
Symptom · 02
git push rejected — 'non-fast-forward' after running git pull
Fix
1. The remote moved between your pull and push. Another developer pushed while you were working. 2. Fetch: git fetch origin to see what changed. 3. Inspect: git log origin/main..HEAD --oneline to see your commits. 4. Rebase: git rebase origin/main to put your commits on top of the latest remote. 5. Push: git push origin — now it is a fast-forward.
Symptom · 03
git pull --rebase causes conflicts during rebase
Fix
1. Your local commits conflict with changes that were pushed to the remote while you were working. 2. Git stops at each conflicting commit. Resolve conflicts in the files, then git add <file> && git rebase --continue. 3. If the conflict is too complex: git rebase --abort to return to pre-rebase state. 4. Alternative: git pull (merge) instead of rebase — merge conflicts are sometimes easier to resolve because you see both sides at once.
Symptom · 04
After git fetch, git log shows no changes but git push still rejected
Fix
1. The remote branch you fetched is not the branch you are pushing to. Check: git remote -v and git branch -vv. 2. You may be tracking a different branch than you think. 3. Fix: git branch -u origin/main main to set the upstream tracking branch. 4. Verify: git branch -vv now shows [origin/main] next to your branch.
Symptom · 05
git pull on a detached HEAD state
Fix
1. You are not on a branch (checked out a tag or commit hash directly). 2. git pull requires a branch to merge into. 3. Create a branch from your current position: git checkout -b temp-branch. 4. Then pull: git pull origin main. 5. Or switch to the intended branch: git checkout main && git pull.
★ Git Fetch vs Pull Triage Cheat SheetFast recovery for merge conflicts, diverged branches, and pull-related issues.
git pull creates merge commits on every pull
Immediate action
Switch to rebase-based pull to eliminate merge commit noise.
Commands
git log --oneline --merges origin/main..HEAD (count merge commits)
git config --global pull.rebase true (set rebase as default)
Fix now
Existing merge commits: git rebase -i HEAD~N to squash them into clean history.
git push rejected with 'non-fast-forward'+
Immediate action
Fetch, rebase your commits on top of remote, then push.
Commands
git fetch origin (download latest remote state)
git rebase origin/main (put your commits on top of remote)
Fix now
After rebase: git push origin — now a fast-forward, no rejection.
git pull --rebase stops with conflicts at each commit+
Immediate action
Resolve conflicts or abort if too complex.
Commands
git status (see conflicted files)
git add <file> && git rebase --continue (resolve and continue)
Fix now
If too complex: git rebase --abort to return to pre-rebase state.
After fetch, git push still rejected — remote moved again+
Immediate action
Another developer pushed between your fetch and push.
Commands
git fetch origin (fetch again to get latest)
git rebase origin/main (rebase on top of latest remote)
Fix now
Push immediately after rebase — minimize the window between fetch and push.
git pull fails on detached HEAD+
Immediate action
You are not on a branch. Create one or switch to the intended branch.
Commands
git branch (check if you are on a branch or detached)
git checkout -b temp-branch (create branch from current position)
Fix now
Then pull normally: git pull origin main.
Fetch vs Pull vs Pull Rebase
OperationDownloads from remote?Updates remote-tracking refs?Modifies local branch?Modifies working dir?
git fetchYesYesNoNo
git pull (merge)YesYesYes (merge commit)Yes
git pull --rebaseYesYesYes (rebases)Yes

Key takeaways

1
git fetch downloads changes and updates remote-tracking refs (origin/main) without touching your local branches or working directory.
2
git pull is git fetch + git merge (or git rebase with --rebase). It's convenient but skips the inspection step.
3
Fetch first, inspect with git log HEAD..origin/main, then decide to merge or rebase. This is the habit that keeps histories clean.
4
git config --global pull.rebase true makes git pull rebase by default, resulting in cleaner linear history on feature branches.
5
Never run git pull in CI/CD pipelines
use git fetch and compare refs programmatically to avoid polluting history with CI-generated merge commits.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is git fetch safe?
02
Should I use git pull or git fetch?
03
What does git pull --rebase do differently?
04
Can I undo a git pull that went wrong?
05
Should I use git pull in CI/CD pipelines?
🔥

That's Git. Mark it forged?

3 min read · try the examples if you haven't

Previous
Git Reset: Hard, Soft and Mixed Explained
18 / 19 · Git
Next
Git Amend: Edit the Last Commit