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.
Branches in Git are lightweight pointers to commits, not copies of files, which is why creating and switching branches is nearly instantaneous. The HEAD pointer indicates your current branch; git switch moves HEAD while git branch creates or lists pointers. Use git switch -c to create and checkout in one step. Delete merged branches safely with git branch -d; use -D to force delete unmerged branches. Recover deleted branches within 90 days via git reflog.
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
Branching adds overhead. Every branch means managing isolation, remembering to switch context, and eventually reconciling changes. For certain situations, that overhead outweighs the benefits entirely.
Trivial one-line changes do not need the ceremony of a branch. A typo fix in a README, a broken link correction, or a minor documentation improvement can go directly to main. Creating a branch for a single-character change means you spend more time on version control than on the actual fix. If the change is smaller than the effort of creating a branch, skip the branch.
Long-lived branches accumulate technical debt silently. A branch that lives for months diverges from main in small, hard-to-trace ways. By the time you merge, you may have dozens of conflicts spanning multiple files, and the logical intent of your changes is buried under layers of adaptation. Keep branches short. A feature branch that merges within a few days rarely creates problems. One that spans weeks starts accumulating drift that grows faster than you expect.
Personal preference divergence refers to cosmetic or stylistic changes that reflect individual taste rather than agreed standards. Reformatting code to match your own indentation preferences, renaming variables to suit your naming instincts, or adjusting spacing because “it looks better” — these changes create noise in the repository without improving functionality. If your team has not adopted a particular style, bring it up in discussion first. Branching for preferences that the rest of the team has not agreed to adopt turns every merge into an unnecessary debate.
Avoiding communication is the anti-pattern that causes the most damage over time. Branching is a technical mechanism for isolation, not a substitute for talking to your teammates. If you open branches without discussing the overall integration plan, you risk building features that conflict with work happening in parallel, duplicating effort, or merging changes that break assumptions the rest of the team is relying on. Use branches to execute decisions already made, not to avoid making them together.
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.