Remote Branches
A remote branch is one thing on the server and another thing in your clone. Once you see the difference between main and origin/main, half of Git's collaboration commands stop feeling magical and start feeling mechanical.
Two kinds of refs
Inside your .git directory, branch names are stored as refs: short text files containing the commit ID a branch currently points at. Git keeps them in two separate namespaces, and the namespace decides who is allowed to move the pointer.
| Ref type | Path under .git | Example | Who moves it |
|---|---|---|---|
| Local branch | refs/heads/ |
refs/heads/main |
You. git commit, git merge, git reset, etc. |
| Remote-tracking branch | refs/remotes/<remote>/ |
refs/remotes/origin/main |
Git, during fetch / pull / push. |
Local branches are where you do work. Remote-tracking branches are read-only bookmarks: Git updates them on your behalf to record what each remote branch looked like the last time you talked to its server. You almost never edit them by hand, and you never commit onto one.
A local ref of the form <remote>/<branch> (for example origin/main) that mirrors the state of a branch on a remote repository as of the last network operation. It lives only on your machine, even though it represents something that lives elsewhere.
origin/main is a snapshot, not main
The single most useful sentence about remotes in Git is this: origin/main is not main. It is your saved picture of where main sat on origin the last time you fetched. It does not autonomously update. It does not call out to the server when you ask about it. It is a piece of data on your disk.
origin/main is not the live state of the server. It is what your last git fetch wrote down. Hours can pass, dozens of teammates can push, and your origin/main will still cheerfully point at the same old commit until you fetch again. Treat it like a cached value, because that is what it is.
Once that idea clicks, three confusing behaviors become obvious:
git log origin/maincan be hours or days out of date and Git will not warn you.git status's "ahead / behind" message is computed against your remote-tracking ref, not against the server. It is only as fresh as your last fetch.git fetchcan change whatorigin/mainmeans even though it never touches the filemain.
Upstream: linking local to remote-tracking
A local branch can have an upstream: a configured pairing with a remote-tracking branch. Once the link exists, Git can answer questions on your behalf without you spelling out the remote and branch each time.
| With upstream set | What it enables |
|---|---|
git status |
Reports "ahead N, behind M" against the upstream. |
git push (no args) |
Knows which remote and which branch to push to. |
git pull (no args) |
Knows which remote branch to fetch and merge. |
git branch -vv |
Shows the upstream name next to each local branch. |
Upstream information is stored in your repo config, not pushed anywhere; it is a personal convenience. The synonyms tracking branch and upstream branch both refer to the remote-tracking ref on the other end of the link.
Setting upstream
There are three common ways to establish the link, depending on where you are in the lifecycle of the branch.
1. Push and link in one step
This is the everyday case: you just created feature/cart locally and you want to publish it.
git switch -c feature/cart
# ...commits...
git push -u origin feature/cart
The -u (long form --set-upstream) tells Git: after this push succeeds, link my local feature/cart to origin/feature/cart. From then on, plain git push and git pull do the right thing.
2. Link an existing local branch to an existing remote-tracking ref
Use this when you already have both sides and just forgot the -u, or someone else created the branch on the server.
git fetch
git branch -u origin/feature/cart # current branch
git branch -u origin/feature/cart feature/cart # explicit
This is pure config; nothing is pushed. The long form is --set-upstream-to.
3. Create a local branch from a remote-tracking ref
When the branch only exists on the server and you want a local copy that tracks it from the start.
git fetch
git switch -c feature/cart origin/feature/cart
Git creates feature/cart pointing at the same commit as origin/feature/cart and configures the upstream automatically.
Ahead and behind
When a local branch has an upstream, git status compares the two commit pointers and tells you the gap, measured in commits.
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
(use "git push" to publish your local commits)
"Ahead by 2" means there are 2 commits reachable from main that are not reachable from origin/main. "Behind by N" means the reverse. Both numbers can be non-zero at once: that is the "diverged" case, the one that needs a merge or rebase before you can fast-forward.
The math is local versus remote-tracking. If you have not fetched in a while, your "ahead / behind" numbers reflect ancient news. Run git fetch first if the numbers matter.
Fetch, and what --prune cleans up
git fetch contacts the remote, downloads any new commits and objects, and updates the remote-tracking refs to match the server. That is the whole job. It deliberately does not touch your local branches, and it does not merge anything into your working tree. If you have main checked out and run git fetch, your files do not change. Only origin/main moves.
The flip side: when someone deletes a branch on the server, your remote-tracking ref for it does not vanish automatically. It just stops getting updated. Run git branch -r a few months into a project and you will see a graveyard of refs like origin/old-experiment that nobody has worked on since spring.
Use git fetch --prune (or -p) to delete remote-tracking refs whose remote branch is gone. To make it permanent, set git config --global fetch.prune true. Every subsequent fetch will then clean up after itself. The dedicated form is git remote prune origin, which prunes without fetching.
git fetch --prune
git fetch -p origin
# permanent: do this every fetch from now on
git config --global fetch.prune true
# prune only, no fetch
git remote prune origin
git remote prune origin --dry-run # preview what would go
Listing branches
Three flags cover almost every "what branches exist?" question.
| Command | Shows |
|---|---|
git branch |
Local branches only. |
git branch -r |
Remote-tracking branches only. |
git branch -a |
All branches, local and remote-tracking. |
git branch -vv |
Local branches with their upstream and ahead/behind info. |
feature/cart 7e424c3 [origin/feature/cart: ahead 2] add quantity selector
* main 1ae2a45 [origin/main] deploy index fix
hotfix/login f8674d9 [origin/hotfix/login: ahead 3, behind 1] try a fix
The bracketed text after each commit subject is the upstream and the ahead/behind summary against it. The * marks the current branch.
Creating a local branch from a remote one
If a teammate pushed feature/checkout and you want to work on it, the modern shorthand is:
git fetch
git switch feature/checkout
When feature/checkout is not already a local branch but matches exactly one remote-tracking ref (origin/feature/checkout), Git creates the local branch and sets the upstream for you. This is called DWIM ("do what I mean") behavior.
The explicit form, useful when the shorthand is ambiguous or when you want a different local name:
git switch -c feature/checkout origin/feature/checkout
git switch -c co origin/feature/checkout # different local name
Deleting branches, locally and remotely
Delete a local branch
git branch -d feature/cart # safe: refuses if not merged
git branch -D feature/cart # force: deletes regardless
This removes only your local refs/heads/feature/cart. The remote and your remote-tracking ref are untouched.
Delete the branch on the remote
git push origin --delete feature/cart
git push origin :feature/cart # older, equivalent syntax
This asks origin to remove its feature/cart. Your local feature/cart still exists, and so does origin/feature/cart on your machine, until you prune. Most teams wire all three together with the workflow: git push origin --delete, then locally git branch -d and git fetch --prune.
Delete a stale remote-tracking ref directly
git branch -dr origin/old-feature
This affects only your local view of what origin has. The next git fetch will recreate the ref if the branch still exists on the server, which is exactly why --prune is the usual tool.
Common pitfalls
Trusting origin/main without fetching. A diff against origin/main compares against your local snapshot, not the live remote. If you have not fetched today, the diff is against yesterday's server. Run git fetch before reading anything important off a remote-tracking ref.
Forgetting -u on the first push. Without it, the new branch exists on the remote but no upstream is configured. Plain git pull or git push on that branch will then complain that they do not know where to go. Fix it once with git branch -u origin/<name>.
Confusing git push origin --delete with git branch -d. The first removes the branch on the server. The second removes only your local branch. Doing one does not do the other, and "deleted my branch" can mean either depending on who is talking.
Letting stale remote-tracking refs accumulate. After months without pruning, git branch -r lists dozens of dead branches. Worse, tab completion and tooling treat them as real, which leads to commits onto local branches that nobody else has any record of. Enable fetch.prune = true early.
Worked examples
Example 1: Publish a new branch and see status work
You start a feature on a fresh local branch:
git switch -c feature/search
echo "work" > notes.md
git add notes.md
git commit -m "Sketch search UI notes"
git status
# On branch feature/search
# nothing to commit, working tree clean
Notice the absence of any "ahead / behind" line: there is no upstream yet. Publish and link in one step:
git push -u origin feature/search
git status
# On branch feature/search
# Your branch is up to date with 'origin/feature/search'.
From now on, plain git push and git pull work without arguments.
Example 2: A teammate force-pushed; explain what changed locally
You had not fetched in a day. A teammate rebased and force-pushed main. You run:
git fetch
# From origin
# + 1ae2a45...c91d702 main -> origin/main (forced update)
The + and "forced update" tell you that origin/main jumped to a commit not descended from the previous one. Your local main did not move; only the remote-tracking ref did. git status on main will now likely report "diverged" because your local main still has the old history and origin/main points elsewhere.
Example 3: Clean up after merged feature branches
Three of your feature branches have been merged via pull request and deleted on the server, but your local view is unchanged.
git branch -r
# origin/HEAD -> origin/main
# origin/feature/cart <- merged and deleted on server
# origin/feature/search <- merged and deleted on server
# origin/main
git fetch --prune
# From origin
# - [deleted] (none) -> origin/feature/cart
# - [deleted] (none) -> origin/feature/search
git branch -d feature/cart feature/search
# Deleted branch feature/cart (was 7e424c3).
# Deleted branch feature/search (was f8674d9).
Now git branch -a lists only living branches. Setting fetch.prune = true globally makes this automatic.
Example 4: Start from a teammate's branch without DWIM
Your teammate pushed experiment/cache. You want a local branch called cache tracking it.
git fetch
git switch -c cache origin/experiment/cache
# Branch 'cache' set up to track remote branch 'experiment/cache' from 'origin'.
git branch -vv
# * cache 9bd1a40 [origin/experiment/cache] tune LRU eviction
# main c91d702 [origin/main] deploy index fix
The local name differs from the remote name, which DWIM cannot guess; the explicit form makes the intent obvious and still configures the upstream.
Sources & further reading
-
The canonical explanation of remote-tracking refs, tracking branches, upstream configuration, ahead/behind reporting, and remote branch deletion.
-
Authoritative reference for what fetch does to remote-tracking refs, the
--prune/-poption, and thefetch.pruneconfiguration. -
Details on
--set-upstream-to,-r/-alisting, the-vvupstream view, and deleting remote-tracking refs with-dr. -
Covers the
prunesubcommand andremote.<name>.prune, useful when you want to clean up without fetching. -
A platform-flavored view of how local branches relate to branches on a hosted remote, including default-branch and protection conventions you will meet alongside the Git primitives.