How to create a new Git branch and switch to it (with naming, tracking, and protection)
· Category: Git
Short answer
Since Git 2.23 (Aug 2019), the canonical command is:
git switch -c feature/user-login
This creates feature/user-login from your current HEAD and checks it out in one step. The legacy git checkout -b feature/user-login still works and produces an identical result — git switch was introduced specifically to split branch switching off from file restoration, which git checkout had been overloaded with for years. See git-switch(1) and the Git 2.23 release notes.
If you want the branch to start from somewhere other than HEAD:
git switch -c hotfix/login-csrf origin/main # branch off remote main
git switch -c experiment/cache abc1234 # branch off a specific commit
git switch -c rerun-tests v2.4.0 # branch off a tag
Steps (the safe sequence)
The most common mistake when creating a branch is starting it from a stale local main that's days behind the remote. Pull first.
# 1. Move to your base branch and pick up the latest commits
git switch main
git pull --ff-only origin main
# 2. Create and switch to your new branch in one step
git switch -c feature/user-login
# 3. (optional) push immediately so the branch is visible to teammates
# and tracked against origin
git push -u origin feature/user-login
Notes on each step:
git pull --ff-onlyaborts if the remote has diverged from local. This is what you want — a regulargit pullwould silently merge, polluting your branch's history before you've made a single commit.git push -u(--set-upstream) is what makes futuregit pushandgit pullon this branch work without arguments. Without-u, every push needs the fullgit push origin feature/user-login.
Branch naming that survives code review
Most teams converge on a <type>/<scope> pattern. Examples that scale:
| Prefix | Use for |
|---|---|
feature/ |
New user-facing functionality |
fix/ or bugfix/ |
Non-urgent bug fix from the backlog |
hotfix/ |
Production fix that bypasses the release cadence |
chore/ |
Tooling, deps, refactors with no behavior change |
release/ |
Release-prep branches (mostly relevant in Git Flow) |
revert/ |
A revert that needs review before merging |
Two rules from experience:
- Avoid uppercase, spaces, and
..— they're either invalid (pergit check-ref-format) or break shell completion. - Don't put ticket IDs at the front (
PROJ-1234-fix-login). Put them at the end or in the commit message — front-loaded IDs sort poorly ingit branchlistings and make tab-completion useless.
git check-ref-format --branch "feature/User Login"
# fatal: 'feature/User Login' is not a valid branch name
Tracking, upstream, and git push -u explained
A "tracking" relationship is a pointer in .git/config saying "this local branch corresponds to that remote branch." Without it:
git push
# fatal: The current branch feature/user-login has no upstream branch.
# To push the current branch and set the remote as upstream, use:
# git push --set-upstream origin feature/user-login
Three ways to set upstream:
git push -u origin feature/user-login # push and set upstream in one shot
git branch --set-upstream-to=origin/feature/user-login # set without pushing
git config --global push.autoSetupRemote true # set upstream automatically on first push
The push.autoSetupRemote setting was added in Git 2.37 (June 2022) and is the modern default — turn it on once and forget about -u.
Confirm what's tracked:
git branch -vv
# * feature/user-login 1a2b3c4 [origin/feature/user-login] WIP login form
# main 5d6e7f8 [origin/main] ...
The [origin/...] annotation is the upstream pointer. Missing brackets means no upstream is set.
Branching from a remote without a local copy first
You don't need to git fetch then git switch. Modern Git auto-creates a local tracking branch when you switch to a remote branch name that has exactly one match:
git switch coworkers-feature
# Branch 'coworkers-feature' set up to track remote branch 'coworkers-feature' from 'origin'.
# Switched to a new branch 'coworkers-feature'
If multiple remotes have the same branch name, this is ambiguous and Git refuses; in that case use git switch -c local-name --track origin/coworkers-feature.
Avoiding accidental detached HEAD
These all check out something that is not a branch:
git switch --detach v2.4.0 # explicit, intentional
git checkout v2.4.0 # legacy: same effect, no warning in older Git
git switch --detach abc1234 # branch off a commit hash
In detached HEAD mode any commits you make are not on a branch — they're reachable only from HEAD and will be garbage-collected once HEAD moves. The fix is to give them a branch name before moving on:
git switch -c experiment/cache # save the work as a real branch
If you've already moved away and lost the commits, git reflog can recover them. See how to use git reflog to recover lost commits.
Common issues
fatal: A branch named 'feature/x' already exists.
You created this branch before. Either switch to the existing one (git switch feature/x), force-replace it with the current HEAD (git switch -C feature/x — uppercase C; destructive), or pick a different name. Never use git branch -D to delete then recreate without checking that the old branch wasn't pushed somewhere.
error: pathspec 'main' did not match any file(s) known to git.
You're in a fresh clone and don't have a local main yet. Run git fetch origin first, or use the remote name directly: git switch -c feature/x origin/main.
My new branch has commits I didn't make.
You branched off the wrong base. git log --oneline main..HEAD shows the commits unique to your branch — if there are extras you don't recognize, you forgot the git pull --ff-only origin main step and branched off a stale local main. Easiest fix: git rebase --onto origin/main old-base feature/x (assumes old-base is where you actually started).
Auto-completion suggests a branch I deleted long ago.
Local Git keeps a list of remote-tracking refs that don't get cleaned up automatically. git fetch --prune (or git config --global fetch.prune true once and forget) drops refs the remote no longer has.
GitHub branch protection blocks my push.
Protected branches like main and release/* reject direct pushes from non-admins. The intended flow is to push to a separate branch and open a PR — see the GitHub branch protection docs. The error message names the rule that blocked you.
Tips
git config --global init.defaultBranch mainsetsmain(notmaster) as the initial branch in new repos. Match what your hosting provider uses.git switch -jumps back to the previous branch — analogous tocd -in shells. Useful when bouncing between feature work andmainfor a quick PR review.- For trunk-based teams that don't run long-lived branches, see how to implement trunk-based development.
- For the Git Flow approach with
develop,release/*, andhotfix/*branches, the samegit switch -cmechanics apply — the difference is policy, not commands.