Learning Objectives

By the end of this module, you will be able to:

  1. Create, list, rename, and delete branches fluently
  2. Switch between branches and understand what happens to your working directory
  3. Explain why branching is cheap in Git (pointer mechanics from Module 4, applied)
  4. Handle uncommitted changes when switching branches
  5. 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-login

This 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 main

Create 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 branch

Create 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 shorthand

3. Listing Branches

Local Branches

git branch
#   feature-login
# * main              ← asterisk marks the current branch
#   fix-header

With Last Commit Info

git branch -v
#   feature-login a1b2c3d Add login form
# * main           e4f5a6b Merge pull request #42
#   fix-header     7c8d9e0 Fix header alignment

With 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 alignment

The [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-header

All Branches (Local + Remote)

git branch -a
#   feature-login
# * main
#   fix-header
#   remotes/origin/main
#   remotes/origin/feature-login
#   remotes/origin/fix-header

Filtering

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 branch

The --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 branch

Why switch was introduced: git checkout is overloaded — it switches branches, restores files, and detaches HEAD. The Git team split it into git switch (for branches) and git restore (for files) in Git 2.23 to reduce confusion. Both syntaxes work; this course prefers switch.

What Happens When You Switch

When you run git switch feature-login, Git does three things:

  1. Updates HEAD.git/HEAD now says ref: refs/heads/feature-login
  2. Updates the working directory — Git replaces your files with the versions from the commit that feature-login points to
  3. Updates the staging area — The index is reset to match the new commit

This means:

  • Files that exist on feature-login but not on main appear
  • Files that exist on main but not on feature-login disappear
  • 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 appeared

The 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 you

The 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-login

Option 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 changes

We'll cover git stash in depth in Module 12.

Option 3: Discard the changes

git restore app.py            # discard changes (destructive!)
git switch feature-login

Force Switch (Dangerous)

git switch -f feature-login    # or git checkout -f

This discards ALL uncommitted changes and forces the switch. Use with extreme caution.


6. Renaming Branches

Rename the Current Branch

git branch -m new-name

Rename Any Branch

git branch -m old-name new-name

Rename + 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-name

The 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 master

Then update the default branch on GitHub/GitLab through the web UI.


7. Deleting Branches

Safe Delete

git branch -d feature-login

This 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-login

Deletes 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-login

Cleaning 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 origin

This 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 -d

8. Branch Naming Conventions

Teams adopt naming conventions to keep branches organized. The most common pattern uses prefix categories:

Common Prefixes

PrefixPurposeExample
feature/New functionalityfeature/user-auth
bugfix/ or fix/Bug repairsbugfix/login-crash
hotfix/Urgent production fixeshotfix/security-patch
release/Release preparationrelease/2.1.0
chore/Maintenance, toolingchore/update-deps
docs/Documentation onlydocs/api-reference
test/Adding or fixing teststest/auth-integration
refactor/Code restructuringrefactor/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

  1. Use lowercase and hyphensfeature/user-auth, not Feature/User_Auth
  2. Keep it short but descriptivefeature/auth is too vague; feature/implement-jwt-based-user-authentication-with-refresh-tokens is too long
  3. Include a ticket number if your team uses them — it's the best way to maintain context
  4. 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
  5. 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

TypeLifespanExamples
Short-livedHours to daysFeature branches, bugfix branches
Long-livedWeeks to permanentmain, 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 main

This prevents your feature branch from being based on stale code.


Command Reference

CommandDescription
git branchList local branches
git branch -aList all branches (local + remote)
git branch -vList branches with last commit
git branch -vvList 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 --mergedList branches merged into current branch
git branch --no-mergedList 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 --pruneRemove 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
# * main

Step 1: Create Branches Without Switching

git branch feature/farewell
git branch feature/multilingual

Checkpoint:

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 app

All three branches point to the same commit. HEAD is on main.

Step 2: Work on the Farewell Branch

git switch feature/farewell

Checkpoint:

cat .git/HEAD
# ref: refs/heads/feature/farewell

Add 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.py

Step 3: Switch to Main and Observe

git switch main
ls
# README.md  app.py          ← farewell_utils.py is GONE
cat 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 app

Step 4: Work on the Multilingual Branch

git switch feature/multilingual
cat > 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 app

Three 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 branch

Each 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.py
git 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 note

Clean up the change:

git restore app.py

Step 7: Rename a Branch

git switch feature/farewell
git branch -m feature/goodbye-message

Checkpoint:

git branch
#   feature/multilingual
# * feature/goodbye-message    ← renamed
#   main

Step 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 branch

Step 9: Recover a Deleted Branch

git reflog
# Find the entry for "Add multilingual greeting support" — note its hash (333ccc)
 
git switch -c feature/multilingual 333ccc

Checkpoint:

git log --oneline --all --graph
# The multilingual branch is back, with its commit intact

Step 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/multilingual

Challenge

  1. Create a release/1.0 branch from main. Make a commit on it ("Bump version to 1.0"). Then switch back to main, make a different commit. Visualize the divergence with git log --oneline --graph --all.

  2. Try switching between branches with uncommitted changes to a file that exists only on one branch (e.g., farewell_utils.py on feature/goodbye-message). What happens? Why?

  3. Set up a branch naming convention for yourself: create branches feature/add-logging, bugfix/null-check, and chore/update-readme. Observe how they sort with git branch --list "feature/*".

Cleanup

rm -rf ~/git-branching-lab

Common Pitfalls & Troubleshooting

PitfallExplanation
"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 mainIf 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 commitsCommits 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 thingsUse git switch for branches and git restore for files. They were split for exactly this reason.
Stale remote-tracking branchesAfter branches are deleted on the remote, run git fetch --prune to clean up local references.
Working on the wrong branchCheck git branch or git status frequently. Your shell prompt can be configured to show the current branch (see Pro Tips).

Pro Tips

  1. 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) $
    
  2. Use git switch - like cd -. It goes to the previous branch. When you're flipping between main and a feature branch, this saves typing.

  3. Create branches from up-to-date main.

    git switch main
    git pull
    git switch -c feature/new-thing

    This prevents your feature from being based on stale code and reduces merge conflicts later.

  4. 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").

  5. Use --merged for cleanup days.

    git switch main
    git branch --merged | grep -v "main" | xargs git branch -d

    This safely deletes all local branches that have been merged into main.

  6. 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 type git switch feature/<TAB> and see all feature branches.

  7. 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
It creates a new branch pointer called 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
A file is created at .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
Git updates HEAD, reads the tree from the target branch's commit, and replaces the files in your working directory. Files unique to the target branch appear; files unique to the source branch disappear; files that differ between branches change their content. The staging area is also updated to match.

4. When will Git refuse to let you switch branches?

Answer
When you have uncommitted changes to files that are different between the two branches. The switch would overwrite your changes. Git refuses to protect your work. You must commit, stash, or discard the changes first.

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
Switches to the previously checked-out branch, like 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
Lists branches whose commits are all reachable from the current branch (i.e., they've been merged). These branches can usually be safely deleted since their work is already incorporated.

9. Why should feature branches be short-lived?

Answer
The longer a branch lives, the more the mainline diverges from it. This increases the chance and severity of merge conflicts. Short-lived branches (merged within days) minimize conflict surface and keep the codebase converging.

10. What's the recommended naming convention for branches?

Answer
Use a prefix indicating the type of work (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.