Tracking Branches
A tracking branch (also called an upstream branch) is a link between a local branch and a specific branch on a remote repository, usually origin. Once that link exists, Git knows exactly where to push and pull without you typing the remote name and branch name every time, and it can tell you how many commits you’re ahead or behind. Tracking branches are the reason git status can say things like “Your branch is ahead of ‘origin/main’ by 2 commits” — without one, Git has no idea what to compare against.
Overview: How Tracking Branches Work
To understand tracking branches you first need to separate three things that are easy to confuse:
- A local branch — a movable pointer to a commit, stored under
refs/heads/, e.g.refs/heads/main. This is what you commit to. - A remote-tracking branch — a read-only local copy of where a branch stood on the remote the last time you talked to it, stored under
refs/remotes/, e.g.refs/remotes/origin/main. You never commit directly ontoorigin/main; Git updates it automatically whenever youfetchorpush. - The upstream link — a small piece of configuration that says “local branch
mainis tracking remote-tracking branchorigin/main“. This is what actually lives in.git/configasbranch.main.remote = originandbranch.main.merge = refs/heads/main.
Remember Git’s object model: every commit points to a tree (a snapshot of your files), and a branch is nothing more than a 40-character SHA-1 (or SHA-256 on newer repos) pointer to a commit. A remote-tracking branch is the same kind of pointer, just labeled with the remote’s name and kept in sync only when you explicitly contact the remote. Nothing updates origin/main automatically in the background — it only moves when you run git fetch, git pull, or git push.
The “ahead/behind” counts you see in git status or git branch -vv come from comparing the commit history reachable from your local branch against the commit history reachable from its remote-tracking branch. “Ahead by 2” means your local branch has 2 commits that the remote-tracking branch doesn’t have yet (you likely need to push). “Behind by 1” means the remote-tracking branch has a commit you don’t have locally (you likely need to pull or merge). Both can be true at once if history has diverged, which is exactly when you’ll hit a merge conflict or need a rebase.
When you git clone a repository, Git automatically creates a remote-tracking branch for every branch on the remote, and sets up the local branch you land on (usually main) to track its remote counterpart. Every other branch you create yourself does not get a tracking relationship for free — you either set it up explicitly or let a Git command infer it for you, as shown below.
Syntax
git push -u <remote> <branch>
git branch -u <remote>/<branch>
git branch --set-upstream-to=<remote>/<branch> [<local-branch>]
git branch --unset-upstream [<local-branch>]
git checkout --track <remote>/<branch>
git switch -c <branch> --track <remote>/<branch>
git branch -vv
| Form | What it does |
|---|---|
-u / --set-upstream (on push) |
Pushes the branch and records it as the upstream for the current local branch in one step. |
-u / --set-upstream-to (on branch) |
Points an existing local branch at a remote-tracking branch without pushing anything. |
--unset-upstream |
Removes the tracking link from a local branch; Git will no longer know a default remote/branch for it. |
--track (on checkout/switch) |
Creates a new local branch and automatically wires it up to track the given remote-tracking branch, using the same short name. |
-vv (on branch) |
Lists local branches with their upstream and ahead/behind status. |
Examples
Example 1: Inspecting existing tracking branches
git branch -vv
Output:
* main a1b2c3d [origin/main] Update README with setup steps
feature/login-page d4e5f6a [origin/feature/login-page: ahead 2, behind 1] Add login form validation
scratch/notes 9f8e7d6 Quick local experiment
main is tracking origin/main and is fully in sync. feature/login-page is tracking origin/feature/login-page but has diverged: 2 local commits haven’t been pushed, and 1 remote commit (probably pushed by a teammate) hasn’t been pulled yet. scratch/notes has no bracketed upstream at all — it isn’t tracking anything, so a plain git push or git pull on that branch will fail until you tell Git where it should go.
Example 2: Creating a branch and setting its tracking link with the first push
git switch -c feature/payment-api
git add payment_api.py
git commit -m "feat: add payment API client skeleton"
git push -u origin feature/payment-api
Output:
Enumerating objects: 5, done.
...
To github.com:acme/checkout-service.git
* [new branch] feature/payment-api -> feature/payment-api
Branch 'feature/payment-api' set up to track remote branch 'feature/payment-api' from 'origin'.
The -u (short for --set-upstream) flag does two things in one command: it pushes the new branch to origin, creating refs/remotes/origin/feature/payment-api, and it writes branch.feature/payment-api.remote = origin plus branch.feature/payment-api.merge = refs/heads/feature/payment-api into .git/config. From this point on, a bare git push or git pull on this branch knows exactly where to go.
Example 3: Checking out someone else’s remote branch, and fixing a branch that’s tracking the wrong thing
git fetch origin
git checkout --track origin/feature/dark-mode
Output:
Branch 'feature/dark-mode' set up to track remote branch 'feature/dark-mode' from 'origin'.
Switched to a new branch 'feature/dark-mode'
After a teammate pushes feature/dark-mode, git fetch downloads it as the remote-tracking branch origin/feature/dark-mode but does not give you a local branch to work on. git checkout --track origin/feature/dark-mode creates a local branch with the matching short name (feature/dark-mode) and wires up tracking automatically — this only works because the local branch name doesn’t already exist. If you already have a local branch pointed at the wrong upstream, fix the link directly instead of recreating the branch:
git branch --set-upstream-to=origin/main main
Output:
Branch 'main' set up to track remote branch 'main' from 'origin'.
How It Works Step by Step
- On clone: Git downloads every branch from the remote into
refs/remotes/origin/*, creates a local branch for the remote’s default branch (e.g.main), and sets its upstream automatically. - On
git push -u origin <branch>: Git uploads your commits, creates or updatesrefs/heads/<branch>on the remote, updates your localrefs/remotes/origin/<branch>to match, and writes the tracking config so future pushes/pulls default to this pair. - On
git fetch: Git downloads new commit objects and moves your remote-tracking refs (refs/remotes/origin/*) to match the remote — it never touches your local branches or working tree. - On plain
git pull(with an upstream set): Git runs the fetch above, then merges (or rebases, with--rebase)origin/<branch>into your current local branch, moving the local branch pointer forward and updating the working tree and index to match. - On
git status/branch -vv: Git walks the commit graph from your local branch and from its upstream to compute how many commits are reachable from one but not the other — that’s the ahead/behind count, no network access required since it uses the last-fetched remote-tracking data.
Common Mistakes
Mistake 1: Pushing a new branch without -u, then wondering why git pull fails
git switch -c feature/search-filters
git push origin feature/search-filters
git pull
Output:
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.
The plain git push origin feature/search-filters uploaded the branch but never recorded an upstream, so a bare git pull has nothing to compare against. The fix is either to push with -u the first time (git push -u origin feature/search-filters), or to run git branch --set-upstream-to=origin/feature/search-filters afterward.
Mistake 2: Pointing a branch’s upstream at the wrong remote branch
Running git branch --set-upstream-to=origin/main feature/search-filters by accident (wrong target) will make git status compare your feature branch against main instead of its own remote branch, producing confusing ahead/behind numbers and tempting you to merge or push into the wrong place. Always double-check the target with git branch -vv right after setting an upstream.
Mistake 3: Trying to edit a remote-tracking branch directly
Remote-tracking branches like origin/main are read-only bookkeeping refs — Git only moves them during fetch/push/pull. Attempting to git checkout origin/main and commit will detach HEAD (you’re now pointing straight at a commit, not a branch), and any commits you make there are easy to lose once you switch away. If you want to work on it, create a real local branch that tracks it first, as in Example 3.
Best Practices
- Always push a brand-new branch with
git push -u origin <branch>the first time, so every futurepush/pullon it “just works”. - Run
git branch -vvbefore a push or pull if you’re unsure whether a branch has an upstream, or what it’s diverged by. - Use
git checkout --track origin/<branch>(orgit switch -c <branch> --track origin/<branch>) rather than manually creating a branch with the same name and hoping it tracks correctly. - Prefer
git push --force-with-leaseover a baregit push --forcewhen you must overwrite a tracked remote branch’s history —--force-with-leaserefuses to push if the remote-tracking ref shows someone else has pushed commits you haven’t seen. - Never rewrite history (
rebase,filter-repo) on a tracking branch that others have already pulled from — it diverges everyone else’s copy and forces messy manual fixes. - Fetch regularly (
git fetch) so ahead/behind counts and remote-tracking refs stay current, even when you’re not ready to merge yet.
Practice Exercises
- Exercise 1: Create a new local branch
feature/user-profile, make a commit on it, and push it so that it tracksorigin/feature/user-profilein a single command. Confirm the tracking link withgit branch -vv. - Exercise 2: A teammate pushed a branch called
fix/typo-navthat you don’t have locally yet. Using onlyfetchand one checkout-style command, create a local branch that tracks it automatically. What doesgit branch -vvshow right after? - Exercise 3: Your local
mainshows “ahead 3, behind 2” againstorigin/main. Without losing your 3 local commits, figure out the sequence of commands to bring your branch up to date with the remote (hint: think aboutfetchversuspull, and what happens if there’s a conflict).
Summary
- A tracking branch links a local branch to a specific remote-tracking branch (e.g.
maintoorigin/main), stored as config in.git/config. - Remote-tracking branches (
refs/remotes/origin/*) are read-only snapshots of the remote’s state, updated only byfetch,pull, orpush. git push -u origin <branch>pushes and sets the upstream in one step;git branch --set-upstream-to=<remote>/<branch>sets it without pushing.git checkout --track/git switch -c --trackcreate a new local branch that automatically tracks a matching remote branch.git branch -vvis the fastest way to see every local branch’s upstream and its ahead/behind status.- Forgetting to set an upstream causes “no tracking information” errors; setting the wrong upstream causes confusing sync status — always verify with
-vv.
