git pull vs fetch: the difference, shown
Git pull vs fetch: fetch updates origin/main and leaves your files alone, pull does that and then merges. Here are the refs before and after.
git pull = git fetch + git merge
git fetch downloads new commits and updates your remote-tracking branches. It does not touch your branch or your files.
git pull does that, then immediately merges the result into your current branch, which does touch your files.
Here is the difference measured on a real clone with git 2.50.0.
Fetch, measured
A clone that is one commit behind. Before fetching:
local main: 02cdc5f first
origin/main: 02cdc5f first
a.txt: one
After git fetch:
local main: 02cdc5f first <- unchanged
origin/main: 4b1e3b0 second from A <- moved
status: ## main...origin/main [behind 1]
a.txt: one <- unchanged
origin/main moved forward. Your branch did not, and neither did your file. Git now knows about the new commit and has done nothing with it.
The status line is the payoff: [behind 1] tells you exactly where you stand, and you could not know that without fetching first.
Then the second half, which is what pull would have done for you:
git merge origin/main
local main: 4b1e3b0 second from A
a.txt: one two
Now the branch moved and the file changed.
What origin/main actually is
The thing that makes this click: origin/main is a local branch. It lives in your repository, not on the server.
It is git's record of where main was on the remote the last time you talked to it. It is not live, it does not update on its own, and it can be out of date by days if you have not fetched.
That is why git status can say "up to date" when your teammate pushed an hour ago. It is comparing you against your local copy of origin/main, which is stale. git fetch is what refreshes that copy, and it is why the habit of fetching before you judge anything is worth building.
You can inspect it like any branch:
git log origin/main
git diff main origin/main
That second command answers "what am I about to pull", which is the question fetch exists to let you ask.
Why fetch first
Fetching is not just cautious, it is more informative:
git fetch
git log --oneline main..origin/main # what is coming in
git diff main origin/main # what will change
git merge origin/main # do it
Four commands instead of one, and you know what happened before it happens. The middle two are the ones people miss, and they are the reason experienced developers fetch by default.
git fetch cannot break anything. It only writes to remote-tracking branches, which are git's bookkeeping and not your work. There is no situation where fetching costs you something.
When pull is fine
Most of the time. If you are on a branch nobody else touches, have no local commits, and just want the latest, git pull is one command and does the right thing.
Pull becomes awkward in two situations:
You have local commits. Now the merge is a real merge, and you get a merge commit joining your work to theirs. Repeated over a busy branch, this produces a history full of "Merge branch 'main' of ..." commits that record nothing except that you pulled.
You have uncommitted changes. Git refuses:
error: Your local changes to the following files would be overwritten by merge
That is git protecting you. Commit or stash first.
pull --rebase
The fix for the merge-commit noise:
git pull --rebase
Instead of merging their work into yours, this replays your commits on top of theirs, producing a straight line with no merge commit.
The trade-off is the one that applies to all rebasing: your commits get new hashes. That is fine for local commits nobody has seen, and a problem for commits you have already pushed. Merge versus rebase covers what actually happens to the history in each case.
To make it the default:
git config --global pull.rebase true
Many teams do this. It is a reasonable default precisely because the commits being rebased are almost always your own unpushed work.
The pull that asks you to choose
On a recent git, pulling with divergent branches and no configured strategy gives this:
hint: You have divergent branches and need to specify how to reconcile them.
Git stopped guessing, because merging and rebasing produce genuinely different history and it will not pick for you. Answer it once, globally:
git config --global pull.rebase true # rebase
git config --global pull.ff only # refuse anything but a fast-forward
ff only is the most conservative: the pull succeeds when you have no local commits and fails otherwise, leaving you to decide deliberately. It is a good setting for people who want to be told before history is reshaped.
Tracking, and where the default comes from
git pull with no arguments works because your branch has an upstream: a remote branch it is paired with. That pairing is what makes pull, push and the ahead/behind counts work without you naming anything.
See it for one branch:
git rev-parse --abbrev-ref --symbolic-full-name @{u}
Or for all of them at once, which is more useful:
git branch -vv
* main 4b1e3b0 [origin/main] second from A
topic a1b2c3d [origin/topic: ahead 2] some work
The bracketed part is the upstream and the current relationship. A branch with no bracket has no upstream, which is why git pull on it answers "There is no tracking information for the current branch".
Set one with -u on the first push:
git push -u origin my-branch
Or after the fact:
git branch -u origin/my-branch
This is the same configuration that silently stays wrong after a rename, which is covered in renaming a branch.
Reading ahead and behind
After a fetch, git status summarises the relationship in one line:
| Status says | Meaning |
|---|---|
up to date |
You and the remote agree, as of your last fetch |
behind 3 |
Three commits to bring in |
ahead 2 |
Two commits to push |
ahead 2, behind 3 |
Both. The branches have diverged |
The last row is the one that needs a decision, because bringing those together means either a merge commit or a rebase. That is where the "divergent branches" prompt below comes from.
The counts are only as fresh as your last fetch, which is worth repeating because it is the single most common misreading of git status. Fetch, then read.
A daily routine that avoids surprises
git fetch --prune # refresh, clean up dead branches
git status # where do I stand
git log --oneline main..origin/main # what is coming in
git merge origin/main # or: git rebase origin/main
Four commands, and each answers a question before acting. The value is not caution for its own sake: it is that you know whether you are about to fast-forward, create a merge commit, or start a conflict, before any of those happen.
Once that sequence is familiar, git pull is a fine shortcut for the case where you already know the answer is "fast-forward". The reason to learn the long version first is that it is the only way to build the mental model that makes the short version safe.
Fetching everything, and pruning
git fetch --all # every remote, not just origin
git fetch --prune # drop refs for branches deleted on the remote
--prune matters more than it looks. Without it, remote-tracking branches for deleted branches stay in your repository indefinitely, which is covered in deleting a branch. Turn it on permanently:
git config --global fetch.prune true
More than one remote
origin is a name, not a keyword. A repository can have several remotes, which is the normal setup when you have forked something:
git remote -v
origin git@github.com:you/project.git (fetch)
upstream git@github.com:original/project.git (fetch)
origin is your fork, upstream is the project you forked from. Keeping current means fetching from the one you do not own:
git fetch upstream
git merge upstream/main
The remote-tracking branches are namespaced per remote, so origin/main and upstream/main are separate refs that can point at different commits. Reading git branch -r with two remotes configured makes the whole remote-tracking idea concrete in a way one remote never does.
Three common mistakes
Trusting git status without fetching. It compares you against your local copy of origin/main, which is only as fresh as your last fetch. "Up to date" can be hours stale, and this is the single most common misreading in git.
Pulling with uncommitted changes and then forcing past the refusal. The refusal is protecting files it would overwrite. Commit or stash instead; there is no version of overriding it that ends well.
Treating git pull as read-only. It merges, which means it can create a merge commit, start a conflict, or move your branch somewhere you did not intend. git fetch is the read-only one, and the distinction matters most on a branch where you have local work.
Quick reference
git fetch # update remote-tracking branches only
git fetch --prune # and clean up deleted ones
git status # ahead/behind, after a fetch
git log main..origin/main # what is incoming
git diff main origin/main # what will change
git merge origin/main # apply it
git pull # fetch + merge in one step
git pull --rebase # fetch + rebase instead
Want to see refs move as you run these? Work through Git basics in a real repository in the browser.
More from the blog
Objects are not valid as a React child: what to render
Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.
Read morenpm install errors: read the code, not the stack
Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.