Git Rebase vs Merge: Which One Should You Use and When
How git merge and git rebase differ, what each does to your commit history, when each strategy is appropriate, and the mistakes that lead to lost work or public history rewrites.
The one-sentence distinction that makes everything else clear
Merge combines two branches by creating a new commit that has two parents, preserving the exact history of both. Rebase replays your commits on top of another branch, rewriting their SHA hashes to produce a linear history with no merge commits.
The practical consequence: merge is safe to use on any branch, including shared ones. Rebase rewrites history, so using it on commits that others have already pulled will cause them serious problems. The rule most teams use is: rebase your own private branches, merge into shared branches.
What git merge does
When you run git merge feature from main, Git finds the common ancestor commit of both branches, applies the changes from both, and creates a new merge commit with both branch tips as parents. The history of both branches is preserved exactly as it happened.
# Before merge
main: A → B → C
↑
feature: A → B → D → E
# After: git checkout main && git merge feature
main: A → B → C → M (M is the merge commit, has parents C and E)
↗ ↘
feature: A → B → D → E
# The --no-ff flag forces a merge commit even when fast-forward is possible
git merge --no-ff feature
# Fast-forward merge: when feature is directly ahead of main, Git just moves
# the main pointer forward — no merge commit is created unless you use --no-ff
git merge feature # fast-forward if possible (default behavior)Merge commits are honest. They show exactly when branches diverged and when they came back together. The downside is that in an active repo with many contributors, the graph can become difficult to read and git log output becomes noisy with merge commits.
What git rebase does
Rebase takes the commits on your branch and replays them on top of another branch, one by one. Each commit gets a new SHA because its parent commit has changed. The result looks like your work was started from the current tip of the target branch — even if the two branches diverged weeks ago.
# Before rebase main: A → B → C feature: A → B → D → E # After: git checkout feature && git rebase main main: A → B → C feature: A → B → C → D' → E' (D' and E' are new commits — same changes, new SHAs) # Then fast-forward main: git checkout main && git merge feature main: A → B → C → D' → E' (perfectly linear, no merge commit) # Interactive rebase — squash, reorder, reword commits git rebase -i main # Common interactive rebase actions: # pick → keep commit as-is # squash → fold commit into the one above it # fixup → like squash but discard the commit message # reword → keep commit but edit the message # drop → remove the commit entirely
The golden rule: never rebase shared commits
When you rebase, you create new commits with new SHAs. If someone else has already based work on your original commits, their branch still points to the old SHAs that no longer exist on your branch. When they try to merge or pull, Git sees two completely unrelated commit sequences that happen to contain the same changes, and the result is duplicate commits, confusing conflicts, and a broken history that is painful to untangle.
# SAFE: rebase your private feature branch on top of main git checkout feature/my-changes git fetch origin git rebase origin/main git push --force-with-lease # ok if no one else has this branch # DANGEROUS: never rebase main, develop, or any shared branch git checkout main git rebase feature # DO NOT do this — others have pulled main # --force-with-lease vs --force # --force-with-lease checks that no one else pushed since your last fetch # It will refuse if someone else pushed — safer than --force # Never use --force on shared branches
Merge strategies and when to use each
Decision guide
Scenario → Recommended approach ────────────────────────────────────────────────────────── Integrating a feature PR into main → Merge (or squash merge) Updating a feature branch with main → Rebase (private branch) Updating a long-lived shared branch → Merge Cleaning up messy WIP commits → Interactive rebase (before PR) Pulling from remote on a shared branch → git pull --rebase (optional) Hotfix into main → Merge with --no-ff for visibility
Squash merging: a middle ground
Squash merge takes all the commits from a feature branch, combines their changes into a single commit, and merges that one commit into the target branch. You get a linear history like rebase without rewriting any commits that others might have — the feature branch is untouched. GitHub and GitLab offer this as a merge option ("Squash and merge").
# Squash all commits from feature into one, then merge git checkout main git merge --squash feature git commit -m "feat: add user authentication (#42)" # Result: one clean commit on main, feature branch unchanged # Before: main has A → B → C # After: main has A → B → C → S (S contains all changes from D, E, F, G) # Tradeoff: individual commit messages from the feature branch are lost # Good for: messy WIP histories that do not need to be preserved # Bad for: features where individual commits tell an important story
Resolving conflicts during rebase
Rebase applies commits one at a time, which means you might encounter conflicts on each commit individually rather than resolving all conflicts in one merge commit. This can mean more conflict resolutions to do, but each one is isolated to a single change rather than a combined diff.
# During a rebase that hits a conflict: # 1. Resolve the conflict in the file as usual # 2. Stage the resolved file git add src/conflicted-file.ts # 3. Continue (do not commit — rebase handles the commit) git rebase --continue # 4. If the commit becomes empty after conflict resolution git rebase --skip # 5. Abort and return to pre-rebase state if things get too messy git rebase --abort # After a successful rebase, push requires --force-with-lease # because you rewrote history git push --force-with-lease origin feature/my-branch
git pull --rebase: keeping a clean fetch
By default, git pull is a fetch followed by a merge. If you and a teammate both pushed to the same branch (common on main in smaller teams), the pull creates a merge commit that just says "merge branch 'main' from origin" — noise with no useful information. Using git pull --rebase replays your unpushed commits on top of the fetched commits instead, keeping the history linear.
# Pull with rebase instead of merge git pull --rebase origin main # Make it the default for all pulls git config --global pull.rebase true # Or per-repo git config pull.rebase true # The --autostash flag stashes local changes before pulling # and re-applies them after — saves you from "can't pull with dirty tree" git pull --rebase --autostash