Topic · Collaboration & Remotes

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.

Local repository Remote (origin) Working tree files on disk main, feature local branches origin/main, origin/feature remote-tracking refs main, feature remote branches fetch pull = fetch + merge or rebase push (must be fast-forward by default)

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

git fetch

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.

basic fetchshell
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:

inspect what came inshell
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

git pull

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.

pull variantsshell
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.)

Tip · pull.rebase

If you prefer a linear, merge-free history on shared branches, set this once:

configure pull behaviourshell
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.

Note

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

git push

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.

everyday pushshell
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:

a rejected pushshell
$ 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.

the canonical recoveryshell
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.

Pitfall · --force

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.

Tip · --force-with-lease

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.

safer force pushshell
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.

Tags and new branches

Pushing tags

Tags are not pushed by default. A normal git push sends commits and updates the branch ref; tags ride along only if you ask:

pushing tagsshell
git push origin v1.2.0    # push one specific tag
git push origin --tags    # push every local tag the remote does not have
git push --follow-tags    # push branch + annotated tags that point at pushed commits

--follow-tags is often the sweet spot for release workflows: it pushes the branch plus any annotated tags relevant to the commits being pushed, without dumping every stray local tag you ever made.

Pushing a new branch the first time

A branch that exists only locally has no upstream — Git does not know which remote branch should receive it. The first push needs to set that up:

publish a new branchshell
git switch -c feature/payments
# ... commits ...
git push -u origin feature/payments    # -u = --set-upstream

From then on, plain git push and plain git pull work on this branch with no arguments. The -u flag records origin/feature/payments as the upstream of your local feature/payments, which is also what enables git status to tell you "your branch is ahead by 2 commits."

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.

the careful loopshell
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

Pitfall 1

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.

Pitfall 2

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.

Pitfall 3

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>.

Pitfall 4

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.

look before you integrateshell
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.

the correct fixshell
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.

safe force pushshell
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.

publish branch + tagshell
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

  • Pro Git, 2.5: Working with Remotes Textbook Scott Chacon and Ben Straub

    The primary tutorial for fetch, pull, and push as everyday operations — including the modern note about configuring pull.rebase on Git 2.27+.

  • git-fetch documentation Reference Git project

    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.

  • git-pull documentation Reference Git project

    Exactly how pull layers an integration step on top of fetch. Documents --ff-only, --rebase, the pull.rebase config values (true, false, merges, interactive), and the warnings that go with each.

  • git-push documentation Reference Git project

    The full rule-set for push, including the fast-forward requirement on branch refs, the precise semantics of --force versus --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.