Git Fetch, Pull, and Push: Understanding the Differences
Master git fetch, pull, and push — understand the differences, when to use each, prune options, force push dangers, and synchronization best practices.
Introduction
The trio of git fetch, git pull, and git push are the commands that synchronize your local repository with remote repositories. They’re among the most frequently used Git commands, yet their differences and implications are often misunderstood.
Fetch downloads changes without modifying your working files. Pull downloads and merges in one step. Push uploads your commits to a remote. Confusing these operations — especially pull and fetch — is a common source of merge conflicts and lost work.
Understanding the precise behavior of each command, their options, and their dangers is essential for safe collaboration. This guide dissects each operation, explains when to use which, and warns about the pitfalls that catch even experienced developers.
When to Use / When Not to Use
When to Use Fetch
- Checking for updates — see what’s changed on the remote without affecting your work
- Before rebasing — fetch first, then rebase onto the updated remote branch
- CI/CD pipelines — fetch to check for new commits before building
- Safe inspection — review remote changes before deciding how to integrate
When to Use Pull
- Quick synchronization — when you want the latest changes merged into your branch
- Simple workflows — solo development or when conflicts are unlikely
- After fetch review — you’ve fetched, reviewed, and decided to integrate
- Trunk-based development — frequent small pulls keep you current
When to Use Push
- Sharing your work — make commits available to collaborators
- Backup — push to remote as a backup of your local work
- CI/CD triggers — pushing triggers automated builds and tests
- Opening PRs — you must push before creating a pull request
When Not to Push
- Broken code — don’t push commits that fail tests
- Without fetching first — you might overwrite others’ work
- To protected branches — use pull requests instead
- Force push without coordination — can destroy teammates’ work
Core Concepts
The three commands operate in different directions and with different safety profiles:
- fetch — Remote → Local (safe, read-only to your branches)
- pull — Remote → Local + Merge (modifies your branches)
- push — Local → Remote (modifies the remote)
fetch: Downloads remote changes to remote-tracking branches
origin/main updated, your main unchanged
pull: fetch + merge (or rebase)
origin/main updated, your main updated too
push: Uploads your commits to the remote
origin/main updated with your commits
graph LR
Local["Local Repository"] -->|push| Remote["Remote Repository"]
Remote -->|fetch| Local
Remote -->|pull = fetch + merge| Local
Local -. "Your commits" .-> Push["git push origin main"]
Remote -. "Their commits" .-> Fetch["git fetch origin"]
Fetch -. "Then merge" .-> Pull["git pull origin main"]
Architecture or Flow Diagram
flowchart TD
A["Working locally\nwith commits"] --> Decision{"Need to sync\nwith remote?"}
Decision -->|Check remote changes| Fetch["git fetch origin"]
Decision -->|Share your work| Push["git push origin main"]
Decision -->|Get and integrate| Pull["git pull origin main"]
Fetch --> Inspect["git log main..origin/main\nInspect changes"]
Inspect --> Integrate{"How to integrate?"}
Integrate -->|Merge| Merge["git merge origin/main"]
Integrate -->|Rebase| Rebase["git rebase origin/main"]
Push --> Check{"Remote has\nnew commits?"}
Check -->|Yes| Reject["Push rejected\nFetch first!"]
Check -->|No| Success["Push successful"]
Reject --> Fetch
Pull --> Conflict{"Conflict?"}
Conflict -->|Yes| Resolve["Resolve conflicts"]
Conflict -->|No| Done["Up to date"]
Resolve --> Done
Step-by-Step Guide / Deep Dive
Git Fetch
# Fetch all remotes
git fetch --all
# Fetch a specific remote
git fetch origin
# Fetch a specific branch
git fetch origin main
# Fetch and prune deleted remote branches
git fetch --prune
# Fetch with verbose output
git fetch -v
# Fetch without updating remote-tracking branches (dry run)
git fetch --dry-run
Git Pull
# Pull (fetch + merge)
git pull origin main
# Pull with rebase instead of merge
git pull --rebase origin main
# Pull with automatic rebase (configured globally)
git config --global pull.rebase true
git pull origin main
# Pull without auto-merging (fetch only, review first)
git pull --no-commit origin main
# Pull a specific branch into current branch
git pull origin feature-x
Git Push
# Push current branch to its upstream
git push
# Push a specific branch
git push origin feature-x
# Push and set upstream tracking
git push -u origin feature-x
# Push all branches
git push --all origin
# Push all tags
git push --tags origin
# Push with lease (safer than force)
git push --force-with-lease origin feature-x
# Force push (dangerous)
git push --force origin feature-x
Pruning
# Prune deleted remote branches during fetch
git fetch --prune
# Prune without fetching
git remote prune origin
# Configure automatic pruning
git config --global fetch.prune true
# See what would be pruned
git remote prune origin --dry-run
Setting Upstream Tracking
# Set upstream for current branch
git push -u origin feature-x
# Set upstream without pushing
git branch --set-upstream-to=origin/main main
# View tracking configuration
git branch -vv
Production Failure Scenarios + Mitigations
| Scenario | Impact | Mitigation |
|---|---|---|
| Force push to shared branch | Destroys teammates’ commits | Use --force-with-lease; protect branches |
| Pull without fetching first | Unexpected merge conflicts | Always fetch before pulling or rebasing |
| Pushing to wrong branch | Code deployed from wrong version | Verify branch name; use branch protection |
| Stale remote-tracking branches | References to deleted branches | Use --prune regularly |
| Push without tests | Broken code on remote | Use pre-push hooks to run tests |
Pre-Push Hook Example
#!/bin/bash
# .git/hooks/pre-push
echo "Running tests before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted."
exit 1
fi
Trade-offs
| Approach | Pros | Cons |
|---|---|---|
| Fetch then merge | Safe, review before integrating | Two-step process |
| Pull (fetch + merge) | One command, convenient | Merges without review |
| Pull —rebase | Clean linear history | Rewrites local commits |
| Force push | Overwrites remote cleanly | Destroys others’ work |
| Force-with-lease | Safer force push | Still risky if not careful |
| Auto-prune | Clean remote-tracking list | May lose references you wanted |
Implementation Snippets
# Safe daily sync workflow
git fetch origin --prune
git log main..origin/main # review what's new
git switch main
git rebase origin/main # or merge
git push
# Push new feature branch
git switch -c feature/auth
# ... work ...
git push -u origin feature/auth
# Force push safely after rebase
git rebase origin/main
git push --force-with-lease origin feature/auth
# Push everything
git push --all origin
git push --tags origin
Observability Checklist
- Logs: Record push operations with commit SHAs in CI logs
- Metrics: Track push frequency and rejection rate
- Alerts: Alert on force pushes to protected branches
- Traces: Link pushes to CI/CD pipeline triggers
- Dashboards: Display branch synchronization status
Security/Compliance Notes
- Force push should be blocked on protected branches
- Use signed commits and signed pushes for supply chain security
- Audit push history for compliance requirements
- Pre-push hooks can enforce security checks before code leaves your machine
- Verify remote URLs before pushing to prevent credential leakage
Common Pitfalls / Anti-Patterns
- Confusing fetch and pull — fetch is safe; pull modifies your working state
- Force pushing without
--force-with-lease— can silently destroy teammates’ work - Not pruning — stale remote-tracking branches accumulate and confuse
- Pushing without pulling — results in rejected pushes and extra merge commits
- Ignoring upstream tracking —
git pushwithout upstream configuration is ambiguous - Pushing WIP commits — use
--no-verifysparingly; clean your history first
Quick Recap Checklist
-
git fetchdownloads changes safely without modifying your branches -
git pull= fetch + merge (or rebase with--rebase) -
git pushuploads your commits to the remote - Use
--pruneto clean up deleted remote branches - Use
--force-with-leaseinstead of--force - Set upstream tracking with
-uon first push - Always fetch before pushing to check for new remote commits
- Use pre-push hooks to run tests before sharing code
Interview Q&A
git fetch and git pull?git fetch downloads remote changes and updates remote-tracking branches (like origin/main) but does not modify your local branches or working files. git pull does a fetch plus a merge (or rebase), immediately integrating remote changes into your current branch.
--force-with-lease safer than --force?--force-with-lease checks that the remote branch is exactly where you last saw it (from your last fetch). If someone else pushed new commits since then, it refuses to push. --force blindly overwrites the remote regardless of what others have pushed, potentially destroying their work.
git fetch --prune do?It removes local references to remote branches that have been deleted on the server. If someone deletes origin/feature-x on GitHub, your local repo still tracks it. --prune cleans up these stale references so git branch -r shows only branches that actually exist on the remote.
git push without specifying a remote or branch?Git pushes the current branch to its configured upstream (set with -u on first push). If no upstream is set, the behavior depends on push.default configuration — typically it pushes to a branch of the same name on origin, or fails if ambiguous.
Architecture: Data Flow for Fetch, Pull, Push
graph LR
subgraph "Local Repository"
Working["Working Directory"]
LocalBranch["Local Branch\n(main)"]
RemoteTracking["Remote-Tracking\n(origin/main)"]
end
subgraph "Remote Server"
RemoteBranch["Remote Branch\n(main)"]
end
RemoteBranch -. "git fetch" .-> RemoteTracking
RemoteTracking -. "git merge" .-> LocalBranch
RemoteBranch -. "git pull\n(fetch + merge)" .-> LocalBranch
LocalBranch -. "git push" .-> RemoteBranch
classDef local fill:#16213e,color:#00fff9
class Working,LocalBranch,RemoteTracking,RemoteBranch local
Key distinction: fetch only updates remote-tracking references (origin/main). It never touches your local branches or working directory. pull does fetch + merge (or rebase), modifying your local branch. push uploads your local commits to the remote.
Production Failure: Force Push Overwriting Team Work
Scenario: A developer rebases their feature branch locally and runs git push --force origin feature-x. Unbeknownst to them, a teammate had also pushed two commits to the same branch. The force push silently overwrites the teammate’s commits, destroying their work.
Impact: Lost commits, broken teammate trust, hours of recovery via reflog, and potential production issues if the lost commits contained critical fixes.
Mitigation:
- Never use
--force— always use--force-with-lease - Block force pushes on protected branches via platform settings
- Communicate before force-pushing to any shared branch
- Use
git push --force-with-leasewhich checks that the remote hasn’t changed since your last fetch
# Safe force push (refuses if remote has new commits)
git push --force-with-lease origin feature-x
# What --force-with-lease actually checks:
# "The remote branch is exactly where I last saw it (from my last fetch)"
# If someone pushed new commits since then, it refuses
# Configure alias for safety
git config --global alias.push-force 'push --force-with-lease'
# Push rejection: non-fast-forward
# If you get this error:
# ! [rejected] main -> main (non-fast-forward)
# Fix: fetch first, then integrate
git fetch origin
git rebase origin/main # or git merge origin/main
git push origin main
Trade-offs: Pull Strategies
| Approach | How It Works | History | Conflict Frequency | Best For |
|---|---|---|---|---|
git pull --merge (default) | Fetch + merge commit | Non-linear, shows integration point | Lower — conflicts resolved once | Teams that prefer merge workflows |
git pull --rebase | Fetch + rebase local commits | Linear, clean | Higher — conflicts per commit | Solo developers, clean history preference |
git pull --ff-only | Fetch + fast-forward only | Linear | N/A — fails if diverged | Trunk-based development, CI pipelines |
git fetch + manual review | Download only, no integration | Unchanged | None — review before integrating | Cautious workflows, complex integrations |
Security/Compliance: Push Protection
# Platform-level push protection (GitHub)
# Branch protection rules:
# - Require pull request reviews before merging
# - Require status checks to pass before merging
# - Include administrators (no bypassing)
# - Restrict who can push to matching branches
# GitLab equivalent:
# - Protected branches: Developers can merge, Maintainers can push
# - Merge request approvals required
# - Pipeline must succeed
# Pre-push hook for local enforcement
#!/bin/bash
# .git/hooks/pre-push
protected_branches=("main" "develop" "release/*")
current_branch=$(git symbolic-ref --short HEAD)
for branch in "${protected_branches[@]}"; do
if [[ "$current_branch" == $branch ]]; then
echo "ERROR: Direct push to $branch is not allowed."
echo "Please create a feature branch and submit a PR."
exit 1
fi
done
# Run tests before push
npm test || { echo "Tests failed. Push aborted."; exit 1; }
Compliance notes:
- Push protection rules should be enforced at the platform level, not just locally
- Audit push history regularly for unauthorized direct pushes to protected branches
- Document push policies in your security policy
- Use signed pushes for supply chain security (
git push --signed)
Resources
Category
Related Posts
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.
Choosing a Git Team Workflow: Decision Framework for Branching Strategies
Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.