Beginner 6 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 & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
June 21, 2026
last updated
1,663
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is Git Fetch vs Pull?

Git fetch and git pull are both commands for integrating remote changes, but they operate at fundamentally different levels of control. Fetch downloads all new commits, branches, and tags from the remote repository into your local .git directory without touching your working tree or current branch.

git fetch downloads changes from the remote but doesn't touch your working directory — it's reconnaissance.

Pull, on the other hand, is a convenience command that runs fetch immediately followed by a merge (or rebase) of the fetched changes into your current branch. The critical distinction: fetch gives you a read-only snapshot of what's happening upstream, while pull automatically modifies your local history — often creating merge commits you didn't intend.

The real-world cost of default pull behavior shows up on shared branches like main or develop. When you run git pull with the default merge strategy, Git creates a new merge commit every time your local branch has diverged from the remote — even if you only need to fast-forward.

Over a team of 10 developers making 5 pulls per day, that's 50 unnecessary merge commits polluting your history weekly. These commits make git bisect unreliable, break linear history expectations in CI/CD pipelines, and turn git log --graph into a tangled mess that obscures actual feature work.

Fetch solves this by letting you inspect upstream changes before committing to a merge or rebase. You can run git fetch origin, then git log origin/main --oneline -10 to see what's new, check for breaking changes, or verify that a hotfix was deployed — all without altering your local branch.

This diagnostic workflow is essential for debugging broken deployments: fetch the remote state, compare it to your last known good commit, and decide whether to rebase, merge, or reset without the risk of an automatic merge commit corrupting your investigation.

Configure your team's pull behavior with git config pull.rebase true to default to rebase instead of merge, or use git pull --ff-only to fail fast when a fast-forward isn't possible — forcing developers to explicitly choose between merge and rebase. This eliminates the 'merge commit trap' where developers unknowingly create merge commits because they never configured their pull strategy.

For shared branches, --ff-only is the safest default: it guarantees linear history and surfaces divergence immediately rather than hiding it behind an automatic 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.
Git Fetch vs Pull: Merge Commits on Shared Branches THECODEFORGE.IO Git Fetch vs Pull: Merge Commits on Shared Branches Visual guide to fetch, pull, merge, rebase, and team hygiene Git Fetch Downloads remote commits without merging Git Pull Fetch + merge (or rebase) into current branch Merge Commit Creates extra commit when pull merges diverged history Rebase Option Pull --rebase replays local commits on top Diagnose with Fetch Inspect remote state before merging or rebasing Automation Rule Never pull in scripts; use fetch + explicit merge ⚠ Default pull creates merge commits on shared branches Use git pull --rebase or configure pull.rebase=true THECODEFORGE.IO
thecodeforge.io
Git Fetch vs Pull: Merge Commits on Shared Branches
Git Fetch Vs Pull

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.

The Merge vs Rebase Trap: Which One Actually Runs With Your Pull?

Most devs think they’re choosing between fetch and pull. The real decision is what happens when git pull finishes downloading commits. By default, git pull runs a merge. That creates a noisy merge commit every time you sync — cluttering history with "Merge branch 'main' of origin" garbage.

Rebase, on the other hand, replays your local commits on top of the remote branch. Clean linear history. No pointless merge bubbles. Sounds better, right? It is — until you rewrite commits someone else depends on. Rebase rewrites SHA hashes. If you’ve already pushed your branch and a teammate pulled it, their history diverges. Now you’re both in a mess that ends with git reset --hard and a Slack apology.

So here’s the rule: Use git pull --rebase for branches you haven’t shared. Feature branches, personal forks, drafts. Use the default merge for shared branches like main or develop where hash changes cause chaos. Your team’s sanity depends on this distinction — not on whether you type fetch or pull.

PullStrategyDecision.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial

# Global config: always rebase on pull (personal)
git config --global pull.rebase true

# Per-repo config: only rebase for feature branches
git config pull.rebase merges  # preserves merge commits

# One-shot override for this pull
git pull --rebase origin main

# Recover from accidental shared-branch rebase:
git reflog
# Find the old commit hash, then:
git reset --hard <old-hash>
Output
Current branch is up to date.
Successfully rebased and updated refs/heads/feature/payment-gateway.
Production Trap:
Never set pull.rebase = true globally on a shared machine or CI runner. One rebased main will force every teammate to force-push their own branches to realign. You’ll own that outage.
Key Takeaway
The safety rule: rebase your own commits, merge everyone else’s.

How to Actually Use Fetch to Diagnose Broken Deployments

When prod is down, you don’t want to accidentally merge a half-baked commit into your working branch. That’s where git fetch becomes your scalpel — not a blunt instrument. Fetch downloads everything from the remote but touches nothing in your working directory. It’s the audit mode of Git.

Here’s a real scenario: Monday morning, the pipeline fails. You check the remote, see three new commits from the late-night deploy. Before you even think about pulling, run git fetch origin then git log origin/main --oneline -5. Now you see the exact commit messages, authors, and timestamps of what went up. You spot the culprit: “fix: hotfix payment timeout.” That’s your guy. With fetch, you can inspect that commit’s diff — git diff HEAD..origin/main — src/payment/ — and decide whether to merge, cherry-pick, or wait.

The move: Keep a dedicated terminal window with git fetch aliased to gf. Run it every 30 minutes during incident response. Never pull until you’ve seen the battlefield. When you finally do merge, you control the moment — not the repo.

IncidentFetchPattern.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — devops tutorial

# Step 1: Fetch without merging
git fetch origin

# Step 2: Inspect what’s new
git log origin/main --oneline -5
# Output:
# a1b2c3d hotfix: revert payment timeout
# e4f5g6h feat: add subscription tier (broken)
# h7i8j9k chore: bump docker image

# Step 3: Diff the suspicious commit
git diff HEAD..origin/main -- src/payment/

# Step 4: Cherry-pick only the safe hotfix
git cherry-pick a1b2c3d
Output
Successfully cherry-picked commit a1b2c3d.
[main 9x8y7z6] hotfix: revert payment timeout
Senior Shortcut:
Alias gf = git fetch --prune to also delete stale remote-tracking branches. Keeps your git branch -r output clean and prevents merging zombie branches.
Key Takeaway
Fetch first, inspect second, merge third — never reverse the order in a firefight.

Visual Guide: What Actually Happens Inside Your Repo

Stop thinking of Git as magic. It's a graph database living in your .git folder. Fetch updates your remote-tracking branches without touching your working tree. Pull runs fetch then merges your current branch with the remote counterpart. That second step is where everything breaks.

Draw this: origin/main is a pointer in .git/refs/remotes. When you fetch, that pointer moves. Your local main stays put. Your editor shows the old files. When you pull, that remote pointer moves AND your local branch gets updated. The merge commit appears. Conflicts happen. History reshapes.

The real insight: fetch gives you a read-only snapshot of the truth. Pull commits you to a new reality. One is interrogation. The other is surgery. Don't mix them up when you're diagnosing production latency.

git-visual-workflow.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — devops tutorial

# Before fetch:
#   HEAD -> main (local: "abc123")
#   origin/main (remote: "abc120")
#   Your working tree is at abc123

git fetch origin

# After fetch:
#   HEAD -> main (local: "abc123")
#   origin/main (remote: "def456")
#   Your working tree is still at abc123

git pull origin main

# After pull:
#   HEAD -> main (local: "def456", merged)
#   origin/main (remote: "def456")
#   Your working tree is now at def456
Output
HEAD is now at def456 (origin/main merged into local main)
Senior Shortcut:
Run git log --oneline --graph --all before and after fetch. See the actual DAG movement. Don't guess where refs point.
Key Takeaway
Fetch updates the remote truth. Pull rewrites your local reality. Know which you're doing in production.

Automation: Why You Should Never Pull in a Script

Every pipeline I've seen that auto-pulls in a cron job eventually breaks. Pull is a user command. It expects a terminal, conflict resolution, and context. Automation expects repeatable, idempotent behavior. Fetch is idempotent. Pull is not.

Your CI/CD should run fetch, check if the remote ref differs from your local ref, then decide based on that diff. If you blindly pull, you'll merge stale branches, create merge commits in a detached HEAD state, or pull in someone's half-baked feature branch because they rebased main.

The fix is dead simple: use fetch + git reset --hard origin/main for deployment branches where you control history. For shared branches, use fetch + git merge --ff-only origin/main. This either succeeds fast-forward or fails cleanly. No surprise merge commits. No automation crying at 3 AM.

deploy-script.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — devops tutorial

deploy:
  steps:
    - name: Check for remote updates
      run: |
        git fetch origin
        LOCAL=$(git rev-parse HEAD)
        REMOTE=$(git rev-parse origin/main)
        if [ "$LOCAL" = "$REMOTE" ]; then
          echo "No changes, skip deploy"
          exit 0
        fi
    - name: Safe fast-forward merge
      run: |
        git merge --ff-only origin/main
    - name: Restart service
      run: systemctl restart myapp
Output
No changes, skip deploy
Production Trap:
Never use git pull --rebase in automation unless you control the entire commit graph. Rebase in scripts creates duplicate commits on failure.
Key Takeaway
Fetch for automation. Pull for interactive work. Script the comparison, not the merge.

Team Hygiene: Why Your Git History Looks Like Spaghetti

Your team's history is a mess because someone pulled with merge, pushed, and everyone else pulled with merge. Now you have diamond merge commits, branch bubbles, and a log that looks like a Jackson Pollock. The root cause: no standard for pull behavior.

Set git config pull.ff only in your team's global config. This forces every pull to fail unless a fast-forward is possible. No merge commits. No rebase disasters. If someone tries to pull and gets a rejection, they have to fetch, manually rebase, or force push. That friction forces them to think.

Better yet: enforce this in branch protection rules. GitHub and GitLab can block merge commits on protected branches. Make your CI reject any branch that isn't linear. Your future self will thank you when tracing a bug through 12 merge commits becomes a 2-command grep.

team-pull-config.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial

# Enforce fast-forward only pulls for the whole team
git config --global pull.ff only

# Or locally per repo:
git config pull.rebase false
git config pull.ff only

# Verify:
git config --get pull.ff
# Output: only

git config --get pull.rebase
# Output: false
Output
only
Senior Shortcut:
Add pull.ff=only to your .gitconfig shared via dotfiles. New engineers inherit clean history. Block merge commits in branch rules as second layer.
Key Takeaway
Force fast-forward pulls. Spaghetti history is a symptom of config laziness, not complexity.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
June 21, 2026
last updated
1,663
articles · all by Naren
🔥

That's Git. Mark it forged?

6 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