Git merge vs rebase: which one to use, and when (with the rules every team should agree on)

· Category: Git

Short answer

Both git merge and git rebase integrate work from one branch into another. They differ in what the resulting history looks like:

  • git merge preserves the actual sequence of work. The result is a merge commit with two parents.
  • git rebase rewrites your branch's commits as if they were made on top of the target branch. The result is a linear history with no merge commit.

The single rule that prevents 90% of the pain: never rebase commits that have been pushed and that other people are basing work on. Rebasing rewrites SHAs, so anyone else's clone now disagrees about what feature/x is. Merge is always safe on shared history; rebase is only safe on history that is yours alone.

What each command actually produces

Starting state — feature branched off main two commits ago, and main has moved on:

       A---B  (feature)
      /
*--*--C---D--E  (main)

git switch feature && git merge main:

       A---B---M  (feature)
      /       /
*--*--C---D--E  (main)

A new merge commit M joins B and E. A and B are unchanged.

git switch feature && git rebase main:

                 A'--B'  (feature)
                /
*--*--C---D--E  (main)

A' and B' are new commits with the same diffs as A and B but new parents — and therefore new SHAs.

This is why "never rebase shared branches" matters. Anyone who had a clone with A/B now has a fork that no longer matches the remote.

When to use which (concrete rules)

Situation Merge Rebase
Integrating a feature into main that several people watched go in
Pulling new main commits into your unpushed feature branch
Cleaning up your own commits before opening a PR ✅ (interactive)
Picking up upstream changes on a long-lived shared release branch
Squashing 12 WIP commits into one before review ✅ (interactive)
Merging a hotfix into both main and release/* ✅ (or cherry-pick)

The cleanest team policy is: rebase your private feature branch onto main while you work; merge the feature branch into main (with --no-ff if you want explicit merge commits) when it's ready.

Steps for each workflow

Merge workflow (default, safe)

git switch main
git pull --ff-only origin main
git merge --no-ff feature/user-login
# Merge made by the 'recursive' strategy.
git push origin main

--no-ff forces a merge commit even when fast-forward is possible. The advantage: every feature merge shows up explicitly in git log --merges, which is useful when bisecting or reviewing history. Pair with git-rerere to make recurring conflicts trivial.

Rebase-then-merge (clean linear history)

# 1. Rebase your feature branch on top of latest main
git switch feature/user-login
git fetch origin
git rebase origin/main
# Resolve conflicts as they appear — git rebase --continue / --abort.

# 2. Push (force-with-lease, because rebase rewrote SHAs)
git push --force-with-lease origin feature/user-login

# 3. Merge into main fast-forward (history stays linear)
git switch main
git merge --ff-only feature/user-login
git push origin main

--ff-only is the key: it refuses to merge if a fast-forward isn't possible, which is a guard against accidentally creating a merge commit you didn't want.

Interactive rebase to clean up before review

git rebase -i HEAD~6
# pick   abc1234  Add login form
# squash def5678  fix typo
# squash 4567abc  fix another typo
# reword 7890def  Add validation
# drop   1111111  WIP debug log
# pick   2222222  Add tests

pick keeps the commit; squash rolls into the previous one; reword keeps the diff but lets you edit the message; drop discards. Save the file and Git replays the commits per the script, asking for messages where needed.

git pull --rebase (almost always what you want)

Default git pull is git fetch + git merge, which produces a merge commit every time someone else pushed before you. That fills history with Merge branch 'main' of origin into main noise. Switch to rebase pulls:

git config --global pull.rebase true
git config --global rebase.autoStash true     # don't fail on dirty tree

Now git pull rebases your unpushed local commits on top of origin/main, no merge commit. Reference: git-pull(1) — pull.rebase.

When merge is required, not optional

  • A pull request from a fork on GitHub: GitHub's merge button can rebase or squash, but the resulting commit on your main is the merge — there's no version of "rebase the PR into main with no commit at all".
  • Two long-lived branches that diverge regularly (e.g. main and next in many large projects). Rebasing one onto the other moves history that other people are basing PRs on.
  • Recovering from a bad rebase: when someone has rebased a shared branch and broken everyone's clones, the fix is git merge to reconcile, not a counter-rebase.

Force-push safety

After a rebase, git push is rejected because the remote branch's SHAs don't match yours. The unsafe option is --force. The safe option is --force-with-lease:

git push --force-with-lease origin feature/user-login

--force-with-lease only succeeds if the remote tip is what you last fetched — meaning nobody else has pushed in the meantime. If they have, the push is refused and you have to fetch + reconcile. Make it the default for anything past git push -u.

Common issues

My rebase had 30 conflicts on the same 5 lines. Each commit in your branch is replayed on top of the new base, so a conflict on a line touched in 6 commits gets resolved 6 times. Two ways to make this less painful: (1) git rebase -i and squash before rebasing onto main, so there are fewer commits to replay; (2) git config --global rerere.enabled true so each resolution is recorded and replayed automatically. See how to resolve Git merge conflicts for the conflict-marker primer.

git push was rejected after rebase even though I'm the only one on this branch. Add --force-with-lease. It's still rejected if your local view of origin/feature/x is stale — run git fetch first.

I rebased a branch that was shared and now my coworker's clone is broken. Their fix: git fetch && git reset --hard origin/feature/x. They lose any local-only commits, so coordinate before doing this. Going forward, never rebase shared branches.

My merge commit message says Merge remote-tracking branch 'origin/main' into main. You ran git pull without rebase config and accumulated merge noise. Set pull.rebase = true once and rewrite history (interactive rebase or git reset --hard origin/main if local commits are disposable).

I rebased and lost commits. You don't lose them — they're orphaned, not deleted. git reflog shows every prior HEAD position. git reset --hard <reflog-sha> to restore. See how to use git reflog to recover lost commits.

Tips