Remotes: fetch, pull, and push
Mid / SeniorWhat a remote and a remote-tracking branch really are, what fetch downloads, why pull is fetch + merge (or rebase), and how push and force-with-lease update someone else's refs.
Created 15 October 2026
6 concepts · detail hidden
01Glossary — terms used in this page
#c-glossary-terms-used-in-this-page- Remote — a named URL of another copy of the repository (
origin,upstream), stored in.git/config. - Remote-tracking branch — a local, read-only ref like
origin/mainthat records where a remote’s branch was at your last fetch. - Upstream (tracking) branch — the remote branch a local branch is configured to sync with; used by bare
git pull/git pushand for ahead/behind counts. - Refspec — a mapping rule
src:dsttelling fetch/push which refs to transfer and where to store them. - Fast-forward — updating a branch by simply moving its pointer forward along existing history, because the new commit already contains the old one as an ancestor.
- Merge base — the best common ancestor of two commits.
- FETCH_HEAD — a temporary ref file recording what the last fetch downloaded.
- Remote — a named URL of another copy of the repository (
02Remotes and remote-tracking branches
#c-remotes-and-remote-tracking-branchesA remote is just a nickname for a URL plus a fetch rule:
$ git remote -v origin git@github.com:acme/app.git (fetch) origin git@github.com:acme/app.git (push) $ git config --get-regexp '^remote\.origin' remote.origin.url git@github.com:acme/app.git remote.origin.fetch +refs/heads/*:refs/remotes/origin/*That
fetchrefspec reads: take every branch underrefs/heads/on the remote and store it locally underrefs/remotes/origin/. The leading+allows non-fast-forward updates (if someone force-pushed, yourorigin/xfollows).So there are three different things named “main”:
Ref Lives Updated by mainon the serverthe remote repo others’ pushes, your push origin/main(refs/remotes/origin/main)your repo only git fetch(and pull/push, which update it as a side effect)main(refs/heads/main)your repo your commits, merges, resets origin/mainis a cached snapshot, not a live connection. It can be hours out of date. Git never talks to the network except duringclone,fetch,pull,push,ls-remote(and a few related commands).Upstream tracking
git switch -c feature/login git push -u origin feature/login # -u sets the upstream git status # On branch feature/login # Your branch is ahead of 'origin/feature/login' by 2 commits.-uwrites into.git/config:[branch "feature/login"] remote = origin merge = refs/heads/feature/login“Ahead by 2” is computed locally by comparing
feature/loginandorigin/feature/login— after a fetch the count may change.git rev-list --left-right --count main...origin/maingives the raw ahead/behind numbers.03What `git fetch` actually does
#c-what-git-fetch-actually-doesfetchis the safe, read-only-for-your-work network operation. It never touches your working tree, index, or local branches.Step by step
- Connect to the remote and get the list of its refs and their hashes (
git ls-remote originshows the same). - Negotiate: your client tells the server which commits it already has (“haves”); the server works out which objects you are missing.
- The server sends a packfile with exactly those objects (commits, trees, blobs).
- Your Git stores the objects and updates remote-tracking refs according to the refspec (
origin/mainmoves). - Writes
.git/FETCH_HEADand reflog entries for the remote-tracking refs.
Before fetch After fetch local: A---B---C main A---B---C main ^ origin/main \ D---E origin/main server: A---B---D---E mainYour
maindid not move. Now you can inspect before integrating:git fetch origin git log --oneline main..origin/main # what they have that I don't git diff main...origin/main # their changes since our merge baseUseful flags
git fetch --all— every remote.git fetch --prune(orfetch.prune=truein config) — deleteorigin/xrefs whose branch was deleted on the server. Without it, stale remote-tracking branches accumulate.git fetch --tags— also fetch all tags (by default only tags pointing into fetched history).git fetch origin main:main— fetch and fast-forward localmainwithout checking it out (refused if not a fast-forward or ifmainis checked out).
- Connect to the remote and get the list of its refs and their hashes (
04What `git pull` actually does — fetch + integrate
#c-what-git-pull-actually-does-fetch-integrategit pull == git fetch + git merge <upstream> (default) git pull --rebase == git fetch + git rebase <upstream>The integration step depends on how the histories relate:
Case 1 — you have no local commits: fast-forward
A---B main \ C---D origin/main => main simply moves to D. No new commit.Case 2 — both sides have new commits: diverged
X---Y main (yours) / A---B \ C---D origin/main- merge (
git pull --no-rebase): creates a merge commitMwith parentsYandD. History shows a “Merge branch ‘main’ of github.com:…” bubble — the infamous noise in shared branches. - rebase (
git pull --rebase): replaysXandYon top ofDas new commitsX'Y'. Linear history, no merge commit; your local commits get new hashes (fine — they were never pushed). - fast-forward only (
git pull --ff-only): refuses and makes you decide.
merge: A---B---X---Y---M rebase: A---B---C---D---X'---Y' \ / C-------DConfigure it once
Since Git 2.27, pulling diverged branches without a configured strategy prints a warning (newer versions refuse) asking you to choose:
git config --global pull.rebase true # rebase local commits onto upstream # or git config --global pull.ff only # only ever fast-forward; integrate deliberately git config --global rebase.autoStash true # stash/unstash dirty changes around pull --rebaseA common team convention:
pull.rebase truefor keeping your own unpushed work linear, and real merges only via pull requests.Pitfalls
pullon a dirty working tree can fail or produce conflicts mixed with your uncommitted edits — commit or stash first (or enableautoStash).pullwith merge conflicts leaves you mid-merge (or mid-rebase): resolve,git add, thengit merge --continue/git rebase --continue— or abort with--abort.- Many experienced engineers prefer
fetchthen explicitmerge/rebase, so they see what is coming in before integrating.
- merge (
05What `git push` does, and why it gets rejected
#c-what-git-push-does-and-why-it-gets-rejectedpushis the mirror of fetch: send the objects the server lacks, then ask the server to move its ref.git push origin feature/login # equivalent refspec: refs/heads/feature/login:refs/heads/feature/loginSteps
- Ask the server for its refs.
- Compute objects the server is missing; send them as a pack.
- Request: “update
refs/heads/feature/loginfrom<old hash>to<new hash>”. - The server accepts only if the update is a fast-forward (old hash is an ancestor of new) — unless forced — and only if server-side hooks / branch protection allow it.
- On success, your local
origin/feature/loginis updated too.
The rejection
! [rejected] main -> main (fetch first) error: failed to push some refs to 'github.com:acme/app.git' hint: Updates were rejected because the remote contains work that you do not have locally.Someone pushed in between. Pushing would discard their commits, so the server refuses. Fix:
git pull --rebase(or fetch + merge), then push again.Force pushing — and force-with-lease
After rebasing or amending an already-pushed branch, your new history is no longer a descendant of the remote one, so you must force:
git push --force # dangerous: overwrite whatever is there git push --force-with-lease # safe(r): overwrite only if the remote is still # where my origin/<branch> says it is--force-with-leasesends the expected old value. If a colleague pushed after your last fetch, the remote no longer matches, and the push is refused instead of silently deleting their commits.Caveat: a plain
git fetch(e.g. by your IDE in the background) updatesorigin/<branch>and silently “renews the lease”. Stricter:--force-with-lease=<branch>:<expected-hash>or--force-if-includes(Git 2.30+), which also checks that the remote tip was integrated into your local branch.Rule: never force-push shared branches (
main,release/*); protect them on the server. Force-pushing your own feature branch after a rebase is normal.Other push forms
git push origin --delete feature/old # delete a remote branch git push origin v1.2.0 # push one tag git push --follow-tags # push commits + annotated tags reachable from them git push origin HEAD:refs/for/main # push to an arbitrary ref (Gerrit-style review)06Clone, forks, and the upstream remote
#c-clone-forks-and-the-upstream-remotegit clone <url>is roughly:git init app && cd app git remote add origin <url> git fetch origin git switch main # creates local main tracking origin/mainOnly the default branch gets a local branch; others exist as
origin/*and become local the first time yougit switch <name>(Git auto-creates a tracking branch when exactly one remote has that name).Fork workflow
upstream (acme/app) ──fetch──▶ your clone ──push──▶ origin (you/app fork) └── open PR into upstreamgit remote add upstream git@github.com:acme/app.git git fetch upstream git rebase upstream/main # keep your feature current git push --force-with-lease origin feature/xPartial and shallow clones (big repos, CI)
git clone --depth 1— shallow: only the latest commit’s history. Fast for CI, butlog,blame, and merge-base computation are limited; deepen withgit fetch --unshallow.git clone --filter=blob:none— partial (blobless): full commit/tree history, file contents downloaded lazily on checkout. Usually a better default for big repos than shallow.git sparse-checkout set src/service-a— only materialise part of the tree in a monorepo.