Merge Strategies
Merging combines changes from one branch into another. Git provides three main merge approaches: fast-forward (linear), three-way merge (preserves branch history), and squash merge (condensed). Choosing the right strategy shapes your project's commit history.
Overview
flowchart TD
A["git merge feature"] --> B{Can fast-forward?}
B -->|Yes| C["Fast-Forward Merge<br/>(no merge commit)"]
B -->|No| D{Strategy?}
D -->|Default| E["Three-Way Merge<br/>(creates merge commit)"]
D -->|"--squash"| F["Squash Merge<br/>(single commit)"]
D -->|"--no-ff"| G["No-FF Merge<br/>(forced merge commit)"]
Fast-Forward Merge
Happens when main has no new commits since the branch was created. Git simply moves the pointer forward.
Before
gitGraph
commit id: "C1"
commit id: "C2"
branch feature
commit id: "C3"
commit id: "C4"
After git merge feature
gitGraph
commit id: "C1"
commit id: "C2"
commit id: "C3"
commit id: "C4"
git checkout main
git merge feature
Updating a1b2c3d..d4e5f6a
Fast-forward
src/feature.js | 25 ++++++++++
1 file changed, 25 insertions(+)
Result: Linear history, no merge commit. Clean but loses branch context.
Three-Way Merge
Happens when both branches have new commits. Git creates a merge commit with two parents.
Before
gitGraph
commit id: "C1"
commit id: "C2"
branch feature
commit id: "C3"
commit id: "C4"
checkout main
commit id: "C5"
After git merge feature
gitGraph
commit id: "C1"
commit id: "C2"
branch feature
commit id: "C3"
commit id: "C4"
checkout main
commit id: "C5"
merge feature id: "M1 (merge)"
git checkout main
git merge feature
Merge made by the 'ort' strategy.
src/feature.js | 25 ++++++++++
1 file changed, 25 insertions(+)
Result: Preserves full branch history. Merge commit shows where branches joined.
No-Fast-Forward Merge (--no-ff)
Forces a merge commit even when fast-forward is possible. Useful for preserving branch structure.
git checkout main
git merge --no-ff feature -m "Merge feature/auth into main"
Why Use --no-ff?
| With Fast-Forward | With --no-ff |
|---|---|
| Linear history, clean but flat | Clear "feature bubbles" in graph |
| Can't tell where a feature started/ended | Easy to see and revert entire features |
| Default behavior | Many teams enforce this as policy |
Squash Merge
Condenses all feature branch commits into a single commit on the target branch.
git checkout main
git merge --squash feature
git commit -m "feat: add user authentication (squashed)"
Before
feature: C3 → C4 → C5 → C6 (4 messy commits)
main: C1 → C2
After Squash Merge
main: C1 → C2 → S1 (one clean commit containing all feature changes)
Result: Cleanest history on main, but loses individual commit history from the branch.
Comparison
| Strategy | Merge Commit? | Branch History | Use When |
|---|---|---|---|
| Fast-forward | No | Lost | Simple, linear changes; solo work |
| Three-way | Yes | Preserved | Collaborative development; parallel work |
| No-FF | Yes (forced) | Preserved | Team policy; want to see feature boundaries |
| Squash | No (single new) | Condensed | Messy branch commits; clean main history |
How to Merge
Step-by-Step
# 1. Switch to the target branch
git checkout main
# 2. Ensure you're up to date
git pull origin main
# 3. Merge the feature branch
git merge feature/login
# 4. Push the merged result
git push origin main
# 5. Delete the merged branch (cleanup)
git branch -d feature/login
git push origin --delete feature/login
Abort a Merge
# If a merge goes wrong — cancel and go back
git merge --abort
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
CONFLICT messages | Both branches modified same lines | See Conflict Resolution |
Already up to date | Nothing new to merge | Verify you're on the right branch |
| Unwanted merge commit | Used merge when you wanted rebase | Use git rebase instead (next page) |
| Merge created wrong history | Used fast-forward when --no-ff was better | Can't easily undo — set as default next time |
Set --no-ff as Default
# Always create merge commits (recommended for teams)
git config --global merge.ff false
Best Practices
- Use
--no-ffonmain— keeps feature boundaries visible in the graph - Use squash for messy branches — "WIP", "fix typo", "oops" don't belong in main history
- Always pull before merging — ensures you merge into the latest code
- Delete branches after merging — reduces clutter
- Test before merging to main — ensure the feature works in the merged state
What's Next
- Rebase Explained — An alternative to merging for cleaner history
- Conflict Resolution — Handle merge conflicts confidently