Lesson 4: Merging & Conflicts — Combining Work
Learn how Git combines branches with fast-forward and three-way merges. Understand why conflicts happen and master the step-by-step process to resolve them confidently.
Lesson 4: Merging & Conflicts — Combining Work 🔀
Introduction
You've created branches for parallel development (Lesson 3), made commits, and now it's time to bring that work back together. Merging is how Git combines the histories of two branches. It sounds simple, but it's where many developers panic — especially when they see their first merge conflict.
Here's the truth: conflicts are normal, not failures. They're Git saying, "Hey, you and your teammate both changed the same line — which version should I keep?" Understanding why conflicts happen and how Git merges branches will transform you from someone who fears merging to someone who handles it confidently.
In this lesson, we'll explore:
- The two types of merges: fast-forward and three-way merge
- What creates merge conflicts and why they're inevitable in collaboration
- Step-by-step conflict resolution
- How to read conflict markers and make smart decisions
- Merge commits and what they represent in your history
💡 Mental Model: Think of merging like combining two edited versions of a Google Doc. If you edited paragraph 1 and your friend edited paragraph 3, it's easy — both changes fit together. But if you both rewrote paragraph 2 differently, someone needs to decide which version (or combination) to keep.
Core Concepts
Understanding Merge Types 🔄
Git has two fundamentally different ways to merge branches, chosen automatically based on your branch structure:
1. Fast-Forward Merge ⚡
This is the simplest merge possible. It happens when:
- You're on branch
main - You want to merge branch
feature - No new commits have been made to
mainsincefeaturebranched off
BEFORE MERGE:
A---B---C (main)
\
D---E (feature)
AFTER FAST-FORWARD:
A---B---C---D---E (main, feature)
Git simply moves the main pointer forward to point at commit E. No new commit is created because there's nothing to "combine" — feature already contains all of main's history plus new work.
Command: git merge feature (from main branch)
💡 Why it's called "fast-forward": The branch pointer just slides forward along the existing commit path, like fast-forwarding a video. No actual merging of content happens.
2. Three-Way Merge 🔺
This happens when both branches have diverged — new commits exist on both branches since they split:
BEFORE MERGE:
A---B---C (main)
\
D---E (feature)
AFTER THREE-WAY MERGE:
A---B---C-------F (main)
\ /
D---E---- (feature)
Git creates a new merge commit (F) that has two parents: commit C (from main) and commit E (from feature). This commit represents the combined state of both branches.
Why "three-way"? Git looks at three snapshots:
- Common ancestor (B) — where the branches split
- Target branch tip (C) — latest commit on
main - Source branch tip (E) — latest commit on
feature
By comparing all three, Git determines:
- Changes made only in
main→ keep them - Changes made only in
feature→ keep them - Changes made in both to the same lines → CONFLICT! 💥
What Are Merge Conflicts? ⚠️
A merge conflict occurs when Git cannot automatically decide which changes to keep because:
- Same file, same lines were modified in both branches
- One branch deleted a file that the other branch modified
- One branch renamed a file that the other branch edited
Git stops the merge and marks the conflicted areas in your files, asking you to make the decision.
🧠 Mnemonic: C.A.S.H. for when conflicts happen:
- Changes
- At
- Same
- Hunk (section of code)
Important: Conflicts are not errors — they're Git being cautious. It's saying, "I don't know which version you want, so I'm letting you choose."
Conflict Markers 🏷️
When a conflict occurs, Git modifies your files to show both versions:
<<<<<<< HEAD
This is the version from your current branch (main)
=======
This is the version from the branch you're merging (feature)
>>>>>>> feature
Breakdown:
<<<<<<< HEAD: Marks the start of your current branch's version=======: Separates the two versions>>>>>>> feature: Marks the end of the incoming branch's version
Your job: Edit the file to keep what you want, then delete all the markers.
The Merge Process Step-by-Step 📋
Here's the complete workflow for merging:
1. Switch to target branch
↓
2. Run git merge <source-branch>
↓
3. Git attempts automatic merge
↓
├─→ SUCCESS → Merge complete (fast-forward or merge commit)
└─→ CONFLICT → Git pauses, marks conflicts
↓
4. Open conflicted files
↓
5. Find conflict markers (<<<, ===, >>>)
↓
6. Edit to resolve (keep one version, combine, or rewrite)
↓
7. Delete all conflict markers
↓
8. git add <resolved-files>
↓
9. git commit (completes the merge)
💡 Key insight: After resolving conflicts, you must stage the files (git add) and commit to finalize the merge. The commit message is pre-filled with "Merge branch 'feature' into main".
Examples with Explanations
Example 1: Fast-Forward Merge (Clean) ✨
Scenario: You created a fix-typo branch from main, made two commits, and no one else has pushed to main.
$ git checkout main
Switched to branch 'main'
$ git merge fix-typo
Updating a1b2c3d..e4f5g6h
Fast-forward
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
What happened:
- Git saw that
mainhad no new commits sincefix-typobranched - It simply moved
main's pointer forward tofix-typo's latest commit - No merge commit was created
- Your history is linear and clean
Visual:
Before: main → C
\
fix-typo → D → E
After: main, fix-typo → C → D → E
💡 Tip: Fast-forward merges are common in solo projects or when you merge quickly before others push changes.
Example 2: Three-Way Merge (No Conflicts) 🌐
Scenario: You worked on feature-login while a teammate pushed updates to main. Different files were modified.
$ git checkout main
$ git pull origin main # Get teammate's changes
$ git merge feature-login
Merge made by the 'recursive' strategy.
auth.js | 15 +++++++++++++++
styles.css | 3 ++-
2 files changed, 17 insertions(+), 1 deletion(-)
What happened:
- Git found divergent histories (both branches had new commits)
- It compared the common ancestor,
main's tip, andfeature-login's tip - Changes were in different files or different parts of files
- Git automatically merged and created a merge commit
- Your editor may open for you to confirm/edit the merge commit message
Visual:
Before:
C (main)
\
D---E (feature-login)
After:
C-------F (main)
\ /
D---E (feature-login)
Commit F has two parents (C and E), preserving both lines of development.
Example 3: Merge with Conflicts 💥
Scenario: You and a teammate both edited config.js, changing the same line.
$ git checkout main
$ git merge feature-config
Auto-merging config.js
CONFLICT (content): Merge conflict in config.js
Automatic merge failed; fix conflicts and then commit the result.
Step 1: Check status
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: config.js
Step 2: Open config.js and see:
const API_URL = "https://api.example.com";
<<<<<<< HEAD
const TIMEOUT = 5000; // Increased for slow networks
=======
const TIMEOUT = 3000; // Reduced for faster responses
>>>>>>> feature-config
const MAX_RETRIES = 3;
Step 3: Decide which version to keep (or combine):
Option A - Keep main's version:
const API_URL = "https://api.example.com";
const TIMEOUT = 5000; // Increased for slow networks
const MAX_RETRIES = 3;
Option B - Keep feature's version:
const API_URL = "https://api.example.com";
const TIMEOUT = 3000; // Reduced for faster responses
const MAX_RETRIES = 3;
Option C - Combine/compromise:
const API_URL = "https://api.example.com";
const TIMEOUT = 4000; // Balanced for performance and reliability
const MAX_RETRIES = 3;
Step 4: Stage and commit
$ git add config.js
$ git commit # Default message: "Merge branch 'feature-config'"
✅ Merge complete! The merge commit records your resolution.
Example 4: Aborting a Merge 🚫
Scenario: You started a merge, saw conflicts, and realize you're not ready to resolve them.
$ git merge feature-complex
CONFLICT (content): Merge conflict in multiple files...
$ git status
# Shows many conflicted files
$ git merge --abort
What happened:
git merge --abortcancels the merge completely- Your working directory returns to the state before
git merge - No changes are lost — both branches remain intact
- You can merge again later when ready
💡 When to abort: If you see conflicts you don't understand, or realize you're missing information to resolve them properly, there's no shame in aborting and regrouping.
Common Mistakes ⚠️
❌ Mistake 1: Forgetting to Stage Resolved Files
Wrong:
$ # Edit conflict in file.js, resolve it
$ git commit # ERROR: file.js still marked as conflicted
Right:
$ # Edit and resolve conflict
$ git add file.js # Mark as resolved
$ git commit # Now works!
Why: Git needs you to explicitly confirm you've resolved the conflict by staging the file.
❌ Mistake 2: Leaving Conflict Markers in Code
Wrong:
// You commit this:
const value = 10;
<<<<<<< HEAD
const result = value * 2;
=======
const result = value * 3;
>>>>>>> feature
Your code now has syntax errors because you forgot to delete the markers!
Right: Always remove <<<<<<<, =======, and >>>>>>> markers completely.
❌ Mistake 3: Not Testing After Resolving Conflicts
Just because you resolved conflicts doesn't mean your code works:
$ git add .
$ git commit
$ # OOPS: Didn't test! Code is broken.
Right:
$ git add .
$ npm test # or your test command
$ # Tests pass? Good!
$ git commit
Why: You might have accidentally kept incompatible pieces from both branches.
❌ Mistake 4: Merging Without Checking Current Branch
Wrong:
$ git merge main # While on feature branch
# You just merged main INTO feature (opposite of intended)
Right:
$ git branch # Check where you are
$ git checkout main # Go to target branch
$ git merge feature # Merge feature INTO main
🧠 Remember: git merge <branch> merges <branch> into your current branch.
❌ Mistake 5: Committing Without Reviewing All Conflicts
With multiple conflicted files, it's easy to miss one:
$ git status # Shows 3 conflicted files
$ # Resolve 2 files, forget the 3rd
$ git add file1.js file2.js
$ git commit # ERROR: file3.js still conflicted
Right: Use git status before committing to ensure all files show as resolved (no longer in "Unmerged paths" section).
Did You Know? 🤔
🔍 The "recursive" strategy: When Git says "Merge made by the 'recursive' strategy", it's using an algorithm that can handle complex cases, including when the common ancestor itself is a merge commit (requiring recursion to find the true base).
🎯 Merge commits are permanent markers: They're the only commits in Git with two parents, making it easy to track when branches were integrated. Tools like git log --graph show this visually.
🛡️ Conflict prevention: Frequent, small merges create fewer conflicts than one giant merge after weeks of divergence. Merge main into your feature branch regularly to stay up-to-date.
Key Takeaways 🎯
- Fast-forward merge happens when no divergence exists — just moves the pointer forward
- Three-way merge creates a merge commit when both branches have new commits
- Conflicts are normal — they occur when the same lines are modified in both branches
- Conflict markers (
<<<<<<<,=======,>>>>>>>) show both versions; you must remove them after choosing - Resolution workflow: Resolve conflicts →
git add→git commit git merge --abortsafely cancels a merge in progress- Always test your code after resolving conflicts before committing
- Check your current branch before merging to avoid merging the wrong direction
📚 Further Study
Git Documentation - Basic Merging: https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging
- Official guide with detailed explanations and visuals
Atlassian Git Tutorial - Merge Conflicts: https://www.atlassian.com/git/tutorials/using-branches/merge-conflicts
- Practical examples and troubleshooting strategies
Git Merge Strategies Explained: https://www.atlassian.com/git/tutorials/using-branches/merge-strategy
- Deep dive into different merge strategies and when to use them
📋 Quick Reference Card
+----------------------------------+----------------------------------------+
| COMMAND | PURPOSE |
+----------------------------------+----------------------------------------+
| git merge <branch> | Merge <branch> into current branch |
| git merge --abort | Cancel merge in progress |
| git status | Check for conflicts during merge |
| git add <file> | Mark conflict as resolved |
| git commit | Complete merge after resolving |
| git log --graph --oneline | Visualize merge history |
| git merge --no-ff <branch> | Force merge commit (no fast-forward) |
+----------------------------------+----------------------------------------+
CONFLICT MARKERS:
<<<<<<< HEAD → Your current branch version starts
======= → Separator
>>>>>>> branch-name → Incoming branch version ends
RESOLUTION STEPS:
1. git merge <branch>
2. Git reports conflicts
3. Open conflicted files
4. Edit to resolve (remove markers!)
5. git add <files>
6. git commit
MERGE TYPES:
Fast-Forward → No new commits on target branch (linear)
Three-Way → Both branches have new commits (creates merge commit)
🔧 Try this: Create a test repository with two branches, make conflicting changes to the same line in a file, and practice resolving the conflict. Do this 3-5 times until the process feels natural!
💡 Pro tip: Use git log --oneline --graph --all after merging to visualize how your branches came together. Understanding the visual representation reinforces the mental model.