Topic · Collaboration & Remotes

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.

Remote-tracking branch

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.

Your local repository refs/heads/main your local branch (you move it) main → C3 refs/remotes/origin/main remote-tracking ref (fetch moves it) origin/main → C2 commits live in the local object database; refs are just pointers into that database. The remote origin main → C4 truth lives here but only changes when someone pushes git fetch updates this ref
Warning

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/main can 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 fetch can change what origin/main means even though it never touches the file main.

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.

publish a new branch with trackingshell
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.

attach upstream to an existing branchshell
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.

start from a remote-tracking refshell
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.

git status outputtext
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.

Warning

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.

Tip

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.

prune stale remote-tracking refsshell
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.
git branch -vvtext
  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:

shorthand: name matches a single remoteshell
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:

explicit formshell
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

local branch deletionshell
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

remote branch deletionshell
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

rarely needed; prune usually sufficesshell
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

Pitfall 1

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.

Pitfall 2

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

Pitfall 3

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.

Pitfall 4

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:

before publishingshell
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:

publish with upstreamshell
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:

after their force-pushshell
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.

see the stale viewshell
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
prune, then drop local copiesshell
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.

explicit create-and-trackshell
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

  • Pro Git, 3.5: Remote Branches Textbook Scott Chacon and Ben Straub

    The canonical explanation of remote-tracking refs, tracking branches, upstream configuration, ahead/behind reporting, and remote branch deletion.

  • git fetch Reference Git project · official manual

    Authoritative reference for what fetch does to remote-tracking refs, the --prune / -p option, and the fetch.prune configuration.

  • git branch Reference Git project · official manual

    Details on --set-upstream-to, -r / -a listing, the -vv upstream view, and deleting remote-tracking refs with -dr.

  • git remote Reference Git project · official manual

    Covers the prune subcommand and remote.<name>.prune, useful when you want to clean up without fetching.

  • About branches Tutorial GitHub Docs

    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.