Git Merge vs Rebase — When to Use Each One
Photo by Unsplash on Unsplash
Table of Contents
Two Ways to Combine Branches
This debate gets weirdly heated for something so mechanical. People treat "merge vs rebase" like a tabs-vs-spaces holy war. It really isn't — they do different jobs, and once you understand what each one does to your history, picking the right one is straightforward.
Let me lay out what actually happens under the hood with each, the one rule that keeps rebase from blowing up in your face, and a simple workflow that uses both.
What Merge Actually Does
Fast-forward merge. If `main` hasn't moved since you branched off, Git doesn't need a merge commit at all — it just slides the `main` pointer forward to the tip of `feature`. The history stays perfectly linear, as if you'd committed straight onto `main`. This happens automatically when there's nothing to reconcile.
Three-way merge. If `main` *has* advanced (someone else committed while you worked), Git creates a new merge commit with two parents — one pointing back into `main`'s history, one into `feature`'s. That merge commit is a permanent record: it shows exactly where the branch split off and where it came back together.
The defining trait of merge is that it never rewrites history. Every original commit stays exactly as it was, with its original ID and timestamp. Nothing is lost or altered. The cost is that your history can get busy — lots of merge commits in an active repo produce that tangled "railroad track" graph where many branches weave in and out.
That's the honest trade. Merge tells the literal truth about how development happened: these commits existed on a branch, they got integrated at this point, here's the seam. Some teams love that fidelity because it's an accurate audit trail. Others find the merge-commit noise distracting and wish the log read as a clean straight line. Neither side is wrong — they just value different things in their history. I lean toward keeping merge commits on the shared `main` branch precisely *because* that seam is useful when you're hunting down when a feature actually landed.
It's also worth knowing that merge is non-destructive in the strongest sense: if a merge goes sideways, you haven't changed any of the original commits, so backing out is straightforward. That safety is a real part of why merge is the default for shared integration — you can always trace and undo without having quietly rewritten anyone's history underneath them. When I'm working on a branch other people depend on, that guarantee alone is usually enough to make merge the right call.
What Rebase Does Differently
Photo by Unsplash on Unsplash
The key word is *replaying*. Git takes each commit on your feature branch, sets it aside, fast-forwards your branch to the latest `main`, then re-applies your changes as brand-new commits. They have the same content and messages, but new commit IDs — because a commit's ID is a hash of its content *and* its parent, and you just changed the parent.
The payoff is a linear history. No merge commits, no railroad tracks — just a clean straight line of commits, as if you'd written your feature on top of the latest `main` from the start. When someone reads the log later, the story is tidy and easy to follow. A lot of teams prefer this for exactly that reason.
But rebase has two sharp edges. First, because it creates new commits, the old ones are effectively discarded — this is the "rewriting history" everyone warns about. Second, conflicts get resolved commit by commit rather than all at once. If your branch has ten commits and they each touch a conflicting line, you might resolve the same area ten times during one rebase. With a merge you'd resolve it once. That can be tedious, though `git rebase --continue` walks you through it step by step.
There's also interactive rebase (`git rebase -i`), which lets you squash, reorder, edit, or drop commits as you replay them. It's genuinely great for cleaning up a messy work-in-progress branch — squashing six "wip" and "fix typo" commits into one coherent commit before you open a pull request. That cleanup is where I use rebase most, and it's entirely safe *as long as* the branch is still private. The Atlassian write-up on merging vs rebasing has clear diagrams if you want to see the commit graphs side by side.
The Golden Rule and a Workflow That Uses Both
Never rebase commits that exist outside your local repository — anything others may have already pulled.
The reason is direct. Rebase rewrites commits, giving them new IDs. If you rebase a branch that a teammate has already based their work on, their copy still points at the *old* commits while yours now has *new* ones. Git sees two diverging histories, and your teammate ends up with duplicate commits, confusing conflicts, and a genuinely painful cleanup. That's why you never, ever rebase a shared branch like `main` or a `dev` branch that other people pull from. Rewriting public history is the cardinal sin of Git.
The safe pattern that uses both tools to their strengths:
1. Work on a private feature branch. While it's yours alone and nobody else has pulled it, rebase freely. Pull in the latest `main` with `git rebase main` to keep your branch current, and use interactive rebase to tidy your commits. Clean, private history. 2. When the feature's ready, merge it into `main`. Integrate the finished branch with a merge so there's a clear, honest record of when it landed. Some teams prefer a fast-forward merge after a rebase for a fully linear history; others keep the merge commit as a marker. Either is fine.
That's the whole philosophy in one line: rebase to clean up your private work, merge to integrate shared work. Rebase is for the history *only you* can see; merge is for history *everyone* shares.
There's one everyday command that lives right at this intersection and confuses a lot of people: `git pull --rebase`. A normal `git pull` is really a fetch followed by a merge, so if your local branch and the remote have both moved, you get a merge commit — and a busy repo ends up littered with little "Merge branch 'main' of origin" commits that nobody wrote on purpose. Running `git pull --rebase` instead replays your local commits on top of what you just fetched, keeping the line straight and skipping that noise. It's safe here because the commits you're rebasing are your *own* un-pushed work — you're not rewriting anything anyone else has. I've set `git config --global pull.rebase true` on every machine I work on for exactly this reason; the default merge-on-pull behavior creates clutter I never want. That single config change quietly removed a whole category of pointless merge commits from my history.
A couple of mistakes I see repeatedly, so you can skip them:
- Rebasing after pushing. Once you've pushed a branch and a teammate has pulled it, rebasing it forces you into a `git push --force`, which can clobber their work. If you must rewrite already-pushed history, use `--force-with-lease` instead of plain `--force`; it refuses to overwrite if the remote has commits you haven't seen, which is a small safety net against destroying someone's work. - Fearing merge commits too much. Some people contort themselves to avoid every single merge commit on `main`. Honestly, a merge commit marking where a real feature landed is *informative*, not clutter. Don't rebase a shared branch just to dodge one.
If you're newer to the broader toolchain around all this, a few related pieces might help: I covered how version numbers communicate change in semantic versioning explained, which pairs naturally with thinking about commit history. And for the smaller utilities that come up around release work — generating IDs, formatting config files — the ToolsFuel developer tools cover the basics in the browser. Get comfortable with the golden rule and the rest of the merge-versus-rebase debate mostly takes care of itself.
Frequently Asked Questions
What's the difference between git merge and git rebase?
Merge combines two branches by creating a new merge commit that preserves the full history, including where the branches diverged. Rebase rewrites history by replaying your branch's commits on top of the target branch, producing new commits with new IDs and a clean linear history. Merge is safe and never alters existing commits; rebase gives a tidier log but rewrites commit IDs, which is risky on shared branches.
When should I use rebase instead of merge?
Use rebase on your own private feature branch to keep it current with main and to clean up messy commits before sharing. It gives you a linear, readable history with no merge commits. Use merge when integrating a finished branch into a shared branch like main, where the merge commit provides an honest record of when the work landed. The short version: rebase private work, merge shared work.
What is the golden rule of git rebase?
Never rebase commits that others have already pulled — in other words, never rewrite public or shared history. Because rebase gives commits new IDs, rebasing a branch someone else has based work on leaves their copy pointing at the old commits, causing diverging histories, duplicate commits, and painful conflicts. Only rebase branches that are still private to you.
Does rebase delete my commits?
Not exactly — it replaces them. Rebase replays each commit as a brand-new commit with the same content but a new ID, because the commit's ID is a hash that includes its parent, which the rebase changes. The original commits are effectively discarded from the branch, which is why it's called rewriting history. This is safe on a private branch but dangerous on a shared one.
What is a fast-forward merge?
A fast-forward merge happens when the target branch hasn't moved since you branched off, so Git simply slides its pointer forward to the tip of your branch with no merge commit. The history stays perfectly linear, as if you'd committed directly. It only works when there's nothing to reconcile; if the target branch advanced, Git creates a three-way merge with a merge commit instead.
Why does rebase make me resolve the same conflict multiple times?
Because rebase replays your commits one at a time and resolves conflicts per commit, not all at once. If several of your commits touch the same conflicting lines, you may resolve that area repeatedly as each commit is reapplied. A merge resolves everything in a single step instead. Understanding version history more broadly helps — I cover related ground in [semantic versioning explained](/blog/semantic-versioning-semver-explained-for-developers).
Try ToolsFuel
23+ free online tools for developers, designers, and everyone. No signup required.
Browse All Tools