Skip to content

Squash Merge


What Will We Learn?

In this post, we’re tackling squash merging — a different way to bring branch changes together compared to the strategies we covered last time. By the end, you’ll know:

  • What squash merging actually does to your history
  • How to do it (both the plain Git way and via GitHub/GitLab’s UI)
  • How conflicts work when you squash
  • The upsides, the trade-offs, and when it’s genuinely the right call
  • Some best practices and gotchas people run into

What is Squash Merge?

A normal merge takes every commit from your feature branch and folds them into the target branch, preserving each one individually, plus it adds a merge commit tying the two histories together. A squash merge does something different: it takes all the changes from your feature branch and squishes them into a single new commit on the target branch — no merge commit, no individual commit history carried over.

Think of it like this: your feature branch history disappears, and only the net result of all that work shows up as one clean commit.

    %%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel': true}} }%%
gitGraph
    commit id: "A"
    branch feature
    checkout feature
    commit id: "wip"
    commit id: "fix typo"
    commit id: "more wip"
    checkout main
    commit id: "B"
  

After a squash merge, main looks like this instead — one commit, no trace of wip, fix typo, more wip:

    %%{init: { 'gitGraph': {'showBranches': true, 'showCommitLabel': true}} }%%
gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "Add feature X (squashed)"
  

Notice something important here: squash merge is not a merge strategy in the same sense as recursive or octopus from our last post. It doesn’t create a merge commit at all, and Git doesn’t record any parent relationship to the feature branch. As far as history is concerned, it looks exactly like you sat down and wrote the whole feature in one commit, directly on main.

How to do Squash Merging?

The core command is git merge --squash:

git checkout main
git merge --squash feature-branch
git commit -m "Add feature X"

Here’s what each step is doing:

  1. git merge --squash feature-branch stages all the combined changes from feature-branch into your working directory and index — but it does not commit them, and it does not create a merge commit.
  2. You then run git commit yourself, writing a fresh commit message that describes the feature as a whole.

That second step matters: since Git doesn’t auto-generate a commit for you, it’s on you to write a message that actually explains what the squashed commit contains — future-you (and your teammates) will thank you.

If you want to inspect what’s staged before committing, use the usual tools:

git diff --staged
git status

Doing it through GitHub/GitLab

If you’re merging a pull/merge request through the web UI, you don’t need any of the above — there’s usually a dropdown next to the merge button:

  • GitHub: “Squash and merge” option on the PR page
  • GitLab: “Squash commits” checkbox when merging an MR
  • Bitbucket: “Squash” merge strategy option in the PR merge dialog

These do the same thing under the hood — combine all commits into one — but they typically also let you edit the resulting commit message right there in the UI, often pre-filled with a summary of the individual commit messages.

    sequenceDiagram
    participant Dev as Developer
    participant GH as GitHub/GitLab
    participant Main as main branch

    Dev->>GH: Open PR from feature-branch
    Dev->>GH: Click "Squash and merge"
    GH->>GH: Combine all commits into one
    GH->>Dev: Show editable commit message
    Dev->>GH: Confirm message
    GH->>Main: Add single squashed commit
  

Handling Squash Merge Conflict

Conflicts during a squash merge look and feel just like conflicts in a regular merge — Git still has to compare your branch against the target and figure out overlapping changes. The difference is only in what happens after you resolve them.

git checkout main
git merge --squash feature-branch
# CONFLICT (content): Merge conflict in src/app.js

You resolve conflicts exactly the way you always do:

  1. Open the conflicted files, look for the <<<<<<<, =======, >>>>>>> markers
  2. Edit them to the correct final content
  3. Stage the resolved files with git add
  4. Commit — but remember, with --squash there’s no merge commit waiting for you, so you just run a normal git commit
git add src/app.js
git commit -m "Add feature X"

One subtlety worth knowing: because a squash merge doesn’t record the feature branch as a parent, Git has no memory that this merge ever happened once it’s committed. That means if you merge feature-branch into main again later (say, after adding more commits to it), Git will try to re-apply everything from the start — including changes you already squashed in — and you’ll likely see the same conflicts resurface. Squash merges are best treated as a one-way, one-time operation for a branch that’s now finished and can be deleted.

Advantages of Using Squash Merge

  • Clean, linear history. main reads like a changelog — one commit per feature/fix, instead of a maze of wip, fix, fix again, oops.
  • Easier to revert. Undoing a whole feature is one git revert <commit> away, instead of hunting down and reverting a dozen scattered commits.
  • Easier git bisect. When you’re bisecting to find a regression, landing on a single squashed commit tells you immediately “this feature introduced it” — no digging through intermediate work-in-progress states that may not even build.
  • Hides messy in-progress commits. Nobody needs to see the 14 commits of trial and error it took to get a feature working — just the final, coherent result.
  • No merge commits cluttering history. Since there’s nothing to tie back to, you skip the extra “Merge branch ‘feature’ into main” noise entirely.

Why Squash Merge?

The honest answer: squash merge trades history detail for history readability.

Regular merges preserve exactly what happened, commit by commit, with full authorship and timing — great for archaeology, bad for skimming. Squash merges throw that detail away in exchange for a main branch history where every entry corresponds to one meaningful unit of work.

Teams tend to reach for squash merging when:

  • They don’t care about preserving fine-grained commit history from feature branches (commits there are often just checkpoints, not meaningful units).
  • Code review happens at the PR/MR level, so the “real” record of what was discussed and changed lives in the PR, not in individual commits.
  • They want main’s history to double as a lightweight changelog.

It’s less appealing when your team does care about granular history — for example, if you write small, atomic commits on purpose and want that story preserved (rebase-and-merge or a plain merge fits that workflow better).

Best Practices

  • Write a real commit message. Don’t let it default to a dump of all the individual commit subjects — summarize what the feature actually does and, if useful, why.
  • Squash at the PR/MR boundary, not mid-development. Keep committing normally (even messily) while you work; squash only when the branch is finished and ready to land.
  • Delete the feature branch after squashing. Since Git has no record of the merge, keeping the branch around invites the “merge it again later” conflict mess described above.
  • Pair it with small, focused PRs. Squash merging works best when each PR represents one coherent unit of change — squashing five unrelated changes into one commit just relocates the messiness instead of removing it.
  • Agree on it as a team convention. Mixing squash merges, regular merges, and rebases in the same repo (inconsistently) makes history genuinely confusing. Pick one default strategy for the team and stick to it.

Things to Watch Out For

  • You lose per-commit blame history. git blame will point to the squashed commit for every line, not the original commit where that line was actually introduced — this can make tracking down why a specific line exists harder.
  • Re-merging a squashed branch is painful. As mentioned above, Git doesn’t know the squash ever happened, so continuing work on the same feature branch and merging again tends to reopen every conflict you already resolved. If you need to keep working, branch fresh off main after the squash instead of reusing the old branch.
  • Authorship gets flattened. The squashed commit is authored by whoever ran the squash (or clicked the button), even if five different people contributed commits on the branch. If per-author attribution matters to your team, this is a real cost — not just a cosmetic one.
  • Not ideal for long-lived branches. Squashing a branch that’s been alive for months and touched dozens of unrelated things produces one giant, hard-to-review commit. Squash merge shines on small, short-lived branches — not sprawling ones.
  • Bisect can point at a “boundary” instead of a “cause.” If a squashed commit bundles multiple unrelated changes, git bisect will still land you on the right commit, but pinpointing which part of that commit caused the issue is now manual work again.

Used well — small branches, clear messages, deleted after merging — squash merge gives you a genuinely pleasant, readable main history. Used carelessly, it just moves the mess from “many small commits” to “one big commit,” so it’s worth being deliberate about when you reach for it.

Last updated on