Checkout a remote branch in Git and keep it tracked
Somebody says the work is on feature/invoice-export. The obvious command comes back with pathspec 'feature/invoice-export' did not match any file(s) known to git. A git fetch later, the same command works and never explains why. On another repository the same command produces a branch that pushes to nowhere, and the first git push asks for a remote and a refspec that nobody expected to type.
All three outcomes come from one mechanism. Git does not have remote branches in the sense the phrase suggests. It has local branches, and it has read only pointers that record where a remote's branches were the last time anything talked to that remote. Checking out a remote branch means creating a local branch from one of those pointers and recording the link between them. Most of the time Git guesses that for you. The useful knowledge is what the guess needs in order to fire.
The name has to be in the repository before it can be checked out
A clone copies the remote's branches into refs/remotes/<remote>/. These are remote-tracking refs, and they only move when a fetch, a pull, or a push moves them. A branch a colleague created five minutes ago does not exist in your repository at all, under any name, until a fetch brings it in. The pathspec did not match error is not a permissions problem or a typo. It is Git saying the name is nowhere in the object store.
git fetch origin
git branch --remotes
git branch --remotes lists exactly what is present locally, which is the set Git is allowed to guess from. If the branch is missing from that list after a fetch, the next question is whether it exists on the server at all. git ls-remote answers that without changing anything locally, because it queries the remote and prints refs and object IDs rather than writing them into the repository.
git ls-remote --branches origin
The two lists should match. When ls-remote shows a branch that git branch --remotes does not, the repository is configured to fetch only part of the remote. That is the situation in the next section but one, and it is worth knowing about early because no amount of retyping the checkout command will fix it.
One more distinction is worth holding onto. git fetch updates remote-tracking refs and nothing else. It never changes your working tree and never moves a local branch. That makes it safe to run at any moment, including in the middle of unrelated work with a dirty working tree.
What the plain checkout does when the branch is remote only
Once the remote-tracking ref exists, git checkout feature/invoice-export works even though no local branch of that name exists. The manual for git checkout states the rule directly. If the branch is not found but there does exist a tracking branch in exactly one remote with a matching name, and --no-guess is not specified, the command is treated as equivalent to:
git checkout -b <branch> --track <remote>/<branch>
git switch behaves the same way and documents the same equivalence with its own flags. This behaviour is called guessing in the documentation, --guess is the default, and it can be turned off per invocation with --no-guess or repository wide with the checkout.guess configuration variable.
Two conditions inside that sentence do the real work. The match has to be in exactly one remote, and the name has to be identical. A repository with only origin almost always satisfies both, which is why the shortcut feels like it always works and then suddenly does not.
The effect of the guess is not just a local branch. The --track part writes two configuration entries for the new branch, branch.<name>.remote and branch.<name>.merge, which is what Git calls upstream configuration. Those entries are the reason a bare git pull and a bare git push later know where to go. A local branch created without them is not wrong, it is simply unconnected, and every push and pull against it has to name the remote and the branch.
Four situations where the shortcut does not fire
More than one remote has the branch
Forks make this common. With both origin and upstream configured, and a branch of the same name on each, the guess has no single answer and the checkout fails. The checkout.defaultRemote configuration variable resolves it. Setting it to origin tells Git to prefer that remote when the name is ambiguous but exists there.
git config set --global checkout.defaultRemote origin
The clone only fetches one branch
git clone --single-branch clones the history leading to one branch, and the manual notes that further fetches into the resulting repository will only update the remote-tracking branch for that branch. --depth implies --single-branch unless --no-single-branch is given, so shallow clones made by continuous integration systems land in this state by default. The branch exists on the server, ls-remote sees it, and fetch will not bring it in because the configured refspec does not ask for it. The fix is to widen what the remote tracks.
git remote set-branches --add origin feature/invoice-export
git fetch origin
The remote name and the local name differ
The guess needs an exact match. A remote branch called users/ana/fix cannot be guessed into a local branch called fix. Naming it explicitly is the only route, and --track still sets the upstream correctly across the rename.
git switch --create fix --track origin/users/ana/fix
Guessing was switched off
checkout.guess set to false, often inherited from a shared dotfiles repository, removes the behaviour entirely. git config get --show-origin checkout.guess says whether the variable is set and which file set it.
The explicit forms, and what each one leaves behind
When the shortcut is unavailable or the intent needs to be unambiguous, these are the options. The differences matter less in how they feel to type and more in what state the repository is in afterwards.
| Command | Result | Upstream set |
|---|---|---|
git switch <branch> |
Local branch created from the matching remote-tracking ref, if the guess applies | Yes |
git switch -c <branch> --track origin/<branch> |
Same result, stated explicitly, works with any number of remotes | Yes |
git checkout -t origin/<branch> |
Local branch named after the remote branch, created and checked out | Yes |
git checkout origin/<branch> |
Detached HEAD at that commit, no branch created | No |
git worktree add ../dir <branch> |
Branch checked out in a second directory, current one untouched | Yes, same rules |
git checkout -t is the short form worth remembering, because --track without -b implies branch creation. The name of the new branch is derived from the remote-tracking ref by stripping the remote prefix, and if that guessing produces an empty name the operation is aborted rather than inventing something.
The last row is the one people reach for least and probably should reach for more. A git worktree add gives the branch its own directory with its own working tree, backed by the same object store. Reviewing a colleague's branch no longer means stashing what is in progress. Two directories, two branches, one repository, and switching between them is a change of folder rather than a change of state. Having the folder, the shell and the file you are reading side by side in one window is what makes that pattern comfortable rather than clumsy, which is the same distinction drawn against keeping a notes app and an editor open beside each other all day.
Detached HEAD is a state, not an error
git checkout origin/main is valid and does something people rarely want. It moves HEAD to the commit that remote-tracking ref points at, without attaching it to any branch. Git prints a paragraph about it and most readers scroll past.
The git checkout manual explains the consequence plainly. HEAD normally refers to a named branch, and each branch refers to a specific commit. When a commit is created in that normal state, the branch is updated to refer to the new commit. In a detached HEAD, commits are created with nothing pointing at them. They are reachable only through HEAD, and the next checkout leaves them unreferenced.
Two habits make this harmless. Read the output of git status, which says HEAD detached at instead of On branch. And if commits were already made, do not check anything out until a branch has been created at the current position:
git switch -c rescue-work
The commits are then referenced by a name and behave like any other branch. Even without that, git reflog records where HEAD has been, so the commit IDs are recoverable for as long as the reflog holds them.
Confirm the tracking, because the failure is silent
A local branch with no upstream looks identical to one with an upstream. The difference appears later, at push time, in a repository where somebody else is waiting. Checking takes one command:
git branch -vv
Each line shows the branch, its tip, and in square brackets the upstream with any ahead and behind counts. A branch with no bracket has no upstream. A bracket reading [origin/feature/x: gone] means the upstream was configured and the remote branch has since been deleted, which is a different problem with a different fix.
Repairing a missing upstream does not require deleting and recreating the branch:
git branch --set-upstream-to=origin/feature/invoice-export
Two configuration variables reduce how often this comes up. push.default has defaulted to simple since Git 2.0, which pushes the current branch to a branch of the same name on the remote and, in a centralised workflow, requires an upstream with that same name to be configured. push.autoSetupRemote set to true assumes --set-upstream on a default push when no upstream tracking exists for the current branch, which the manual notes suits workflows where all branches are expected to have the same name on the remote. Together they mean a branch created locally and pushed for the first time ends up tracked without anyone thinking about it.
Keeping the branch current after the checkout
A tracked branch is a starting point, not a subscription. The remote-tracking ref updates on fetch, the local branch does not, and the ahead and behind counts in git branch -vv are read from the last fetch rather than from the server. A branch that reports itself up to date can be a week behind, because nothing has asked the remote since.
The question of how to catch up has two defensible answers, and the choice is worth making once rather than per branch. A merge preserves the shape of what happened and produces a merge commit. A rebase replays your commits on top of the fetched tip and produces a linear history, at the cost of rewriting the commit IDs of anything not yet pushed. pull.rebase set to true picks the second for every git pull, and the manual attaches a note to it: this is a possibly dangerous operation, and it should not be used without understanding the implications.
For a branch that only you have touched, rebasing is usually what reviewers want to read. For a shared branch, rewriting commits that other people have already pulled creates work for them. The distinction is not about taste. It is about whether anyone else has the commits.
There is also a quieter benefit to having the upstream set. git status reports the divergence against it without any flags, so the first line of output after a fetch tells you whether a pull is needed at all. Without an upstream, git status has nothing to compare against and says nothing.
What to change first
Run git fetch before concluding a branch does not exist, then git branch -vv after every checkout of somebody else's work, because that single line is the difference between a branch that pushes and a branch that argues. If the repository has more than one remote, set checkout.defaultRemote once and stop meeting the ambiguity. Reviewing branches side by side instead of switching between them is easier when the folder, the shell and the diff share a window, which is the shape Atriens is built around.
Frequently asked questions
Why does git checkout work for a branch after a fetch but not before?
Because the branch has to exist in your repository before it can be checked out. A clone or a fetch writes the remote's branches into refs/remotes/<remote>/, and Git guesses a local branch from those refs. Before the fetch there is no ref of that name anywhere locally, which is what pathspec did not match reports.
What is the difference between git switch and git checkout for this?
For checking out a remote branch, nothing functional. Both document the same guessing rule, both accept --track, and both honour checkout.guess and checkout.defaultRemote. git switch covers only branch switching, so it cannot silently restore files the way git checkout with a path can, which is the reason to prefer it.
How do I check out a remote branch under a different local name?
Name both sides explicitly with git switch -c <local-name> --track origin/<remote-name>. The automatic guess only fires when the two names are identical, so a rename always has to be spelled out. The --track flag still records the upstream correctly, so git pull and git push work without further arguments.
Why does my new local branch push to nothing?
It was created without upstream configuration, which is what --track writes. git branch -vv shows a branch with no square brackets in that case. git branch --set-upstream-to=origin/<branch> fixes the existing branch, and setting push.autoSetupRemote to true makes the first push configure it for future branches.
The branch is on the server but fetch will not bring it in. What now?
The repository is probably a single branch or shallow clone, which configures a refspec covering only one branch. git clone --depth implies --single-branch unless told otherwise, so this is common in build environments. git remote set-branches --add origin <branch> widens the refspec, and the next fetch brings the branch in.
Is it safe to run git fetch while there are uncommitted changes?
Yes. git fetch only updates remote-tracking refs and downloads objects. It does not touch the working tree, the index, or any local branch, so a dirty working tree is irrelevant to it. That is the difference from git pull, which fetches and then merges or rebases, and can therefore stop partway on a conflict.