Skip to main content
Git Rebase vs Merge — When to Use Each

Git Rebase vs Merge — When to Use Each

KU
• 6 min read•18 views

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.

code
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.

code
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.

bash.bash
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:

bash.bash
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:

bash.bash
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.

Explore topics:DevOps
React
Share this post
KU

Kiril Urbonas

AI Engineer

553 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.