Learning Objectives
By the end of this module, you will be able to:
- Create, list, rename, and delete branches fluently
- Switch between branches and understand what happens to your working directory
- Explain why branching is cheap in Git (pointer mechanics from Module 4, applied)
- Handle uncommitted changes when switching branches
- Follow branch naming conventions used by professional teams
1. Why Branch?
In any real project, multiple things happen at once: a new feature is in development, a bug needs fixing, a release is being prepared. Without branches, all of this work would pile into one stream of commits, making it impossible to ship a bug fix without also shipping a half-finished feature.
Branches let you diverge from the main line of development, work independently, and merge back when you're ready.
In older VCS tools (CVS, SVN), creating a branch meant copying the entire directory tree — slow and expensive. In Git, creating a branch means writing a 41-byte file. It's instantaneous.
The Mental Model
From Module 4, you know that a branch is just a pointer to a commit. When you "create a branch," Git creates a tiny file in .git/refs/heads/ containing a commit hash. When you "switch to a branch," Git moves HEAD to point to that branch and updates your working directory to match the commit the branch points to.
Before branching:
○ ◄── ○ ◄── ○
C1 C2 C3
▲
main ◄── HEAD
After "git branch feature":
○ ◄── ○ ◄── ○
C1 C2 C3
▲
main ◄── HEAD
▲
feature ← new pointer, same commit
After "git switch feature" + 2 commits:
○ ◄── ○
C4 C5
╱ ▲
○ ◄── ○ ◄── ○ feature ◄── HEAD
C1 C2 C3
▲
main
No files were copied. No directories were duplicated. The only thing that changed was which pointer HEAD follows and where each pointer sits in the commit graph.
2. Creating Branches
Create a Branch (Without Switching)
git branch feature-loginThis creates the pointer. HEAD stays where it is (on your current branch). You're not "on" the new branch yet.
Proof:
cat .git/refs/heads/feature-login
# same hash as your current commit
cat .git/HEAD
# ref: refs/heads/main ← still on mainCreate a Branch at a Specific Commit
git branch hotfix-123 a1b2c3d # branch from a specific commit
git branch experiment HEAD~3 # branch from 3 commits ago
git branch release-v2 origin/main # branch from a remote-tracking branchCreate and Switch in One Step
This is what you'll use 95% of the time:
git switch -c feature-login # modern syntax (Git 2.23+)
git checkout -b feature-login # older syntax (still works everywhere)Both create the branch AND move HEAD to it.
Create a Branch Tracking a Remote Branch
When a remote has a branch you don't have locally:
git switch feature-login
# If 'feature-login' doesn't exist locally but 'origin/feature-login' does,
# Git automatically creates a local branch tracking the remote one.Or explicitly:
git switch -c feature-login origin/feature-login
git switch --track origin/feature-login # equivalent shorthand3. Listing Branches
Local Branches
git branch
# feature-login
# * main ← asterisk marks the current branch
# fix-headerWith Last Commit Info
git branch -v
# feature-login a1b2c3d Add login form
# * main e4f5a6b Merge pull request #42
# fix-header 7c8d9e0 Fix header alignmentWith Tracking Information
git branch -vv
# feature-login a1b2c3d [origin/feature-login] Add login form
# * main e4f5a6b [origin/main] Merge pull request #42
# fix-header 7c8d9e0 Fix header alignmentThe [origin/feature-login] shows which remote branch each local branch tracks. You may also see:
* main e4f5a6b [origin/main: ahead 2] ← you have 2 unpushed commits
dev a1b2c3d [origin/dev: behind 3] ← remote has 3 commits you don't
feat 7c8d9e0 [origin/feat: ahead 1, behind 2] ← diverged
Remote-Tracking Branches
git branch -r
# origin/main
# origin/feature-login
# origin/fix-headerAll Branches (Local + Remote)
git branch -a
# feature-login
# * main
# fix-header
# remotes/origin/main
# remotes/origin/feature-login
# remotes/origin/fix-headerFiltering
git branch --list "feature-*" # local branches matching a pattern
git branch -r --list "origin/release*" # remote branches matching a pattern
git branch --merged # branches already merged into current branch
git branch --no-merged # branches NOT yet merged into current branchThe --merged and --no-merged flags are useful for cleanup — branches that are merged can usually be safely deleted.
4. Switching Branches
Modern Syntax (Git 2.23+)
git switch main # switch to an existing branch
git switch -c new-branch # create and switch
git switch - # switch to the previous branch (like cd -)Older Syntax
git checkout main # switch to an existing branch
git checkout -b new-branch # create and switch
git checkout - # switch to the previous branchWhy
switchwas introduced:git checkoutis overloaded — it switches branches, restores files, and detaches HEAD. The Git team split it intogit switch(for branches) andgit restore(for files) in Git 2.23 to reduce confusion. Both syntaxes work; this course prefersswitch.
What Happens When You Switch
When you run git switch feature-login, Git does three things:
- Updates HEAD —
.git/HEADnow saysref: refs/heads/feature-login - Updates the working directory — Git replaces your files with the versions from the commit that
feature-loginpoints to - Updates the staging area — The index is reset to match the new commit
This means:
- Files that exist on
feature-loginbut not onmainappear - Files that exist on
mainbut not onfeature-logindisappear - Files that differ between branches change their content
git switch main
ls
# README.md app.py
git switch feature-login
ls
# README.md app.py login.py auth_middleware.py ← new files appearedThe files aren't being copied from somewhere — Git is reading the tree object for that commit from its database and writing the files to your working directory.
5. Switching with Uncommitted Changes
This is one of the most common friction points for beginners.
When Git Allows the Switch
If your uncommitted changes don't conflict with the branch you're switching to, Git carries them along:
# On main, edit README.md (which is identical on both branches)
echo "note to self" >> README.md
git switch feature-login
# Switched to branch 'feature-login'
# M README.md ← changes came with youThe modification to README.md is still in your working directory. Git didn't discard it.
When Git Blocks the Switch
If your uncommitted changes would be overwritten by the switch, Git refuses:
# On main, edit app.py (which is DIFFERENT on feature-login)
echo "debug line" >> app.py
git switch feature-login
# error: Your local changes to the following files would be overwritten by checkout:
# app.py
# Please commit your changes or stash them before you switch branches.Git is protecting your work. You have three options:
Option 1: Commit first
git add app.py
git commit -m "WIP: debug line"
git switch feature-loginOption 2: Stash (save for later)
git stash # saves changes to a temporary stack
git switch feature-login
# ... do work on feature-login ...
git switch main
git stash pop # restores the saved changesWe'll cover git stash in depth in Module 12.
Option 3: Discard the changes
git restore app.py # discard changes (destructive!)
git switch feature-loginForce Switch (Dangerous)
git switch -f feature-login # or git checkout -fThis discards ALL uncommitted changes and forces the switch. Use with extreme caution.
6. Renaming Branches
Rename the Current Branch
git branch -m new-nameRename Any Branch
git branch -m old-name new-nameRename + Update Remote
Renaming locally doesn't rename the remote branch. You need to delete the old remote branch and push the new one:
git branch -m old-name new-name
git push origin --delete old-name
git push -u origin new-nameThe master → main Rename
A common operation when working with older repositories:
git branch -m master main
git push -u origin main
git push origin --delete masterThen update the default branch on GitHub/GitLab through the web UI.
7. Deleting Branches
Safe Delete
git branch -d feature-loginThis refuses if the branch has commits that haven't been merged into the current branch:
error: The branch 'feature-login' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-login'.
This is a safety net — it prevents you from accidentally losing work.
Force Delete
git branch -D feature-loginDeletes the branch regardless of merge status. The commits aren't immediately destroyed — they remain in the object database and are recoverable via git reflog for about 2 weeks.
Delete a Remote Branch
git push origin --delete feature-loginCleaning Up Stale Remote-Tracking Branches
After someone deletes a branch on the remote, your local remote-tracking reference (origin/feature-login) still exists until you clean it up:
git fetch --prune
# or
git remote prune originThis removes local remote-tracking branches that no longer exist on the remote.
Finding Merged Branches to Delete
# List branches already merged into main
git branch --merged main
# feature-login
# fix-header
# * main
# Delete them all (except main)
git branch --merged main | grep -v "main" | xargs git branch -d8. Branch Naming Conventions
Teams adopt naming conventions to keep branches organized. The most common pattern uses prefix categories:
Common Prefixes
| Prefix | Purpose | Example |
|---|---|---|
feature/ | New functionality | feature/user-auth |
bugfix/ or fix/ | Bug repairs | bugfix/login-crash |
hotfix/ | Urgent production fixes | hotfix/security-patch |
release/ | Release preparation | release/2.1.0 |
chore/ | Maintenance, tooling | chore/update-deps |
docs/ | Documentation only | docs/api-reference |
test/ | Adding or fixing tests | test/auth-integration |
refactor/ | Code restructuring | refactor/db-layer |
Including Ticket Numbers
Many teams include the ticket/issue number:
feature/JIRA-1337-user-auth
bugfix/GH-42-login-crash
fix/PROJ-891-null-pointer
This makes it easy to trace a branch back to its requirements.
Rules of Thumb
- Use lowercase and hyphens —
feature/user-auth, notFeature/User_Auth - Keep it short but descriptive —
feature/authis too vague;feature/implement-jwt-based-user-authentication-with-refresh-tokensis too long - Include a ticket number if your team uses them — it's the best way to maintain context
- Don't reuse branch names — after merging and deleting
feature/user-auth, don't create a new branch with the same name for different work - Agree on conventions as a team — consistency matters more than which specific convention you pick
What NOT to Name Branches
# Bad:
my-branch ← meaningless
test ← too generic, conflicts with common names
fix ← fix what?
wip ← work in progress of what?
asdf ← ...
john/stuff ← not descriptive
9. Practical Branching Patterns
The Feature Branch Workflow
The most common pattern in professional teams:
1. Start on main (up to date)
2. Create a feature branch
3. Work, commit, push
4. Open a pull request
5. Get code review
6. Merge into main
7. Delete the feature branch
8. Repeat
main: ○──○──○─────────────○──○── (merges land here)
\ /
feature: ○──○──○──○── (work happens here)
(deleted after merge)
Short-Lived vs. Long-Lived Branches
| Type | Lifespan | Examples |
|---|---|---|
| Short-lived | Hours to days | Feature branches, bugfix branches |
| Long-lived | Weeks to permanent | main, develop, release/1.x |
Most branches should be short-lived. Long-lived branches accumulate merge conflicts and drift from the mainline. The sooner you merge, the fewer conflicts you'll face.
The "Start Fresh" Discipline
Before starting new work, always ensure you're up to date:
git switch main
git pull # fetch + merge/rebase
git switch -c feature/new-thing # branch from the latest mainThis prevents your feature branch from being based on stale code.
Command Reference
| Command | Description |
|---|---|
git branch | List local branches |
git branch -a | List all branches (local + remote) |
git branch -v | List branches with last commit |
git branch -vv | List branches with tracking info |
git branch <name> | Create a branch (don't switch) |
git branch <name> <commit> | Create a branch at a specific commit |
git branch -d <name> | Delete a branch (safe — refuses if unmerged) |
git branch -D <name> | Force-delete a branch |
git branch -m <new> | Rename the current branch |
git branch -m <old> <new> | Rename any branch |
git branch --merged | List branches merged into current branch |
git branch --no-merged | List branches not yet merged |
git switch <branch> | Switch to a branch |
git switch -c <branch> | Create and switch |
git switch - | Switch to the previous branch |
git switch -c <branch> <start> | Create a branch from a specific commit/ref |
git checkout <branch> | Switch (older syntax) |
git checkout -b <branch> | Create and switch (older syntax) |
git push origin --delete <branch> | Delete a remote branch |
git fetch --prune | Remove stale remote-tracking branches |
Hands-On Lab: Branching in Action
This lab creates multiple branches, switches between them, and observes how the working directory changes with each switch.
Setup
mkdir ~/git-branching-lab
cd ~/git-branching-lab
git init
cat > app.py << 'EOF'
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
print(greet("World"))
EOF
cat > README.md << 'EOF'
# Greeter App
A simple greeting application.
EOF
git add .
git commit -m "Initial commit: basic greeter app"Checkpoint:
git log --oneline
# abc1234 Initial commit: basic greeter app
git branch
# * mainStep 1: Create Branches Without Switching
git branch feature/farewell
git branch feature/multilingualCheckpoint:
git branch
# feature/farewell
# feature/multilingual
# * main ← still on main
git log --oneline --all --graph
# * abc1234 (HEAD -> main, feature/multilingual, feature/farewell) Initial commit: basic greeter appAll three branches point to the same commit. HEAD is on main.
Step 2: Work on the Farewell Branch
git switch feature/farewellCheckpoint:
cat .git/HEAD
# ref: refs/heads/feature/farewellAdd a farewell function:
cat > app.py << 'EOF'
def greet(name):
return f"Hello, {name}!"
def farewell(name):
return f"Goodbye, {name}!"
if __name__ == "__main__":
print(greet("World"))
print(farewell("World"))
EOF
git add app.py
git commit -m "Add farewell function"Add a second commit:
cat > farewell_utils.py << 'EOF'
FAREWELLS = ["Goodbye", "See you later", "Farewell", "Bye"]
def random_farewell():
import random
return random.choice(FAREWELLS)
EOF
git add farewell_utils.py
git commit -m "Add farewell utilities module"Checkpoint:
git log --oneline
# 222bbb Add farewell utilities module
# 111aaa Add farewell function
# abc1234 Initial commit: basic greeter app
ls
# README.md app.py farewell_utils.pyStep 3: Switch to Main and Observe
git switch main
ls
# README.md app.py ← farewell_utils.py is GONEcat app.py
# def greet(name):
# return f"Hello, {name}!"
#
# if __name__ == "__main__":
# print(greet("World"))The farewell function is gone too. Your working directory reflects main, which knows nothing about the farewell work.
Checkpoint:
git log --oneline --all --graph
# * 222bbb (feature/farewell) Add farewell utilities module
# * 111aaa Add farewell function
# | * abc1234 (HEAD -> main, feature/multilingual) Initial commit: basic greeter appStep 4: Work on the Multilingual Branch
git switch feature/multilingualcat > app.py << 'EOF'
GREETINGS = {
"en": "Hello",
"es": "Hola",
"fr": "Bonjour",
"de": "Hallo",
"ja": "こんにちは",
}
def greet(name, lang="en"):
greeting = GREETINGS.get(lang, GREETINGS["en"])
return f"{greeting}, {name}!"
if __name__ == "__main__":
print(greet("World"))
print(greet("World", "es"))
print(greet("World", "ja"))
EOF
git add app.py
git commit -m "Add multilingual greeting support"Checkpoint:
git log --oneline --all --graph
# * 333ccc (HEAD -> feature/multilingual) Add multilingual greeting support
# | * 222bbb (feature/farewell) Add farewell utilities module
# | * 111aaa Add farewell function
# |/
# * abc1234 (main) Initial commit: basic greeter appThree branches have diverged from the same base commit. Each has its own independent line of work.
Step 5: Rapid Switching
Watch the working directory change:
git switch main
cat app.py | head -2
# def greet(name):
# return f"Hello, {name}!"
git switch feature/farewell
cat app.py | head -5
# def greet(name):
# return f"Hello, {name}!"
#
# def farewell(name):
# return f"Goodbye, {name}!"
git switch feature/multilingual
cat app.py | head -3
# GREETINGS = {
# "en": "Hello",
# "es": "Hola",
git switch -
# Switched to branch 'feature/farewell' ← '-' goes to previous branchEach switch is instantaneous. Git is reading different tree objects from its database and writing the corresponding files.
Step 6: Attempting to Switch with Uncommitted Changes
git switch feature/farewell
# Make a change to a file that differs between branches
echo "# Extra note" >> app.pygit switch feature/multilingual
# error: Your local changes to the following files would be overwritten by checkout:
# app.py
# Please commit your changes or stash them before you switch branches.Git blocks the switch because app.py is different between the branches AND you have local changes.
Resolve with stash:
git stash
git switch feature/multilingual
# success!
git switch feature/farewell
git stash pop
# Your changes are back
cat app.py | tail -1
# # Extra noteClean up the change:
git restore app.pyStep 7: Rename a Branch
git switch feature/farewell
git branch -m feature/goodbye-messageCheckpoint:
git branch
# feature/multilingual
# * feature/goodbye-message ← renamed
# mainStep 8: Delete a Branch
git switch main
# Try safe delete on an unmerged branch
git branch -d feature/multilingual
# error: The branch 'feature/multilingual' is not fully merged.Git refuses because the multilingual work hasn't been merged into main. Force it:
git branch -D feature/multilingual
# Deleted branch feature/multilingual (was 333ccc).Checkpoint:
git branch
# * main
# feature/goodbye-message
git log --oneline --all --graph
# The multilingual commits are no longer visible from any branchStep 9: Recover a Deleted Branch
git reflog
# Find the entry for "Add multilingual greeting support" — note its hash (333ccc)
git switch -c feature/multilingual 333cccCheckpoint:
git log --oneline --all --graph
# The multilingual branch is back, with its commit intactStep 10: Check Merged vs. Unmerged Branches
git switch main
git branch --merged
# * main ← only main (nothing has been merged)
git branch --no-merged
# feature/goodbye-message
# feature/multilingualChallenge
-
Create a
release/1.0branch frommain. Make a commit on it ("Bump version to 1.0"). Then switch back tomain, make a different commit. Visualize the divergence withgit log --oneline --graph --all. -
Try switching between branches with uncommitted changes to a file that exists only on one branch (e.g.,
farewell_utils.pyonfeature/goodbye-message). What happens? Why? -
Set up a branch naming convention for yourself: create branches
feature/add-logging,bugfix/null-check, andchore/update-readme. Observe how they sort withgit branch --list "feature/*".
Cleanup
rm -rf ~/git-branching-labCommon Pitfalls & Troubleshooting
| Pitfall | Explanation |
|---|---|
| "I can't switch — local changes would be overwritten" | You have uncommitted changes to files that differ between branches. Commit, stash, or discard before switching. |
Accidentally committing on main | If you meant to be on a feature branch: git branch feature-oops (saves the commit), then git reset --hard HEAD~1 on main (moves main back). We cover git reset in Module 11. |
| Deleting a branch and losing commits | Commits aren't immediately gone — use git reflog to find the hash and recreate the branch. They survive ~2 weeks before git gc. |
| "Branch already exists" | You're trying to create a branch with a name that's taken. Either delete the old one first or pick a different name. |
Confused by checkout doing multiple things | Use git switch for branches and git restore for files. They were split for exactly this reason. |
| Stale remote-tracking branches | After branches are deleted on the remote, run git fetch --prune to clean up local references. |
| Working on the wrong branch | Check git branch or git status frequently. Your shell prompt can be configured to show the current branch (see Pro Tips). |
Pro Tips
-
Show the branch in your shell prompt. Most shell frameworks (Oh My Zsh, Starship, bash-git-prompt) show the current Git branch in your prompt. This prevents "wrong branch" mistakes:
~/project (feature/auth) $ -
Use
git switch -likecd -. It goes to the previous branch. When you're flipping betweenmainand a feature branch, this saves typing. -
Create branches from up-to-date
main.git switch main git pull git switch -c feature/new-thingThis prevents your feature from being based on stale code and reduces merge conflicts later.
-
Delete branches after merging. Branches are cheap to create but clutter accumulates. After a PR is merged, delete the branch both locally and remotely. GitHub can auto-delete branches after merge (Settings → General → "Automatically delete head branches").
-
Use
--mergedfor cleanup days.git switch main git branch --merged | grep -v "main" | xargs git branch -dThis safely deletes all local branches that have been merged into
main. -
Tab completion is your friend. In bash/zsh, typing
git switch feat<TAB>auto-completes branch names. This is why descriptive prefixes help — you can typegit switch feature/<TAB>and see all feature branches. -
Alias frequent operations.
git config --global alias.co checkout git config --global alias.sw switch git config --global alias.br branch git config --global alias.new "switch -c" # git new feature/foo
Quiz / Self-Assessment
1. What does git branch feature-x do, and what does it NOT do?
Answer
feature-x pointing to the current commit. It does NOT switch to the new branch — HEAD remains on whatever branch you were on. Use git switch -c feature-x to create and switch in one step.
2. What physically happens in .git when you create a branch?
Answer
.git/refs/heads/<branch-name> containing the 40-character SHA-1 hash of the commit the branch points to. That's it — a 41-byte text file.
3. What happens to your working directory when you switch branches?
Answer
4. When will Git refuse to let you switch branches?
Answer
5. What's the difference between git branch -d and git branch -D?
Answer
-d (safe delete) refuses to delete a branch if its commits haven't been merged into the current branch. -D (force delete) deletes regardless. Use -d by default; -D only when you're certain you want to discard unmerged work.
6. How do you rename a branch?
Answer
git branch -m new-name renames the current branch. git branch -m old-name new-name renames any branch. If the branch is pushed, you also need to delete the old remote branch and push the new name.
7. What does git switch - do?
Answer
cd - for directories. Useful for flipping back and forth between two branches.
8. What does git branch --merged show, and why is it useful?
Answer
9. Why should feature branches be short-lived?
Answer
10. What's the recommended naming convention for branches?
Answer
feature/, bugfix/, hotfix/, chore/), followed by a ticket number and short description: feature/JIRA-1337-user-auth. Use lowercase and hyphens. Agree on conventions as a team — consistency matters most.