Merge, Squash, or Rebase? Choosing How to Integrate a Git Feature Branch
Merge commit, squash, or rebase-and-fast-forward? A decision guide comparing Git's three supported branch integration modes, with constraints, trade-offs, and validation commands.
24 Nov 2025, 16:43 UTC

Your feature branch is reviewed and ready. Now someone has to pick how it lands on main: a merge commit, a squash merge, or a rebase followed by a fast-forward. All three are fully supported by Git, all three produce the same final tree of files, and yet they create very different histories — and different problems later. This guide compares the three modes, states the constraints that rule options in or out, and shows how to verify the result.
The decision and its constraints
The choice is not about what the code looks like after integration — the working tree is identical either way. It is about what the commit graph records and what future operations remain easy. Three constraints usually decide it:
- Has the branch been pushed and pulled by others? If yes, rebasing is off the table, because rebase rewrites commits that collaborators may already have.
- Do the intermediate commits have lasting value? Careful, bisectable commits argue for preserving them; a stream of "wip", "fix typo", "actually fix it" commits argues for squashing.
- Will this branch be merged again later? Long-lived branches that integrate repeatedly behave badly with squash merges, because Git loses the record of what was already merged.
The three supported modes compared
| Mode | Command | History shape | Intermediate commits | Safe on shared branches |
|---|---|---|---|---|
| Merge commit | git merge --no-ff feature | Non-linear, branch topology preserved | Kept, with original authorship | Yes |
| Squash merge | git merge --squash feature | Linear, one new commit on main | Collapsed into a single commit | Yes |
| Rebase + fast-forward | git rebase main feature, then git merge --ff-only feature | Linear, individual commits intact | Kept, but with new commit IDs | No — rewrites published history |
Trade-offs in practice
Merge commits: maximum auditability, busier graph
--no-ff forces a merge commit even when a fast-forward is possible, so the branch boundary stays visible in git log --graph. Every original commit keeps its ID, author, and timestamp, which matters for regulated environments, release branches, and any work where "who changed what, when" must be reconstructable. The cost is a graph that gets noisy on teams merging many times a day, and a first-parent view (git log --first-parent) becomes the practical way to read main.
Squash merges: clean main, dropped linkage
A squash merge stages all the branch's changes and creates one new commit on the target. Main stays linear and each feature becomes a single revertable unit — handy for git revert. The catch is that Git records no parent relationship to the original branch. If the same branch is merged again later, Git no longer sees the shared history and you can hit the same conflicts a second time. Squash is therefore best for branches that are deleted immediately after merging.
Rebase + fast-forward: linear history, real danger
Rebasing replays the feature branch's commits onto the current tip of main, so the subsequent merge is a fast-forward: linear history with every commit preserved. The rule that cannot be bent: never rebase commits that have been pushed and pulled by others. Rewriting published history forces collaborators to recover with git pull --rebase or manual resets. For solo, short-lived branches this is the tidiest option; for anything shared, use a merge commit.
A concrete workflow and validation
Assume the common policy: rebase a solo branch before integrating. Run these from a local clone with a clean working tree; no special permissions are needed, but you need push rights to publish the result.
# On the feature branch, replay it onto current main
git switch feature
git fetch origin
git rebase origin/main
# Integrate with a fast-forward only (fails loudly if not possible)
git switch main
git merge --ff-only featureNow validate the shape and completeness of the result:
# 1. Confirm linear history: no merge commit, commits sit on top of main
git log --graph --oneline -10
# 2. Confirm Git considers the branch fully merged
git branch --merged main
# 3. Review exactly what the integration changed (reflog permitting)
git diff main@{1}..main --statThe first command should show your feature commits in a straight line above the previous main tip. The second should list feature; if it does not, something was left behind. The third uses the reflog, so it only works in the local repo where the merge just happened — run it before fetching or further commits push the entry down.
If you chose a squash merge instead, one extra check is worth running:
# After a squash merge, these commits show as unmerged ('+'),
# confirming Git dropped the linkage to the original branch
git cherry main featureLines prefixed with + are commits Git considers not yet on main — expected after a squash, and the reason you should delete the branch rather than merge it again.
Checking that local config doesn't surprise you
Default behaviors are config- and version-sensitive, so two teammates can run the same command and get different history. Before standardizing a workflow, check:
git --version
git config --get pull.rebase # if 'true', git pull rebases instead of merging
git config --get merge.ff # if 'only', merges refuse to create merge commitsConfirm flag details against your installed version with git help merge and git help rebase. If you want to see the three shapes side by side before adopting a policy, create a scratch repository with two branches and apply each mode in turn, running git log --graph --oneline --all after each — the difference is immediately visible.
Limitations and a practical policy
No mode fixes genuinely conflicting changes; all three require the same conflict resolution work, just at different times (rebase surfaces conflicts commit-by-commit, merge all at once). And none of this is enforceable by Git itself — consistency comes from team convention, protected-branch settings on your hosting platform, or server-side hooks.
A policy that works well for many teams: rebase for solo short-lived branches, merge commits for shared or release branches, squash for noisy work-in-progress branches whose intermediate commits add no value. Write it down, set the matching options on the hosting platform, and verify with git log --graph after the first few integrations that reality matches the policy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.