Git Rebase vs Merge — When to Use Each
The rebase-vs-merge fight is mostly people talking past each other. Here's the rule we actually follow, and the one time it saved a release.
Key takeaways
- The rebase-vs-merge fight is mostly people talking past each other.
- Here's the rule we actually follow, and the one time it saved a release.
On this page
Git Rebase vs Merge — When to Use Each#
Every team I've joined has had the same argument at least once, usually in a pull request comment thread that got way too long. Someone force-pushed a shared branch, someone else lost an afternoon of work, and now there's a company policy written in anger. The frustrating part is that rebase and merge aren't competing tools. They do different jobs, and most of the pain comes from using one where the other belonged.
So let me lay out what each actually does, then give you the rule we follow without much thinking about it anymore.
What merge does#
A merge takes two branches and ties them together with a new commit that has two parents. Your history keeps every commit exactly as it happened, plus a merge commit that records the join.
main: A---B---C-------M
\ /
feature: D---E
Commit M is the merge commit. D and E keep their original hashes. Nothing gets rewritten. If you ran git checkout main && git merge feature, that graph is what you get.
The upside: this is the true history. It shows that D and E were developed in parallel with C, and it never lies about when things happened. The downside: on a busy repo the graph turns into a plate of spaghetti. Twenty feature branches merging over a week produces a railroad map that nobody can read during an incident.
What rebase does#
A rebase takes your commits and replays them, one at a time, on top of a new base. Same changes, new parent, new hashes.
before:
main: A---B---C
\
feature: D---E
after git rebase main (from feature):
main: A---B---C
\
feature: D'---E'
D' and E' are new commits. They contain the same diffs as D and E, but they're rewritten on top of C, and their hashes changed. Merge that back into main with a fast-forward and you get one straight line. No merge commit, no parallel branches in the graph, just a clean sequence.
That clean line is the whole appeal. git log reads like a story instead of a subway map.
The golden rule#
Here it is, and it's the one thing to remember from this post: never rebase commits that other people already have.
Rebase rewrites history. When you rebase a branch that's only on your laptop, you're rewriting commits nobody else has seen, so no harm done. When you rebase a branch that's been pushed and that a teammate has pulled, you've created a parallel universe. Their D and your D' are now different commits with the same content, and the next time they push or pull, git tries to reconcile two histories that diverged. That's when work gets duplicated or lost, and that's the afternoon someone loses.
If it's shared, you merge. If it's yours alone, rebase is fair game. That single distinction resolves about ninety percent of the arguments.
The workflow we actually run#
Here's the sequence for a feature branch, start to finish.
Work locally, commit whenever. Don't worry about tidy commits yet.
git checkout -b feature/checkout-retry
# ... commits: "wip", "fix typo", "actually works now"
Before opening the PR, catch up to main and clean the history:
git fetch origin
git rebase origin/main
This replays your commits on top of the latest main. You resolve any conflicts once, here, on your own branch, instead of dumping them into a merge commit later. Conflicts during a rebase come one commit at a time: git stops, you fix, git add, git rebase --continue. It feels like more steps than a merge, but each conflict is smaller and tied to the specific change that caused it. A merge hands you every conflict at once in a single pile.
Then tidy the messy commits with an interactive rebase:
git rebase -i origin/main
You get an editor listing your commits. Squash the "fix typo" and "wip" noise into real commits, reword messages that read like keyboard mashing. The reviewer sees three clean commits instead of eleven.
Push and open the PR. Now the branch is shared, so the golden rule kicks in: no more rebasing it if a teammate is reviewing off it.
Merging the PR: ff, no-ff, or squash#
When the PR is approved, you've got three ways to land it.
Fast-forward (git merge --ff-only) just moves the main pointer forward. No merge commit, perfectly linear. Only works when main hasn't moved since you rebased. This is what you get when the history is already clean.
No-fast-forward (git merge --no-ff) forces a merge commit even when a fast-forward was possible. Some teams like this because every feature shows up as one merge commit you can revert in a single move, and the branch structure stays visible.
Squash merge collapses the whole branch into one commit on main. The individual commits vanish from main's history. This is great when you don't care about the internal steps of a feature and want one commit per PR. The tradeoff is you lose the granular history, so git bisect has coarser resolution later.
We default to squash for small features and no-ff for anything big enough that the intermediate commits carry real context. Both keep main readable.
Where merge earns its keep#
Rebase is not always the answer. When you're integrating two long-lived shared branches, a release branch back into main, say, merge is correct. You want the record that these two lines of work existed separately and joined at a known point. Rewriting that history would erase real information about how the release was built. Merge preserves context; that's its job, and on shared integration branches context is exactly what you want.
If you ever land in a tangle mid-rebase and can't see a way out, our Git troubleshooting guide covers the recovery commands, and git reflog will almost always get you back to where you started.
The call we'd make#
Rebase local, merge shared. Rebase your own feature branch onto main to stay current, use interactive rebase to make your commits presentable, and squash or no-ff the PR when it lands. Reach for a plain merge only when you're joining branches that other people already share, because there the honest, messy history is the point. Follow that one split and the argument stops being an argument.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Short-Lived Credentials — STS, Dynamic Secrets, and Why Static Keys Die
Static keys leak and live forever. Short-lived credentials from STS and Vault expire on their own — here's the token-exchange machinery and the TTL math that make it work.
Detecting and Rotating Leaked Cloud Credentials
Static keys leak. The question isn't if but how fast you notice and how clean your response runbook is when the pager goes off.
More from DevOps
Explore more articles in this category
Best Kubernetes IDE and GUI Tools in 2026
kubectl is fine until you're juggling five namespaces across three clusters. These are the tools that make that manageable, compared.
Chef vs Puppet vs Ansible: Configuration Management in 2026
One is agentless and Python-based, the other two run a persistent agent and a domain-specific language. The architecture difference matters more than the syntax.
PagerDuty vs Opsgenie: Choosing an Incident Alerting Tool
Both page the right person at 3am and both integrate with everything. The real differences show up in pricing structure, workflow depth, and who already owns the ecosystem around you.
You might have missed
Evergreen posts worth revisiting.