Git Branch Basics: Creating, Switching, Listing, and Deleting Branches
Master the fundamentals of Git branching — creating, switching, listing, and deleting branches. Learn the core commands that enable parallel development workflows.
Introduction
Branching is Git’s superpower compared to centralized version control systems. In Git, branches are lightweight, fast to create, and designed to be used liberally. Every commit you make lives on a branch, and the default main branch is no different from any branch you create yourself.
Branch fundamentals unlock effective parallel development. Whether you’re isolating a feature, fixing a bug, or experimenting with a refactor, branches let you work without disrupting the main codebase. This guide covers every essential branch operation you’ll need in daily development.
Branches in Git are simply pointers to commits. When you create a new branch, Git creates a new pointer — it doesn’t copy files or duplicate history. This is why branching is nearly instantaneous, even in repositories with hundreds of thousands of commits.
When to Use / When Not to Use
When to Use Branches
- Feature development — isolate new functionality until it’s ready for integration
- Bug fixes — patch production issues without pulling in unfinished work
- Experiments — try radical changes with zero risk to stable code
- Release management — stabilize a release while main continues to evolve
- Code reviews — each branch becomes a natural unit for pull request review
When Not to Use Branches
- Trivial one-line changes — commit directly to main for typo fixes or documentation tweaks
- Long-lived branches — branches that live for months accumulate drift and merge pain
- Personal preference divergence — don’t branch for formatting or style changes that create noise
- Avoiding communication — branches shouldn’t replace talking to your team about integration plans
Core Concepts
A Git branch is a movable pointer to a commit. The default branch is typically named main (or master in older repositories). Git tracks the current branch with a special pointer called HEAD.
commit A ── commit B ── commit C ← main (HEAD)
When you create a new branch, Git creates a new pointer at the current commit:
commit A ── commit B ── commit C ← main
└────── feature ← HEAD
As you commit on the new branch, only that branch pointer advances:
commit A ── commit B ── commit C ← main
└── commit D ── commit E ← feature (HEAD)
graph LR
A["commit A"] --> B["commit B"]
B --> C["commit C"]
C --> D["commit D"]
D --> E["commit E"]
C -. "main" .-> C
E -. "feature (HEAD)" .-> E
Architecture or Flow Diagram
flowchart TD
Start["Start on main"] --> Create["git branch feature-x"]
Create --> Switch["git switch feature-x"]
Switch --> Work["Make changes and commit"]
Work --> Check{"Need another branch?"}
Check -->|Yes| SwitchBack["git switch main"]
SwitchBack --> Create2["git branch feature-y"]
Create2 --> Switch2["git switch feature-y"]
Switch2 --> Work2["Work on feature-y"]
Work2 --> List["git branch -a"]
Check -->|No| List
List --> Delete["git branch -d feature-x"]
Delete --> End["Clean branch state"]
Step-by-Step Guide / Deep Dive
Listing Branches
# List local branches (current branch marked with *)
git branch
# List all branches including remote tracking branches
git branch -a
# List remote branches only
git branch -r
# Show last commit on each branch
git branch -v
# Show branches merged into current branch
git branch --merged
# Show branches NOT merged into current branch
git branch --no-merged
Creating Branches
# Create a new branch from current HEAD
git branch feature-x
# Create a branch from a specific commit
git branch feature-x abc1234
# Create a branch from another branch
git branch feature-x main
# Create and switch in one command (most common)
git switch -c feature-x
# Legacy equivalent (still widely used)
git checkout -b feature-x
Switching Branches
# Switch to an existing branch
git switch feature-x
# Switch to main
git switch main
# Switch to previous branch (like cd -)
git switch -
# Switch and create if it doesn't exist
git switch -c new-branch
# Switch and discard local changes
git switch --discard-changes feature-x
Deleting Branches
# Delete a merged branch (safe — refuses if unmerged)
git branch -d feature-x
# Force delete an unmerged branch (dangerous)
git branch -D feature-x
# Delete a remote branch
git push origin --delete feature-x
# Prune local tracking branches that no longer exist on remote
git remote prune origin
Branch Naming Conventions
# Good naming patterns
git switch -c feature/user-authentication
git switch -c fix/login-timeout-bug
git switch -c hotfix/security-patch
git switch -c release/v2.1.0
git switch -c refactor/database-layer
# Avoid these
git switch -c my-branch # too vague
git switch -c fix # what are you fixing?
git switch -c temp # will be forgotten
git switch -c feature/FEAT-123 # redundant prefix
Production Failure Scenarios
| Scenario | Impact | Mitigation |
|---|---|---|
| Accidentally delete unmerged branch | Lost work | Use git branch -d (not -D); enable git reflog recovery |
| Switch with uncommitted changes | Work carried to wrong branch | Use git stash before switching, or git switch --discard-changes intentionally |
| Branch name collision with remote | Push/pull confusion | Use unique naming conventions; verify with git branch -a |
| Orphaned branches accumulate | Repository clutter | Regular git branch --merged cleanup; automate with CI |
| HEAD detached state | Confusion about where commits go | Use git switch -c recovery-branch to rescue detached commits |
Recovery: Deleted Branch
# Find the commit SHA from reflog
git reflog
# Recreate the branch at that commit
git branch recovered-branch <sha>
Trade-off Analysis
| Approach | Pros | Cons |
|---|---|---|
git branch + git switch | Explicit, clear intent | Two commands instead of one |
git checkout -b | Single command | Overloads checkout with branch creation |
| Short-lived branches | Easy to merge, minimal drift | Requires discipline to clean up |
| Long-lived branches | Stable integration target | Merge conflicts accumulate, diverges from main |
| Descriptive names | Self-documenting, searchable | Longer to type |
| Short names | Quick to type | Ambiguous, hard to track purpose |
Implementation Snippets
# Complete feature branch workflow
git switch main
git pull origin main
git switch -c feature/payment-integration
# ... work, commit, test ...
git push -u origin feature/payment-integration
# Cleanup after merge
git switch main
git pull origin main
git branch -d feature/payment-integration
git push origin --delete feature/payment-integration
# List stale branches (not merged in 30 days)
git branch --no-merged main --sort=-committerdate
# Batch delete merged branches
git branch --merged main | grep -v '^\*\|main\|develop' | xargs -n 1 git branch -d
Observability Checklist
- Logs: Track branch creation/deletion in CI/CD pipeline logs
- Metrics: Monitor branch age (stale branches indicate workflow issues)
- Alerts: Alert on branches older than 30 days without activity
- Traces: Link branch names to issue tracker IDs for traceability
- Dashboards: Display open branches per developer in team dashboards
Security & Compliance Considerations
- Branch names can appear in logs and URLs — avoid embedding secrets or sensitive project codenames
- Protected branches (main, release) should require PR approval before merging
- Use branch protection rules to prevent force pushes to shared branches
- Audit branch deletion with
git reflogfor compliance requirements - Consider signed commits on release branches for supply chain security
Common Pitfalls / Anti-Patterns
- Branch hoarding — keeping dozens of abandoned branches clutters the repository and confuses CI
- Naming without context —
fix-bugtells no one what was fixed or why - Switching with dirty working tree — uncommitted changes follow you to the new branch, causing confusion
- Deleting before verifying merge — always use
-d(safe delete) instead of-D(force delete) - Ignoring remote branches — local branches can diverge from their remote counterparts; use
git fetchregularly - Working directly on main — defeats the purpose of branching; always create a feature branch
Quick Recap Checklist
- List branches with
git branchandgit branch -a - Create branches with
git switch -c <name> - Switch branches with
git switch <name> - Delete merged branches with
git branch -d <name> - Delete remote branches with
git push origin --delete <name> - Use descriptive, hierarchical naming conventions
- Clean up stale branches regularly
- Recover deleted branches via
git reflog
Branch Architecture: Lightweight Pointers
Branches in Git are not copies of files — they are lightweight pointers to commit SHAs. Understanding this mental model explains why branching is instantaneous and why operations like git switch only change which commit your working tree points to.
graph LR
HEAD["HEAD"] -->|points to| Feature["feature (branch ref)"]
Feature -->|points to| E["commit E"]
E --> D["commit D"]
D --> C["commit C"]
C --> B["commit B"]
B --> A["commit A"]
Main["main (branch ref)"] -.->|also points to| C
classDef pointer color:#00fff9
class HEAD,Feature,Main pointer
The branch ref file (.git/refs/heads/feature) contains only a 40-character SHA. Switching branches means updating HEAD to point to a different ref file — no file copying, no history duplication.
Production Failure: Deleting Unmerged Branches
Scenario: A developer runs git branch -D feature/payment to clean up, not realizing the branch contained two weeks of unremerged payment integration work. The branch was never pushed to remote.
Impact: Complete loss of unremerged work. The commits become dangling objects, reachable only via git reflog for 90 days before garbage collection permanently removes them.
Mitigation:
- Always use
git branch -d(safe delete) which refuses to delete unmerged branches - Push feature branches to remote before local cleanup
- Run
git branch --no-mergedbefore any bulk deletion - Set up a pre-delete hook or alias that warns about unmerged branches
# Safe cleanup workflow
git branch --no-merged main # see what would be lost
git branch --merged main # safe to delete
git branch --merged main | grep -v '^\*\|main\|develop' | xargs git branch -d
Trade-offs: git switch vs git checkout
| Dimension | git switch | git checkout |
|---|---|---|
| Safety | Explicit intent — only switches branches | Overloaded — switches branches AND restores files, easy to misuse |
| Clarity | Self-documenting: switch means change branch | Ambiguous: checkout file vs checkout branch do different things |
| Git version | Requires Git 2.23+ (Aug 2019) | Available in all Git versions |
| Create + switch | git switch -c branch | git checkout -b branch |
| Discard changes | git switch --discard-changes branch | git checkout -- branch |
| Detached HEAD | git switch --detach <commit> | git checkout <commit> |
| Recommendation | Use for all branch switching | Use only on older Git versions or for file restoration |
Implementation: Branch Naming Convention Enforcement
Enforce consistent branch naming via a prepare-commit-msg or pre-commit hook:
#!/bin/bash
# .git/hooks/pre-commit — enforce branch naming conventions
BRANCH_NAME=$(git symbolic-ref --short HEAD 2>/dev/null)
if [ -z "$BRANCH_NAME" ]; then
exit 0 # detached HEAD, skip
fi
# Pattern: type/description (e.g., feature/login, fix/bug-123, hotfix/security)
PATTERN="^(feature|fix|hotfix|release|refactor|docs|chore)/[a-z0-9-]+$"
if ! echo "$BRANCH_NAME" | grep -qE "$PATTERN"; then
echo "ERROR: Branch name '$BRANCH_NAME' does not follow naming convention."
echo "Expected: type/description (e.g., feature/user-auth, fix/login-bug)"
echo "Valid types: feature, fix, hotfix, release, refactor, docs, chore"
exit 1
fi
Make it executable: chmod +x .git/hooks/pre-commit
Summary Checklist
- Branches are lightweight pointers, not file copies
- Use
git switchfor branch operations (Git 2.23+) - Use
git branch -d(safe) not-D(force) for deletion - Push branches to remote before local cleanup
- Enforce naming conventions with hooks
- Run
git branch --no-mergedbefore bulk deletion - Recover deleted branches via
git reflogwithin 90 days
Interview Questions
git branch and git checkout (or git switch)?git branch creates or lists branches but does not change your working directory. git switch (or git checkout) changes your working directory to match the target branch. The modern best practice is git switch -c to create and switch in one step.
git branch -d refuses to delete an unmerged branch, protecting your work. git branch -D force deletes it regardless. The commits aren't immediately lost — they remain reachable via git reflog for typically 90 days until garbage collection removes them.
A detached HEAD occurs when you check out a specific commit instead of a branch. Any new commits won't belong to any branch. To recover, create a new branch at the current commit: git switch -c recovery-branch.
Git branches are simply pointers to commit SHAs, not copies of files. Creating a branch means writing a 41-byte file (the SHA reference) to .git/refs/heads/. This is why branching is instantaneous even in massive repositories, unlike SVN which copies the entire tree.
Use git reflog to find the last commit SHA of the deleted branch, then recreate it: git branch recovered-branch <sha>. The reflog records every HEAD movement and is retained for 90 days by default.
Git stores each branch as a single file in .git/refs/heads/. The file contains only the 40-character SHA-1 hash of the commit it points to. This is why creating branches is instantaneous — Git simply writes one small file. The branch name maps to this file path, which is why branch names are case-sensitive on Linux but not on macOS/Windows due to filesystem differences.
A local branch lives in your repository and you control it directly. A remote tracking branch (e.g., origin/main) is a local copy of the branch state on the remote — it updates when you run git fetch. Remote tracking branches are read-only; you cannot check them out directly. They exist purely to compare your local state against the remote state.
git switch --discard-changes <branch> permanently discards uncommitted changes — they are gone. git stash temporarily saves changes to a stack, allowing you to recover them later with git stash pop. Use discard only when you intentionally want to abandon changes. Use stash when you need to switch context but might need those changes again.
The reflog (git reflog) records every position where HEAD has moved in your repository. It serves as a safety net for recovering lost commits. By default, reflog entries are retained for 90 days for reachable commits, and 30 days for unreachable commits (before garbage collection). Reflogs are local — they don't sync between machines or survive a fresh clone.
git fetch downloads remote commits, tags, and branches into your remote tracking branches — no local branches change. git pull does fetch + merge in one step — it updates your current local branch to incorporate remote changes. Fetch is safer for reviewing changes before integration; pull is faster for simple updates when you know the remote is ahead.
Branch protection rules (configured in GitHub, GitLab, Bitbucket) enforce that certain branches require pull request reviews, status checks, or approval before merging. Common settings: require 2 approvals, block force-push, require signed commits, require status checks to pass. This prevents direct commits to protected branches and ensures code review before integration.
git merge creates a merge commit that ties together the histories of both branches — preserves context, non-linear history, safe for shared branches. git rebase rewrites your branch to apply commits on top of the target branch, creating a linear history — cleaner but dangerous on shared/public branches because it rewrites commit SHAs. Rebase is best for local feature branch cleanup before PR merge.
When you check out a specific commit (not a branch), you enter detached HEAD — commits aren't attached to any branch. To recover: git switch -c recovery-branch creates a new branch at the current commit. Alternatively, git branch backup-branch preserves the commit reference without switching. If you've made commits while detached, the recovery branch command ensures those commits aren't lost.
Effective team workflow: 1) Create feature branch from updated main, 2) Work and commit locally, 3) Push to remote when ready for CI, 4) Open PR for review, 5) After approval, squash-merge to main, 6) Delete feature branch after merge. Key practices: rebase onto main before PR (not after), use protected main branch, require passing CI before merge, delete branches after merge to reduce clutter.
Short-lived branches (hours to a few days) minimize merge conflict accumulation and integration debt. Long-lived branches (weeks/months) diverge significantly from main, making eventual merge painful with numerous conflicts. Branch age also signals workflow problems — old branches often mean work was abandoned or integration was repeatedly deferred. Target: merge within 1-3 days for typical features.
Use git switch -t origin/feature-x to create a local branch that tracks the remote. This is equivalent to git switch -c feature-x --track origin/feature-x. The local branch will track the remote and you can push to it with git push (with push.autoSetupRemote = true configured).
-d (lowercase) is the safe delete — Git refuses to delete a branch if it contains unmerged commits, protecting your work from accidental loss. -D (uppercase) is the force delete — it deletes the branch regardless of merge status. Use -d first; only use -D when you're certain the branch is fully merged or the work is obsolete.
Use git branch -m old-name new-name to rename the current branch, or git branch -m old-name new-name specifying both. Only local branch names are renamed — the remote branch name must be deleted and recreated by pushing the renamed branch.
git branch -vv shows verbose output including upstream tracking relationship and ahead/behind count relative to the remote. For example: feature-x abc1234 [origin/feature-x: ahead 2, behind 1] means the local branch is 2 commits ahead and 1 behind the remote. This is the most detailed view for understanding the sync state between local and remote branches.
Use git branch --set-upstream-to=origin/feature-x or git push -u origin feature-x when first pushing. With push.autoSetupRemote = true configured, Git automatically sets up the tracking relationship for new branches. Once tracked, git push without arguments pushes to the correct remote branch.
Further Reading
- Git Branching - Pro Git Book — The definitive reference on Git branching from the official Pro Git book
- Atlassian Git Branching Tutorial — Comprehensive guide covering branch workflows and best practices
- GitHub: About Branches — GitHub’s official documentation on branch management and pull requests
- Git Branching Strategies — GitLab’s branching strategy and naming convention recommendations
- Learn Git Branching — Interactive visual tutorial for mastering Git branching concepts
Conclusion
Branches are Git’s superpower for parallel development. Understanding the lightweight nature of branches — they’re just pointers — makes everything else: merging, rebasing, strategies. Master branch basics first, and the rest of Git becomes far more intuitive.
Category
Related Posts
Master git add: Selective Staging, Patch Mode, and Staging Strategies
Master git add including selective staging, interactive mode, patch mode, and staging strategies for clean atomic commits in version control.
Git Cherry-Pick: Selectively Applying Commits
Master git cherry-pick to selectively apply commits between branches. Learn use cases, pitfalls, and best practices for targeted commit transplantation.
Rebase vs Merge: When to Use Each in Git
Decision framework for choosing between git rebase and git merge. Understand trade-offs, team conventions, history implications, and production best practices.