Creating and Deleting Branches
A branch in Git is nothing more than a movable pointer to a single commit. Creating one costs almost nothing, and deleting one is just as cheap, which is why Git workflows encourage you to branch constantly: one branch per feature, one per bug fix, one per experiment. This lesson covers every common way to create a branch, the difference between the older git checkout and the newer git switch, and how to delete branches locally and on GitHub without losing work by accident.
Overview: How Branches Actually Work
To understand branch creation and deletion, you first need to understand what a branch is inside Git’s object database. Every commit you make is a commit object identified by a SHA-1 (or SHA-256, on newer repos) content hash. That commit object points to a tree object, which is a snapshot of your project’s directory structure; the tree in turn points to blob objects, which store the actual file contents, and to other trees for subdirectories. None of that changes when you create or delete a branch.
A branch itself is just a small text file under .git/refs/heads/ that contains the SHA of one commit. When you run git branch feature/login-page, Git writes a new file, .git/refs/heads/feature/login-page, containing the SHA of whatever commit HEAD currently points to. That’s it — no files are copied, no history is duplicated, and the operation is effectively instantaneous no matter how large your repository is.
HEAD is a separate, special pointer that tracks which branch (or, occasionally, which raw commit) you currently have checked out. Normally HEAD points to a branch name, and that branch name points to a commit — this indirection is what lets a new commit “move the branch forward” automatically. When you commit, Git creates a new commit object whose parent is the old commit, then updates the current branch’s ref file to the new commit’s SHA. HEAD didn’t move; the branch it points to did.
If you check out a specific commit SHA or a tag instead of a branch, HEAD points directly at a commit with no branch in between. This is called detached HEAD state. Git will warn you about it. Any new commits you make in detached HEAD are still real commit objects, but no branch ref is updated to track them, so they become easy to lose once you switch elsewhere — unless you create a branch at that point with git switch -c or git branch, which simply plants a new ref pointing at your current, detached commit.
Deleting a branch is the mirror image of creating one: Git deletes the small ref file. The commits that branch pointed to are not deleted immediately. If no other branch, tag, or ref still points to them (directly or through history), they become “unreachable” and are only cleaned up later by Git’s garbage collector (git gc). Until then, they’re usually still recoverable through the reflog (git reflog), which is why an accidental branch deletion is rarely a permanent disaster — but you shouldn’t rely on that as a safety net.
Syntax
There are two ways to create a branch: create it without moving to it, or create-and-switch in a single command. There are two ways to delete a local branch depending on whether Git should double-check your work, plus a separate command to delete the branch’s copy on a remote like GitHub.
git branch "<branch-name>"
git branch "<branch-name>" "<start-point>"
git switch -c "<branch-name>"
git switch -c "<branch-name>" "<start-point>"
git checkout -b "<branch-name>"
git branch -d "<branch-name>"
git branch -D "<branch-name>"
git push origin --delete "<branch-name>"
| Command | What it does |
|---|---|
git branch <name> |
Creates a new branch pointing at the current commit, but does not switch to it. |
git branch <name> <start-point> |
Creates a new branch starting from a specific commit, tag, or other branch instead of HEAD. |
git switch -c <name> |
Creates a new branch and switches to it in one step. The modern, recommended form. |
git checkout -b <name> |
Creates a new branch and switches to it in one step. The older form; still extremely common and functionally equivalent to git switch -c here. |
git branch -d <name> |
Deletes a local branch, but refuses if it has commits not yet merged elsewhere. Safe delete. |
git branch -D <name> |
Force-deletes a local branch regardless of merge status. Shorthand for --delete --force. Destructive. |
git push origin --delete <name> |
Deletes the branch’s copy on the remote (e.g. GitHub). Does not touch your local branch. |
git switch and git restore were introduced in Git 2.23 to split apart the overloaded responsibilities of git checkout, which historically handled switching branches, restoring files, and creating branches all at once. git switch only switches branches (and, with -c, creates one first); prefer it for clarity, but expect to see git checkout -b in older tutorials, scripts, and colleagues’ habits — both do the same job for branch creation.
Examples
Example 1: Create a branch, then switch to it separately
git branch feature/login-page
git branch
git switch feature/login-page
Output:
feature/login-page
* main
Switched to branch 'feature/login-page'
The first command creates the branch ref but leaves you on main (note the * stays on main in the git branch listing). Only the third command actually moves HEAD, updating your working directory and the index to match the snapshot at feature/login-page‘s commit — which, since we just branched, is identical to main.
Example 2: Create and switch in a single command
git switch -c feature/user-auth
Output:
Switched to a new branch 'feature/user-auth'
This is the same as running git branch feature/user-auth followed by git switch feature/user-auth, just faster to type. The older equivalent is:
git checkout -b feature/payment-api
Output:
Switched to a new branch 'feature/payment-api'
Both commands write a new ref under .git/refs/heads/ pointing at the current commit and then update HEAD to point at that new ref. Choose git switch -c for new scripts and habits; recognize git checkout -b when you see it in the wild.
Example 3: Deleting local branches, safely and forcibly
git switch main
git merge feature/login-page
git branch -d feature/login-page
Output:
Switched to branch 'main'
Updating 4f2a1c9..8b3d2e0
Fast-forward
src/login.js | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
Deleted branch feature/login-page (was 8b3d2e0).
Because feature/login-page‘s commits are now reachable from main (we just merged them), -d deletes it without complaint. Contrast this with a branch that has unique, unmerged work:
git branch -d feature/experimental-search
git branch -D feature/experimental-search
Output:
error: The branch 'feature/experimental-search' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature/experimental-search'.
Deleted branch feature/experimental-search (was 3c9f7a1).
Git’s safe delete (-d) refuses because those commits would become unreachable. The forced delete (-D) does it anyway — use it deliberately, never as a reflex. Finally, deleting a branch on GitHub after a pull request merges is a separate step:
git push origin --delete feature/login-page
git fetch --prune
Output:
To github.com:yourname/your-repo.git
- [deleted] feature/login-page
git fetch --prune afterward cleans up your local “remote-tracking” ref (origin/feature/login-page), which otherwise lingers as a stale pointer even though the branch is gone on GitHub.
How It Works Step by Step
When you run git switch -c feature/user-auth, Git performs, in order: (1) reads the SHA that HEAD currently resolves to; (2) writes a new file at .git/refs/heads/feature/user-auth containing that SHA — the branch now exists; (3) rewrites .git/HEAD so it contains ref: refs/heads/feature/user-auth instead of the previous branch; (4) compares the tree of the new branch’s commit against your working directory and the index, and since they’re identical at this point, nothing in your files changes. If you instead switched to a branch whose commit differs from your current one, step 4 is where Git updates tracked files to match the target commit’s tree and updates the index to match — this is also the moment merge conflicts or “local changes would be overwritten” errors can surface if your working tree has uncommitted edits that collide with the switch.
Deleting a branch reverses only step 2 and step 3 in miniature: Git removes the ref file. It first checks (for -d) whether every commit reachable from that branch is also reachable from some other ref, like main or an upstream tracking branch; if not, it refuses. -D skips that check entirely. Either way, the commit objects, tree objects, and blobs themselves stay in .git/objects/ untouched until garbage collection eventually prunes anything unreachable.
Common Mistakes
Mistake: Branching off a stale local main
git switch -c feature/checkout-flow
If you haven’t run git pull recently, your local main may be missing commits your teammates already pushed. Branching from it silently bases your new work on outdated code, which surfaces later as a messy, unnecessary merge conflict. Fix it by updating first:
git switch main
git pull
git switch -c feature/checkout-flow
Mistake: Force-deleting a branch with irreplaceable work
git branch -D feature/prototype
-D bypasses the merged-status check entirely. If feature/prototype had commits that exist nowhere else, they’re now unreachable and will eventually be garbage collected — effectively gone. Before force-deleting, verify what you’d lose, and back it up if you’re unsure:
git log main..feature/prototype
git push origin feature/prototype
git branch -D feature/prototype
Mistake: Assuming a local delete removes the remote branch too
git branch -d feature/login-page
git branch -d only ever touches your local repository. The branch will still exist on GitHub, visible to everyone else, until you explicitly delete it there as well:
git branch -d feature/login-page
git push origin --delete feature/login-page
Best Practices
- Use descriptive, namespaced branch names like
feature/login-page,bugfix/null-pointer-checkout, orhotfix/payment-timeoutinstead of vague names liketestorbranch2. - Prefer
git switch -covergit checkout -bin new work and documentation; both work, butswitch‘s narrower purpose makes commands easier to read. - Update your base branch (
git pull) before branching off it, so new work starts from the latest shared history. - Default to
git branch -d, never-D, unless you’ve specifically confirmed the commits are safe to lose or are backed up elsewhere. - Delete a feature branch (both locally and on the remote) promptly after its pull request merges, to keep the branch list readable for the whole team.
- Run
git fetch --pruneperiodically so deleted remote branches stop showing up as stale remote-tracking refs in your local listings. - Write commit messages on new branches following Conventional Commits (
feat: add login page,fix: correct checkout total) so history stays scannable regardless of which branch it came from.
Practice Exercises
- Create a branch named
feature/dark-modewithout switching to it, confirm withgit branchthat you’re still onmain, then switch to the new branch. Then start over and do the same thing in a single command usinggit switch -c. - On
feature/dark-mode, make and commit a small change to any file. Try to delete the branch withgit branch -dwhile still on it, and note the error. Switch back tomain, mergefeature/dark-mode, and delete it safely with-d. Confirm-dnow succeeds without needing-D. - Create a branch, commit a change on it, but do not merge it anywhere. Attempt
git branch -dand observe the refusal. Usegit log main..<branch>to see exactly what commits would be lost, then decide for yourself whether-Dis appropriate.
Summary
- A branch is a small, cheap-to-create ref file pointing at a single commit — not a copy of your project.
git branch <name>creates a branch without switching;git switch -c <name>and the oldergit checkout -b <name>create and switch in one step.git branch -dis a safe delete that refuses unmerged work;git branch -Dforce-deletes and can lose commits permanently.- Deleting a local branch never deletes its remote counterpart — use
git push origin --delete <name>for that, andgit fetch --pruneto clean up stale remote-tracking refs locally. - Deleted branch commits aren’t erased immediately; they become unreachable and linger until garbage collection, which is why accidental deletions are often recoverable via
git reflog— but that’s a safety net, not a plan.
