Every developer has that one Git argument they'll take to the grave. Tabs versus spaces. Commit message conventions. And somewhere near the top of the list: rebase versus merge.
It's one of those debates that can get weirdly heated. People have strong opinions, and they'll defend their preferred approach with an almost religious intensity. But here's the thing: both commands solve the same fundamental problem—integrating changes from one branch into another—and they do it in completely different ways.
I learned this lesson the hard way a few years back. I was on a team where one developer was a rebase evangelist. He rebased everything. Feature branches, shared branches, the main branch itself on one terrifying occasion. His reasoning was that a clean history was worth any cost. Then came the crisis. Three developers were collaborating on a long-running feature branch. They'd all pushed commits and were coordinating changes. The rebase evangelist decided the branch's history was too messy and rebased the entire thing against main—then force-pushed. The other two developers pulled the next morning to find their commit hashes had changed, their local work had diverged, and half their changes appeared to be gone. It took most of a day to reconcile. Several commits were accidentally dropped. The launch was delayed by a week.
The lesson wasn't that rebase is bad. It was that rebasing shared branches is bad. He had the right instinct—a clean history is valuable—but he applied the technique at the wrong scope. That single incident shaped how I think about this entire debate, and it's the lens I'll use to walk you through it.
Git rebase vs merge: the short answer
If you want the fastest possible answer, here it is:
| Feature | git merge |
git rebase |
|---|---|---|
| What it does | Combines branches with a merge commit | Replays your commits on top of another branch |
| History shape | Branching tree | Linear line |
| Commit hashes | Preserved | Rewritten |
| Safety on shared branches | Safe | Dangerous |
| Conflict resolution | All at once | One commit at a time |
| Best for | Integrating feature branches into main |
Cleaning up your own branch before merging |
| Reverting | Revert the merge commit in one step | Requires reverting multiple commits |
git bisect |
More complex | Cleaner |
Now let's look at the details and real examples.
What git merge actually does
Merging takes two branches and combines them, creating a new "merge commit" that ties their histories together. It's non-destructive. Every commit you made on your feature branch stays exactly as it was. Git just adds a new commit that has two parents—one from your branch, one from the branch you're merging into.
git checkout main
git merge feature
The result is a commit history that looks like a branching tree. You can see exactly when the feature branch split off, what work happened in parallel, and when it came back together. The history is complete and accurate.
The downside is that it can get noisy. If you're merging branches frequently—say, a team of five developers all merging into main multiple times a day—the commit history fills up with merge commits. Half the graph is just "Merge branch 'feature' into main." It's honest, but it's not always readable.
There's also the issue of bisecting. If you're trying to find the commit that introduced a bug, merge commits complicate the search. You have to decide which parent to follow. It's not impossible, but it adds friction.
What git rebase actually does
Rebasing takes your commits and replays them on top of another branch, one by one. Instead of creating a merge commit, it rewrites your commits so they appear as if you wrote them after the latest changes on the target branch.
git checkout feature
git rebase main
The result is a perfectly linear history. No merge commits. No branching spaghetti. Your feature commits sit cleanly at the tip of main, as if you'd written them sequentially. It's aesthetically pleasing and makes tools like git log --oneline much easier to read.
But there's a catch. Rebasing rewrites history. The commit hashes change because the parent commits are different. If you've already pushed your branch and someone else is working on top of it, rebasing creates a nightmare of force pushes and lost work. The golden rule of rebasing is: never rebase a shared branch.
This is where a lot of the anxiety around rebase comes from. People hear "rewriting history" and picture catastrophic data loss. In practice, rebasing your own feature branch before merging it into main is perfectly safe and often desirable. It's rebasing branches that other people are using that causes problems.
Fast-forward merge vs rebase: where things get confusing
There's a third concept that trips people up constantly: the fast-forward merge. It shows up in about a dozen different search queries, and for good reason.
A fast-forward merge happens when the branch you're merging into hasn't changed since your feature branch split off. In that case, Git doesn't need to create a merge commit at all—it just moves the branch pointer forward. The result is a linear history, but achieved without rewriting anything.
# This produces a fast-forward by default
git checkout main
git merge feature
# main is now pointing at the same commit as feature
So you have three possible outcomes:
- Fast-forward merge: No merge commit, linear history, no rewriting. Happens when
mainhasn't moved since you branched off. - Three-way merge (with merge commit): Creates a merge commit, preserves branch context, no rewriting. Happens when
mainhas moved on. - Rebase then fast-forward: Rewrites your commits onto the current tip of
main, then fast-forwards. Linear history, but with rewritten hashes.
This is why people get confused when they hear "rebase gives you a linear history"—well, so does a fast-forward merge. The difference is that a fast-forward merge only works when there's no divergence, and rebase forces the linear outcome even when there is.
If you want to force a merge commit even when a fast-forward is possible, you can use:
git merge --no-ff feature
The --no-ff flag is a common convention on teams that want every feature to have its own merge commit for easy reverting.
The conflict resolution difference
Both merge and rebase can produce conflicts. How you deal with them is different, and that difference influences which approach people prefer.
When you merge and hit a conflict, you resolve all the conflicts at once and create the merge commit. It's a single event. You see the combined state of both branches, fix the issues, and move on. For complex merges with dozens of conflicting files, this can be overwhelming.
When you rebase and hit a conflict, you resolve it one commit at a time. Git replays each of your commits, pausing when one doesn't apply cleanly. You fix the conflict, stage the resolution, and continue the rebase. Then it moves to your next commit. This means you're resolving smaller, more targeted conflicts, which can be easier to reason about.
# During an interactive rebase
git rebase main
# CONFLICT in some-file.js
# Fix the conflict
git add some-file.js
git rebase --continue
The downside is that you might have to resolve the same conflict multiple times if your later commits touch the same code. It can feel repetitive. But you also have the option to squash commits together before rebasing, which reduces the number of times you have to resolve conflicts.
What about "ours" vs "theirs" during a rebase?
This is one of the most confusing parts of rebasing, and it comes up in the GSC data as "git rebase theirs vs ours" and "git rebase ours vs theirs." The confusion is real: during a rebase, "ours" and "theirs" are swapped compared to a merge.
During a merge:
- ours = the branch you're merging INTO (e.g.,
main) - theirs = the branch you're merging FROM (e.g.,
feature)
During a rebase:
- ours = the branch you're rebasing ONTO (e.g.,
main) — this is the opposite of what you'd expect - theirs = the commits being replayed (e.g., your feature commits)
So if you're rebasing feature onto main and see a conflict, choosing "ours" gives you what's currently in main, and "theirs" gives you what you wrote in feature. It feels backwards. It's a historical artifact of how Git implements rebase internally, and there's no fix coming. Just remember the swap and you'll be fine.
# During a rebase, force the current branch's version
git checkout --ours some-file.js
# Or force the version from the commits being replayed
git checkout --theirs some-file.js
Interactive rebase: the real superpower
The real power of rebase isn't the linear history it produces. It's the interactive mode. git rebase -i opens an editor where you can reorder, combine, edit, and drop commits. This is where you turn a messy development history into a coherent narrative.
git rebase -i HEAD~5
In the editor, you'll see something like:
pick a1b2c3d Add user authentication
pick b2c3d4e Fix typo in auth module
pick c3d4e5f WIP: start payment integration
pick d4e5f6g Complete payment integration
pick e5f6g7h Remove debug logging
You can change pick to squash to merge the typo fix into the authentication commit. You can reorder the payment commits to group them together. You can reword a commit message that's unclear. The result is a set of commits that tells a story someone else can follow.
This is the workflow that most experienced Git users actually rely on, and it's the one that changes how you think about commits. Your local commits are drafts. Interactive rebase is the editing pass. The final product is what gets pushed.
The pull request workflow that works
Most teams I've worked with settle on a hybrid approach that's become pretty standard in the industry. It goes like this:
- You create a feature branch off
main. - You commit your work as you go, making small, messy commits—"WIP," "fix typo," "try different approach." These aren't the commits you want immortalized in the project history. They're checkpoints for you.
- When you're ready to open a pull request, you do an interactive rebase against
main. This lets you clean up your commits—squash the WIP commits, reword messages, reorder changes—so your branch tells a coherent story. - Push (with
--force-with-leaseif you'd pushed earlier) and open the PR. - When the PR is approved, you have a choice. You can merge it with a merge commit, preserving the fact that the work was done on a branch. Or you can rebase and fast-forward, adding your clean commits directly to the tip of
main.
# Clean up your branch before the PR
git fetch origin
git rebase -i origin/main
# Push the cleaned-up branch safely
git push --force-with-lease
This hybrid workflow gives you the best of both worlds. The project history is readable. The development process wasn't bogged down by commit perfectionism. And nobody had to force-push a shared branch.
What GitHub, GitLab, and Bitbucket actually do
One thing worth clarifying, because it trips people up: the "Rebase and merge" button on GitHub, GitLab, and Bitbucket is not the same as running git rebase locally. It replays your branch's commits onto the base branch and then fast-forwards, producing a linear history without a merge commit. The commits get new hashes, but they're added to the shared branch without the messy conflict-resolution dance you'd have locally.
Meanwhile, the "Create a merge commit" option does a three-way merge with a merge commit. And "Squash and merge" collapses all your commits into one, then adds that single commit to the base branch. Three different options, three different histories.
If your team has a Git policy, this is usually the place it's enforced—not in the command line, but in the merge button your PR platform shows by default.
When a merge commit is actually useful
Merge commits get a bad rap, but they serve a purpose. They group related work together. If you look at a merge commit, you can see all the commits that were part of a feature. You can revert the entire feature by reverting the merge commit, which is cleaner than reverting a dozen individual commits.
git revert -m 1 <merge-commit-hash>
Reverting a merge commit takes one command. Reverting a rebased set of commits takes multiple commands and careful ordering. For projects that prioritize maintainability and the ability to undo changes cleanly, merge commits are a feature, not a bug.
For large features where multiple developers collaborated, a merge commit documents that collaboration. The history shows the branch, the conversations in the pull request, the review process. That context is valuable when you're trying to understand why a change was made six months later.
When a linear history really shines
Linear history makes certain operations much simpler. git bisect works more predictably because there are no merge commits to navigate. git log --oneline produces a single, clean list of changes. Understanding the order in which changes were introduced is straightforward.
For projects with frequent releases, a linear history makes release notes easier to generate. You can see every commit that's going into the release in order, without untangling branches. CI/CD pipelines that trigger on every commit to main benefit from the clarity of a linear sequence.
If you've been optimizing your development workflow—like implementing Redis caching patterns or speeding up modern websites—a clean Git history is another tool in the same toolbox. It reduces the cognitive load of understanding what changed and when.
The force push taboo (and when it's okay)
Force pushing has a bad reputation, and for good reason. git push --force overwrites the remote branch with your local version, potentially destroying commits that other people have based work on. It's the Git equivalent of yelling "fire" in a crowded theater.
But there's a time when force pushing is perfectly fine: when you're the only person working on a branch. If you've cleaned up your feature branch with an interactive rebase, you need to force push to update the remote. This is expected. This is normal.
git push --force-with-lease
The --force-with-lease flag is a safer version that checks whether the remote branch has changed since you last fetched. If someone else pushed commits, it refuses to overwrite them. It's a safety net for the times when you thought you were the only one on the branch but weren't. There is essentially no reason to use plain --force in 2026.
Making the decision for your team
If you're working alone, the choice is yours. Rebasing gives you a clean, linear history. Merging gives you a complete, unaltered record. Neither is wrong. Pick the one that matches your aesthetic preference.
If you're on a team, you need a shared policy. Most teams I've worked with end up with something like this:
- Rebase your own feature branches before merging to keep them up to date with
mainand clean up commits. - Merge feature branches into
mainwith a merge commit to preserve context and enable easy reverts. - Never rebase
mainor any shared branch.
This gives you the readability of rebased feature branches and the safety of merge commits on main. It's a compromise, but it works.
Some teams go further and enforce squash merging. Every PR becomes a single commit on main. The history is linear, and each commit represents a complete feature. You lose the granular commit history, but for projects where features are the unit of change, that's often fine.
Frequently asked questions
What's the difference between git rebase and git merge?
git merge combines two branches by creating a new merge commit that has both histories as parents. It's non-destructive and preserves the full history of the branch. git rebase replays your commits on top of another branch, producing a linear history but rewriting the commit hashes. Merge is safe on shared branches; rebase is only safe on your own branches.
When should I use rebase instead of merge?
Use rebase when you want a clean, linear history and you're working on your own feature branch. It's especially useful before opening a pull request, so you can clean up messy commits. Use merge when you're integrating a feature branch into a shared branch like main, when you want to preserve the branch structure, or when you need to revert the entire feature with a single command.
Is git rebase dangerous?
Rebase is only dangerous if you use it on a branch that other people are working on. If you rebase a shared branch and force-push, you can overwrite commits that others depend on, and they'll have to reconcile their work manually. If you only rebase your own feature branch, rebase is completely safe.
What's the difference between a fast-forward merge and a rebase?
A fast-forward merge simply moves the branch pointer forward without creating a merge commit or rewriting any commits. It only works when the target branch hasn't diverged. A rebase replays your commits onto the current tip of the target branch and produces new hashes—it works even when the target branch has moved on. Both produce linear history, but rebase rewrites history while fast-forward doesn't.
What's the difference between merge and rebase in terms of commit hashes?
Merge preserves commit hashes because it doesn't rewrite anything—it just adds a new merge commit on top. Rebase rewrites commit hashes because it replays your commits on a different parent, which changes every hash from that point forward. This is why rebase breaks shared branches: your local hashes no longer match the remote's.
How do I resolve conflicts during a rebase?
When rebase pauses on a conflict, open the conflicting file and resolve it, then stage the changes with git add, and continue with git rebase --continue. If you want to abort the whole rebase and return to your original state, use git rebase --abort. To skip the current commit entirely, use git rebase --skip.
What does "ours" vs "theirs" mean during a rebase?
During a rebase, "ours" refers to the branch you're rebasing onto (usually main), and "theirs" refers to the commits being replayed (usually your feature branch). This is the opposite of how it works during a merge, which is confusing and a common source of mistakes. Remember: during a rebase, "ours" is the target branch.
Is rebase better than merge?
Neither is universally better. Rebase gives you a linear, readable history, which many developers prefer. Merge preserves the full context of branches, which is valuable for reverting and understanding how work was integrated. Most experienced teams use both: rebase for cleaning up their own feature branches, merge for integrating into main.
Wrapping up
The rebase vs merge debate isn't about right and wrong. It's about tradeoffs. Merging preserves history but creates noise. Rebasing creates clarity but rewrites history. The right choice depends on your project, your team, and what you value in a commit log.
If you're new to Git, start with merging. It's simpler and safer. As you get comfortable, learn interactive rebasing for cleaning up your own branches. Eventually, you'll develop an intuition for when each tool is appropriate.
The worst outcome isn't picking the wrong strategy. It's picking no strategy at all—letting the Git history grow chaotically, with no shared understanding of how the team integrates work. A clear, documented policy, whatever it is, beats ambiguity every time.
Looking to streamline your development workflow, from Git strategy to CI/CD pipelines? Red Surge Technology helps teams adopt practical processes that actually stick. Get in touch to discuss how we can help.