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, or hotfix/payment-timeout instead of vague names like test or branch2.
  • Prefer git switch -c over git checkout -b in new work and documentation; both work, but switch‘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 --prune periodically 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

  1. Create a branch named feature/dark-mode without switching to it, confirm with git branch that you’re still on main, then switch to the new branch. Then start over and do the same thing in a single command using git switch -c.
  2. On feature/dark-mode, make and commit a small change to any file. Try to delete the branch with git branch -d while still on it, and note the error. Switch back to main, merge feature/dark-mode, and delete it safely with -d. Confirm -d now succeeds without needing -D.
  3. Create a branch, commit a change on it, but do not merge it anywhere. Attempt git branch -d and observe the refusal. Use git log main..<branch> to see exactly what commits would be lost, then decide for yourself whether -D is 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 older git checkout -b <name> create and switch in one step.
  • git branch -d is a safe delete that refuses unmerged work; git branch -D force-deletes and can lose commits permanently.
  • Deleting a local branch never deletes its remote counterpart — use git push origin --delete <name> for that, and git fetch --prune to 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.