}

Git Rebase vs Merge vs Squash: When to Use Each (2026 Guide)

Git Rebase vs Merge vs Squash: When to Use Each (2026 Guide)

Every developer hits this wall: your feature branch is ready, and now you have to decide how to integrate it. Do you git merge? git rebase? git merge --squash? The wrong choice leads to a messy history full of noise commits or, worse, a rewritten public branch that breaks your teammates' work.

This guide cuts through the confusion with commit-graph diagrams, concrete command examples, a comparison table, and team workflow recommendations you can adopt today.


The Core Difference in One Sentence

  • Merge preserves history exactly as it happened, adding a merge commit to tie branches together.
  • Rebase replays your commits on top of another branch, rewriting them with new SHAs — producing a linear history.
  • Squash (either via git merge --squash or interactive rebase) collapses multiple commits into one before integrating.

The right choice depends on who will see the history, how many people share the branch, and how much granularity you want to preserve.


Visual Foundation: Reading Commit Graphs

Before diving into commands, it helps to see what each strategy does to the commit graph. Assume you have this starting state:

main:    A --- B --- C
                      \
feature:               D --- E --- F

A, B, C are commits on main. Your feature branch diverged at C and has three additional commits (D, E, F).


git merge

git merge integrates a branch by creating a merge commit that has two parents — one from each branch being joined.

Fast-forward merge

If main has not moved since the feature branch was created, Git can simply move the main pointer forward. No merge commit is created:

Before:
main:    A --- B --- C
                      \
feature:               D --- E --- F

After (git merge feature, fast-forward possible):
main:    A --- B --- C --- D --- E --- F
                                       ^
                                     HEAD (main)

Fast-forward is the default when it is possible. It produces a clean linear graph, but you lose the fact that D–F were once a separate branch.

Merge commit (--no-ff)

Using --no-ff (no fast-forward) forces Git to create a merge commit even when fast-forward is possible:

git checkout main
git merge --no-ff feature
After (--no-ff):
main:    A --- B --- C ----------- M
                      \           /
feature:               D --- E --- F

M is the merge commit. It records when the feature was integrated and keeps the branch topology visible. This is valuable for understanding the project's history months later.

When merge commits add value:

  • Long-lived feature branches where the "when did this land?" question matters.
  • Release branches — you want a clear marker that version 2.3 was merged into main on a specific date.
  • Open-source projects where the commit history is a record of contribution.

Basic merge command

# Merge feature into main, creating a merge commit
git checkout main
git merge --no-ff feature -m "Merge feature/payment-overhaul into main"

git rebase

Rebase takes the commits from your branch and replays them one by one on top of another branch. The original commits are abandoned and replaced with new ones that have different SHAs (even if the content is identical).

git checkout feature
git rebase main
Before:
main:    A --- B --- C
                      \
feature:               D --- E --- F

After (git rebase main):
main:    A --- B --- C
                      \
feature:               D' --- E' --- F'

D', E', F' are new commits. They carry the same changes as D, E, F, but they now sit directly on top of C rather than branching away from it. After rebasing, a fast-forward merge to main is trivial:

git checkout main
git merge feature   # fast-forward, no merge commit needed
Result:
main:    A --- B --- C --- D' --- E' --- F'

The history looks as if the feature was developed directly on main all along.

The Golden Rule of Rebase

Never rebase a branch that other people are using.

When you rebase, you replace existing commits with new ones. If a colleague has already based their work on the old commits (D, E, F) and you replace them with D', E', F', their local branch is now diverged from yours in a way that Git cannot cleanly reconcile. The result is duplicated commits and painful merges.

Safe targets for rebase: - Your local feature branch that has not been pushed yet. - A personal branch that only you are working on. - After coordinating with all contributors (rare, advanced use case).

Never rebase: main, develop, staging, or any branch tracked by CI/CD pipelines.

Rebasing onto main to stay current

While working on a feature branch, main moves forward. Rather than creating a merge commit to pull in those upstream changes, you can rebase:

git fetch origin
git rebase origin/main

This keeps your branch current and conflict resolution happens commit by commit, making it easier to pinpoint exactly which of your changes conflicts with which upstream change.


Interactive Rebase: Cleaning Up Before a PR

Interactive rebase (git rebase -i) lets you rewrite a sequence of commits before they are shared. It is one of the most powerful tools for producing a clean, reviewable pull request.

# Rewrite the last 4 commits interactively
git rebase -i HEAD~4

Git opens your editor with a list like this:

pick a1b2c3d Add payment model
pick d4e5f6a WIP: forgot to add migration
pick 7890abc Fix typo in payment model
pick b1c2d3e Add payment API endpoint

Each line is a commit. You change the word at the start to control what happens:

CommandWhat it does
pickKeep the commit as-is
rewordKeep the commit but edit its message
editPause the rebase so you can amend the commit
squashMelt this commit into the previous one; combine messages
fixupMelt into previous commit; discard this commit's message
dropDelete this commit entirely

Practical example: squash WIP commits, fix typo commit

pick a1b2c3d Add payment model
fixup d4e5f6a WIP: forgot to add migration
fixup 7890abc Fix typo in payment model
pick b1c2d3e Add payment API endpoint

After saving and closing the editor, Git produces two clean commits:

pick a1b2c3d Add payment model        ← includes migration and typo fix
pick b1c2d3e Add payment API endpoint

The WIP and typo commits vanish from history. Your reviewer sees a logical two-step implementation instead of your messy development process.

Rewording a commit message

reword a1b2c3d Add payment model

Git pauses and opens your editor on that commit message alone. Edit it, save, and the rebase continues.

Editing a commit mid-rebase

edit d4e5f6a WIP: forgot to add migration

Git pauses after applying that commit. You can run git add, git rm, or git commit --amend to change the commit's content, then:

git rebase --continue

Dropping a commit

drop 7890abc Fix typo in payment model

That commit is simply removed. Be careful — if later commits depend on its changes, you will get conflicts.


git merge --squash

git merge --squash takes all the commits on a branch and collapses them into a single set of staged changes without actually creating a merge commit. You then commit manually:

git checkout main
git merge --squash feature
git commit -m "Add payment module (squash merge from feature/payment)"
Before:
main:    A --- B --- C
                      \
feature:               D --- E --- F

After:
main:    A --- B --- C --- S

S is a single new commit containing all the changes from D, E, and F combined. The feature branch history (D, E, F) is not part of main's history at all.

Key difference from interactive rebase squash: The feature branch is not modified. D, E, F still exist on feature. The squash only affects what lands on main.


Comparison Table

Dimensiongit merge --no-ffgit rebasegit merge --squash
History shapeNon-linear (branch topology preserved)LinearLinear
Commit countAll original commits + merge commitAll original commits (rewritten)One commit
Original SHAs preservedYesNo (new SHAs)N/A
Collaboration safetySafe for shared branchesDangerous on shared branchesSafe (squash is on target)
TraceabilityHighest — branch + individual commits visibleHigh — individual commits in orderLow — single commit only
Bisect friendlinessGoodExcellentPoor (one large commit)
PR review experienceCan be noisy with WIP commitsClean if commits are well-structuredAlways clean, one commit
Best forLong-lived branches, releases, OSS contributionsLocal cleanup, staying current with upstreamFeature branches merged to main (GitHub/GitLab flow)
RiskLowMedium (never on public branches)Low

Team Workflow Recommendations

Workflow 1: GitHub/GitLab Flow — Squash merge to main

This is the dominant pattern for small-to-medium product teams in 2026. Every pull request is squash-merged into main, producing one commit per feature or bug fix.

feature:  A --- B --- C --- D (messy WIP commits, that's OK)
                              \
main:     ... --- X ---------- S   (S = squash of A–D, clean message)

Advantages: - main's log is a clean, human-readable record of features shipped. - git bisect is highly effective because each commit represents a meaningful change. - Developers can commit freely on their branch without worrying about hygiene.

Setup (GitHub): In repository settings, enable "Allow squash merging" and disable the others, or make squash the default. GitLab equivalent: set "Squash commits" as the default merge method.

Locally:

# Keep your feature branch rebased on main while developing
git fetch origin
git rebase origin/main

# When ready, squash-merge via the UI — or locally:
git checkout main
git merge --squash feature/payment
git commit -m "Add payment module

Implements Stripe integration, refund flow, and webhook handler.
Closes #142."

Workflow 2: Long-lived branches — regular merge commits

For projects with develop, staging, and main branches (Gitflow-style), or for monorepos with release branches, regular merge commits preserve the audit trail.

# Merge develop into main with a descriptive merge commit
git checkout main
git merge --no-ff develop -m "Merge develop into main for v2.4.0 release"
git tag v2.4.0

The merge commit becomes a milestone marker. Tools like git log --merges --oneline give you a release history at a glance.

Workflow 3: Local cleanup before PR — interactive rebase

Before opening a pull request, use interactive rebase to turn your raw development commits into a clear narrative:

# See how many commits are on your branch relative to main
git log --oneline origin/main..HEAD

# Interactively rebase all of them
git rebase -i origin/main

Polish the commits: squash WIPs, reword vague messages, drop debug commits. Then push the clean branch and open the PR. Your reviewer sees a logical sequence, not your work diary.

# Force-push is necessary after rewriting (only safe on your personal branch)
git push --force-with-lease origin feature/payment

--force-with-lease is safer than --force: it fails if someone else has pushed to the branch since your last fetch, preventing accidental overwrites.


git pull --rebase vs git pull

git pull is shorthand for git fetch followed by git merge. When you pull a shared branch, this creates a merge commit like:

Merge branch 'main' of github.com:org/repo

These commits carry no information and pollute the log. Use --rebase instead:

git pull --rebase origin main

This fetches the remote changes and rebases your local commits on top of them, keeping the history linear.

Make it the default:

git config --global pull.rebase true

Or per-repository:

git config pull.rebase true

With this setting, git pull always rebases. If you ever explicitly want a merge pull, use git pull --no-rebase.


The rerere Config: Reusing Conflict Resolutions

Rebasing applies commits one by one, which means a conflict that exists in one commit may need to be resolved again for the next commit. This becomes painful on long branches.

rerere (Reuse Recorded Resolution) remembers how you resolved a conflict and automatically replays that resolution the next time the same conflict appears.

# Enable globally
git config --global rerere.enabled true

When you resolve a conflict, Git records it in .git/rr-cache/. Next time the same hunk conflicts — even on a different branch or after git rebase --abort and retry — Git resolves it automatically and tells you:

Resolved 'src/payment/models.py' using previous resolution.

This is especially valuable when you rebase a long feature branch against a frequently-updated main.


Fixing a Botched Rebase

Rebases go wrong. You misread the conflict, applied the wrong resolution, or accidentally dropped commits. Here is how to recover.

Step 1: Abort an in-progress rebase

If the rebase is still running (you are in the middle of resolving conflicts):

git rebase --abort

This restores your branch to exactly the state it was in before you ran git rebase. No harm done.

Step 2: Recover after the rebase completed

If you already completed the rebase but the result is wrong, use git reflog. The reflog is a local log of every position your HEAD has been at, including before the rebase:

git reflog

Output:

f3a1b2c HEAD@{0}: rebase (finish): returning to refs/heads/feature/payment
e9d8c7b HEAD@{1}: rebase (pick): Add payment API endpoint
d6c5b4a HEAD@{2}: rebase (pick): Add payment model
1a2b3c4 HEAD@{3}: checkout: moving from feature/payment to feature/payment
8f7e6d5 HEAD@{4}: commit: Add payment API endpoint    ← original, before rebase
7c6b5a4 HEAD@{5}: commit: Add payment model           ← original, before rebase

Find the entry just before the rebase started (the last commit entry before checkout: moving...). In this example, HEAD@{4} or its SHA 8f7e6d5 is the pre-rebase tip.

Reset your branch to that point:

git reset --hard HEAD@{4}
# or equivalently:
git reset --hard 8f7e6d5

Your branch is now exactly as it was before the botched rebase. The reflog entries are safe for 90 days by default (controlled by gc.reflogExpire).

Step 3: Verify recovery

git log --oneline -10
git status

Confirm the commits look right and there are no unexpected changes. Then retry the rebase carefully, or choose a different integration strategy.


Decision Flowchart

Use this flowchart to choose the right strategy:

Is the branch shared with other developers?
├── Yes → Use git merge (--no-ff for long-lived branches)
└── No (personal/feature branch)
    ├── Do you want a single clean commit on main?
    │   └── Yes → git merge --squash (or squash-merge via PR UI)
    └── Do you want individual commits preserved?
        ├── Are they already clean/meaningful?
        │   └── Yes → git rebase onto main, then fast-forward merge
        └── Are there WIP/fixup commits to clean up?
            └── Yes → git rebase -i (interactive), then merge or push PR

Quick Reference Cheat Sheet

# Merge with explicit merge commit (preserves branch topology)
git merge --no-ff feature

# Rebase feature onto current main (linear history, new SHAs)
git checkout feature && git rebase main

# Interactive rebase: clean up last N commits
git rebase -i HEAD~N

# Squash entire feature branch into one commit on main
git checkout main && git merge --squash feature && git commit

# Pull with rebase instead of merge (keep history linear)
git pull --rebase origin main

# Make pull.rebase the default
git config --global pull.rebase true

# Enable rerere (reuse recorded conflict resolutions)
git config --global rerere.enabled true

# Abort an in-progress rebase
git rebase --abort

# Find pre-rebase SHA in reflog
git reflog

# Hard-reset to pre-rebase state (destructive — verify SHA first)
git reset --hard HEAD@{N}

# Force-push rebased branch (safer than --force)
git push --force-with-lease origin feature/my-branch

Summary

There is no universally correct strategy — each tool fits a different context:

  • git merge --no-ff: Reach for this on long-lived branches, release integrations, and open-source contribution histories where the branch topology is meaningful information.
  • git rebase: Use locally to keep your feature branch current with main and to produce a linear, bisect-friendly history. Never use it on branches others depend on.
  • git rebase -i (interactive): The most valuable pre-PR tool. Turn a messy development history into a clean, reviewable commit sequence before anyone else sees it.
  • git merge --squash: The workhorse of modern team workflows. One commit per feature keeps main's log readable and bisect effective. Perfect for GitHub Flow and GitLab Flow.
  • git pull --rebase: Set as default globally. Eliminates the meaningless "Merge branch 'main' of..." commits from your log.
  • rerere: Enable it globally. It pays dividends every time you rebase a long branch against a busy main.

When in doubt, match your team's existing convention — consistency in a shared history matters more than perfection.


Further Reading

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro