Squash vs merge vs rebase: a decision matrix Jump to heading

Every pull request ends with one irreversible choice: how its commits land on the trunk. GitHub, GitLab, and Bitbucket all expose three buttons — squash-merge, a merge commit (--no-ff), and rebase-merge — and teams tend to pick one by habit rather than by consequence. That choice propagates into everything downstream: how readable git log is a year later, how many steps git bisect needs to find a regression, whether a bad release reverts in one command or five, and how much CI compute each merge burns. This page turns the choice into a scored decision within Squash & Fixup Strategies, which covers the mechanics of collapsing history before it reaches the trunk.

This is a merge-time policy question, distinct from the day-to-day Merge vs Rebase Decision Matrix that governs how you keep a working branch up to date. Both matter; this page is about the final integration button, not the branch-maintenance workflow.

The three strategies precisely Jump to heading

Before scoring, pin down exactly what each button does to the graph:

  • Squash-merge takes every commit on the branch, combines their diffs into a single new commit, and applies it to the trunk. The branch’s individual commits never appear on the trunk. The result is one commit per PR, with a linear history.
  • Merge commit (--no-ff) creates a new commit with two parents — the trunk tip and the branch tip — preserving every original branch commit and recording an explicit integration point. History is non-linear but complete.
  • Rebase-merge replays each branch commit onto the trunk tip one by one, generating new SHAs, then fast-forwards the trunk. Every original commit survives, but there is no merge commit and no branch topology — the history is linear as if the work had always been sequential.

The distinction that drives most trade-offs is how many commits reach the trunk: one (squash), all-plus-one (merge commit), or all (rebase). That number decides per-commit CI cost, bisect granularity, and how noisy git log --first-parent looks.

The decision matrix Jump to heading

Score each strategy against the six criteria that matter operationally. “Best” is relative to the criterion, not absolute — the point is that no strategy wins every row, so you are trading axes against each other. The middle column is the non-fast-forward merge commit produced by git merge --no-ff.

CriterionSquash-mergeMerge commitRebase-merge
Trunk history readabilityOne clean commit per PR; git log reads as a feature listExplicit integration points, but noisy with WIP commitsLinear, but every WIP commit pollutes the trunk
git bisect efficiencyFewest steps — one commit per feature; culprit is a whole PR--first-parent bisect steps per-PR; full bisect sees all commitsFine-grained, but only if every commit builds
Per-commit attributionLost — sub-commits collapse; only PR author on trunkFully preserved with original authors and timestampsFully preserved, authors intact, timestamps rewritten
RevertabilityOne git revert <sha> undoes the entire featuregit revert -m 1 <merge-sha> undoes the whole PRMust revert N commits or a range; no single handle
CI cost per mergeOne post-merge pipeline (one new trunk commit)One post-merge pipeline (one new trunk commit)N pipelines if CI runs per-commit
Audit & complianceSimple 1:1 PR-to-commit trail; loses granular authorshipRichest — records who integrated what and whenLinear trail, but rewritten SHAs break external references

Read the table as a set of dominant defaults. If your top priority is a legible trunk and trivial reverts, squash-merge wins the most rows. If your priority is a complete, tamper-evident integration record for regulated review, the merge commit’s second parent is the feature you are paying for. Rebase-merge only pulls ahead when you have invested in per-commit discipline — every commit atomic, buildable, and independently meaningful — and you want that granularity on the trunk without merge-commit noise.

The decision flow Jump to heading

Most teams do not need a per-PR decision; they need one enforced default with a documented exception. The tree below resolves to that default in three questions.

Decision tree for choosing squash, merge commit, or rebase-mergeA flow chart. First question: are individual branch commits atomic and independently buildable? If no, use squash-merge. If yes, second question: do you require a complete integration audit record with preserved authorship? If yes, use a merge commit with no fast-forward. If no, third question: does CI run per commit on the trunk? If yes, use squash-merge to avoid N builds; if no, use rebase-merge for a linear history with full commit granularity.Are branch commits atomicand independently buildable?noyesSquash-mergeone commit per PRRequire a complete auditrecord + preserved authorship?yesMerge commit--no-ff, two parentsnoDoes CI run per commiton the trunk?yes — avoid N buildsnoSquash-mergecollapse to one buildRebase-mergelinear, full granularityDefault for most teams: squash-merge. Exception: merge commits for release promotions.

Enforcing each strategy Jump to heading

A decision is worthless if individual contributors can still pick a different button. Lock the chosen strategy at the platform level so the merge shape is uniform across every PR.

GitHub — restrict the allowed merge buttons Jump to heading

GitHub lets you enable or disable each of the three merge types per repository. Enable only the one you chose:

# Squash-only: disable merge commits and rebase-merge
gh api -X PATCH repos/OWNER/REPO \
  -F allow_squash_merge=true \
  -F allow_merge_commit=false \
  -F allow_rebase_merge=false

# Also standardize the squashed commit message to the PR title + body
gh api -X PATCH repos/OWNER/REPO \
  -F squash_merge_commit_title=PR_TITLE \
  -F squash_merge_commit_message=PR_BODY

For a merge-commit-only or rebase-only policy, flip the corresponding booleans:

# Merge-commit-only (preserves branch topology and every commit)
gh api -X PATCH repos/OWNER/REPO \
  -F allow_merge_commit=true \
  -F allow_squash_merge=false \
  -F allow_rebase_merge=false

Verify the repository now exposes only the intended button:

# Should show your chosen type as true, the other two as false
gh api repos/OWNER/REPO \
  --jq '{squash: .allow_squash_merge, merge: .allow_merge_commit, rebase: .allow_rebase_merge}'

GitLab — set the project merge method Jump to heading

GitLab expresses the same choice as a single merge_method plus a squash setting:

# merge_method: "merge" (merge commit), "rebase_merge" (semi-linear), or "ff" (fast-forward only)
# For squash-on-merge, set ff or rebase_merge and force squash on every MR:
curl -X PUT --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/PROJECT_ID" \
  --data "merge_method=ff" \
  --data "squash_option=always"

Verify the applied method:

# Confirms merge_method and squash_option are what you set
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/PROJECT_ID" \
  | grep -Eo '"(merge_method|squash_option)":"[^"]*"'

Local Git defaults for the rebase-merge camp Jump to heading

If you standardize on rebase-merge, align each contributor’s local pull behavior so their branch maintenance matches the integration model and produces fewer conflicts at merge time. The detailed git rebase -i mechanics live in Interactive Rebase Workflows:

# Rebase local work onto the updated upstream instead of creating merge commits
git config --global pull.rebase true
# Reuse recorded conflict resolutions so repeated rebases stop re-asking
git config --global rerere.enabled true

Verify the config took effect:

# Both should print "true"
git config --get pull.rebase
git config --get rerere.enabled

SAFETY WARNING: Rebase-merge rewrites commit SHAs, and any local rebase of a branch others have already pulled will diverge their history — a subsequent git push --force can destroy teammates’ commits. Only rebase branches you own, always use git push --force-with-lease instead of --force, and recover a clobbered branch from git reflog (git reset --hard <sha> back to the pre-rebase entry) before the reflog entry expires.

Validation checklist Jump to heading

Before declaring the policy enforced, confirm each item:

Frequently asked questions Jump to heading

Can I mix strategies per pull request instead of enforcing one? Jump to heading

You can, but a mixed history is harder to reason about and to script against. A pragmatic compromise is to set one default — usually squash-merge — and permit merge commits only for long-lived integration branches such as release-to-main promotions, where preserving the individual release commits is the point. Configure the platform to enable the two button types you want and disable the rest, so the exception is a deliberate, visible choice recorded in the merge event rather than an ad-hoc decision made under deadline pressure.

Does squash-merge break git bisect? Jump to heading

No — it improves it for most teams. Squash-merge collapses each PR into one commit on the trunk, so git bisect steps over whole features and lands on the offending PR in fewer iterations. What you lose is the ability to bisect within a feature’s intermediate commits, which only helps if every work-in-progress commit builds and passes tests on its own. Most branches do not maintain that discipline, so the collapsed commit is both cleaner and faster to bisect. Reserve rebase-merge for the rare teams that keep every commit green.

Why does rebase-merge cost more CI than squash or a merge commit? Jump to heading

Rebase-merge replays every branch commit onto the trunk tip, producing N new commits with new SHAs. If your CI runs per-commit — required status checks on each pushed SHA, or a default-branch pipeline that builds every new commit — you pay for N builds instead of one. Squash-merge and a single merge commit each add exactly one new commit to the trunk, so they trigger one post-merge pipeline. If you want rebase-merge’s linear granularity without the CI multiplier, scope your pipeline to build only the branch tip rather than every replayed commit.

  • Squash & Fixup Strategies — the parent page on collapsing and reshaping commit history before it reaches the trunk, of which this decision matrix is the merge-time endpoint.
  • When to Use git revert vs git reset — how each integration strategy changes what “undo this feature” actually requires, and which of revert or reset is safe on a shared trunk.
  • Interactive Rebase Workflows — the git rebase -i mechanics that make a rebase-merge policy safe, including conflict reuse and history-rewrite guardrails.
  • Merge vs Rebase Decision Matrix — the complementary branch-maintenance decision that governs how you keep a working branch current, distinct from this final integration button.