Git Merge vs Fast-Forward vs Rebase — A Visual Guide
Understanding how your commits come together — and why it matters.
Table of Contents
- The Three Strategies at a Glance
- Merge Commit (Three-Way Merge)
- Fast-Forward Merge
- Rebase
- Side-by-Side Comparison
- Interactive Rebase — The Power Tool
- Squash Merge — The Middle Ground
- When and Why to Use Each
- The Golden Rules
1. The Three Strategies at a Glance
When you want to integrate changes from one branch into another, Git gives you fundamentally different strategies. Each produces a different commit history, and choosing the right one affects readability, traceability, and collaboration.
| Strategy | Command | Preserves Branch History? | Creates Extra Commit? | Rewrites History? |
|---|---|---|---|---|
| Merge Commit | git merge feature | Yes | Yes (merge commit) | No |
| Fast-Forward | git merge --ff-only feature | No | No | No |
| Rebase | git rebase main | No | No | Yes |
2. Merge Commit (Three-Way Merge)
A merge commit is created when Git combines two branches that have diverged — both branches have commits the other doesn’t. Git finds the common ancestor, compares both tips against it, and creates a new merge commit with two parents.
Command
git checkout main
git merge feature
Before Merge
Both main and feature have progressed independently since they diverged at commit B.
After Merge
Commit M is the merge commit. It has two parents: G (from main) and E (from feature). The feature branch topology is fully preserved.
Pros
- Complete history — You can always see exactly when a branch was created, what it contained, and when it was merged.
- Non-destructive — No existing commits are modified. Safe for shared/public branches.
- Traceable —
git log --mergesshows every integration point. Great for auditing. - Conflict resolution is contained — Conflicts are resolved once in the merge commit.
Cons
- Noisy history — Frequent merges create a tangled web of criss-crossing lines in
git log --graph. - Extra commits — Every merge adds a commit that carries no “real” code change.
- Harder to bisect —
git bisectcan get confused navigating merge commits.
3. Fast-Forward Merge
A fast-forward merge happens when the target branch has no new commits since the feature branch was created. Git simply moves the branch pointer forward — no merge commit is created.
Command
git checkout main
git merge feature # Git auto-detects FF possibility
# or explicitly:
git merge --ff-only feature # Fails if FF is not possible
Before Merge
main has not moved since feature was branched off at C.
After Merge (Fast-Forward)
The main pointer simply jumps forward to E. The history is perfectly linear — as if the feature work was done directly on main.
Pros
- Clean, linear history — No merge commits cluttering the log.
- Simple to read —
git logshows a straight line of commits. - Easy to bisect —
git bisectworks flawlessly on a linear history. - No conflict — If FF is possible, there are zero conflicts by definition.
Cons
- No record of the branch — You lose the information that these commits were developed on a separate branch.
- Requires no divergence — Only works when the target branch is strictly behind. If anyone else pushed to
main, this won’t work. - Can’t always be used — In active teams,
mainalmost always has new commits, making FF impossible without a rebase first.
When Fast-Forward Is Not Possible
Here, main has moved forward (F, G) since feature branched off. A fast-forward is impossible — Git must create a merge commit or you must rebase first.
4. Rebase
Rebase replays your branch’s commits on top of the target branch, one by one. It rewrites commit hashes, creating brand-new commits with the same changes but different ancestry.
Command
git checkout feature
git rebase main
# Then fast-forward main:
git checkout main
git merge feature # This will now be a fast-forward
Before Rebase
After Rebase
The original commits C, D, E are gone. New commits C', D', E' have the same diffs but sit on top of G. Their hashes are different because their parent changed.
After Fast-Forward Merge
The end result is a perfectly linear history with no merge commit.
Pros
- Cleanest possible history — A straight line of meaningful commits.
- Easy to review — Each commit stands on its own, making code review simpler.
- Bisect-friendly — Linear history makes debugging with
git bisecttrivial. - Eliminates “merge bubbles” — No criss-cross patterns in the graph.
Cons
- Rewrites history — Commits get new hashes. Dangerous on shared branches.
- Conflicts multiply — You may have to resolve conflicts for each replayed commit rather than once.
- Force-push required — After rebasing a pushed branch, you need
git push --force-with-lease, which can confuse collaborators. - Lost context — The original branch point and merge point are erased from history.
The Danger Zone
# NEVER do this on a shared branch:
git checkout main
git rebase feature # Rewrites main's history — breaks everyone!
Rebasing rewrites history. If other people have based work on the old commits, their branches will diverge from the rewritten history, causing chaos.
5. Side-by-Side Comparison
Same Feature, Three Strategies
Starting point:
After git merge feature (Merge Commit):
After git rebase main + git merge feature (Rebase + FF):
After git merge --squash feature (Squash Merge):
Where S is a single commit containing all changes from C and D combined.
Comparison Table
| Aspect | Merge Commit | Fast-Forward | Rebase + FF | Squash Merge |
|---|---|---|---|---|
| History Shape | Non-linear (branching) | Linear | Linear | Linear |
| Commit Count | All + 1 merge commit | All original commits | All (new hashes) | 1 squashed commit |
| Branch Context | Preserved | Lost | Lost | Lost |
| Safety | Safe for all branches | Safe for all branches | Unsafe on shared branches | Safe for all branches |
| Conflict Resolution | Once | N/A | Per commit | Once |
git bisect | Harder | Easy | Easy | Coarse (1 commit) |
| Code Review | Good | Good | Best | Coarse |
6. Interactive Rebase — The Power Tool
Interactive rebase lets you rewrite, reorder, squash, or drop commits before integrating.
Command
git rebase -i main
This opens an editor:
pick a1b2c3d Add user model
pick e4f5g6h Fix typo in user model
pick i7j8k9l Add user validation
pick m0n1o2p WIP - debugging
You can change the commands:
pick a1b2c3d Add user model
squash e4f5g6h Fix typo in user model # Merge into previous commit
pick i7j8k9l Add user validation
drop m0n1o2p WIP - debugging # Remove this commit entirely
Result
Before — 4 messy commits:
After interactive rebase — 2 clean commits:
Interactive rebase is invaluable for cleaning up your work before sharing it, turning a messy stream-of-consciousness into a logical, reviewable sequence.
7. Squash Merge — The Middle Ground
A squash merge condenses all commits from a branch into a single commit on the target branch. It doesn’t create a merge commit — it creates a regular commit.
Command
git checkout main
git merge --squash feature
git commit -m "Add user authentication feature"
Before
After
S contains all the changes from C, D, and E but as a single commit.
Pros
- Clean main branch — One commit per feature, easy to scan.
- Safe — No history rewriting on shared branches.
- Good for noisy branches — Hides “WIP”, “fix typo”, “oops” commits.
Cons
- Lost granularity — Individual commits from the feature branch are gone. Can’t bisect within the feature.
- Lost attribution — If multiple people worked on the branch, only the squash committer shows up.
- Branch becomes stale — After squash merge, the feature branch’s commits aren’t recognized as merged, causing Git to think the branch still has unmerged work.
8. When and Why to Use Each
Use Merge Commit When…
- You’re merging a long-lived feature branch or release branch and want a clear record.
- You’re working in a regulated environment where audit trails matter.
- You’re integrating shared branches (e.g., merging
releaseintomain) — never rewrite shared history. - Multiple people contributed to the branch and you want to preserve attribution.
# Example: merging a release branch
git checkout main
git merge --no-ff release/v2.1
Use
--no-ffto force a merge commit even when fast-forward is possible, preserving the branch topology.
Use Fast-Forward When…
- You’re the only person working on a small branch.
- The branch has 1–2 commits and main hasn’t moved.
- You want the simplest possible history.
- You’re working in a trunk-based development model with very short-lived branches.
# Example: merging a quick fix
git checkout main
git merge --ff-only hotfix/typo
Use Rebase When…
- You want to update your feature branch with the latest changes from main before merging.
- You’re preparing a branch for a clean pull request — rebasing produces a tidy diff.
- You’re working on a personal branch that no one else has pulled.
- Your team follows a “rebase before merge” workflow.
# Example: updating feature branch before PR
git checkout feature/auth
git rebase main
git push --force-with-lease
Use Squash Merge When…
- Your feature branch is full of WIP and fixup commits.
- You want one commit per feature on
mainfor a clean, scannable log. - The individual commits don’t add value for future debugging.
- You’re merging a pull request where the granularity doesn’t matter.
# Example: squash merging a PR
git checkout main
git merge --squash feature/signup-form
git commit -m "feat: add signup form with validation"
Decision Flowchart
9. The Golden Rules
-
Never rebase a public/shared branch. If others have pulled it, rewriting history will cause conflicts for everyone.
-
Rebase local, merge shared. Use rebase to keep your personal branches up to date. Use merge commits to integrate shared work.
-
Use
--force-with-lease, never--force. If you must force-push after a rebase,--force-with-leaseensures you don’t overwrite someone else’s work. -
Clean up before sharing. Use interactive rebase (
git rebase -i) to squash fixup commits and write meaningful messages before opening a PR. -
Be consistent. Pick a strategy as a team and stick with it. Mixed strategies lead to an unreadable history.
-
When in doubt, merge. A merge commit is always safe. It never rewrites history and always preserves context.
Quick Reference
# Safe merge with history
git merge --no-ff feature
# Fast-forward only (fails if not possible)
git merge --ff-only feature
# Rebase feature onto latest main
git checkout feature && git rebase main
# Interactive rebase to clean up last 5 commits
git rebase -i HEAD~5
# Squash merge
git merge --squash feature && git commit
# Safe force-push after rebase
git push --force-with-lease
Choose the strategy that serves your team’s workflow, not just your preference. A clean history is valuable, but so is a safe and traceable one. The best teams know when each tool is the right one.