Branches in a Nutshell
A branch is not a copy of your project. It is a small text file holding a commit identifier - a movable label that says "here is where this line of work is." Internalize that one sentence and the rest of branching becomes mechanical.
What a branch actually is
Forget every metaphor you have heard. A Git branch is not a folder, not a directory copy, not a parallel universe of your files. It is a reference: a tiny file on disk whose contents are a single commit identifier and a newline. That is the entire object.
If you peek into a normal repository, you can see it for yourself:
$ cat .git/refs/heads/main
24b9da6537c8b3f9a1d2e4c5b6a7f8e9d0c1b2a3
That is it. The branch named main exists because there is a file at .git/refs/heads/main containing a commit SHA. Create another file there with another SHA and you have another branch. Delete the file and the branch is gone. Pro Git describes a branch as "a lightweight movable pointer to one of these commits," and the phrase is not a teaching analogy - it is a literal description of what is on disk.
A branch is a named reference (a ref) that points to a single commit. The name lives at .git/refs/heads/<name> as a 41-byte text file containing the commit's hash. A branch moves: when you commit on it, the file is rewritten to hold the new commit's hash.
This is the source of branches being "cheap." Creating one means writing 41 bytes. No files are copied, no tree is duplicated, no directory is forked. On a million-line repository, git branch feature finishes the instant you press enter, because all it does is create one more small file.
HEAD: where new commits attach
If a branch is a label on a commit, something has to mark which branch is currently the active one. That is HEAD. In normal operation, HEAD is a special file at the top of .git/ that does not point at a commit directly - it points at a branch, which points at a commit. Git calls this a symbolic reference.
$ cat .git/HEAD
ref: refs/heads/main
When you make a new commit, Git resolves HEAD through that indirection to find the current branch, creates the commit object with the old branch tip as its parent, then updates the branch file to hold the new commit's hash. HEAD itself does not move - the branch it points at does. That is why every new commit "lands on" the current branch automatically.
Think of commits as fixed points in history, branches as sticky labels you can move, and HEAD as an arrow that says "this is the branch I'm working on right now." Commit -> the branch label slides forward to the new commit. Switch branches -> the HEAD arrow swings to a different label.
There is one exception worth flagging now: detached HEAD. If you check out a commit directly instead of a branch, HEAD drops the indirection and points straight at a commit hash. New commits in that state are not attached to any branch and become hard to find. We will treat that case fully in the next topic.
The commit graph and how branches move
Every commit stores its parent commit's hash. That makes history not a list but a graph - a directed acyclic graph, or DAG, where arrows point from each commit back to its parent. A normal commit has one parent; a merge commit has two or more. The very first commit has none.
Branches are names hovering over particular commits in that graph. Start with three commits on main:
Both main and feature point at commit C. They are the same commit; the names just give two different handles on it. HEAD points at feature, so that is the active branch - the one a new commit will move.
Now make a commit on feature. The new commit D has C as its parent. main stays put. feature slides forward to D. HEAD still points at feature:
That is the entire mechanic. Commits accumulate in a graph and never change identity. Branch names are little stickers that slide along the graph as work happens. Switching branches changes which sticker HEAD follows.
Creating a branch
There are three commands that create a branch. They all do the same underlying thing - write a new file under .git/refs/heads/ - but they differ in whether they also move HEAD.
# 1. Modern: create AND switch (one atomic step). Git 2.23+.
git switch -c feature/auth
# 2. Older equivalent that still works everywhere.
git checkout -b feature/auth
# 3. Create WITHOUT switching. Useful when staging a branch
# you do not want to work on yet.
git branch feature/auth
The third form is the most surprising for newcomers: git branch <name> only creates the ref. It does not move HEAD. After running it you are still on whatever branch you started on, with a new branch label pointing at the same commit.
By default, the new branch points at the commit you are currently on. To branch from somewhere else, pass a starting point - any commit, tag, or branch name:
git switch -c hotfix v1.4.0 # branch from a tag
git switch -c fixup HEAD~3 # branch from 3 commits ago
git branch experiment 24b9da6 # branch at a specific commit, no switch
Prefer git switch -c in new work. It was introduced in Git 2.23 specifically to make the everyday "make a branch and start working on it" intent explicit, and to separate it from git checkout's many other jobs (restoring files, detaching HEAD, and so on). git checkout -b still works and is what you will see in older docs and team scripts.
Switching, briefly
To move HEAD to another existing branch:
git switch main # modern
git checkout main # older form, same effect
git switch - # switch to the previous branch
Two things happen. HEAD swings to point at the new branch, and the working tree is updated so its files match the commit that branch points at. Files that exist on the target branch appear; files that exist only on the old branch disappear; modified-but-unchanged-in-the-other-branch files are left alone where Git can keep them safely.
Git will refuse to switch if doing so would silently overwrite uncommitted changes. The next topic, Switching & Checkout, covers detached HEAD, partial restores, and the exact rules around uncommitted work. For now: switch moves you, the working tree follows.
Listing and reading the graph
Two commands answer the everyday "what branches exist, and where is each one?" question.
git branch # local branches; current marked with *
git branch -v # add tip commit hash and subject line
git branch -a # include remote-tracking branches
git branch --merged # only branches already merged into HEAD
git branch --no-merged # only branches with unmerged commits
To see the graph structure - which commits each branch reaches and where they diverge - use git log's graph mode:
git log --oneline --graph --all --decorate
Pro Git uses this exact incantation throughout because it shows everything that matters: commit IDs, subjects, parent links as ASCII edges, and the branch labels riding on each commit. Add it as a shell alias and you will reach for it dozens of times a week.
Renaming and deleting
Renaming a branch updates the ref file (and the reflog and any config tied to the name) - the commits underneath are untouched:
git branch -m new-name # rename the current branch
git branch -m old-name new-name # rename a specific branch
git branch -M new-name # force rename, overwriting if new-name exists
Deleting comes in two flavors, and they are not interchangeable:
git branch -d feature/auth # SAFE: only deletes if fully merged
git branch -D feature/auth # FORCE: deletes regardless of merge state
Lowercase -d refuses to delete a branch whose commits are not reachable from your current HEAD or its upstream - the guardrail exists precisely so you do not throw away unmerged work by accident. Uppercase -D is the shortcut for --delete --force and will delete the ref no matter what, leaving the unique commits unreachable and headed for garbage collection. Reach for -D only when you are sure (or you can recover via git reflog).
Deleting a branch is just removing the label. The commits it pointed at still exist in the object database until garbage collection prunes them, which is why git reflog can usually rescue a branch you nuked five minutes ago.
Naming conventions
Git itself does not enforce a naming style beyond a handful of character restrictions (no spaces, no .., no leading dash, and so on). Teams enforce the rest through habit. Two conventions dominate.
Slash-separated namespaces. Names like feature/auth, fix/css-overflow, or chore/deps read as a category plus a slug. Git treats the slash as a hierarchy in tooling - tab completion groups by prefix, and git branch --list 'feature/*' filters to one namespace. Pick a small set of prefixes and stick to them.
Default branch name. Historically Git initialized new repositories with a branch called master. The current convention is main; GitHub and most newer projects default to it. You can set Git's own default with git config --global init.defaultBranch main. Either name is just a name - the branch is not special to Git apart from being the one git init creates.
| Good | Avoid | Why |
|---|---|---|
feature/payment-retry |
Payment Retry |
No spaces; slashes group related work. |
fix/issue-1284 |
fix_for_thing |
Predictable prefix + concrete reference. |
chore/upgrade-node-20 |
temp |
A name a coworker can understand a week later. |
Why this design is powerful
The everyday workflow becomes very different once branching is cheap. You do not branch only when you are sure a piece of work is going to land. You branch as a way of trying things.
- Cheap experiments. Want to test a refactor? Branch, hack, commit. If it works, merge or rebase it in. If it does not, switch back and delete the branch - the unreachable commits get garbage collected and the repository looks as if you never tried.
- Parallel work. Multiple branches can advance independently from the same commit. A long-running feature and an urgent hotfix can both proceed without disturbing each other; the graph holds them as separate paths until you decide to integrate.
- Checkpoints with names. A branch is also a label.
release/2026-q2at a specific commit is a stable handle for "the code we shipped that quarter" that survives any later work onmain.
Pro Git singles out this as Git's distinguishing feature: branching is so fast that it changes when you reach for it. Centralized tools made branches expensive and rare. Git makes them so cheap that you treat them like draft documents.
Common pitfalls
git branch <name> does not switch. It creates the ref and leaves you on the branch you were already on. New commits will land on the old branch unless you also run git switch <name>. Prefer git switch -c <name> to avoid the surprise.
Deleting a branch before merging loses work. If you run git branch -D feature while feature has commits no other branch reaches, those commits become unreachable and will eventually be pruned. Use -d (safe) by default; -D only after a deliberate "yes, throw it away."
Renaming a branch does not retag remotes. git branch -m old new only touches your local repository. The remote still has a branch called old. Renaming on the remote is a separate step - usually delete the old remote branch and push the new one.
A branch is not a backup. Until you push, a branch lives only on your machine. A laptop failure takes uncommitted edits, unpushed commits, and unpushed branches with it. Branches are cheap and local; durability requires a remote.
Worked examples
Example 1: Create a branch, work on it, merge it back
Start from main, branch off for a small feature, make a commit, then bring it back.
git switch main
git switch -c feature/readme-cleanup
# ...edit README.md...
git add README.md
git commit -m "Tighten setup instructions"
git switch main
git merge feature/readme-cleanup # fast-forward if main has not moved
git branch -d feature/readme-cleanup # safe: it was merged
The branch existed for a few minutes. It served its purpose - holding a coherent change in isolation - and then went away. Merging and the difference between fast-forward and three-way merges are covered in the merging topics later in this chapter.
Example 2: I made commits on the wrong branch
You meant to start a feature branch but forgot, and made three commits on main. Move those commits onto a new branch:
git branch feature/auth # create label at current commit (still on main)
git reset --hard HEAD~3 # rewind main back 3 commits
git switch feature/auth # the 3 commits live here now
Because a branch is just a label, you "saved" the commits by creating the label first, then moved main back. The commits never moved - only the names did. This trick works because nothing has been pushed yet; rewriting a remote branch's history is a different conversation (Chapter 4).
Example 3: Inspect a branch you have not switched to
You want to see what's on feature/auth without changing your working tree.
git log --oneline feature/auth -10 # last 10 commits on that branch
git log --oneline --graph main..feature/auth # commits feature has that main does not
git show feature/auth # the tip commit and its diff
Almost every Git read command accepts a branch name where it would otherwise take a commit. That is because a branch name is, definitionally, a way of saying "the commit this label currently points at."
Example 4: Rename main to something else, locally
An older repository still calls its default branch master and you want to switch to main.
git switch master
git branch -m master main
git branch # confirm main is current; master is gone
That handles the local side. If the branch is also published to a remote, you will need to push the new name, update the remote's default branch in its web UI, and delete the old remote branch. That coordination step is covered in Chapter 3.
Sources & further reading
-
Primary source for the mental model used here: branch as movable pointer,
HEADas symbolic ref, and the graph-with-labels picture of history. -
The natural follow-on: walks a realistic feature-and-hotfix workflow showing branches in the small. Read after this page when you want a longer worked example.
-
Authoritative documentation for every flag mentioned above:
-d,-D,-m,-M,-v,-a,--merged,--no-merged, and the listing options. -
Documents the modern alternative to
git checkoutfor branch switching, including-cfor create-and-switch,-Cfor force-create, and the-shortcut for the previous branch. Introduced in Git 2.23. -
A friendlier walkthrough with diagrams of the same model. Useful if Pro Git's prose feels dense; the conceptual content overlaps but the framing is different.
-
GitHub's beginner framing of Git including branches in the context of pull requests and collaboration. Helpful when your daily Git use is mediated by a hosting platform.