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.
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.
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.
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.
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.
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.
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.
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.
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 latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A team was burning 40,000 CI minutes a month and could not say why. Here is how GitHub Actions billing actually works and where the money leaks.
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.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Evergreen posts worth revisiting.