Git merge vs rebase: when to use which (without wrecking history)
Git merge vs rebase both integrate branches but produce different histories. Learn when each is right, the golden rule, and how to recover when things go wrong.
Every developer eventually stares at a git log that looks like a bowl of spaghetti and wonders: should I have used rebase? Then they try rebase on the wrong branch and spend an afternoon untangling force-push fallout. Both operations integrate one branch into another. The history they leave behind is completely different, and that difference is what determines which one belongs where.
This post maps out the mechanics of each approach, explains the golden rule that keeps teams out of trouble, and walks through the practical decision points you will encounter on real projects.
What merge and rebase actually do
When you run git merge feature on your main branch, Git looks at three commits: the tip of main, the tip of feature, and the common ancestor they share. If your branch has not diverged at all, Git fast-forwards the pointer and no merge commit is created. Once divergence exists, Git performs a three-way merge and records a new merge commit with two parents. That commit is an honest record in the graph: it says “at this point in time, these two lines of work came together.”
When you run git rebase main from your feature branch, Git does something fundamentally different. It finds the common ancestor, lifts each of your feature commits off the branch one by one, and replays them on top of the current tip of main. The result looks as if you had started your feature work from main’s latest commit all along. The history is linear, the log is tidy, but here is the catch: every replayed commit gets a new SHA. The old commits still exist in the reflog, but they are no longer reachable from any branch pointer.
The diagram below shows both outcomes side by side.
Merge: when the honest record matters
A merge commit carries something that a rebase throws away: proof of parallel work. When you look at a project a year from now and see that commit M, you can reconstruct exactly what state main was in when feature landed, and exactly what the feature branch contained. That context is invaluable during incident investigations, audits, and archaeology sessions.
Three-way merges also have a soft advantage in conflict resolution. Because Git is integrating two complete branch states, the conflict markers you see are usually the full context of what each side was trying to do. You resolve once at integration time and move on.
Merge is the right default for:
- Integrating a finished, reviewed feature branch into
mainordevelop - Any branch that one or more colleagues have checked out or rebased against
- Situations where you want the graph to show the true shape of how work progressed — especially on long-lived projects that do post-mortems
If the branch has not diverged (no new commits on main since you branched), Git automatically fast-forwards: the pointer moves and no merge commit appears at all. You get the clean history without doing anything special.
For more on how branches work under the hood, see Git branches.
Rebase: when clean linearity is the goal
A linear history is genuinely easier to read when you are reviewing changes commit-by-commit with git log --oneline or bisecting to find a regression. Each commit sits directly after the previous one with no forks. git bisect performs fewer iterations because the graph is a straight line.
Rebase shines when you want to:
- Keep a local feature branch current as
mainmoves forward before opening a pull request - Clean up your own private commit history before sharing — squash “WIP” saves, reorder logical steps, rewrite poor commit messages — using
git rebase -i - Apply
git pull --rebaseto integrate upstream changes without creating a noisy merge commit in your local history every time you sync
Interactive rebase (git rebase -i HEAD~N) is one of the most useful tools in the Git toolkit. It lets you squash, fixup, reword, drop, and reorder commits before anyone else ever sees them. The result is a thoughtful, readable chain of commits that tells a coherent story rather than a raw transcript of every “oops” save.
For a workflow that ties this together in practice, see GitHub Flow and pull requests.
The golden rule
The second diagram shows exactly what happens.
The practical boundary is simple: rebase is safe on commits that only exist in your local repository or on a remote branch that only you use. Once a commit is on a branch other people have pulled, it is shared history and must not be rewritten.
Squash merge: the third option
Many teams settle on a hybrid approach: squash-merge. When a pull request is merged, GitHub (and GitLab, Bitbucket) can squash all commits from the feature branch into a single commit on main. The result is a clean, linear main with one commit per feature — no merge commit bubbles, no individual “WIP save” noise. The original branch detail is discarded but the PR conversation and diff remain accessible in the pull request UI.
This works well when:
- Individual commits on a feature branch are not expected to be bisectable units
- The team wants a tidy
mainwithout requiring each contributor to do an interactive rebase manually - The PR review flow is the authoritative record, not the raw commit graph
The tradeoff: git bisect will only be able to identify which feature introduced a bug, not which commit within that feature.
Handling conflicts
Both operations force you to resolve conflicts when the same lines diverge — there is no escape hatch. The experience differs in one important way.
With merge, you resolve the conflict once for the entire integration. With rebase, you may resolve conflicts per replayed commit. If your feature branch has ten commits and three of them touch a file that also changed on main, you will be prompted three times. The upside: each resolution is smaller and more contextual. The downside: it is more total work.
When a rebase conflict happens mid-way, git rebase --abort restores the branch to its pre-rebase state. Once you fix a conflict, git rebase --continue moves to the next commit.
git pull --rebase in daily use
The noisiest history problem in most repos is not feature-branch merges — it is the merge commit that appears every time someone runs git pull while the remote has moved forward. Those “Merge branch ‘main’ of github.com/…” commits clutter the log.
git pull --rebase replays your unpushed local commits on top of the fetched remote state instead of creating a merge commit. Because you have not shared those local commits yet, the golden rule is not violated. Most teams configure this as the default:
git config --global pull.rebase true
This single setting removes a large fraction of the noise in collaborative repositories.
Recovery with reflog
When a rebase goes wrong, git reflog is your escape hatch. Git records every movement of HEAD in the reflog, including the state before the rebase began. Running git reflog will show a line like HEAD@{5}: rebase (start): checkout main. The entry immediately before that represents the state of your branch before the rebase.
git reflog
git reset --hard HEAD@{N}
Replace N with the index of the pre-rebase state. This restores the branch pointer to exactly where it was. The reflog entries expire after 90 days by default, so this recovery window is wide for anything you would realistically need to undo.
See the main Git reference for a broader map of the commands and concepts covered here, or check the glossary if any term above is unfamiliar.
A practical decision guide
| Situation | Use |
|---|---|
Merging a finished feature PR into main | git merge (or squash-merge via the host UI) |
Keeping a local feature branch current with main | git rebase main |
| Cleaning up your own unpushed commits | git rebase -i |
| Pulling while the remote has moved forward | git pull --rebase |
| Shared branch, team has pulled it | git merge only — never rebase |
Want one clean commit per feature on main | Squash merge |
Frequently asked questions
Can I rebase after I have already pushed a branch?
Yes, but only if you are the sole user of that remote branch and no one else has pulled it. You will need git push --force-with-lease (prefer this over --force — it refuses to overwrite if the remote has moved since your last fetch). The moment a colleague has cloned or pulled that branch, it is shared history and the golden rule applies.
Does rebase lose my commits?
No. Rebased commits become unreachable from the branch pointer but remain in the reflog for 90 days. If something goes wrong, git reflog will show the pre-rebase state and you can git reset --hard to it. The old commits are not deleted; they are just no longer pointed to by a branch.
Which does GitHub use when I click “Merge pull request”?
GitHub offers three options per repository: “Create a merge commit” (standard three-way merge), “Squash and merge” (squash all commits into one on the target branch), and “Rebase and merge” (rebase the PR commits onto the target without a merge commit). Repository admins control which options are available. The default is a merge commit unless the repo owner has changed it.
When should I use interactive rebase?
Before opening a pull request, git rebase -i HEAD~N is ideal for tidying your local commit history: squashing “fix typo” saves into the commit they belong to, rewording vague messages, and reordering commits so reviewers see a logical progression. It has no effect on shared history as long as you do it before pushing. After pushing to a shared branch, interactive rebase violates the golden rule and should not be used.