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 --squashor 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
mainon 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:
| Command | What it does |
|---|---|
pick | Keep the commit as-is |
reword | Keep the commit but edit its message |
edit | Pause the rebase so you can amend the commit |
squash | Melt this commit into the previous one; combine messages |
fixup | Melt into previous commit; discard this commit's message |
drop | Delete 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
| Dimension | git merge --no-ff | git rebase | git merge --squash |
|---|---|---|---|
| History shape | Non-linear (branch topology preserved) | Linear | Linear |
| Commit count | All original commits + merge commit | All original commits (rewritten) | One commit |
| Original SHAs preserved | Yes | No (new SHAs) | N/A |
| Collaboration safety | Safe for shared branches | Dangerous on shared branches | Safe (squash is on target) |
| Traceability | Highest — branch + individual commits visible | High — individual commits in order | Low — single commit only |
| Bisect friendliness | Good | Excellent | Poor (one large commit) |
| PR review experience | Can be noisy with WIP commits | Clean if commits are well-structured | Always clean, one commit |
| Best for | Long-lived branches, releases, OSS contributions | Local cleanup, staying current with upstream | Feature branches merged to main (GitHub/GitLab flow) |
| Risk | Low | Medium (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 withmainand 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 keepsmain'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 busymain.
When in doubt, match your team's existing convention — consistency in a shared history matters more than perfection.
Further Reading
- Pro Git Book — Git Branching: Rebasing — the authoritative reference, free online.
- Pro Git Book — Git Tools: Rewriting History — interactive rebase, filter-branch, and more.
- Atlassian Git Tutorials: Merging vs. Rebasing — excellent visual explanations of the commit graph.
- GitHub Blog: Squash your commits — GitHub's own rationale for the squash merge button.