git branch

The git branch command is the main tool for managing branches in Git: it lists the branches that exist in your repository, creates new ones, renames them, and deletes them. Despite how central branching is to the Git workflow, a branch itself is a remarkably simple thing — and understanding exactly what git branch is manipulating will make every other branching command (switch, merge, rebase) click into place. This lesson covers everything from basic listing to deleting unmerged branches, renaming, tracking upstreams, and the internals of what a branch actually is.

Overview / How it works

In Git, a commit is an object identified by a SHA-1 hash that points to a tree (a snapshot of the project’s directory structure) and to zero or more parent commits. The tree points to blobs (the raw content of files) and to other trees (subdirectories). None of that structure knows anything about branches — branches live one layer above it.

A branch is nothing more than a small file containing a 40-character commit SHA (or, in newer repositories using SHA-256, a longer hash). When you create a branch named feature/login-page, Git writes a file at .git/refs/heads/feature/login-page whose entire contents is the SHA of the commit that branch currently points to. That’s it — no copy of your files, no separate history, just a pointer. This is why creating a branch in Git is instantaneous and cheap, unlike some older version control systems where branching meant copying the whole codebase.

HEAD is a special reference that usually points not directly at a commit, but at a branch — via a file .git/HEAD containing something like ref: refs/heads/main. When you commit, Git creates a new commit object, then moves the branch that HEAD points to forward to that new commit. That’s the entire mechanism behind “a branch moves forward as you commit.” If HEAD points directly at a commit SHA instead of at a branch, you’re in detached HEAD state — commits you make there aren’t attached to any branch and can become unreachable once you switch away, unless you create a branch to save them.

The git branch command itself is deliberately narrow in scope: it creates, lists, renames, and deletes these pointer files. It does not change what files are checked out in your working tree, and it does not touch the index (Git’s staging area). That job belongs to git switch and git checkout. This is a common point of confusion for beginners: running git branch new-feature creates the branch but leaves you on whatever branch you were already on — you must separately switch to it.

Syntax

git branch [options] [branch-name] [start-point]

Run with no arguments, git branch simply lists local branches. Supplying a name creates a new branch there. The most commonly used options are:

Option What it does
-a, --all List both local branches and remote-tracking branches
-r, --remotes List only remote-tracking branches (e.g. origin/main)
-v, --verbose Show the latest commit on each branch alongside its name
-vv Also show the upstream branch each local branch tracks, and how far ahead/behind it is
-d, --delete Delete a branch, but only if it’s fully merged into its upstream or the current branch
-D Force-delete a branch, even if it has unmerged commits (shorthand for --delete --force)
-m, --move Rename a branch (moves the ref, keeps history and reflog)
-M Force-rename, even if the target name already exists
-c, --copy Copy a branch to a new name, including its reflog
--show-current Print just the name of the branch you’re currently on
-u <upstream>, --set-upstream-to=<upstream> Set which remote-tracking branch a local branch tracks
--merged / --no-merged List only branches that are / aren’t already merged into the current branch

Examples

1. Listing local branches

git branch

Output:

  feature/login-page
* main

Git lists every local branch alphabetically and marks the branch you currently have checked out with an asterisk and green text (in a color-capable terminal). Here, main is checked out and feature/login-page exists but is not currently active.

2. Creating a branch and trying to delete it too early

git branch feature/user-auth
git switch feature/user-auth
echo "module auth" >> auth.py
git add auth.py
git commit -m "feat: scaffold authentication module"

Output:

Switched to branch 'feature/user-auth'
[feature/user-auth 4e1a9c2] feat: scaffold authentication module
 1 file changed, 1 insertion(+)

git branch feature/user-auth created the pointer but did not move HEAD; git switch then updated HEAD to point at the new branch and rewrote the working tree and index to match its commit. After committing, feature/user-auth now points one commit ahead of main. Now suppose we switch back to main without merging, and try to delete the feature branch:

git switch main
git branch -d feature/user-auth

Output:

error: The branch 'feature/user-auth' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature/user-auth'.

Git’s safety check kicked in: -d refuses to delete a branch whose commits aren’t reachable from another branch, because doing so would make that work unreachable and eventually eligible for garbage collection. Once feature/user-auth is actually merged into main (via git merge or a merged pull request), the same -d command will succeed cleanly, because the commit is now reachable from main too.

3. Renaming a branch and inspecting tracking info

git branch -m feature/user-auth feature/login-page
git branch -vv

Output:

* feature/login-page  4e1a9c2 [origin/feature/user-auth: ahead 1] feat: scaffold authentication module
  main                 9f8e7d6 [origin/main] Merge pull request #42 from feature/nav-bar

-m renamed the branch ref in place — the commit history, reflog, and any upstream tracking configuration all move with it, they don’t reset. Notice the -vv output still shows the old upstream name origin/feature/user-auth, because renaming a local branch does not rename its remote counterpart; you’d need to push the new name and update the upstream separately with git push -u origin feature/login-page.

4. Listing remote-tracking branches

git branch -r

Output:

  origin/HEAD -> origin/main
  origin/feature/login-page
  origin/main

These aren’t real branches on your machine — they’re read-only bookmarks (refs/remotes/origin/*) recording where each branch on origin was last seen after your most recent fetch. git branch -r only reads local state; it never contacts the remote server. To refresh these pointers you need git fetch first.

How it works step by step

  • Creating (git branch name): Git resolves the given start-point (or HEAD if omitted) to a commit SHA and writes that SHA into a new file .git/refs/heads/name. No objects are created, no files are checked out, HEAD is untouched.
  • Listing (git branch): Git reads every file under .git/refs/heads/ (and, with -r/-a, under .git/refs/remotes/) and compares each SHA to what HEAD currently resolves to, marking the match with *.
  • Renaming (-m): Git copies the ref file to the new name (and its reflog, which records where the branch has pointed over time), then removes the old ref file. If the branch being renamed is the current branch, .git/HEAD is updated to point at the new ref name.
  • Deleting (-d/-D): Git checks whether the branch’s commit is an ancestor of another ref (unless -D is used to skip the check), then simply removes the ref file. The commit objects themselves aren’t deleted immediately — they become unreachable and are only cleaned up later by garbage collection (git gc), which is why an accidental deletion is often recoverable via the reflog within a grace period.

Common Mistakes

Mistake 1: Assuming git branch new-name switches you to the new branch

git branch feature/checkout-flow
# still on main! the next commit lands on main, not the new branch
git commit -am "feat: add checkout summary step"

git branch only creates the pointer; it never moves HEAD. The fix is to use git switch -c feature/checkout-flow (or the older git checkout -b feature/checkout-flow) when you want to create and move onto a branch in one step.

Mistake 2: Reaching for -D as a reflex when -d is refused

git branch -d experiment/rate-limiter
# error: not fully merged -> developer just retries with -D
git branch -D experiment/rate-limiter

-d‘s refusal is a warning that the branch’s commits aren’t reachable anywhere else — deleting it with -D can permanently lose work if you haven’t pushed or merged it. Before forcing, confirm you really meant to discard the commits, e.g. with git log experiment/rate-limiter, or merge/push the branch first.

Mistake 3: Thinking git branch -d/-D deletes a branch on GitHub

git branch -d origin/feature/login-page

Deleting a local ref (even a remote-tracking one) never touches the remote server — it only removes your local bookmark, and Git will refetch it right back on the next git fetch unless the branch was also deleted on the remote. To actually delete a branch on GitHub, either delete it in the GitHub web UI (the “Delete branch” button after a PR merges) or run git push origin --delete feature/login-page from the command line.

Best Practices

  • Use descriptive, namespaced branch names like feature/login-page, fix/null-pointer-cart, or chore/upgrade-deps rather than branch1 or temp.
  • Prefer git switch -c over plain git branch when your intent is to start working on a new branch immediately — it removes the “forgot to switch” mistake entirely.
  • Run git branch --merged periodically to find local branches already merged into main and safe to delete with -d.
  • Delete feature branches promptly after they’re merged (locally with -d, remotely via GitHub or git push origin --delete) to keep git branch -a readable.
  • Use git branch -vv before pushing or opening a pull request to confirm which upstream a branch tracks and whether you’re ahead or behind it.
  • Never use -D as a default habit — treat the -d refusal as a signal to double-check, not an obstacle to bypass.

Practice Exercises

  • Exercise 1: In a scratch repository, create a branch called feature/dark-mode without switching to it, verify with git branch that you’re still on main, then switch to it and make one commit. Confirm with git branch -v that the branch has advanced past main.
  • Exercise 2: On that same branch, try to delete it with git branch -d while unmerged, observe the refusal message, then merge it into main and delete it again — this time it should succeed silently.
  • Exercise 3: Create a branch, rename it with -m, then inspect .git/refs/heads/ with a file listing command to see that the old ref file is gone and a new one exists with the new name.

Summary

  • A branch is just a small file holding a commit SHA — creating and renaming branches is cheap because no project files are copied.
  • HEAD normally points at a branch, not directly at a commit; committing moves that branch’s pointer forward.
  • git branch creates, lists, renames, and deletes branch pointers, but never changes your working tree or switches you onto a branch — that’s git switch/git checkout‘s job.
  • -d safely refuses to delete unmerged work; -D forces it and can lose commits, so use it deliberately.
  • -r/-a show remote-tracking branches, which are local bookmarks updated only by fetch/pull, not live views of the remote.
  • Deleting a local or remote-tracking branch never deletes the branch on GitHub — that requires the web UI or git push origin --delete.