~/blog/-blog-git-rebase-vs-merge-
blog · DevOps

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.

last updated · June 20, 2026by @vultio

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