Fetch, Pull & Push
Three commands move commits between your repository and a remote. They look similar, but only one of them is a pure download, only one of them is a publish, and only one of them silently changes your branch. Knowing which is which is the difference between a calm collaborator and a frantic one.
Three operations, one diagram
Every exchange with a remote is one of three things: download, download-and-merge, or upload. Git separates them into three commands precisely so you can pick the level of automation you want. The picture below is worth more than the prose; once it clicks, the commands stop blurring together.
The middle column is the bit most beginners miss. Remote-tracking refs like origin/main live in your local repository. They are your local cached view of what the remote looked like the last time you talked to it. git fetch only updates this middle column. Your own branches and your working tree are untouched.
git fetch: pure download
Downloads objects and refs from a remote into your local repository and updates the remote-tracking refs (such as origin/main). It does not touch your working tree and it does not move your local branch pointers.
Fetch is the safe command. You can run it any time, on any branch, in any state. It only reads from the remote and writes to refs Git considers yours-but-about-them. Nothing about your local work changes.
git fetch # fetch from the default remote (origin)
git fetch origin # explicit
git fetch --all # fetch from every configured remote
git fetch --prune # also delete origin/* refs whose branches were deleted upstream
git fetch origin main # fetch only one branch
After fetching, you can inspect what arrived before changing anything:
git log --oneline HEAD..origin/main # commits on the remote that you do not have yet
git log --oneline origin/main..HEAD # commits on you that the remote does not have yet
git diff HEAD origin/main # the actual content difference
This is why fetch is the foundation of everything else. Pull is built from it, and a careful push is preceded by it. If you remember nothing else from this page, remember: fetch first, then decide.
git pull: fetch plus integrate
Runs git fetch and then integrates the fetched commits into your current branch — by default with git merge, or with git rebase if you have configured pull.rebase or passed --rebase.
Pull is a convenience. It saves typing two commands. The trade-off is that it does change your branch — it can create a merge commit, replay your work on top of theirs, or stop midway with conflicts. You should know which of those is going to happen before you run it.
git pull # fetch + merge (or fetch + rebase if pull.rebase is true)
git pull --rebase # fetch + rebase, this run only
git pull --ff-only # fetch + only fast-forward; fail if branches have diverged
The default merge behaviour creates a merge commit any time your branch and the remote both have new work. Many teams find these "pull merges" noisy because they record the act of synchronising rather than any real design decision. The rebase variant rewrites your local commits onto the new tip, leaving a linear history at the cost of changing commit IDs. (Rebase has its own rules — covered in Rebasing.)
If you prefer a linear, merge-free history on shared branches, set this once:
git config --global pull.rebase true
# or, to preserve any local merge commits you actually meant to make:
git config --global pull.rebase merges
You can also use --ff-only as your default to force yourself to stop and look whenever a real integration is needed.
Pull is exactly git fetch followed by an integration step. If pull confuses you, run the two steps separately for a week. Almost every "what did pull just do?" question disappears once you can see the two halves.
git push: publish your commits
Uploads commits from a local branch to a remote, then asks the remote to update its branch ref to point at your tip. By default it refuses any update that is not a fast-forward.
Push is the only command in this trio that writes to someone else's repository. That asymmetry is why Git is conservative about it: a careless push can erase work that other people have already based their own commits on.
git push # push the current branch to its configured upstream
git push origin main # explicit: push local main to origin's main
git push origin feature:main # push local feature to a remote branch named main (rare)
Fast-forward, the normal case
A push is a fast-forward when the remote's current tip is an ancestor of the commit you are pushing. Picture the remote sitting on commit C and your local branch sitting on C → D → E. Git can simply move the remote's branch pointer from C to E — no history is rewritten, no commits are lost. This is what almost every push looks like on a personal feature branch nobody else has touched.
Comparing fetch, pull, and push
| Aspect | git fetch | git pull | git push |
|---|---|---|---|
| Direction | Remote → local | Remote → local | Local → remote |
| Updates working tree? | No | Yes | No |
| Moves your local branch? | No | Yes | No (it moves the remote's branch) |
| Can create a merge commit? | No | Yes (default integration) | No |
| Network write? | No | No | Yes |
| Refusal conditions | Auth / network | Dirty tree, conflicts | Non-fast-forward by default |
Non-fast-forward rejection
If someone else pushed to main after you last fetched, the remote tip is no longer an ancestor of your tip. Git refuses the push:
$ git push origin main
To github.com:org/repo.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:org/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
The fix is the workflow Git is trying to teach you: fetch, look at what arrived, integrate it, then push.
git fetch origin
git log --oneline HEAD..origin/main # what did they add?
git rebase origin/main # or: git merge origin/main
git push origin main
This is not Git being difficult. The rejection exists so that two people pushing at the same time cannot silently overwrite each other. Without it, the last writer wins and the loser's commits become unreachable on the remote.
Force pushing: the lease rule
Sometimes you genuinely need to overwrite a remote branch — you rebased a feature branch, amended its last commit, or squashed it for review. The remote and your local now disagree about history that is "yours" anyway. For that, Git has two escape hatches, and the difference between them matters.
git push --force tells the remote: "Make your branch ref point at my commit, no matter what." It does not check whether anyone else pushed in the meantime. If they did, their commits silently fall off the tip of the branch — still in the repository as orphans, but invisible to anyone cloning fresh. On a shared branch this can erase a colleague's day of work. Do not use --force as your default.
git push --force-with-lease is the safer cousin. It says: "Overwrite the branch only if its current tip on the remote matches what I last saw locally." If someone else pushed since your last fetch, the lease check fails and your push is rejected — protecting their commits.
git fetch origin
git rebase origin/main
git push --force-with-lease origin feature
Make --force-with-lease your reflex. Reach for plain --force only when you have an explicit reason — and never on a branch that other people are committing to. See Rebasing for when and why this comes up.
The fetch-first habit
Plenty of trouble disappears when you make one small change to your routine: fetch before you act. Not pull — fetch. The two commands you would otherwise run blind become two commands you run with eyes open.
git fetch origin
git status # am I ahead, behind, or both?
git log --oneline HEAD..origin/main # what did they add?
# now choose, deliberately:
git merge origin/main # preserve their commits as-is
# or
git rebase origin/main # replay my commits on top of theirs
# then publish
git push
The same loop covers the awkward cases: a conflict you would rather face on a clean working tree, a colleague who force-pushed and rewrote a branch you were tracking, a tag you had not noticed. Fetch is cheap. Run it often.
Common pitfalls
Treating origin/main as live. origin/main is a local cache of what the remote looked like at your last fetch. If you have not fetched today, it is yesterday's news. When something feels off, fetch first — do not trust the remote-tracking ref by itself.
Pulling on a branch with uncommitted changes. If the integration touches files you have modified, Git will refuse to overwrite them and pull stops half-done. Either commit, stash, or clean up before pulling. git stash then git stash pop after the pull is a reasonable rescue.
Forgetting -u on a new branch. Without an upstream, git push alone fails and git pull has nothing to consult. The first push of a new branch should always be git push -u origin <branch>.
Using --force on a shared branch. A force push to main or any branch your team is actively working on can silently drop other people's commits. If you must rewrite published history, use --force-with-lease, announce the rewrite, and prefer doing it on your own feature branch — never on the shared trunk.
Worked examples
Example 1: Inspect remote changes before integrating
You sit down in the morning and want to know what changed on main overnight before you commit yourself to any merge or rebase.
git fetch origin
git log --oneline --graph HEAD..origin/main
git diff --stat HEAD origin/main
The first command refreshes origin/main. The second lists commits the remote has and you do not. The third shows which files moved and by how much. Only after this do you decide between git merge origin/main, git rebase origin/main, or simply continuing on a feature branch.
Example 2: Recover from a non-fast-forward rejection
You try to push and Git refuses. Resist the urge to add --force.
git fetch origin
git log --oneline HEAD..origin/main # what new work landed?
git rebase origin/main # replay your commits on top
# resolve any conflicts, then:
git rebase --continue
git push origin main
If the integration genuinely needs to be a merge (for example, two long-lived branches converging), use git merge origin/main instead of rebase. Either way, the second push is a clean fast-forward.
Example 3: Force-push a rebased feature branch safely
You rebased feature/payments onto the latest main to clean up its history before review. The remote still has the old version.
git fetch origin # know exactly what the remote has
git rebase origin/main # rewrite local feature on top of main
git push --force-with-lease origin feature/payments
The lease fails if anyone else pushed to feature/payments since your last fetch — protecting their work. If it fails, fetch again, integrate their commits, then try once more.
Example 4: Publish a new branch and its release tag
You finished a release on a local branch and tagged the final commit. You want both visible on the remote.
git switch -c release/1.4
# ... commits ...
git tag -a v1.4.0 -m "Release 1.4.0"
git push -u origin release/1.4 # publishes the branch and sets upstream
git push origin v1.4.0 # tags are not pushed automatically
Alternatively, git push --follow-tags in place of the second line will push annotated tags that point at commits you are publishing — convenient when you tag often.
Sources & further reading
-
The primary tutorial for fetch, pull, and push as everyday operations — including the modern note about configuring
pull.rebaseon Git 2.27+. -
The authoritative reference for fetch and its flags —
--all,--prune,--tags,--depth, and friends. Use it when you need exact behaviour rather than the friendly summary. -
Exactly how pull layers an integration step on top of fetch. Documents
--ff-only,--rebase, thepull.rebaseconfig values (true,false,merges,interactive), and the warnings that go with each. -
The full rule-set for push, including the fast-forward requirement on branch refs, the precise semantics of
--forceversus--force-with-lease, and the+refspec prefix that means "force this one ref." -
Practical, GitHub-flavoured walkthrough of
git push: renaming a branch on push, handling non-fast-forward errors, pushing tags individually or with--tags, and what push protection does when secrets are detected.