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.
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.
Best LLM APIs in 2026: By Use Case
A practitioner's guide to picking the right LLM API for coding, long docs, cheap extraction, reasoning, vision, voice, and on-device work.
Production RAG Reliability — Making LLM Answers Trustworthy
A demo RAG app is easy; one users trust is not. This is the map for reliable retrieval-augmented generation: grounding, evaluation, retrieval quality, guardrails, and safe rollout.
More from DevOps
Explore more articles in this category
GitHub Copilot CLI Sandboxing Is GA: What Actually Changed
GitHub made local sandboxing generally available for Copilot CLI on October 7, so the default now restricts the agent instead of trusting it.
The Tensorlake npm Compromise Targeted Your AI Tool Configs
A compromised npm release went after cloud credentials as expected, and local AI coding-assistant config files, which most teams never treated as secrets.
GitLab's New Rate Limits: What to Fix Before Oct 19
GitLab is capping unauthenticated API calls at 60 an hour starting October 19, and the preview windows land before most teams will have noticed.
You might have missed
Evergreen posts worth revisiting.