06 - Branching and Merging
#06 - Branching and Merging
##Why Branches Exist
Imagine four developers on the same project. Without branches, they all commit to the same main codebase simultaneously. Developer A breaks something halfway through her feature. Developer B’s half-finished login page goes live. Everyone is blocked, nothing is testable.
Branches solve this.Each developer works in isolation. When their feature is done and tested, it merges into main. Main stays clean and deployable at all times.
1
2
3
4
5
6
7
8
Without branches: With branches:
A, B, C, D all commit to main feature-A merge
feature-B merge
main: ????? feature-C merge
broken half the time feature-D merge
main: always works
##What a Branch Actually Is
A branch is just a lightweight pointer to a commit. Creating a branch does not copy any files. It creates a tiny label that points at the current commit.
1
2
3
4
5
6
7
8
9
10
11
12
13
Before creating a branch:
commit A commit B commit C
main (pointer)
HEAD (where you are)
After: git checkout -b feature-login
commit A commit B commit C
main feature-login (new pointer, same commit)
HEAD
As you make new commits on feature-login, that pointer moves forward. main stays put.
1
2
3
4
5
6
After 2 commits on feature-login:
commit A commit B commit C commit D commit E
main feature-login (moved forward)
HEAD
This is why branches are so cheap — Git is just moving a label, not copying hundreds of files.
##Creating and Switching Branches
###Set up a practice project first
1
2
3
4
mkdir branch_practice && cd branch_practice && git init
echo "<html><body>Home Page</body></html>" > index.html
git add index.html
git commit -m "Initial commit: home page"
###Create a branch
1
git branch feature-header
The branch is created but you are still on main. Verify:
1
git branch
Expected:
1
2
feature-header
* main the * shows where you are
###Switch to the branch
1
2
3
git checkout feature-header
#modern syntax:
git switch feature-header
Verify:
1
git branch
Expected:
1
2
* feature-header now you are here
main
###Shortcut: create and switch in one command
1
2
3
git checkout -b feature-footer
#modern syntax:
git switch -c feature-footer
This is what you will use 99% of the time.
##Working on a Branch
Now make commits on feature-header:
1
2
3
4
5
6
7
8
9
10
11
git checkout -b feature-header #create fresh if needed
echo "<header><nav>Home | About | Contact</nav></header>" > header.html
git add header.html
git commit -m "Add site navigation header"
echo "header { background: #333; color: white; }" >> style.css
git add style.css
git commit -m "Style the navigation header"
git log --oneline
Expected:
1
2
3
b4c5d6e Style the navigation header
a3b4c5d Add site navigation header
9f8e7d6 Initial commit: home page
Now switch back to main:
1
2
git checkout main
ls
Notice: header.html is gone. The files on main are exactly as you left them. The branch-specific files exist only on feature-header. Switch back and they reappear:
1
2
3
git checkout feature-header
ls
#header.html is back
This is the key value of branches: complete isolation until you are ready to merge.
##Viewing All Branches and History
1
git branch -a
Expected:
1
2
3
* feature-header
main
remotes/origin/main if connected to a remote
Visual graph of all branches:
1
git log --oneline --graph --decorate --all
Expected:
1
2
3
* b4c5d6e (HEAD -> feature-header) Style the navigation header
* a3b4c5d Add site navigation header
* 9f8e7d6 (main) Initial commit: home page
You can see feature-header is 2 commits ahead of main.
##Merging: Bringing Work Back to Main
Always merge INTO the target branch. Switch to main first:
1
2
git checkout main
git merge feature-header
###Fast-Forward Merge
In this case, main has not changed since we created feature-header. Git does a fast-forward merge— it simply moves the main pointer forward to match feature-header. No extra commit is created.
Expected output:
1
2
3
4
5
6
Updating 9f8e7d6..b4c5d6e
Fast-forward
header.html | 1 +
style.css | 1 +
2 files changed, 2 insertions(+)
create mode 100644 header.html
1
git log --oneline
Expected:
1
2
3
b4c5d6e (HEAD -> main, feature-header) Style the navigation header
a3b4c5d Add site navigation header
9f8e7d6 Initial commit: home page
main and feature-header now point to the same commit — the merge moved main forward.
###Three-Way Merge
A three-way merge happens when both branches have new commitssince the branch point. Git cannot just move a pointer — it has to create a new merge commit that combines both histories.
Let’s set this up:
1
2
3
4
5
6
7
8
9
10
11
12
13
#Commit something on main
git checkout main
echo "<footer>Copyright 2026</footer>" > footer.html
git add footer.html
git commit -m "Add footer to main"
#Create a branch and commit there too
git checkout -b feature-about
echo "<h1>About Us</h1>" > about.html
git add about.html
git commit -m "Add about page"
git log --oneline --graph --all
Expected — you can see the two branches diverge:
1
2
3
4
5
* c7d8e9f (HEAD -> feature-about) Add about page
| * d6e7f8a (main) Add footer to main
|/
* b4c5d6e Style the navigation header
...
Now merge feature-about into main:
1
2
git checkout main
git merge feature-about
Git opens your editor for a merge commit message. The default is fine — save and close (:wq in vi).
Expected:
1
2
3
4
Merge made by the 'ort' strategy.
about.html | 1 +
1 file changed, 1 insertion(+)
create mode 100644 about.html
1
git log --oneline --graph
Expected — you can see the two branches join:
1
2
3
4
5
6
* e1f2a3b (HEAD -> main) Merge branch 'feature-about'
|\
| * c7d8e9f Add about page
* | d6e7f8a Add footer to main
|/
* b4c5d6e Style the navigation header
###Delete the Branch After Merging
The branch pointer is no longer needed once merged:
1
git branch -d feature-about
The commits still exist in main — only the label is deleted.
Use -D (capital) to force-delete an unmerged branch:
1
git branch -D feature-experiment #deletes even if not merged
##Branch Naming Conventions
Use consistent prefixes so everyone on the team understands a branch’s purpose at a glance:
1
2
3
4
5
feature/user-authentication
feature/dark-mode-toggle
bugfix/crash-on-empty-login
hotfix/security-patch-xss
release/v2.1.0
Avoid vague names like my-branch, test, fix, wip.
##Remote Branches
###Push a branch to GitHub/GitLab
1
2
3
4
5
6
7
git checkout -b feature-contact
echo "<form><input type='email'/><button>Submit</button></form>" > contact.html
git add contact.html
git commit -m "Add contact form page"
git push -u origin feature-contact
Expected:
1
Branch 'feature-contact' set up to track remote branch 'feature-contact' from 'origin'.
Your branch is now visible on GitHub. Others can see it, review it, or collaborate on it.
###Fetch and work on a teammate’s branch
Your teammate created feature-analytics and pushed it. To work on it:
1
2
3
4
git fetch
git checkout --track origin/feature-analytics
#shortcut that also works:
git checkout feature-analytics
###View all remote branches
1
git branch -r
Expected:
1
2
3
origin/main
origin/feature-contact
origin/feature-analytics
###Delete a remote branch
1
git push origin --delete feature-contact
Then delete your local copy:
1
git branch -d feature-contact
##Part 2: Merge Conflicts
##What Causes a Conflict?
A conflict happens when two branches modify the exact same linesof the same file. Git can automatically merge changes to different lines or different files. But when two people both edit line 47 of app.py, Git cannot decide which version to keep — so it stops and asks you.
Common scenarios:
- Two developers edit the same function
- One branch deletes a file the other branch modified
- Both branches add code at the exact same position
##Conflict Markers — Reading the File
When a conflict occurs, Git writes special markers directly into the conflicted file:
1
2
3
4
5
<<<<<<< HEAD
This is the version from YOUR current branch
=======
This is the version from the BRANCH BEING MERGED IN
>>>>>>> feature-branch-name
Everything between <<<<<<< HEAD and ======= is your branch’s version.
Everything between ======= and >>>>>>> is the incoming branch’s version.
You must manually decide what the final content should be and delete all three marker lines.
##Step-by-Step Conflict Example
Let’s create a real conflict and resolve it.
###Step 1: Set up the conflict
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mkdir conflict_practice && cd conflict_practice && git init
echo "Welcome to the homepage" > index.html
git add index.html
git commit -m "Add homepage"
#Branch A: change the homepage message
git checkout -b branch-a
echo "Welcome to our amazing product!" > index.html
git add index.html
git commit -m "Update homepage headline - branch A"
#Branch B: also change the homepage message (from the SAME starting point)
git checkout main
git checkout -b branch-b
echo "Welcome! Discover what we can do for you." > index.html
git add index.html
git commit -m "Update homepage headline - branch B"
###Step 2: Merge the first branch (easy)
1
2
git checkout main
git merge branch-a
This works fine — fast-forward merge.
###Step 3: Merge the second branch (conflict!)
1
git merge branch-b
Expected:
1
2
3
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
###Step 4: See which files conflict
1
git status
Expected:
1
2
3
4
5
6
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
###Step 5: Open and read the conflict
1
cat index.html
Expected:
1
2
3
4
5
<<<<<<< HEAD
Welcome to our amazing product!
=======
Welcome! Discover what we can do for you.
>>>>>>> branch-b
Now you decide. Four options:
Option A — Keep your version (HEAD):
1
echo "Welcome to our amazing product!" > index.html
Option B — Keep the incoming version:
1
echo "Welcome! Discover what we can do for you." > index.html
Option C — Keep both:
1
printf "Welcome to our amazing product!\nWelcome! Discover what we can do for you.\n" > index.html
Option D — Write something entirely new(often the best choice):
1
echo "Welcome — discover our amazing product today!" > index.html
###Step 6: Complete the merge
After editing, verify there are NO conflict markers left in the file:
1
2
cat index.html
#should show only your chosen content, no <<<< or >>>>
Then mark as resolved and commit:
1
2
git add index.html
git status
Expected:
1
2
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
1
2
git commit
#Git opens editor with pre-filled merge commit message — just save and close
1
git log --oneline --graph
Expected: The two branches are now joined at the merge commit.
##Multiple File Conflicts
If several files conflict, resolve them one at a time:
1
2
3
4
git status
#both modified: app.py
#both modified: config.json
#both modified: README.md
Fix each file, remove all conflict markers, then:
1
2
3
4
git add app.py
git add config.json
git add README.md
git commit #one single commit resolves all
##Aborting a Merge
Changed your mind mid-conflict? Get back to the state before the merge:
1
2
git merge --abort
git status #back to clean state
Use this when you need to stop and think, or when someone else should resolve it.
##Resolving Conflicts in VSCode
If you use VSCode, it shows conflicts as clickable buttons instead of raw text:
- Accept Current Change— keep the HEAD version
- Accept Incoming Change— keep the branch-being-merged version
- Accept Both Changes— insert both (review carefully)
- Compare Changes— side-by-side view
VSCode 1.69+ also has a 3-way merge editor: left panel shows incoming, right panel shows current, bottom panel shows the result (which you can edit freely).
Click the button, edit the result, save the file, then git add and git commit as normal.
##Strategies for Fewer Conflicts
| Strategy | How it helps |
|---|---|
| Keep branches short-lived | Less divergence = less conflict chance |
| Pull from main frequently | Stay close to the latest codebase |
| Communicate with teammates | “I am editing auth.py this morning” |
| Keep PRs small | A 50-line PR rarely conflicts; a 2000-line PR always does |
| Split work into different files | Two people editing different files cannot conflict |
##5-Conflict Drill
Practice until conflict resolution feels routine:
1
2
3
4
5
6
7
8
9
10
11
12
#Round 1-5: for each round
#1. Create main with a file
#2. Create branch-x and branch-y, both editing the same line
#3. Merge branch-x into main (easy)
#4. Merge branch-y — conflict!
#5. Resolve it using a different strategy each time:
# Round 1: keep main's version
# Round 2: keep incoming version
# Round 3: keep both
# Round 4: write something new
# Round 5: use VSCode merge editor
#6. git log --oneline --graph to verify clean history
##Summary
| Command | What it does |
|---|---|
git branch |
List local branches |
git checkout -b <name> |
Create and switch to a new branch |
git checkout <name> |
Switch to an existing branch |
git merge <branch> |
Merge a branch into your current branch |
git merge --abort |
Cancel a merge in progress |
git branch -d <name> |
Delete a merged branch |
git push -u origin <name> |
Push branch to remote |
git push origin --delete <name> |
Delete a remote branch |
git log --oneline --graph --all |
Visual history of all branches |
Next: 07-advanced-workflows.md — Stash, rebase, cherry-pick, and pull requests