git merge and git rebase solve the same problem — you have work on one branch and need the changes from another — but they leave behind two very different histories. Merge records what happened: it ties two branches together with a merge commit that has two parents, and every original commit stays exactly where it was. Rebase rewrites what happened: it lifts your branch's commits off their old base and replays them, one by one, on top of the target, producing brand-new commits with a perfectly straight line.
The takeaway up front: merge preserves history; rebase rewrites it into a linear story. Neither is more correct. Merge keeps a faithful, non-destructive record at the cost of a busier graph. Rebase buys a clean, linear, easy-to-read history at the cost of rewriting commits — which is safe on your private branch and dangerous the moment those commits are shared. Learn what each does to the commit graph, memorize one golden rule, and the choice stops being a coin flip.
What merge and rebase actually do
Start with a feature branch that diverged from main a few commits ago; both lines have moved on since.
A merge (git switch feature; git merge main) creates a single new merge commit with two parents — the tip of feature and the tip of main — that stitches the two lines together. Every existing commit keeps its original SHA, author, and parent. Nothing is rewritten, so the branch structure is preserved forever, including the fact that a branch existed and when it was integrated. If feature hadn't diverged — if main were a direct ancestor — Git skips the merge commit and just slides the branch pointer forward. That's a fast-forward, and it's why not every merge produces a merge commit.
A rebase (git switch feature; git rebase main) does something structurally different. It finds the commits unique to feature, sets them aside, moves the branch's base to the tip of main, and re-applies each saved commit in order. The changes are the same, but the results are new commits — new parents, new committer dates, new SHAs. The originals become unreferenced and are eventually garbage-collected. The payoff is a history with no merge commit and no fork: feature looks as though you wrote it on top of the latest main all along.
One divergence, two histories
Say feature has commits A B C and main gained F G after they split:
A---B---C feature
/
D---E---F---G main
Merging main into feature keeps both lines and joins them with M:
A---B---C
/ \
D---E---F---G---M feature (M has two parents: C and G)
Rebasing feature onto main replays A B C as new commits on top of G:
A'--B'--C' feature
/
D---E---F---G main
The two commands that produce these outcomes:
git switch feature
git merge main # creates merge commit M (unless it can fast-forward)
# Rebase: replay feature's commits on top of the latest main
git switch feature
git rebase main # A B C become new commits A' B' C'
The file contents at the tip are identical either way. What differs is the story: merge says "these two lines of work met here," while rebase says "this work happened, in order, on top of everything else."
The golden rule: never rebase shared history
There is exactly one rule that keeps rebase from causing pain: never rebase commits that other people already have. Rewriting a commit changes its SHA, so if you rebase commits you've already pushed to a shared branch, your local history and everyone else's now disagree about commits that are supposed to be identical. Git refuses a normal push (your branch has "diverged"), and if you force it, every teammate who pulled the old commits inherits a tangled, duplicated history the next time they sync.
The safe boundary is ownership. Rebase freely while your commits are private — local-only, or on a feature branch nobody else has pulled. The moment history is shared, switch to merge to integrate it. If you must rewrite a branch you've already pushed — say, tidying your own PR branch that only you use — push with --force-with-lease instead of --force: it refuses to overwrite if someone else pushed in the meantime, turning a silent clobber into a safe error. Agree on this policy as a team and enforce it in review; it's a shared convention worth settling early, much like a monorepo vs polyrepo decision, before habits calcify around the wrong default.
When merge is the right call
Reach for merge whenever the history in play is shared, or when the record itself has value:
- Integrating a shared or long-lived branch. Bringing
maininto a release branch, or landing a completed feature, touches commits others already have. Rewriting them is off the table, so merge is the only safe option. - Merging a pull request. A merge commit is a natural, revertable integration point.
git revert -m 1 <merge-sha>backs out an entire feature in one move, which a rebased-flat history makes harder. - You want the true record. Merge preserves an auditable trail of when each branch was integrated — useful for tracing a regression to the change that introduced it, and for any compliance requirement to keep history intact.
- You'd rather resolve conflicts once. A merge presents all conflicts together at the single merge point, so you fix them one time.
The honest trade-off: many merges of short-lived branches produce "merge bubbles" that clutter git log --graph. If that noise bothers you, that's the argument for rebase — or for a squash merge.
When rebase is the right call
Reach for rebase when the commits are yours and you want a cleaner story:
- Cleaning up your own branch before sharing it. Interactive rebase (
git rebase -i) lets you squash "fix typo" into the commit it fixes, reword messages, reorder, or drop a commit entirely — so reviewers read your intent, not your keystroke-by-keystroke process. - Keeping a feature branch current with
main.git rebase mainreplays your work on top of the latest base without littering the branch with merge commits, so the eventual PR is a clean diff. - You value a linear history for tooling.
git bisectandgit logare easier to reason about on a straight line, and "what changed between these two points" becomes unambiguous.
A quick interactive cleanup before opening a PR:
git rebase -i HEAD~4
# In the editor, per line:
# pick – keep the commit as-is
# squash – fold this commit into the previous one (keep both messages)
# fixup – fold in, discarding this commit's message
# reword – keep the change, edit the message
# drop – delete the commit entirely
The trade-off mirrors the benefit: because rebase rewrites commits, it's off-limits on shared history, and replaying many commits can mean resolving the same conflict repeatedly. Turn on git rerere and Git remembers your resolutions and replays them automatically.
git pull: merge or rebase?
The most common place this decision shows up isn't integrating feature branches — it's the daily git pull. A plain git pull is fetch + merge: if you have local commits and the remote also moved, it mints a merge commit every time you sync. Do that a few times a day and your history fills with "Merge branch 'main' of origin" commits that record nothing useful.
git pull --rebase instead replays your local, unpushed commits on top of what you just fetched. Those commits are private by definition, so rebasing them is safe and leaves no merge noise:
git pull --rebase # rebase local commits on top of upstream
git config --global pull.rebase true # make rebase the default for every pull
The one caveat: if a conflict appears mid-rebase, resolve it and run git rebase --continue (not git commit), or git rebase --abort to bail out and return exactly to where you started.
Quick reference and undoing a bad one
| Situation | Use | Why |
|---|---|---|
| Integrating a shared or public branch | merge | rewriting shared commits breaks everyone else |
Landing a finished PR into main |
merge | keeps an auditable, revertable integration point |
| Cleaning up your own branch pre-PR | rebase -i |
squash and reword into a readable story |
Syncing a private feature branch with main |
rebase | avoids merge bubbles, gives a clean PR diff |
Everyday git pull with local commits |
pull --rebase |
no merge commit for routine syncs |
| History is already pushed and shared | merge | private-only is the line rebase must not cross |
Both operations are recoverable, which is what makes experimenting safe. git reflog shows every position HEAD has held; find the line just before the operation and git reset --hard HEAD@{n} to return to it. Git also records ORIG_HEAD pointing at where you were before the last merge or rebase, so git reset --hard ORIG_HEAD undoes the most recent one directly. And any merge or rebase caught mid-conflict is cancelable with git merge --abort or git rebase --abort.
FAQ
Is git rebase better than merge?
Neither is better in the abstract; they optimize for different things. Rebase gives a clean, linear history and is ideal for tidying your own work before sharing it. Merge preserves the true history and is the safe choice for integrating anything other people already have. Use rebase on private commits, merge on shared ones.
Does rebasing delete commits?
Not exactly — it replaces them. Rebase creates new commits with new SHAs and leaves the originals unreferenced, so the old ones survive in the reflog until Git's garbage collection removes them (typically after a few weeks). That's why git reflog can rescue a rebase you regret, and also why rewriting shared commits is so disruptive.
What is the golden rule of rebasing?
Never rebase commits that exist outside your own repository — anything pushed to a shared branch that others may have based work on. Rewriting those commits changes their SHAs and forces everyone holding the originals into a messy reconciliation. Rebase private commits freely; merge to integrate shared ones.
Should I use git pull --rebase?
For everyday syncing, usually yes. A normal git pull merges and creates a merge commit whenever your local branch and the remote both moved; --rebase replays your local, unpushed commits on top instead, keeping history linear with no merge noise. Since those commits are private, rebasing them is safe. Set pull.rebase true to make it the default.
How do I undo a rebase or merge?
If you're mid-operation, git rebase --abort or git merge --abort returns you to the start. If it already completed, git reset --hard ORIG_HEAD undoes the most recent merge or rebase, or use git reflog to find the exact prior position and git reset --hard HEAD@{n} to jump back to it.
Does a squash merge count as merge or rebase?
It's a third option. A squash merge collapses all of a branch's commits into one new commit on the target, giving a clean single-commit result like rebase but through a normal merge that never rewrites the shared branch. The trade-off is that you lose the branch's individual commits — good for noisy feature branches, less good when the granular history matters.
Next step
Open the next branch you're about to integrate and decide on purpose. If those commits are private — local, or a feature branch only you have touched — rebase to hand reviewers a clean, linear story. If they're shared, merge and keep the honest record. And run git config --global pull.rebase true so your daily syncs stop minting pointless merge commits. More pragmatic engineering guides are at TheAppCode.