Skip to content

Squash and Merge: A Better Default

Every GitHub repository shows three options when a pull request is ready to be merged:

  1. Create a Merge Commit
  2. Squash and Merge
  3. Rebase and Merge

The default is usually Create a Merge Commit, which produces history that becomes harder to read and reason about over time. And most teams tend to leave this default as-is.

This post covers what each strategy does, why Squash and Merge is a better default, and how to configure GitHub to enforce it.

The Three Merge Strategies

Create a Merge Commit

This is Git's default behavior. All commits from the feature branch are preserved in the history, and a new merge commit ties them back to main. The result looks like this:

gitGraph
   commit id: "feat: initial setup"
   branch feature/my-work
   checkout feature/my-work
   commit id: "WIP"
   commit id: "fix typo"
   commit id: "oops forgot a file"
   commit id: "actually done now"
   checkout main
   merge feature/my-work id: "Merge pull request #42"

Every commit from the branch lands in main's history, including the merge commit itself. The full shape of the branch (all the "WIP" and "fix typo" commits from your working session) is permanently visible.

Rebase and Merge

Rebase replays each commit from the feature branch onto the tip of main, one at a time, without a merge commit. The result is a linear history, but all individual commits from the branch are still present:

gitGraph
   commit id: "feat: initial setup"
   commit id: "WIP"
   commit id: "fix typo"
   commit id: "oops forgot a file"
   commit id: "actually done now"

The history is linear, which is an improvement, but the intermediate commits are still all there.

Squash and Merge

Squash takes every commit from the feature branch, combines them into a single new commit, and applies that commit to main. No merge commit, no intermediate noise:

gitGraph
   commit id: "feat: initial setup"
   commit id: "feat: add feature X (#42)"

One pull request, one commit.

Why Squash and Merge is a Better Default

The main Branch History Should Tell a Story

When I run git log --oneline on main, I want to see the history of the project, not the history of how someone debugged a feature over an afternoon.

a3f9e12 fix: broken image path in post header (#51)
c7d2b44 docs: add squash and merge blog post (#50)
88f4120 chore: update MkDocs Material to 9.6.11 (#49)
1a2b3c4 docs: add setup-task GitHub Action blog post (#48)
f09e8d1 refactor: deploy workflow for clarity (#47)

Every line corresponds to one logical unit of work.

One pull request, one feature, one fix, one change.

git log becomes a meaningful changelog.

With standard merge commits or rebased branches, the same history looks more like this:

d8e6f21 Merge pull request #51
b1c2d3e fix typo in alt text
9a8b7c6 actually fix image path this time
3e4f5a6 WIP trying something
a3f9e12 add post header image
c7d2b44 Merge pull request #50
...

The signal is buried in the noise. You have to mentally filter out the intermediate commits to understand what actually changed.

Every Squash Commit is a Deployable Unit

When every commit on main represents a complete, reviewed, merged pull request, each commit is a unit you can reason about:

  • It passed CI before it merged.
  • It was reviewed by at least one other person (or you, in a solo project with enforced review).
  • It represents a complete change, not half of one.

This matters enormously when something breaks.

git bisect Actually Works

git bisect is the fastest way to find which change introduced a bug. You tell Git a known-good commit and a known-bad commit, and it binary-searches the history, checking out commits for you to test. The tool is only as useful as the commits in the range.

If your history is full of "WIP", "fix typo", and "temp commit before rebase", half your bisect steps will land on commits that are either broken by design or represent no meaningful state change. Bisect becomes frustrating instead of fast.

With squash commits, every step in the bisect is a complete, intentional change. If the bug is between commit A and commit B, bisect will land you on exactly the PR that introduced it. Every time.

git revert is Surgical

Sometimes you need to back out a change. With squash commits, reverting is clean: one git revert undoes exactly one pull request's worth of changes, leaving everything else intact.

With a merge commit strategy, reverting is more complicated. Do you revert the merge commit itself? That can have surprising effects on subsequent merges. Do you revert each individual commit? Now you have to figure out which commits belonged to which PR, and hope they apply cleanly in reverse.

Squash commits make the atomic unit of work crystal clear. Revert is a single, predictable operation.

It Encourages Good PR Titles and Descriptions

When you squash and merge, GitHub uses the pull request title as the commit subject and the PR description as the commit body. This creates a strong incentive to write useful PR titles and descriptions.

If you use Conventional Commits, squash and merge is the natural enforcement mechanism. The PR title becomes the commit message, so naming the PR feat: add user authentication or fix: correct null pointer in session handler means that convention shows up cleanly in main without any extra tooling. No commit-msg hooks to fight with on feature branches. No lint failures on WIP commits. One place to get it right.

A well-written PR description becomes a permanent part of the project's history. Anyone running git show on a commit six months later gets the full context: what changed, why it changed, what alternatives were considered, what the testing approach was.

Write the PR Title and Description for the git log

Treat the PR title and description as the commit message. The title follows Conventional Commits (feat:, fix:, docs:, chore:, etc.) and the description is written for the reader who will find this commit via git log or git show in the future, not just for the reviewer who sees it today.

It Stops Punishing Messy Development

This is the one that surprises people. Squash and merge is actually more permissive for developers, not less.

When individual commits land in main verbatim (standard merge or rebase and merge), teams often develop informal rules about commit hygiene on feature branches: messages must follow Conventional Commits format, "WIP" commits are forbidden, you must rebase before merging. Enforcing this is friction. People forget, reviewers have to ask for cleanup, PRs get delayed.

With squash commits, the only commit that matters is the squash commit itself. Developers can work however is natural for them: commit early and often, commit WIP checkpoints, commit "fix typo" all they want. None of that reaches main. Conventional Commits compliance only needs to be right once, in the PR title. The only thing that shows up in main is the clean, reviewed, squash-merged result.

Freedom during development, cleanliness in production. That is the right trade-off.

The Objections

"But We Lose the Individual Commit History"

Yes. That is intentional.

The individual commits on a feature branch are working notes, not history. They document the process of getting to a solution, not the solution itself. Some of them represent states where the code was intentionally broken, or where the developer was mid-thought. That working state is not useful to preserve in main.

If someone wants to see the detailed commit history of a pull request, they can look at the PR itself on GitHub. The PR branch and its commits are accessible for as long as the PR exists. The squash commit links back to the PR by number. The breadcrumb trail is there for anyone who needs it.

"Rebase and Merge Gives You Linear History Without Losing Commits"

Rebase does give you a linear history, which is better than standard merge commits. But it still puts every intermediate commit on main. If your team's development discipline is high and every commit on every branch is meaningful and complete, rebase and merge works fine.

In practice, most developers do not work that way, nor should they have to. A linear sequence of "WIP", "fix compilation error", "fix test", and "add missing null check" on main is not meaningfully better than having those commits bundled behind a merge commit. You still have to mentally ignore them when reading the history.

"Standard Merges Make It Clear Where Branches Joined"

This is true in the abstract. In practice, reading a branchy git log --graph is harder than reading a linear one. The visual branches tell you that parallel work happened, but not much about what happened. Once the merge points start accumulating, the graph becomes dense and hard to follow.

A linear log of well-titled squash commits communicates more information more clearly.

Configuring GitHub for Squash and Merge

In GitHub repository settings, under General > Pull Requests, make the following selections:

Option Setting
Allow merge commits Disabled
Allow squash merging Enabled
Allow rebase merging Disabled
Default commit message Pull request title and description

Setting "Default commit message" to Pull request title and description is important. It pre-populates the squash commit message with the PR title as the subject line and the PR description as the body. GitHub even appends the PR number to the title automatically, giving you traceable commits like feat: add feature X (#42) with no extra effort.

Enforcing Squash Across an Organization

GitHub allows setting repository defaults at the organization level. If you manage multiple repositories and want to enforce squash merging consistently, the GitHub REST API and tools like Terraform's GitHub provider let you codify these settings as infrastructure and apply them at scale.

The Bottom Line

Squash and merge produces a clean, readable history where every commit is a complete, reviewed, and revertable unit of work. It makes git bisect effective, git revert surgical, and git log useful as a changelog. It removes friction from development by letting contributors commit freely on branches without worrying about what lands in main. And it creates a natural incentive to write PR descriptions that double as permanent commit messages.

The other strategies have their place. Standard merge commits preserve full branch topology, which matters in some long-running integration scenarios. Rebase and merge works well for teams with strong individual-commit discipline. But as a default, for the kind of feature-branch, pull-request-based workflow that most teams use on GitHub, squash and merge wins.

References