Git merge vs rebase: which one to use, and when (with the rules every team should agree on)
· Category: Git
Short answer
Both git merge and git rebase integrate work from one branch into another. They differ in what the resulting history looks like:
git mergepreserves the actual sequence of work. The result is a merge commit with two parents.git rebaserewrites your branch's commits as if they were made on top of the target branch. The result is a linear history with no merge commit.
The single rule that prevents 90% of the pain: never rebase commits that have been pushed and that other people are basing work on. Rebasing rewrites SHAs, so anyone else's clone now disagrees about what feature/x is. Merge is always safe on shared history; rebase is only safe on history that is yours alone.
What each command actually produces
Starting state — feature branched off main two commits ago, and main has moved on:
A---B (feature)
/
*--*--C---D--E (main)
git switch feature && git merge main:
A---B---M (feature)
/ /
*--*--C---D--E (main)
A new merge commit M joins B and E. A and B are unchanged.
git switch feature && git rebase main:
A'--B' (feature)
/
*--*--C---D--E (main)
A' and B' are new commits with the same diffs as A and B but new parents — and therefore new SHAs.
This is why "never rebase shared branches" matters. Anyone who had a clone with A/B now has a fork that no longer matches the remote.
When to use which (concrete rules)
| Situation | Merge | Rebase |
|---|---|---|
Integrating a feature into main that several people watched go in |
✅ | |
Pulling new main commits into your unpushed feature branch |
✅ | |
| Cleaning up your own commits before opening a PR | ✅ (interactive) | |
| Picking up upstream changes on a long-lived shared release branch | ✅ | |
| Squashing 12 WIP commits into one before review | ✅ (interactive) | |
Merging a hotfix into both main and release/* |
✅ (or cherry-pick) |
The cleanest team policy is: rebase your private feature branch onto main while you work; merge the feature branch into main (with --no-ff if you want explicit merge commits) when it's ready.
Steps for each workflow
Merge workflow (default, safe)
git switch main
git pull --ff-only origin main
git merge --no-ff feature/user-login
# Merge made by the 'recursive' strategy.
git push origin main
--no-ff forces a merge commit even when fast-forward is possible. The advantage: every feature merge shows up explicitly in git log --merges, which is useful when bisecting or reviewing history. Pair with git-rerere to make recurring conflicts trivial.
Rebase-then-merge (clean linear history)
# 1. Rebase your feature branch on top of latest main
git switch feature/user-login
git fetch origin
git rebase origin/main
# Resolve conflicts as they appear — git rebase --continue / --abort.
# 2. Push (force-with-lease, because rebase rewrote SHAs)
git push --force-with-lease origin feature/user-login
# 3. Merge into main fast-forward (history stays linear)
git switch main
git merge --ff-only feature/user-login
git push origin main
--ff-only is the key: it refuses to merge if a fast-forward isn't possible, which is a guard against accidentally creating a merge commit you didn't want.
Interactive rebase to clean up before review
git rebase -i HEAD~6
# pick abc1234 Add login form
# squash def5678 fix typo
# squash 4567abc fix another typo
# reword 7890def Add validation
# drop 1111111 WIP debug log
# pick 2222222 Add tests
pick keeps the commit; squash rolls into the previous one; reword keeps the diff but lets you edit the message; drop discards. Save the file and Git replays the commits per the script, asking for messages where needed.
git pull --rebase (almost always what you want)
Default git pull is git fetch + git merge, which produces a merge commit every time someone else pushed before you. That fills history with Merge branch 'main' of origin into main noise. Switch to rebase pulls:
git config --global pull.rebase true
git config --global rebase.autoStash true # don't fail on dirty tree
Now git pull rebases your unpushed local commits on top of origin/main, no merge commit. Reference: git-pull(1) — pull.rebase.
When merge is required, not optional
- A pull request from a fork on GitHub: GitHub's merge button can rebase or squash, but the resulting commit on your
mainis the merge — there's no version of "rebase the PR into main with no commit at all". - Two long-lived branches that diverge regularly (e.g.
mainandnextin many large projects). Rebasing one onto the other moves history that other people are basing PRs on. - Recovering from a bad rebase: when someone has rebased a shared branch and broken everyone's clones, the fix is
git mergeto reconcile, not a counter-rebase.
Force-push safety
After a rebase, git push is rejected because the remote branch's SHAs don't match yours. The unsafe option is --force. The safe option is --force-with-lease:
git push --force-with-lease origin feature/user-login
--force-with-lease only succeeds if the remote tip is what you last fetched — meaning nobody else has pushed in the meantime. If they have, the push is refused and you have to fetch + reconcile. Make it the default for anything past git push -u.
Common issues
My rebase had 30 conflicts on the same 5 lines.
Each commit in your branch is replayed on top of the new base, so a conflict on a line touched in 6 commits gets resolved 6 times. Two ways to make this less painful: (1) git rebase -i and squash before rebasing onto main, so there are fewer commits to replay; (2) git config --global rerere.enabled true so each resolution is recorded and replayed automatically. See how to resolve Git merge conflicts for the conflict-marker primer.
git push was rejected after rebase even though I'm the only one on this branch.
Add --force-with-lease. It's still rejected if your local view of origin/feature/x is stale — run git fetch first.
I rebased a branch that was shared and now my coworker's clone is broken.
Their fix: git fetch && git reset --hard origin/feature/x. They lose any local-only commits, so coordinate before doing this. Going forward, never rebase shared branches.
My merge commit message says Merge remote-tracking branch 'origin/main' into main.
You ran git pull without rebase config and accumulated merge noise. Set pull.rebase = true once and rewrite history (interactive rebase or git reset --hard origin/main if local commits are disposable).
I rebased and lost commits.
You don't lose them — they're orphaned, not deleted. git reflog shows every prior HEAD position. git reset --hard <reflog-sha> to restore. See how to use git reflog to recover lost commits.
Tips
- The Pro Git book's chapter on rebasing is the canonical 20-minute read on this subject. It includes the "golden rule" with the same emphasis: never rebase what's been pushed.
git log --graph --oneline --decorate --allis the one command that makes the merge-vs-rebase difference visible. Use it before and after each integration to confirm the history shape matches what you intended.- For team workflows that lock in the right defaults, see how to implement trunk-based development and how to set up GitFlow workflow.
- GitHub's merge methods (Merge / Squash / Rebase) and GitLab's equivalents are documented at GitHub: About merge methods and GitLab: Merge methods.