Git branch: see where you are before you change any files
git branch prints a list of names with an asterisk next to one of them, and that is where most working knowledge of the command stops. The list is doing more than naming branches. It reports which branch the current working tree is on, which branches are held open by other working trees, how each one relates to its upstream, and, with the right filter, which ones can be deleted without losing anything. All of that is available from one command, and none of it is visible in the default output.
The gap matters because the same command creates branches, renames them, deletes them and rewrites where they point. Reading the list correctly is what keeps the destructive forms from being run on the wrong name.
The default list has three colours in it
The manual page is explicit about what the output marks, and the second marker is the one almost nobody recognises:
If --list is given, or if there are no non-option arguments, existing branches are listed; the current branch will be highlighted in green and marked with an asterisk. Any branches checked out in linked worktrees will be highlighted in cyan and marked with a plus sign. Source: git-scm.com
A plus sign means that branch is checked out somewhere else on the same machine, in a linked working tree. Git will refuse several operations on it for that reason, and the refusals read like bugs until the marker is understood. The manual page spells out two of them. Creating a branch with -b fails when the branch is in use in another worktree, and in that case the branch is not reset to the start point either. And git branch -f, even with the force flag, refuses to change an existing branch that is checked out in another worktree linked to the same repository.
-r lists remote-tracking branches instead of local ones, and -a lists both. Those are separate namespaces rather than two views of the same thing, which is why a name can appear under -r and not exist locally at all.
Getting a single unambiguous answer about the current branch
The asterisk is fine for a person reading a terminal and useless for anything else. --show-current prints the name of the current branch on its own, which is the form to use in a prompt, a script or a commit hook.
Its behaviour in detached HEAD state is the part worth remembering: it prints nothing. That empty output is not a failure. It is the correct answer to the question, because in that state HEAD points at a commit rather than at a named branch, so there is no branch name to print. A script that treats empty output as an error will misreport a perfectly ordinary situation.
-v adds real information to the list: the abbreviated object name and commit subject line for each head, along with its relationship to its upstream branch if one is set. Given twice as -vv, it also prints the path of the linked worktree, if any, and the name of the upstream branch. One detail in the manual page prevents confusion when reading that output, since the current worktree's HEAD will not have its path printed. A missing path on one line means that line is the tree currently being worked in.
--abbrev=<n> controls how short the object names are, with a default of 7 hex digits and core.abbrev as the configuration equivalent, and --no-abbrev prints them in full.
Creating a branch does not put you on it
This is the most common early surprise, and the manual page warns about it directly. The creating form makes a new branch head pointing at the current HEAD, or at a given start point, and then notes that this will create the new branch but will not switch the working tree to it.
So after git branch feature-x, the working tree is still on whatever branch it was on, and any commit made next goes there. The NOTES section gives the shorter path for the usual intention, which is that creating a branch to switch to immediately is easier with git switch and its -c option, in a single command.
The start point accepts a shortcut that is worth knowing for review work. A...B stands for the merge base of A and B when there is exactly one, and either side can be omitted, in which case it defaults to HEAD. That is how a branch gets started from the point where two lines of history diverged rather than from the tip of either one.
Upstream tracking is configuration, not a naming coincidence
A local branch and a remote branch with the same name are not connected by their names. The connection is two configuration entries, branch.<name>.remote and branch.<name>.merge, and whether they get written depends on a setting.
branch.autoSetupMerge controls it, and it defaults to true, which means automatic setup happens when the starting point is a remote-tracking branch. The other values change the rule rather than just switching it off.
| Value | When tracking is set up automatically |
|---|---|
false |
Never |
true |
When the starting point is a remote-tracking branch (the default) |
always |
When the starting point is a local branch or a remote-tracking branch |
inherit |
Copies the starting point's tracking configuration, if it has one |
simple |
Only when the starting point is a remote-tracking branch and the new branch has the same name |
The per-command overrides are --track and --no-track, and --track takes a value: direct uses the start point branch itself as the upstream, while inherit copies the upstream configuration of the start point branch. For a branch that already exists, --set-upstream-to=<upstream> or -u writes the link, and --unset-upstream removes it. Setting it is what makes git status and git branch -v report the ahead and behind counts, and what lets git pull with no arguments know where to pull from.
Finding out which branches are safe to delete
The manual page's NOTES section separates four filters that are easy to confuse, and each one answers a different question about risk.
| Filter | What it lists | What it is for |
|---|---|---|
--merged |
Branches fully contained by HEAD | Finding branches that can be safely deleted |
--no-merged |
Branches not fully contained by HEAD | Finding candidates for merging into HEAD |
--contains <commit> |
Branches containing that commit | Finding branches that need attention if that commit is rebased or amended |
--no-contains <commit> |
Branches not containing it | The inverse of the above |
With no commit argument, --merged and --no-merged default to HEAD, meaning the tip of the current branch. That default is the reason the same command gives different answers on different days, and it is worth naming the commit explicitly when the question is about the release branch rather than about wherever the working tree happens to be.
The filters combine, with rules the manual page states precisely. Combining multiple --contains and --no-contains shows only references that contain at least one of the --contains commits and none of the --no-contains ones. Combining multiple --merged and --no-merged shows only references reachable from at least one of the --merged commits and from none of the --no-merged ones.
-d is the safe delete: the branch must be fully merged in its upstream branch, or in HEAD if no upstream was set. -D is documented as a shortcut for --delete --force, and the force flag allows deletion irrespective of merged status, or even when the branch does not point to a valid commit. Deleting a branch deletes its reflog along with it, which is worth weighing before reaching for -D.
Remote-tracking branches need -r together with -d, and the manual page adds a caution that is easy to skip. It only makes sense to delete remote-tracking branches if they no longer exist in the remote repository, or if fetch was configured not to fetch them again. Otherwise the next fetch brings them straight back, and the prune subcommand of git remote is the tool for clearing out obsolete ones in bulk.
Renaming and copying carry more than the name
-m moves or renames a branch together with its config and reflog, and -M is the forced version. The manual page describes what happens to the history of the rename itself: if the old branch had a reflog, it is renamed to match the new branch, and a reflog entry is created to remember the renaming. So the rename is recorded rather than silently applied.
-c and -C have exactly the same semantics except that the branch is copied to a new name rather than renamed, again along with its config and reflog. That is the form for keeping a branch as it stands while starting a variation of it with the same upstream settings.
-f on its own resets an existing branch to a start point, and without it git branch refuses to change a branch that already exists. The refusal that force does not override is the worktree one, described above: a branch checked out in another linked worktree will not be moved even with -f. --create-reflog turns on recording of changes to a branch ref, which enables date based expressions such as <branchname>@{yesterday}, though the manual page notes reflogs are usually enabled by default in non-bare repositories through core.logAllRefUpdates.
Making the list readable when there are dozens of branches
The default order is not chronological. Sorting defaults to the value of the branch.sort configuration variable if it exists, and otherwise to the full refname including the refs/ prefix, which lists detached HEAD first if present, then local branches, then remote-tracking branches.
--sort=<key> overrides it, with a leading - for descending order, and it can be given several times, in which case the last key becomes the primary key. The keys are the same as those in git for-each-ref, so sorting by the date of the last commit on each branch is available, which is usually what "show me what is current" actually means. --format interpolates %(fieldname) from the branch ref and the object it points at, using the same format language, and --points-at <object> narrows the list to branches at a given object.
Two smaller switches change the output in ways that matter for long lists. -i makes sorting and filtering case insensitive, which keeps a repository that mixes Feature/ and feature/ prefixes from splitting into two blocks. And pager.branch is respected only when listing, meaning when --list is used or implied, with a pager as the default. That is why the output scrolls into a pager for the listing forms and prints straight to the terminal for the creating and deleting ones.
One more option carries an explicit warning in the documentation. --recurse-submodules is marked as experimental, depends on submodule.propagateBranches being enabled, and currently supports branch creation only. It is not a general way to run branch operations across a superproject and its submodules, and treating it as one will produce partial results.
One syntax trap is worth memorising. The manual page notes that when providing a pattern, --list must be used, because otherwise the command may be interpreted as branch creation. A stray pattern without --list can therefore create a branch instead of filtering the list. Two smaller conveniences round it out: --omit-empty suppresses the newline after refs whose format expands to nothing, and --column displays the listing in columns, applicable only in non-verbose mode.
What to change first
Run the list once with -vv and read the plus signs and the upstream column, because those two pieces of information explain most of the refusals this command produces. Keeping that list beside the folder the files are actually in is what a file manager with a built-in terminal is for, which is the idea behind Atriens, and the same window is available on the move from iPhone and iPad.
Frequently asked questions
Why does `git branch` show a plus sign next to a branch name?
That branch is checked out in a linked worktree, which the manual page describes as being highlighted in cyan and marked with a plus sign. Git refuses several operations on a branch in that state, including resetting it with -f, so the marker is the explanation for refusals that otherwise look arbitrary.
Why did creating a branch not switch to it?
Because creating and switching are separate operations. The manual page states that the creating form will create the new branch but will not switch the working tree to it, and points to git switch for the move. When the intention is both at once, git switch -c does it in one command.
How do you get just the current branch name for a script?
Use --show-current, which prints the name on its own. In detached HEAD state it prints nothing, and that is the documented behaviour rather than an error, because HEAD is pointing at a commit rather than at a named branch. A script should treat empty output as that state, not as a failure.
Which branches can be deleted without losing work?
--merged lists the branches fully contained by HEAD, which the manual page names as the way to find branches that can be safely deleted. With no argument it compares against the current branch, so name the integration branch explicitly when that is the real reference point. -d then enforces the same condition, while -D skips the check.
Why does a deleted remote-tracking branch come back after a fetch?
Because it still exists in the remote. The manual page notes that deleting remote-tracking branches only makes sense when they no longer exist in the remote repository, or when fetch was configured not to fetch them again. For clearing out branches that are genuinely gone upstream, the prune subcommand of git remote handles them in one pass.
Does a local branch automatically follow a remote branch with the same name?
No. The link is the branch.<name>.remote and branch.<name>.merge configuration entries, and whether they are written depends on branch.autoSetupMerge, which defaults to setting them up when the starting point is a remote-tracking branch. For an existing branch, --set-upstream-to or -u writes the link explicitly.