git fetch vs git pull
Every Git project that has a remote — like a repository hosted on GitHub — needs a way to bring down commits that other people have pushed. Two commands do this: git fetch and git pull. They sound like synonyms, and many beginners treat them as interchangeable, but they behave very differently. git fetch only downloads new data from the remote; it never touches your current branch or working tree. git pull downloads that same data and then immediately tries to integrate it into your current branch, either by merging or rebasing. Understanding the difference gives you control over exactly when your branch changes, and it is the single best way to avoid confusing, unexpected merge commits.
Overview / How it works
To understand fetch and pull, you first need to understand remote-tracking branches. When you clone a repository, Git creates a local copy of every branch on the remote, but it stores them under a special namespace, usually written as origin/main, origin/feature-x, and so on. These are not your branches — they are read-only bookmarks that record where each branch on the remote was, as of your last contact with it. Your own branch, main, is a completely separate pointer that you move yourself, normally by committing or merging.
git fetch contacts the remote, figures out which commits it has that you don’t, downloads the missing commit, tree, and blob objects into your local object database, and then moves the remote-tracking branches (origin/main, etc.) to match the remote. That’s it. Your local main branch, your working tree, and your index (staging area) are completely untouched. Fetching is always safe — it never rewrites your work and never creates a merge commit.
git pull does exactly what git fetch does, and then runs a second command automatically: by default, a merge of the fetched commit into your current branch. That merge (or, if you use git pull --rebase, a rebase) is what actually moves your local branch pointer and updates your working tree. In other words:
git pull = git fetch + git merge FETCH_HEAD
git pull --rebase = git fetch + git rebase origin/main
Recall the Git object model: a commit object contains a pointer to a tree (a snapshot of the project at that commit), a pointer to its parent commit(s), and metadata (author, message, timestamp). A tree points to blobs (file contents) and other trees (subdirectories). A branch is nothing more than a small text file containing a commit’s SHA-1 hash — a movable label. HEAD normally points to a branch (not directly to a commit), so when that branch moves, HEAD moves with it. git fetch only adds new commit, tree, and blob objects to your database and moves the origin/* labels — no branch you control moves, and HEAD stays put. git pull is the step where your own branch pointer and HEAD actually move, because a merge or rebase creates or replays commits on your current branch.
Syntax
git fetch [remote] [branch]
git pull [remote] [branch]
git fetch options:
| Flag | Meaning |
|---|---|
git fetch origin |
Download new objects and update remote-tracking branches for the origin remote only. |
--all |
Fetch from every configured remote, not just one. |
--prune / -p |
Delete local remote-tracking branches whose branch was deleted on the remote. |
-v |
Verbose: print each ref that changed and its old and new hash. |
--tags |
Also download any tags the fetch would otherwise skip. |
git pull options:
| Flag | Meaning |
|---|---|
--rebase |
After fetching, replay your local commits on top of the updated remote branch instead of merging — produces a linear history with no merge commit. |
--no-rebase |
Force a merge even if pull.rebase is configured to true. |
--ff-only |
Only integrate if it can be done as a fast-forward (no divergent local commits); otherwise abort. Good for scripts and CI. |
--no-commit |
Perform the merge but stop before creating the merge commit, so you can inspect the result first. |
Examples
Example 1: Fetch first, inspect, then decide
git fetch origin
Output:
remote: Enumerating objects: 7, done.
remote: Counting objects: 100% (7/7), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (5/5), done.
From github.com:octocat/hello-world
3a1f2c9..9be0dd1 main -> origin/main
This downloaded two new commits and moved the remote-tracking branch origin/main forward. Nothing about your local main branch or working tree changed. You can now inspect exactly what’s new before touching anything:
git log main..origin/main --oneline
Output:
9be0dd1 fix: handle empty response from search API
7c4e21a docs: update README install steps
If you’re happy with what you see, fast-forward your branch manually:
git merge origin/main
Output:
Updating 3a1f2c9..9be0dd1
Fast-forward
README.md | 4 +++-
src/search.js | 9 ++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
Because your local main had no commits of its own beyond 3a1f2c9, Git could simply slide the main pointer forward to 9be0dd1 — a fast-forward, with no merge commit needed.
Example 2: git pull with a default merge
Now imagine you already made a local commit while a teammate pushed one too, so the histories have diverged.
git pull origin main
Output:
remote: Enumerating objects: 5, done.
From github.com:octocat/hello-world
9be0dd1..b7a44f0 main -> origin/main
Merge made by the 'ort' strategy.
src/search.js | 3 +++
1 file changed, 3 insertions(+)
git pull fetched the new commit into origin/main, saw that your local main had diverged, and automatically ran a three-way merge, creating a new merge commit with two parents: your last local commit and the new tip of origin/main.
Example 3: git pull –rebase for a linear history
git pull --rebase origin main
Output:
From github.com:octocat/hello-world
b7a44f0..e21ac03 main -> origin/main
Rebasing (1/1)
Successfully rebased and updated refs/heads/main.
Instead of creating a merge commit, Git temporarily removed your local commit, fast-forwarded main to origin/main, and then replayed your commit on top with a brand-new hash. The result is a straight line of commits instead of a branching-and-joining graph. You can make this the default for a repository so you never have to remember the flag:
git config pull.rebase true
How it works step by step
When you run git fetch origin:
- Git contacts the remote (over HTTPS or SSH) and asks what refs (branches/tags) it has and what commits they point to.
- Git compares that against the commits already in your local object database and works out the minimal set of missing objects.
- The remote packs those objects and sends them; your local Git unpacks them into your object database.
- Git updates your remote-tracking refs (for example
origin/main) to point at the new commit, and writesFETCH_HEADto record what was just fetched. - Nothing else happens — your branch pointer, index, and working tree are all left exactly as they were.
When you run git pull, the fetch steps above happen first, and then Git runs a second operation using the newly updated remote-tracking branch as the target:
- Merge (default): Git finds the common ancestor of your branch and the fetched commit, computes a three-way diff, applies the combined changes to your working tree and index, and — if your branch had commits the remote didn’t — creates a new merge commit with two parents. If your branch had no new commits, it’s a fast-forward and no merge commit is created.
- Rebase (
--rebase): Git temporarily sets your branch commits aside, moves your branch pointer to the tip of the fetched branch, and replays your commits one by one on top, generating new commits with new hashes but the same changes.
Common Mistakes
Mistake 1: Running git pull without knowing what’s coming
git pull origin main
If your local branch has diverged, this silently creates a merge commit (or triggers a rebase) the moment you run it — you find out what changed only after your branch has already moved. On a team repository this can produce a surprising tangle of merge commits. Fetch first so you can review, then merge deliberately:
git fetch origin
git log main..origin/main
git merge origin/main
Mistake 2: Pulling with uncommitted changes
git pull origin main
Output:
error: Your local changes to the following files would be overwritten by merge:
src/search.js
Please commit your changes or stash them before you merge.
Aborting
Git refuses to merge over uncommitted changes that conflict with incoming ones. Commit or stash first:
git stash
git pull origin main
git stash pop
Mistake 3: Rebasing a branch other people are already using
git pull --rebase origin main
git push --force
Rebasing rewrites commit hashes. If main is a shared branch and teammates already pulled the old commits, force-pushing a rebased history breaks their local copies and can silently drop their work. This is Git’s golden rule: never rebase, or force-push, commits that others may already have based work on. Reserve pull --rebase for your own feature branches, and if you must force-push a branch you own, prefer git push --force-with-lease, which refuses to overwrite commits you haven’t seen yet, instead of bare --force.
Best Practices
- Default to
git fetchplus a manualgit log main..origin/mainorgit diff main origin/mainreview when you want to know exactly what’s about to change before integrating it. - Use
git pull --ff-onlyin scripts or CI so an unexpected divergence fails loudly instead of silently creating a merge commit. - Set
git config pull.rebase trueon personal feature branches for a clean, linear history; leave shared or integration branches on the default merge strategy unless the whole team agrees on rebasing. - Run
git fetch --pruneperiodically so deleted remote branches don’t linger as staleorigin/*refs. - Never rebase or force-push a branch other people are pulling from; use
--force-with-leaseinstead of bare--forcewhen you do need to rewrite a branch you own. - Commit or stash local changes before pulling to avoid merge-blocking errors.
Practice Exercises
- Clone a repository, then make a commit locally while a collaborator (or a second clone of your own) pushes a different commit to
main. Rungit fetch origin, inspect the difference withgit log main..origin/main, and only then merge. Confirm a merge commit with two parents was created. - Repeat the same divergence scenario, but this time use
git pull --rebase origin maininstead of a plain merge. Compare the resultinggit log --oneline --graphoutput between the two approaches — one should show a merge commit, the other a straight line. - Make an uncommitted edit to a tracked file, then run
git pullwhile a conflicting upstream change exists. Observe the “would be overwritten” error, then resolve it correctly usinggit stash,git pull, andgit stash pop.
Summary
git fetchdownloads new commits and updates remote-tracking branches likeorigin/mainonly — it never changes your current branch or working tree.git pullisgit fetchfollowed automatically by agit merge(default) orgit rebase(with--rebase) into your current branch.- Fetching is always safe to run at any time; pulling can create merge commits, trigger conflicts, or rewrite history depending on the mode.
- Fast-forwards happen when your branch has no commits the remote doesn’t already have; otherwise Git needs a real merge or rebase.
- Never rebase or force-push a branch that others already have local copies of — prefer
--force-with-leaseover bare--force.
