Beginner 11 min · March 28, 2026

Git Pull — Half-Finished Code Deployed from Staged Changes

Production revenue dropped 40% in 30 mins when git pull silently merged staged changes.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • git fetch: downloads remote commits, updates origin/main — your files stay untouched
  • git merge: integrates fetched commits into your current branch — this is where conflicts happen
  • git pull --rebase: replays your local commits on top of remote — linear history, no merge commit
  • git pull --ff-only: only fast-forward, fail if branches have diverged
  • git pull --autostash: auto-stash dirty files before pull, pop after
Plain-English First

Picture your team is editing a shared Google Doc, but everyone works offline on their own printed copy. Git pull is the moment you pick up the latest printout from the office printer and paste the new paragraphs into your own copy. If someone else changed the same sentence you changed, you've got a conflict to sort out before your copy makes sense. That's it — no magic, just syncing two versions of the same thing.

The critical nuance most people miss: git pull is actually two separate steps — first you go to the printer and grab the pages (git fetch), then you sit down and merge them into your copy (git merge). You can do step one without step two. You can look at the new pages before deciding how to integrate them. The disasters happen when people skip the looking part and just staple everything together blind.

git pull synchronizes your local branch with the remote by running git fetch then git merge in sequence. It is the most frequently used git command on shared repositories and the most common source of merge conflicts and lost work.

The fetch half is always safe — it downloads commits without touching your working directory. The merge half is where risk lives: it modifies your files, can create conflicts, and silently folds staged changes into merge commits. Understanding this split is the foundation of safe pulling.

Common misconceptions: that pull is atomic (it is two operations), that pull always fails (it sometimes succeeds silently), and that pull --rebase is always better (it rewrites commit hashes, which breaks shared branches).

What Git Pull Actually Does (It's Two Commands Wearing a Trenchcoat)

Before you can use git pull safely, you need to understand that it isn't one atomic operation — it's two separate commands stapled together. Every single time you run git pull, Git runs git fetch first, then git merge. That's the whole secret. Once you see it that way, its behaviour stops feeling mysterious.

git fetch goes to the remote server (usually called 'origin') and downloads any commits, branches, or tags that your local repo doesn't have yet. Critically, it does NOT touch your working files. It stashes the new data in a hidden reference called origin/main (or origin/whatever-your-branch-is) and leaves your actual code completely alone. You can run git fetch all day without risking a single line of your work.

git merge then takes that downloaded reference and integrates it into your current branch. This is the step that actually changes your files. If your work and your teammate's work touched different files, Git handles it automatically and you'll never even notice. If you both touched the same lines, Git stops and hands you a conflict to resolve manually. Understanding that fetch is always safe and merge is where the risk lives is the mental model that prevents 90% of beginner mistakes with this command.

io/thecodeforge/git/UnderstandingGitPull.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
# io.thecodeforge — Understanding Git Pull

# ─────────────────────────────────────────────────────────────
# SCENARIO: You're on the 'main' branch of a team project.
# A colleague just pushed a bug fix to the remote repo.
# You need to get their changes before starting your next task.
# ─────────────────────────────────────────────────────────────

# Step 1: Check your current status before touching anything.
# Always do this. Know what state you're in before you pull.
git status
# Expected: 'nothing to commit, working tree clean'
# If you see modified files here — STOP. Stash or commit first.

# Step 2: See what branch you're on and where it sits vs. remote.
git log --oneline --decorate -5
# Shows the last 5 commits with branch pointers.
# Look for: HEAD -> main (your local) vs. origin/main (remote).
# If origin/main is ahead, you're behind — pull is needed.

# Step 3: Run fetch first to SEE what's coming, safely.
# This downloads remote changes but touches NOTHING in your working tree.
git fetch origin

# Step 4: Check what just arrived from the remote without merging yet.
# This shows commits on origin/main that aren't on your local main.
git log main..origin/main --oneline
# Output shows each incoming commit on its own line.
# If this is empty, you're already up to date.

# Step 5: Now merge the fetched changes into your local branch.
# This is the step that actually updates your files.
git merge origin/main

# ─── OR ────────────────────────────────────────────────────────
# Do both steps at once with git pull (only when working tree is clean).
git pull origin main
# Equivalent to: git fetch origin && git merge origin/main
# The 'origin' is the remote name. 'main' is the branch. Both are explicit here.
# Relying on defaults is fine locally but be explicit in scripts.
Output
# After git fetch origin:
# From https://github.com/acme-corp/checkout-service
# a3f92c1..d7e841b main -> origin/main
# After git log main..origin/main --oneline:
# d7e841b Fix null pointer in PaymentProcessor when card token expires
# c2a190f Add retry logic to StripeClient on 429 response
# After git merge origin/main (or git pull origin main):
# Updating a3f92c1..d7e841b
# Fast-forward
# src/payments/PaymentProcessor.java | 12 ++++++------
# src/stripe/StripeClient.java | 8 ++++++++
# 2 files changed, 14 insertions(+), 6 deletions(-)
# Meaning: Git walked straight forward — no conflict, clean merge.
Fetch Is Safe. Merge Is Where Risk Lives.
  • git fetch: downloads objects, updates origin/main — your files stay untouched
  • git merge: integrates fetched commits into your current branch — this modifies files
  • git pull = git fetch + git merge — two operations, not one
  • Inspect before merging: git log main..origin/main shows what is incoming
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 5 seconds more and prevents the 'my code was working, I pulled, now it is broken' debugging sessions.
Key Takeaway
git pull is two commands: git fetch (safe — downloads only) then git merge (risky — modifies files). Understanding this split is the foundation of safe pulling. Always check git status before pulling. Inspect incoming changes with git log main..origin/main before merging.

Running Git Pull Safely: The Three States Your Repo Can Be In

git pull behaves completely differently depending on the state of your working tree when you run it. There aren't two states — there are three, and each one needs a different response. Treating them all the same is exactly how people lose work.

State one: clean working tree. Nothing modified, nothing staged. This is the only state where git pull is safe to run without thinking. Git will fetch and merge without hesitation, and you'll get the remote changes cleanly.

State two: uncommitted changes that don't conflict with incoming changes. Git will actually let you pull here and it'll succeed — but this is a trap. You've now mixed your in-progress work with a merge commit in your history. It looks fine until you realise you can't bisect the history cleanly later, and your in-progress changes are now invisible in the merge commit's noise. Stash your work first, every single time.

State three: uncommitted changes that DO conflict with incoming changes. Git refuses to merge and throws 'error: Your local changes to the following files would be overwritten by merge'. This is actually the safe failure — Git is protecting you. Your fix is git stash, then git pull, then git stash pop, then resolve conflicts if any remain. Never force your way through this with git checkout -- file unless you genuinely want to throw that work away.

io/thecodeforge/git/SafePullWorkflow.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
# io.thecodeforge — Safe Pull Workflow

# ─────────────────────────────────────────────────────────────
# SCENARIO: Mid-morning on the checkout-service team.
# You've been editing OrderValidator.java for 45 minutes.
# Your team lead just Slacked: "pushed the tax fix, pull when ready."
# Your working tree is NOT clean. Here's the safe workflow.
# ─────────────────────────────────────────────────────────────

# Step 1: Always check status first. Non-negotiable.
git status
# You see: 'modified: src/orders/OrderValidator.java'
# Do NOT pull yet.

# Step 2: Stash your in-progress changes.
# git stash is a stack — it saves your current diff and reverts the file.
# The message makes it identifiable when you have multiple stashes.
git stash push -m "WIP: adding discount threshold logic to OrderValidator"
# Output: Saved working directory and index state On main: WIP: adding...

# Step 3: Confirm working tree is now clean before pulling.
git status
# Output: 'nothing to commit, working tree clean' — safe to pull.

# Step 4: Pull the remote changes cleanly.
git pull origin main
# Output: Updating 9b1c233..e4f9021 — fast-forward or merge commit.

# Step 5: Re-apply your stashed work on top of the fresh code.
git stash pop
# Output: On branch main — your changes are restored on top of the new base.

# Step 6: Check if your work and the pulled changes conflict.
git status
# If OrderValidator.java shows 'both modified', you have a conflict to fix.
# If it shows 'modified' with no conflict markers — you're clean.
Output
# git stash push -m "WIP: adding discount threshold..."
# Saved working directory and index state On main: WIP: adding discount threshold logic to OrderValidator
# git pull origin main
# From https://github.com/acme-corp/checkout-service
# 9b1c233..e4f9021 main -> origin/main
# Updating 9b1c233..e4f9021
# Fast-forward
# src/orders/TaxCalculator.java | 6 +++---
# 1 file changed, 3 insertions(+), 3 deletions(-)
# git stash pop
# On branch main
# Changes not staged for commit:
# (use "git add <file>..." to update what will be committed)
# modified: src/orders/OrderValidator.java
# Dropped refs/stash@{0}
The Silent Overwrite Trap
  • State 1 (clean): pull is safe — no thinking required
  • State 2 (dirty, no conflict): pull succeeds but silently folds staged changes into merge commit — this is the trap
  • State 3 (dirty, conflict): pull fails — Git protects you. Stash, pull, pop, resolve.
  • Always run git status before git pull. If not clean, stash or commit first.
Production Insight
The silent overwrite (State 2) is the most dangerous pull failure mode because it does not look like a failure. The pull succeeds. The merge commit is created. The staged changes are folded in. The CI pipeline runs. The half-finished code deploys. Nobody notices until production breaks. The fix is a pre-pull discipline: always run git status, always stash if dirty. Enforce this with a team norm, not a hook — hooks can be bypassed, but habits persist.
Key Takeaway
Three states: clean (safe to pull), dirty-no-conflict (silent trap — staged changes folded into merge commit), dirty-conflict (safe failure — Git protects you). Always run git status before pulling. If dirty, stash first. The silent overwrite in State 2 is the most dangerous failure mode.

git pull --rebase: The Flag That Keeps Your History Clean

Here's something the basic tutorials skip: the default merge strategy for git pull creates a merge commit every single time your history has diverged. On a busy team, this turns your git log into a spaghetti graph of endless 'Merge branch main into main' commits. After three months it's unreadable, bisecting is a nightmare, and code review is a chore.

The --rebase flag changes git pull's second step from a merge to a rebase. Instead of creating a new merge commit, Git replays your local commits on top of the freshly fetched remote commits. The result is a clean, linear history that reads like everyone worked in perfect sequence — even when they didn't. This is what the Git maintainers and most senior engineers actually use day-to-day.

The trade-off is worth knowing: rebasing rewrites your local commit SHAs. That's fine as long as you haven't pushed those commits anywhere. If you're rebasing commits that already exist on the remote — on a shared branch — stop. That causes the classic 'force push required' disaster where you rewrite history everyone else is building on. The rule is simple: rebase local-only commits freely, never rebase shared commits.

io/thecodeforge/git/PullRebaseVsMerge.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
56
57
# io.thecodeforge — Pull Rebase vs Merge

# ─────────────────────────────────────────────────────────────
# SCENARIO: You're working on the checkout-service feature branch.
# You've made 2 local commits (not pushed yet).
# The remote main has 3 new commits since you branched.
# Goal: get remote changes without polluting history with merge commits.
# ─────────────────────────────────────────────────────────────

# ── WITHOUT --rebase (default merge behaviour) ──────────────────
# Your history before pull:
# A - B - C (origin/main)
#          \
#           D - E (your local commits, not pushed yet)
#
# After: git pull origin main
# A - B - C --------- M (merge commit — clutters history)
#          \         /
#           D - E --/

# ── WITH --rebase ───────────────────────────────────────────────
# Your history before pull:
# A - B - C (origin/main)
#          \
#           D - E (your local commits)
#
# After: git pull --rebase origin main
# A - B - C - D' - E' (linear — D and E replayed on top of C)
# D' and E' are new SHAs but same changes — history reads cleanly.

# ─────────────────────────────────────────────────────────────
# Running it:
# ─────────────────────────────────────────────────────────────

# Check current state before pulling
git log --oneline -5
# Shows your 2 local commits on top

# Pull with rebase — safe because your commits are not yet on remote
git pull --rebase origin main

# If a rebase conflict occurs during the replay of your commits,
# Git pauses and shows you the conflict.
# Fix the file, stage it, then continue the rebase:
git add src/payments/CheckoutOrchestrator.java
git rebase --continue
# Do NOT run git commit here — rebase --continue does the commit for you.

# If the conflict is too messy and you want to abort:
git rebase --abort
# This resets everything as if you never ran the rebase.

# ─────────────────────────────────────────────────────────────
# Make rebase the default for ALL future pulls (recommended):
# ─────────────────────────────────────────────────────────────
git config --global pull.rebase true
# Now every 'git pull' behaves like 'git pull --rebase' automatically.
Output
# git pull --rebase origin main
# From https://github.com/acme-corp/checkout-service
# c2a190f..d7e841b main -> origin/main
# Successfully rebased and updated refs/heads/feature/discount.
# git log --oneline -5
# e8d1b20 (HEAD -> feature/discount) Add logging to discount calculation
# f3a2c91 Implement discount threshold validation
# d7e841b (origin/main) Fix null pointer in PaymentProcessor
# c2a190f Add retry logic to StripeClient
# a3f92c1 Initial commit
Rebase Local Commits. Merge Shared Commits.
  • --rebase replays your commits on top of remote — linear history, no merge commit
  • Rebasing changes commit SHAs — fine for local-only, dangerous for shared commits
  • Set pull.rebase = true globally for cleaner history on feature branches
  • Never rebase commits that have been pushed and shared with teammates
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
--rebase replays your local commits on top of remote — linear history, no merge commit. Safe for local-only commits. Dangerous for shared commits (rewrites SHAs, causes force-push). Set pull.rebase = true globally. Enforce 'never rebase after push' as a team policy.

git pull --autostash: The One Command That Changes Everything

The manual stash-pull-pop workflow works, but it's three commands when you want one. git pull --autostash collapses it into a single command: it stashes dirty files, pulls, and pops the stash automatically. If the pop causes a conflict, Git stops and tells you to resolve it manually, then run git stash drop to clean up.

The best part: you can make --autostash the default for all rebase-based pulls with git config --global rebase.autoStash true. After this setting, git pull --rebase automatically includes --autostash. No more 'cannot pull with dirty working tree' errors. No more forgetting to stash before pulling. No more losing your place when you get interrupted.

I've configured this on every machine I've touched for the last four years. It's one of those settings that seems minor until you try to go back, and then you realise how much friction it removed. The only downside: if the autostash pop conflicts, you need to know how to resolve it. But you already know how to resolve conflicts from the merge section — the same rules apply.

io/thecodeforge/git/PullAutostash.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
# io/thecodeforge — Pull Autostash

# ─────────────────────────────────────────────────────────────
# SCENARIO: You have uncommitted changes across 3 files.
# Your teammate pushes an urgent hotfix to main.
# You need to pull before you forget, but you don't want to
# interrupt your flow with manual stash commands.
# ─────────────────────────────────────────────────────────────

# Without --autostash: three commands
git stash push -m "WIP: cart service refactor"
git pull --rebase origin main
git stash pop

# With --autostash: one command
git pull --rebase --autostash origin main
# Git: 'Created autostash: refs/autostash'
# Git: pulls and rebases
# Git: 'Applied autostash'
# Your dirty files are back exactly where you left them.

# ─────────────────────────────────────────────────────────────
# CONFIGURE autostash as default (do this once, globally)
# ─────────────────────────────────────────────────────────────
git config --global rebase.autoStash true
# Now git pull --rebase automatically includes --autostash.
# You never need to type --autostash again.

# Verify
git config --global rebase.autoStash
# Output: true
Output
# git pull --rebase --autostash origin main
# Created autostash: refs/autostash
# From https://github.com/acme-corp/checkout-service
# a7b9c04..f1e2d38 main -> origin/main
# Successfully rebased and updated refs/heads/main.
# Applied autostash.
# git status (after autostash completes)
# On branch main
# Changes not staged for commit:
# modified: src/cart/CartService.java
# modified: src/cart/CartValidator.java
# modified: src/payments/CheckoutOrchestrator.java
# Your 3 files are back, on top of the freshly pulled code.
Set rebase.autoStash = true on Every Machine
  • --autostash: auto-stash dirty files before pull, pop after — one command
  • rebase.autoStash = true: makes --autostash the default for all rebase pulls
  • If autostash pop conflicts: resolve manually, then git stash drop
  • Add to onboarding script so every developer has it from day one
Production Insight
The rebase.autoStash setting eliminates the most common 'cannot pull with dirty working tree' error. Without it, developers hit the error, google the fix, stash manually, pull, pop — a 30-second interruption that breaks flow. With it, the pull just works. The setting costs nothing and saves minutes per day per developer. It should be in every team's onboarding script alongside pull.rebase = true and fetch.prune = true.
Key Takeaway
--autostash auto-stashes dirty files before pulling and pops them after. Set rebase.autoStash = true globally to make this the default. This eliminates the manual stash-pull-pop workflow and the 'cannot pull with dirty working tree' error.

Fast-Forward vs Three-Way Merge: What Actually Happens Inside

When you run git pull (or git merge), Git has two strategies for integrating changes: fast-forward and three-way merge. Understanding which one Git picks — and why — is essential for predicting what your history will look like after a pull.

Fast-forward — Your local branch has no new commits since it last synced with the remote. The remote branch is simply ahead of you. Git can just move your branch pointer forward to match the remote — no new commit is created, no merge commit, just a pointer update. This is the cleanest possible outcome. When you see 'Fast-forward' in the output, nothing controversial happened — Git walked straight forward.

Three-way merge — Both your local branch AND the remote branch have new commits since they last shared a common ancestor. Git can't just move a pointer — it has to combine two divergent histories. It finds the common ancestor, diffs both branches against it, and creates a new 'merge commit' that has two parents. This merge commit is what clutters your history. It's also what surfaces conflicts — if both branches touched the same lines, the three-way merge is where Git stops and asks you to choose.

The --ff-only flag forces Git to ONLY do fast-forward merges. If a fast-forward isn't possible (your branch has diverged), it fails with 'fatal: Not possible to fast-forward, aborting.' This is a safety net: if you expected a clean fast-forward and something weird happened, --ff-only tells you immediately instead of creating an unexpected merge commit.

The --no-ff flag does the opposite: it forces a merge commit even when a fast-forward is possible. Useful on feature branches where you want the merge commit to mark the boundary of the feature in history.

io/thecodeforge/git/FastForwardVsMerge.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
# io.thecodeforge — Fast-Forward vs Three-Way Merge

# ─────────────────────────────────────────────────────────────
# FAST-FORWARD: Your branch has no local commits.
# Remote is simply ahead. Git moves your pointer forward.
# ─────────────────────────────────────────────────────────────

# Before: your main is at commit B, remote main is at commit E
#   A - B (your main, HEAD)
#        \
#         C - D - E (origin/main)

git pull origin main
# Output: Updating b2a3c1f..e4f9021
#         Fast-forward
# After: your main pointer moved to E. No merge commit created.

# ─────────────────────────────────────────────────────────────
# THREE-WAY MERGE: Both branches have new commits.
# Git creates a merge commit with two parents.
# ─────────────────────────────────────────────────────────────

# Before: you committed F and G locally. Remote added C, D, E.
#   A - B - C - D - E (origin/main)
#          \
#           F - G (your local main, HEAD)

git pull origin main
# Output: Merge made by the 'ort' strategy.
# After: merge commit M created with two parents (E and G).

# ─────────────────────────────────────────────────────────────
# --ff-only: Force fast-forward, fail if diverged.
# Use this in CI scripts or automated deploys.
# ─────────────────────────────────────────────────────────────

git pull --ff-only origin main
# If fast-forward is possible: succeeds silently.
# If branches have diverged: fatal: Not possible to fast-forward, aborting.

# ─────────────────────────────────────────────────────────────
# --no-ff: Force merge commit even when fast-forward is possible.
# Use this when merging feature branches.
# ─────────────────────────────────────────────────────────────

git checkout main
git pull --no-ff feature/discount-engine
# Creates a merge commit even if a fast-forward was possible.
Output
# git pull --ff-only origin main (diverged case — fails safely)
# fatal: Not possible to fast-forward, aborting.
# This means: your branch has local commits that aren't on the remote.
# Solution: use git pull --rebase or merge manually, not --ff-only.
ff-only Is a Safety Net. no-ff Is a Marker.
  • Fast-forward: no local commits, Git moves pointer forward — no merge commit
  • Three-way merge: both branches have commits, Git creates merge commit with two parents
  • --ff-only: fail if fast-forward is not possible — essential for CI/CD
  • --no-ff: force merge commit even when fast-forward is possible — marks feature boundaries
Production Insight
CI/CD pipelines must use --ff-only. A bare git pull in a pipeline creates a merge commit if the branch has diverged. That merge commit was never reviewed, never tested, and is now the code being deployed. I have seen this deploy a teammate's half-finished commit to production because the CI pipeline pulled while the teammate was mid-push. Always use --ff-only and fail loudly if the branch is not clean.
Key Takeaway
Fast-forward: no local commits, pointer moves forward, no merge commit. Three-way merge: both branches have commits, merge commit created with two parents. --ff-only fails if diverged — essential for CI. --no-ff forces merge commit — useful for feature branch boundaries.

git stash: Your Safety Net for In-Progress Work

git stash is the tool that saves your in-progress work when you need to context-switch or pull changes. It takes your uncommitted changes (both staged and unstaged), saves them to a stack, and reverts your working tree to the last commit. Your work isn't gone — it's just set aside.

The stash stack is LIFO (last in, first out). git stash push adds to the top. git stash pop removes and applies the top stash. git stash apply applies without removing. git stash list shows all stashes with identifiers like stash@{0}, stash@{1}.

The most important practice: always use git stash push -m 'description' so you can identify stashes later. Without a message, every stash looks identical in git stash list, and you'll eventually pop the wrong one and lose work.

Production workflow: before pulling, stash with a descriptive message. After pulling, pop the stash. If the pop conflicts, resolve the conflicts, then run git stash drop to clean up the stash entry. If you never want to lose a stash, git stash apply (which leaves the stash on the stack) is safer than pop.

io/thecodeforge/git/GitStashDeepDive.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
# io/thecodeforge — Git Stash Deep Dive

# ─────────────────────────────────────────────────────────────
# BASIC STASH OPERATIONS
# ─────────────────────────────────────────────────────────────

# Save current work with a descriptive message
git stash push -m "WIP: discount threshold validation for orders over 500"

# See all stashes with their messages
git stash list
# stash@{0}: On main: WIP: discount threshold validation
# stash@{1}: On main: WIP: refactoring CartService for multi-currency
# stash@{2}: On main: WIP: adding logging to PaymentProcessor

# See what is IN a stash without applying it
git stash show -p stash@{1}

# Apply the most recent stash and KEEP it on the stack
git stash apply

# Apply the most recent stash and REMOVE it from the stack
git stash pop

# Apply a specific stash (by index)
git stash apply stash@{1}

# Remove a specific stash without applying it
git stash drop stash@{1}

# Clear ALL stashes (nuclear — no confirmation)
git stash clear
Output
# git stash push -m "WIP: discount threshold validation"
# Saved working directory and index state On main: WIP: discount threshold validation
# git stash list
# stash@{0}: On main: WIP: discount threshold validation
# stash@{1}: On main: WIP: refactoring CartService for multi-currency
# stash@{2}: On main: WIP: adding logging to PaymentProcessor
# git stash show -p stash@{0}
# diff --git a/src/orders/OrderValidator.java b/src/orders/OrderValidator.java
# @@ -42,6 +42,12 @@ public class OrderValidator {
# + private static final int DISCOUNT_THRESHOLD = 500;
# + public boolean isValidForDiscount(Order order) {
# + return order.getTotal() > DISCOUNT_THRESHOLD;
# + }
Always Use -m When Stashing
  • git stash push -m 'description' — identifiable entries in git stash list
  • git stash apply: applies without removing — safer if you want to keep the stash
  • git stash pop: applies and removes — use when you are confident the apply will succeed
  • git stash show -p stash@{N}: inspect a stash without applying it
Production Insight
The apply-vs-pop decision is about confidence. If you are unsure whether the stash will apply cleanly (you pulled significant changes while the stash was set aside), use apply first. If it applies cleanly and your code works, then drop the stash manually. If it conflicts, you still have the stash on the stack and can try a different approach. Pop is for high-confidence situations where you know the stash will apply cleanly.
Key Takeaway
git stash saves uncommitted changes to a stack. Always use push -m 'description' for identifiable entries. apply keeps the stash on the stack (safer). pop removes it (riskier if conflicts). Before pulling with dirty files: stash, pull, pop. This is the safe pattern.

Working with Forks: Pulling from Upstream

If you contribute to open source, you work with a fork: you have a remote called origin (your fork) and a remote called upstream (the original repo). git pull by itself pulls from origin — but you need to pull from upstream to sync with the main project.

The workflow: git fetch upstream → git checkout main → git merge upstream/main → git push origin main. This pulls changes from the original repo, merges them into your local main, and pushes them to your fork.

Set up the upstream remote once: git remote add upstream https://github.com/original/repo.git. After that, git fetch upstream works. You can create an alias: alias syncfork='git fetch upstream && git checkout main && git merge upstream/main && git push origin main'.

The same pattern works for any branch: git checkout feature/x, git merge upstream/main to keep your feature branch in sync with the main project. Never rebase feature branches that are open as PRs — it confuses the PR diff and can require force-pushing.

io/thecodeforge/git/ForkSyncWorkflow.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
# io/thecodeforge — Fork Sync Workflow

# ─────────────────────────────────────────────────────────────
# SCENARIO: You've forked github.com/acme-corp/checkout-service
# to github.com/your-username/checkout-service.
# ─────────────────────────────────────────────────────────────

# Step 1: Add the original repo as 'upstream' (only needed once)
git remote add upstream https://github.com/acme-corp/checkout-service.git

# Step 2: Verify both remotes
git remote -v
# origin    https://github.com/your-username/checkout-service.git (fetch)
# upstream  https://github.com/acme-corp/checkout-service.git (fetch)

# Step 3: Fetch all branches from upstream
git fetch upstream

# Step 4: Switch to your local main and merge upstream/main
git checkout main
git merge upstream/main

# Step 5: Push the synced main to your fork
git push origin main

# ─────────────────────────────────────────────────────────────
# ALIAS FOR DAILY SYNC
# ─────────────────────────────────────────────────────────────
git config --global alias.syncfork '!git fetch upstream && git checkout main && git merge upstream/main && git push origin main'
# Now run: git syncfork
Output
# git fetch upstream
# From https://github.com/acme-corp/checkout-service
# e4f9021..7a3c912 main -> upstream/main
# git merge upstream/main
# Updating e4f9021..7a3c912
# Fast-forward
# src/payments/PaymentProcessor.java | 8 +++++++-
# 1 file changed, 7 insertions(+), 1 deletion(-)
# git push origin main
# To https://github.com/your-username/checkout-service.git
# e4f9021..7a3c912 main -> main
Sync Your Fork Daily When Contributing to Open Source
  • git remote add upstream <url> — one-time setup
  • git fetch upstream — downloads changes from original repo
  • git merge upstream/main — integrates into your local main
  • Never rebase feature branches that are open as PRs — confuses the PR diff
Production Insight
The fork-sync workflow is the open-source equivalent of git pull for non-forked repos. The key difference: origin points to your fork (not the original repo), so git pull alone does not sync with the upstream project. You must explicitly fetch and merge from upstream. Forgetting to sync daily leads to large merge conflicts when you finally sync after a week of upstream changes.
Key Takeaway
For forked repos: git fetch upstream, git merge upstream/main, git push origin main. Set up the upstream remote once. Automate with an alias. Never rebase feature branches that are open as PRs.

Upstream Tracking: Why Git Knows Where to Pull From

When you run git pull without specifying a remote or branch, Git somehow knows to pull from origin/main. How? Every local branch can have an 'upstream' — a tracking reference that tells Git which remote branch this local branch corresponds to.

You can see the upstream for your current branch with git branch -vv. The output shows something like: * main a3f92c1 [origin/main] Fix rounding bug. The [origin/main] part is the upstream. If this is missing, git pull without arguments will fail with 'There is no tracking information for the current branch.'

When you create a branch with git checkout -b feature/x, it has no upstream. The first time you push with git push -u origin feature/x, the -u flag sets the upstream. After that, git pull and git push work without arguments on that branch.

You can manually set or change the upstream: git branch --set-upstream-to=origin/main. You can unset it: git branch --unset-upstream. This matters when you're working with forks or multiple remotes.

io/thecodeforge/git/UpstreamTracking.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
# io/thecodeforge — Upstream Tracking

# ─────────────────────────────────────────────────────────────
# See the upstream for all local branches
# ─────────────────────────────────────────────────────────────
git branch -vv
# * main        a3f92c1 [origin/main] Fix rounding bug
#   feature/pay e8d1b20 [origin/feature/pay: ahead 2, behind 1] Add Apple Pay
#   hotfix/tax  3c2fa01 Add tax override logic  <- no upstream

# ─────────────────────────────────────────────────────────────
# SET UPSTREAM on first push
# ─────────────────────────────────────────────────────────────
git checkout -b feature/apple-pay
git push -u origin feature/apple-pay
# The -u sets upstream. Now git pull and git push work without arguments.

# ─────────────────────────────────────────────────────────────
# SET UPSTREAM on existing branch
# ─────────────────────────────────────────────────────────────
git branch --set-upstream-to=origin/feature/apple-pay

# ─────────────────────────────────────────────────────────────
# REMOVE UPSTREAM
# ─────────────────────────────────────────────────────────────
git branch --unset-upstream
# Now git pull without arguments will fail.
Output
# git branch -vv
# * main a3f92c1 [origin/main] Fix rounding bug
# feature/pay e8d1b20 [origin/feature/pay: ahead 2, behind 1] Add Apple Pay
# hotfix/tax 3c2fa01 Add tax override logic
# git push -u origin feature/apple-pay
# Branch 'feature/apple-pay' set up to track remote branch 'feature/apple-pay' from 'origin'.
git branch -vv Is the Fastest Way to See Your Sync State
  • Upstream = the remote branch your local branch tracks
  • git push -u origin branch-name sets upstream on first push
  • git branch -vv shows ahead/behind counts — quickest sync status check
  • Without upstream, git pull without arguments fails
Production Insight
Always set upstream on new branches with git push -u origin branch-name. Without upstream, git pull without arguments fails, and developers resort to git pull origin main (which pulls main into their feature branch — usually wrong). The -u flag is a one-time setup that makes every subsequent pull and push on that branch work without specifying the remote and branch explicitly.
Key Takeaway
Upstream tells Git which remote branch your local branch tracks. Set it with git push -u on first push. git branch -vv shows sync state for all branches. Without upstream, git pull without arguments fails.

Undoing a Pull: How to Recover When You Pulled by Accident

You ran git pull and it created a merge commit you didn't want, or it pulled changes that broke your build, or you just realized you pulled from the wrong branch. How do you undo it?

The answer depends on what happened during the pull. If it was a fast-forward (no merge commit), Git moved your branch pointer forward — you can move it back with git reset --hard ORIG_HEAD. ORIG_HEAD is a special reference Git creates before dangerous operations — it points to where your branch was before the pull.

If it was a three-way merge that created a merge commit, you can undo it with git reset --hard HEAD~1 (move back one commit) or git revert -m 1 HEAD (create a new commit that undoes the merge — safer because it doesn't rewrite history).

The critical distinction: reset rewrites history (changes where the branch pointer points). revert creates a new commit that undoes the changes (history stays intact). Use reset on local-only branches. Use revert on shared branches where rewriting history would break teammates.

If you pulled with --rebase and want to undo it: git reset --hard ORIG_HEAD works here too, because Git saves ORIG_HEAD before rebasing.

io/thecodeforge/git/UndoPull.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
# io/thecodeforge — Undo Pull

# ─────────────────────────────────────────────────────────────
# SCENARIO 1: Fast-forward pull — undo with ORIG_HEAD
# ─────────────────────────────────────────────────────────────
git reset --hard ORIG_HEAD
# Moves branch pointer back to pre-pull state.

# ─────────────────────────────────────────────────────────────
# SCENARIO 2: Three-way merge — undo the merge commit
# ─────────────────────────────────────────────────────────────

# Option A: reset (rewrites history — safe if not pushed)
git reset --hard HEAD~1

# Option B: revert (creates undo commit — safe even if pushed)
git revert -m 1 HEAD
# -m 1 means: revert to the first parent (your branch before the merge).

# ─────────────────────────────────────────────────────────────
# SCENARIO 3: Rebase pull — undo the rebase
# ─────────────────────────────────────────────────────────────

# If rebase is still in progress:
git rebase --abort

# If rebase completed:
git reset --hard ORIG_HEAD
Output
# git reset --hard ORIG_HEAD
# HEAD is now at a3f92c1 Fix rounding bug
# git revert -m 1 HEAD
# [main 7a3c912] Revert "Merge branch 'origin/main' into main"
# 1 file changed, 3 deletions(-)
ORIG_HEAD Is Temporary — Use It Immediately
  • git reset --hard ORIG_HEAD: undo fast-forward or rebase pull
  • git reset --hard HEAD~1: undo merge commit (local-only branches)
  • git revert -m 1 HEAD: undo merge commit (shared branches — does not rewrite history)
  • git rebase --abort: undo in-progress rebase
Production Insight
The reset-vs-revert decision is about whether you have pushed. reset rewrites history — safe on local-only branches, dangerous on shared branches. revert creates a new commit that undoes the changes — safe everywhere, including shared branches. In a team environment, prefer revert unless you are certain nobody has pulled your merge commit.
Key Takeaway
ORIG_HEAD is your undo button — Git saves it before every dangerous operation. git reset --hard ORIG_HEAD undoes the last pull. For shared branches, use git revert -m 1 HEAD instead (does not rewrite history). ORIG_HEAD is temporary — use it immediately or fall back to git reflog.

Git Pull in CI/CD Pipelines: Shallow Clones and Deterministic Deploys

CI/CD pipelines pull repos differently than developers. Developers want full history for bisecting and blame. Pipelines want speed — they don't need 10,000 commits, they need the latest code to build and test.

git clone --depth=1 creates a 'shallow clone' with only the latest commit. This is 10-100x faster than a full clone for large repos. In the clone, git pull works normally — it fetches new commits from the remote and merges them.

The gotcha: shallow clones have limited history. git log only shows the latest commit. git bisect won't work. Some CI operations (like calculating the diff between two commits) may fail if one of the commits isn't in the shallow history. Most CI systems handle this automatically, but if you're writing custom deploy scripts, be aware.

For deterministic deploys: always use git pull --ff-only in CI scripts. If the branch has diverged (someone pushed while the pipeline was running), the deploy fails loudly instead of creating an unexpected merge commit. The pipeline should fail, not silently merge unreviewed code into the deploy.

io/thecodeforge/git/CiPullPatterns.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
# io/thecodeforge — CI/CD Pull Patterns

# ─────────────────────────────────────────────────────────────
# Pattern 1: Shallow clone for speed
# ─────────────────────────────────────────────────────────────
git clone --depth=1 --branch main https://github.com/acme-corp/checkout-service.git

# ─────────────────────────────────────────────────────────────
# Pattern 2: Fetch deeper history if needed
# ─────────────────────────────────────────────────────────────
git fetch --deepen=50

# ─────────────────────────────────────────────────────────────
# Pattern 3: Deterministic pull — fail if branch has diverged
# ─────────────────────────────────────────────────────────────
git pull --ff-only origin main

# ─────────────────────────────────────────────────────────────
# Pattern 4: Deploy only if pull was clean
# ─────────────────────────────────────────────────────────────
if git pull --ff-only origin main; then
    echo "Deploying $(git rev-parse --short HEAD)"
    ./deploy.sh
else
    echo "ABORT: branch has diverged. Manual intervention required."
    exit 1
fi
Output
# git clone --depth=1 --branch main https://github.com/acme-corp/checkout-service.git
# Cloning into 'checkout-service'...
# remote: Enumerating objects: 87, done.
# (Full clone would download 4,000+ objects for this repo)
# git pull --ff-only origin main (diverged — fails safely)
# fatal: Not possible to fast-forward, aborting.
# Pipeline exits with code 1. Deploy does not run.
Never Use git pull Without --ff-only in CI
  • --depth=1: shallow clone, 10-100x faster, but limited history
  • --ff-only: fail if branch has diverged — essential for CI safety
  • Shallow clones break git bisect and some diff operations
  • CI should fail loudly on divergence, not silently merge
Production Insight
The --ff-only flag in CI is not optional — it is a safety mechanism. Without it, a pipeline that pulls while a teammate is mid-push creates a merge commit with unreviewed code and deploys it. This has caused production outages in every team I have worked with. The fix is one flag. The cost of adding it is zero. The cost of not adding it is measured in incidents.
Key Takeaway
In CI/CD: use --ff-only always. Shallow clones (--depth=1) are fast but break git bisect. The pipeline should fail loudly on divergence, not silently merge unreviewed code. --ff-only is the safety net.

Detached HEAD and Git Pull: What Happens and How to Recover

A detached HEAD state means you're not on a branch — you're pointing directly at a specific commit. This happens when you checkout a specific commit hash, a tag, or a remote branch without creating a local branch first.

In detached HEAD state, git pull still works — it fetches remote changes and can merge them into your detached state. But here's the trap: any commits you make in detached HEAD are not on any branch. If you switch to another branch without creating a branch first, those commits become orphaned and eventually garbage-collected.

The safe pattern: if you need to pull and work in detached HEAD, create a branch first with git checkout -b temp-branch. Now you're on a real branch, and git pull works normally.

If you're already in detached HEAD and want to get back to a branch: git checkout main (or whatever branch you want). If you made commits in detached HEAD and want to keep them: git checkout -b rescue-branch before switching away.

io/thecodeforge/git/DetachedHeadPull.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# io/thecodeforge — Detached HEAD and Pull

# ─────────────────────────────────────────────────────────────
# You checked out a tag — now in detached HEAD
# ─────────────────────────────────────────────────────────────
git checkout v2.1.0
# Output: You are in 'detached HEAD' state...

# ─────────────────────────────────────────────────────────────
# SAFE: Create a branch before working
# ─────────────────────────────────────────────────────────────
git checkout -b bugfix/v2.1.0-hotfix
# Now on a real branch. git pull works normally.

# ─────────────────────────────────────────────────────────────
# RECOVERY: Commits lost in detached HEAD
# ─────────────────────────────────────────────────────────────
git reflog
# Find the commit hash of your lost work
git checkout -b rescue/lost-work <commit-hash>
# Commits rescued onto a branch.
Output
# git checkout v2.1.0
# Note: switching to 'v2.1.0'.
# You are in 'detached HEAD' state...
# HEAD is now at e4f9021 Release v2.1.0
# git checkout -b bugfix/v2.1.0-hotfix
# Switched to a new branch 'bugfix/v2.1.0-hotfix'
git reflog Rescues Lost Commits for 90 Days
  • Detached HEAD: HEAD points to a commit, not a branch
  • Commits in detached HEAD are orphaned if you switch branches without creating one
  • Always create a branch before working in detached HEAD
  • git reflog recovers lost commits within 90 days
Production Insight
Detached HEAD is most common when checking out tags for bug reproduction or release verification. The trap: you start fixing the bug while detached, make commits, then switch to main to pull — and the commits are gone. The safe pattern: always create a branch immediately after checking out a tag if you plan to do any work. The recovery: git reflog within 90 days.
Key Takeaway
In detached HEAD, create a branch before working or committing. Commits in detached HEAD are orphaned if you switch branches. git reflog recovers lost commits within 90 days. Always create a branch immediately after checking out a tag.
● Production incidentPOST-MORTEMseverity: high

Silent Overwrite: Half-Finished Pricing Rule Ships to Production on Monday Morning

Symptom
Production revenue dropped 40% within 30 minutes of the Monday deploy. Monitoring showed orders over 500 were being priced at zero. The pricing service logs showed the discount rule was executing but returning 0.0 for the threshold check. No error logs — the code was syntactically valid but logically incomplete.
Assumption
The on-call engineer assumed a database issue or a pricing configuration change. They checked the pricing database, the feature flags, and the configuration service. Nobody suspected the deploy — it was a routine Monday morning pipeline run that had passed CI.
Root cause
1. A developer had been working on a discount threshold feature on Friday afternoon. 2. They staged changes to PricingService.java but did not commit before leaving. 3. On Monday morning, they ran git pull origin main without checking git status. 4. The remote had 3 new commits from the weekend hotfix. 5. Their staged changes did not conflict with the incoming changes (different lines). 6. Git silently merged the staged diff into the merge commit created by git pull. 7. The merge commit contained both the hotfix AND the half-finished discount code. 8. The CI pipeline ran on the merge commit. The half-finished discount code passed syntax checks but had a logic bug: the threshold comparison was > instead of >=, and the return value was hardcoded to 0.0 in the else branch. 9. The deploy pushed the half-finished code to production.
Fix
1. Immediate: rolled back the deploy to the previous known-good release. 2. Root cause: identified the merge commit in git log that contained the staged changes. 3. Developer committed the fix (correct threshold comparison, proper return value) and pushed. 4. Team rule: always run git status before git pull. If working tree is not clean, stash or commit first. 5. Added a pre-push hook that warns if a commit contains changes that were not in the original branch (detects silent merge artifacts). 6. Configured CI to run a 'clean checkout' step: git clean -fd && git checkout -- . before building, to catch uncommitted artifacts.
Key lesson
  • git pull with staged but uncommitted changes silently folds the staged diff into the merge commit if there is no conflict. Your work is not lost but it is buried in the merge commit.
  • Always run git status before git pull. If the working tree is not clean, stash or commit first. This is non-negotiable.
  • CI pipelines that pull before building can with dirty files deploy half changes that were pushed while you were working. 2. Git pauses at each conflicting commit. Resolve: edit file, git add, git rebase --continue. 3. If too complex: git rebase --abort to return to pre-rebase state. 4. Alternative: switch to merge-based pull: git pull origin main (without --rebase).
★ Git Pull Triage Cheat SheetFast recovery for merge conflicts, dirty working tree issues, and pull-related failures.
'Your local changes would be overwritten by merge'
Immediate action
Stash your work before pulling. Do not force through.
Commands
git stash push -m 'WIP: description' (save dirty files)
git pull origin main (pull cleanly)
Fix now
After pull: git stash pop to restore your work. Resolve conflicts if any.
Merge conflict after pull — files have <<<<<<< markers+
Immediate action
Resolve conflicts or abort the merge.
Commands
git status (see which files are conflicted)
git merge --abort (if too complex — return to pre-pull state)
Fix now
If resolvable: edit file, remove markers, git add, git commit.
'fatal: Not possible to fast-forward, aborting' in CI+
Immediate action
The branch has diverged. This is correct CI behavior — fail loudly.
Commands
git log --oneline HEAD..origin/main (see what remote has that you don't)
git pull --rebase origin main (replay local commits on top of remote)
Fix now
In CI: do not auto-merge. Alert the team. Manual intervention required.
Staged changes silently merged into pull commit — half-finished code deployed+
Immediate action
Roll back the deploy. Identify the merge commit with the staged changes.
Commands
git log --oneline -5 (find the merge commit)
git show <merge-commit> (inspect what was merged)
Fix now
Rollback deploy. Fix the code. Push. Add git status check to pre-pull workflow.
Commits lost after switching from detached HEAD+
Immediate action
Use reflog to find the lost commits before they expire (90 days).
Commands
git reflog | grep 'commit' (find the lost commit hashes)
git checkout -b rescue-branch <hash> (recover commits onto a branch)
Fix now
Prevention: always create a branch before working in detached HEAD.
Pull Strategies Compared
StrategyCreates merge commit?Rewrites SHAs?Safe for shared branches?Best for
git pull (default merge)Yes — if branches divergedNoYesShared branches where merge commits mark integration points
git pull --rebaseNo — linear historyYes — local commits get new SHAsOnly if commits not pushedFeature branches with local-only commits
git pull --ff-onlyNo — pointer moves forwardNoYesCI/CD pipelines — fail if branch has diverged
git pull --autostashDepends on merge or rebaseDependsDependsDirty working tree — auto-stash before pull

Key takeaways

1
git pull is always git fetch then git merge
understanding which half causes the risk (merge, not fetch) means you can safely inspect incoming changes before they touch your files.
2
Never run git pull with a dirty working tree. Stash first, pull, pop
those three commands in that order have saved me from losing in-progress work more times than I can count.
3
Reach for git pull --rebase when your local commits haven't been pushed yet and you want a clean linear history. Switch back to the default merge when you're integrating completed feature branches.
4
The counterintuitive truth
a merge conflict isn't git pull failing — it's git pull succeeding at its most important job. It caught a collision and refused to guess which version wins.
5
git pull --ff-only is the safety net for CI/CD pipelines
it fails loudly if the branch has diverged instead of silently creating an unreviewed merge commit.
6
git pull --autostash eliminates the manual stash-pull-pop workflow. Set rebase.autoStash=true globally.
7
Always set upstream on new branches with git push -u origin branch-name.
8
ORIG_HEAD is your undo button
Git saves it before every dangerous operation.
9
In detached HEAD state, create a branch before pulling or committing. git reflog rescues lost commits within 90 days.
10
For fork-based workflows
git fetch upstream && git merge upstream/main keeps your fork in sync.
11
git stash push -m 'message' beats bare git stash every time.
12
git branch -vv shows every local branch, its upstream, and how many commits you're ahead/behind.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 10 QUESTIONS

Frequently Asked Questions

01
What does git pull actually do step by step?
02
What's the difference between git pull and git fetch?
03
How do I pull changes without overwriting my local work?
04
Should I use git pull or git pull --rebase on a high-velocity team?
05
What is the difference between fast-forward and three-way merge?
06
How do I undo a git pull that went wrong?
07
What is git pull --autostash and when should I use it?
08
How do I pull changes from a forked repository?
09
What happens if I pull in detached HEAD state?
10
Should I use git pull in a CI/CD pipeline?
🔥

That's Git. Mark it forged?

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

Previous
Git Clone: Clone a Repository Step by Step
11 / 19 · Git
Next
Git Checkout -b: Creating and Switching Branches