The Same Goal, Different History
Both git merge and git rebase integrate changes from one branch into another. The difference is in how they handle commit history — and that difference has real consequences for readability, debugging, and collaboration.
Git Merge: Preserving History
git merge creates a new commit that joins two branch histories. It's non-destructive: no existing commits are changed.
Before merge:
main: A --- B --- C
feature: \ D --- E
After git merge main into feature:
main: A --- B --- C
feature: \ D --- E --- M (merge commit)
\___________/
M is the merge commit with two parents: E (tip of feature) and C (tip of main). All history is preserved exactly as it happened.
git checkout feature
git merge main
# or: git merge main --no-ff # force a merge commit even for fast-forwards
Fast-forward merge: If main hasn't diverged (no new commits since feature branched), git can simply move the pointer forward without creating a merge commit:
Before:
main: A --- B
feature: \ C --- D
After git checkout main && git merge feature (fast-forward):
main: A --- B --- C --- D
feature: \________/ (now same as main)
Use --no-ff to always create a merge commit, preserving the fact that a feature branch existed.
Git Rebase: Linear History
git rebase replays commits from one branch on top of another. It rewrites history — the commits get new SHAs.
Before:
main: A --- B --- C
feature: \ D --- E
After git rebase main (run on feature branch):
main: A --- B --- C
feature: \ D' --- E' (D and E reapplied on top of C)
D' and E' have the same changes as D and E, but different parent commits (and therefore different SHAs). The branch history is now linear — as if you had branched from C all along.
git checkout feature
git rebase main
Handling Conflicts
Both approaches require resolving conflicts when the same lines were changed differently.
During merge:
git merge main
# CONFLICT: resolve files
git add resolved-files
git merge --continue
# or: git merge --abort (cancel the merge)
During rebase:
git rebase main
# CONFLICT on commit D: resolve files
git add resolved-files
git rebase --continue
# Conflict on commit E: resolve again
git add resolved-files
git rebase --continue
# or: git rebase --abort (cancel, go back to pre-rebase state)
Rebase conflicts can appear multiple times — once per commit being replayed. With merge, you resolve all conflicts once in the merge commit.
Interactive Rebase: Cleaning Up History
git rebase -i (interactive) lets you rewrite, squash, reorder, and edit commits before sharing them:
git rebase -i HEAD~4 # interactive rebase of last 4 commits
This opens an editor:
pick a1b2c3 Add user authentication
pick d4e5f6 Fix typo in auth module
pick g7h8i9 Add token refresh logic
pick j0k1l2 WIP: fix edge case
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = edit commit message
# e, edit <commit> = stop for amending
# s, squash <commit> = meld into previous commit
# f, fixup <commit> = meld, discard log message
# d, drop <commit> = remove commit
You can change to:
pick a1b2c3 Add user authentication
fixup d4e5f6 Fix typo in auth module ← squash into previous
pick g7h8i9 Add token refresh logic
drop j0k1l2 WIP: fix edge case ← delete this commit
Result: clean two-commit history, typo fix absorbed into the original commit, WIP commit gone.
The Golden Rule of Rebase
Never rebase commits that have been pushed to a shared branch.
When you rebase, you're rewriting history — creating new commits with new SHAs. If teammates have based work on your original commits, their git histories diverge from yours. Merging becomes a mess.
# ❌ Don't do this if others have pulled from main
git checkout main
git rebase feature # rewrites main history
# ✅ Rebase is safe here
git checkout feature
git rebase main # rewrites feature (not yet shared or force-push to your own branch)
When is force-push acceptable? When working on your own feature branch that no one else is tracking:
git rebase -i HEAD~3 # clean up your feature branch
git push --force-with-lease origin feature # safer than --force: fails if remote changed
--force-with-lease is safer than --force: it fails if the remote was updated since your last fetch, preventing accidental overwrites.
Comparison Table
| Aspect | Merge | Rebase |
|---|---|---|
| History shape | Non-linear (branching) | Linear |
| Commit SHAs | Unchanged | Rewritten |
| Merge commit | Yes (usually) | No |
| Conflict resolution | Once, in merge commit | Per commit |
| Safe to use on shared branches | ✅ Yes | ❌ No |
| Good for feature branches | ✅ Yes (no-ff merge) | ✅ Yes (before push) |
| Debugging with git bisect | Works | Works better (linear) |
| CHANGELOG readability | Shows feature as unit | Harder to see features |
Team Workflows
Feature Branch Workflow (Merge)
# Start feature
git checkout -b feature/user-auth main
# Work, commit, work, commit
git commit -m "Add login endpoint"
git commit -m "Add JWT validation"
# Merge back when done
git checkout main
git merge --no-ff feature/user-auth -m "Merge feature/user-auth"
git branch -d feature/user-auth
This preserves the feature as a unit in the history. Good for seeing which commits belong to which feature.
Rebase Before Merge (Clean History)
# Start feature
git checkout -b feature/user-auth main
# Work, work, work (messy commits OK)
git commit -m "WIP auth"
git commit -m "fix"
git commit -m "more fixes"
# Before merging, clean up with rebase
git rebase -i main # squash WIP commits, fix messages
git rebase main # bring up to date with main
# Now merge — one clean commit or a few logical commits
git checkout main
git merge feature/user-auth
Trunk-Based Development (Squash Merge)
Many teams merge feature branches with --squash, combining all feature commits into one:
git checkout main
git merge --squash feature/user-auth
git commit -m "Add user authentication (#142)"
Main always has one commit per feature. History is clean but you lose granular feature commits.
When to Use Each
Use merge when:
- Merging long-lived feature branches back to main
- Working on shared branches others are tracking
- You want to preserve the exact sequence of when things happened
- Code review happens at the PR level (merging the whole feature)
Use rebase when:
- Updating a feature branch with the latest main before a PR
- Cleaning up messy WIP commits before code review
- You want a linear, readable git log
- Working alone on a feature branch not yet shared
→ Reference common git commands with the Git Memo tool.