Git

What Is Git Squash? Clean Up Your Commits

What Is Git Squash? Clean Up Your Commits

A feature branch usually ends up with a few commits nobody will want to read later. Squashing folds them into one new commit, and that commit gets a new hash.

Git has no stand-alone squash command. It shows up as a keyword in interactive rebase and as the --squash flag on git merge.

Most developers use it right before a pull request merges, so “wip” and “fix typo” never reach the main history.

One detail matters on shared work. When the folded commits have different authors, the git-rebase manual gives the combined commit to the author of the first commit. The suggested message joins the first commit’s message with each squash message, and fixup messages are left out.

What Is Git Squash?

YouTube player

Squashing rewrites history by folding two or more commits into one new commit.

It lives inside other commands. In an interactive rebase (git rebase -i) it’s a keyword in the todo list, and on git merge it’s a flag (git merge --squash).

Both are in the official Git manual. The git-rebase page lists squash and fixup side by side as the ways to fold a commit into the one before it.

Why developers squash commits

YouTube player

A feature branch collects commits like “wip” and “actually fix it”. Squash turns that run into one commit with a message that describes the change.

A version control log only helps when people read it, and a branch full of “wip” entries stops getting read. After a squash the log is shorter, with one entry per change, and there’s a single commit to revert if the feature has to come out.

Pro Git frames the habit as rewriting local history before it is shared. Once a commit is pushed, the book treats it as final unless there’s a good reason to change it.

Why is GitHub the heart of open source?

Uncover GitHub statistics: developer community growth, repository trends, collaboration patterns, and the platform that powers modern software development.

Explore GitHub Data →

What squash is not

git commit --amend replaces only the latest commit, so it can’t fold a run of them. A rebase without squash and a drop are the other two things people mix it up with. The first replays every commit onto a new base and keeps each one. A dropped commit loses its changes entirely, where a squashed commit keeps them inside the combined one.

What Squashing Does to Commit History

YouTube player

The squashed commits get replaced by one new commit with a new hash, and the originals leave the branch.

Say a branch holds base, A, B and C. That’s three commits and three hashes. After the squash it holds base and D, one commit with one new hash, and A, B and C no longer sit on the branch.

The commit count, the hashes and the message all change (the message gets merged by hand in the editor). File contents at the branch tip usually stay the same.

Git computes each commit hash from the commit’s tree, parents, author, committer and message (timestamps included). So D can’t share a hash with A, B or C.

Looking to sharpen your Git skills? Branching, merging, rebasing, and everything else you need - including git rebase, git stash, and commit workflows - is on one page in the Git Cheat Sheet.

Descendants pay for it too. Each commit stores its parent’s hash, so a squash near the start of a range gives every later commit a new hash as well (Pro Git makes the same point about deleting or editing a commit).

The further back the squash reaches, the more commits Git has to recreate. More replayed commits also means more chances to hit conflicts.

The old commits don’t vanish at once. The git-rebase manual sets ORIG\_HEAD to the old branch tip when the rebase starts, and the previous tip also stays reachable through the reflog as branch@{1}.

ORIG\_HEAD is fragile, though. The manual warns that a later git reset during the rebase can overwrite it, which makes the reflog the safer way back.

Rewritten hashes are also why a pushed branch needs a force push after a squash.

How to Squash Commits in Git

YouTube player

Interactive rebase (git rebase -i HEAD~N), git merge --squash from the target branch, and git reset --soft HEAD~N followed by a new commit all get the job done.

MethodCommandControlFails when
Interactive rebasegit rebase -i HEAD~3Choose which commits fold, edit the messageConflicts appear while Git replays each commit
Merge squashgit merge –squash featureOne result for the whole branchNobody runs git commit afterward, so nothing is recorded
Soft resetgit reset –soft HEAD~3, then git commitLast N commits onlyThe commits to fold are not at the branch tip

Which one to pick depends on what has been pushed. For unpushed local commits any of them works, and rebase is the better choice when the message needs care. A finished branch headed into main suits merge --squash, or the squash option on the hosting platform. Commits other people have already pulled should be left alone, since Pro Git treats pushed work as final.

Squash with interactive rebase

Interactive rebase gives the most control, because the todo list names every commit in the range.

  1. Run git rebase -i HEAD~3 to open the last three commits in the editor.
  2. Read the list top to bottom. Git puts the oldest commit first, the reverse of git log.
  3. Keep pick on the first line.
  4. Change pick to squash (or s) on each line that should fold into the one above it.
  5. Save and close. Git applies the changes and opens a second editor with the combined message.
  6. Edit that message, save, and check the result with git log.

The first line can’t be squashed, because squash melds a commit into the one before it and nothing comes before the first. Also, deleting a line from the list deletes that commit, not just its label.

The git-rebase manual lists rebase.missingCommitsCheck as a guard. Set it to warn or error and a deleted line triggers a message. The default is ignore.

If a conflict stops the rebase, resolve it, run git add, then git rebase --continue. git rebase --abort returns the branch to its original state.

Squash with git merge –squash

YouTube player

From the target branch, run git merge --squash feature, then git commit.

Per the git merge manual, the flag stages the result of the merge but doesn’t commit, move HEAD or record MERGE\_HEAD.

That last part separates it from a regular merge, which writes a merge commit with two parents. A squash merge writes an ordinary single-parent commit, so the main branch keeps a linear history.

The catch is that the history shows no link back to the feature branch. git branch --merged won’t list that branch as merged either.

Squash with git reset –soft

Soft reset squashes without an editor session. It moves HEAD back and leaves every change staged.

git reset --soft HEAD~3
git commit -m "Add login form validation"

The git reset manual gives this exact recipe for the case where nothing else is staged: git reset --soft HEAD~5, then git commit, combines the last five commits into one. It also warns that using the shortcut with other working tree changes can lead to confusion.

Reset stores the old tip in ORIG\_HEAD, so git commit -c ORIG_HEAD starts the new commit from the old tip’s message. That reuses one message, not all of them.

Squash vs Fixup, and How Autosquash Works

Squash keeps the folded commit’s message for editing, while fixup throws that message away.

Squash and fixup compared

With squash, the suggested message is the first commit’s message plus the message of every squash commit, and an editor opens so you can merge the text by hand. Fixup folds the changes in and leaves its message out of the suggested text.

As of the current git-rebase manual (last updated in Git 2.54.0, April 2026), fixup -c keeps only that commit’s message and opens an editor. fixup -C does the same without the editor.

With several fixup -c commits in a row, the last one wins.

Pick fixup when the later commit is a plain correction with nothing worth saying. Pick squash when its message adds detail the final message should keep.

Autosquash

Autosquash does the marking by title. A commit whose message starts with “fixup! “, “squash! ” or “amend! ” is matched to an earlier commit by that commit’s title or hash.

In the todo list, Git moves the marked commit directly under its target and switches pick to squash, fixup or fixup -C.

git commit --fixup=abc1234 builds the “fixup! ” title from the target commit, and git rebase -i --autosquash main applies it. Setting rebase.autoSquash to true turns it on by default for interactive rebases, and --no-autosquash overrides it for a single run.

A round of code review is the usual trigger. A reviewer flags commit three of five, a fixup commit carries the patch, and one autosquash rebase folds it back before the merge.

The target has to be one of the commits being rebased. Prefix matching is also loose, so pass the hash when two titles start alike.

Both actions rewrite hashes the same way, so a pushed branch needs a force push either way.

Squashing Pushed Commits and Recovering from a Bad Squash

Pushed commits can be squashed, but the remote rejects the push until it’s forced, and only a lease-based force protects teammates’ commits.

Force push after a squash

The remote refuses the push because the squash commit doesn’t descend from the old remote tip. The git push manual calls that a non-fast-forward update and says it loses history, which is why Git rejects it by default.

Forcing is the only way through. The real question is which flag.

  • git push --force skips every safety check, the lease included, and can make the remote lose commits.
  • git push --force-with-lease updates the branch only if the remote still points where the local remote-tracking copy says it does.
  • git push --force-with-lease=feature:abc1234 checks against a hash you name, with no reliance on tracking info.
  • Adding --force-if-includes to a lease without an explicit hash also requires the remote-tracking tip to be reachable from the local branch’s reflog. With an explicit hash, it does nothing.

Bare --force-with-lease has a weak spot. A background git fetch from an editor plugin or a cron job updates the remote-tracking branch, and the lease then compares against a value nobody looked at.

The manual’s own advice is to pass the expected hash explicitly. As of the current git-push manual (last updated in Git 2.55.0, June 2026, and unchanged in 2.56.0), every lease form without an explicit expected value is still labeled experimental.

Forcing also has a ceiling. If the server sets receive.denyNonFastForwards or runs a hook that refuses the update, the remote rejects it no matter which flag the client sends.

Undo a squash with reflog

A squash that went wrong is recoverable while the old tip is still in the reflog.

  1. Commit or stash current work, because the final reset overwrites files in the working tree.
  2. Run git reflog and find the entry from just before the rebase or reset started, such as HEAD@{5}.
  3. Confirm it with git show HEAD@{5}. The message and file list identify the old tip.
  4. Pin it with git branch pre-squash HEAD@{5}, so a mistyped step can’t lose it again.
  5. Run git reset --hard pre-squash to move the branch back.
  6. If the squashed branch was already force pushed, push the restored history with git push --force-with-lease.

The reflog manual sets two default expiry windows, 90 days for reachable entries and 30 days for entries no longer reachable from the branch tip.

A squashed-away commit is no longer reachable from the tip, so the shorter window applies to it. Treat 30 days as the real recovery deadline, and raise gc.reflogExpireUnreachable if a team needs longer.

The reflog also lives only in the local repository. A teammate’s clone has no entry for the old tip, so only the person who ran the squash can recover this way.

Squash and Merge on GitHub, GitLab, Bitbucket and Azure DevOps

YouTube player

Hosting platforms squash a pull request or merge request on the server, so the feature branch keeps its commits and no force push is involved.

PlatformWhere it is setAdmin controlDetail worth knowing
GitHubSquash and merge buttonRepository must allow squash mergingSeveral commits give a PR-title message plus a commit list
GitLabSquash commits checkboxDo not allow, Allow, Encourage, RequireAuthor is the MR creator, committer is who started the squash
BitbucketSquash and Squash (fast-forward only)Admins set the default under Merge strategiesFast-forward only rejects a source that is out of date
Azure DevOpsSquash merge on completionLimit merge types branch policyA policy can lock a branch to squash only

Messages and authorship

The default message depends on the platform and on how many commits the request holds.

On GitHub, a single-commit pull request reuses that commit’s message plus the pull request number. With several commits, the summary becomes the pull request title and the description lists every commit message in date order (GitHub Docs).

GitLab sets the squash commit’s author to the merge request creator and its committer to whoever started the squash. Project owners can replace the default text with commit message templates.

Edit the message in the dialog. A bare pull request title as the only subject line drops the detail the individual commits carried.

Long-running branches after a squash merge

GitHub and GitLab give the same warning: don’t squash and merge a branch that keeps receiving commits.

The squash commit exists only on the base branch, so the two branches keep their old common ancestor. The next pull request lists the already-merged commits again, and the same conflicts come back.

  • On GitLab, rebase the source branch onto the target, or merge the target back into it, after each squash merge.
  • On GitHub, stop squashing that branch, or accept resolving the same conflicts repeatedly.
  • Bitbucket Cloud has its own trap. A local git merge --squash pushed to the target leaves the pull request open, because Atlassian detects merges through the commit graph.

Enforcement differs too. GitLab’s Require setting and the Limit merge types policy in Azure DevOps both take the choice away from the person pressing merge.

When Squashing Is the Wrong Choice

Skip the squash on shared branches, on long-running branches, and on large changes whose commits are reviewable steps.

Shared branches

YouTube player

The git-rebase manual calls rewriting a branch that others have based work on a bad idea, because everyone downstream has to repair their history by hand.

It splits that repair into an easy case and a hard one, and interactive squash lands in the hard one.

A plain rebase without conflicts is the easy case. Patch IDs match, so downstream work rebases onto the new tip cleanly. Any interactive squash, fixup, edit or omit, or any rebase that hit conflicts, falls into the hard case, where downstream work moves with git rebase --onto and the old tip has to be named by hand.

Easy-case recovery can look successful in the hard case. The manual warns that a commit dropped during the rebase comes back, and that everyone downstream of the person repairing must then repair too.

Debugging precision

GitLab’s documentation says squashing makes it simpler to revert all parts of a change. The same collapse costs precision in git bisect, which runs a binary search through commit history to find the commit that introduced a bug (Pro Git).

After a squash, the first bad commit can only be the squashed one. Narrowing the cause inside it is manual work.

Line attribution suffers the same way. Git blame reports the last commit to touch each line, with its author and date, so every line a feature changed points to one commit and one message.

Large changes with reviewable steps

A refactor split into five commits, each reviewed on its own, fits squash worse than a branch full of “wip” commits.

GitHub’s default merge keeps those commits by adding them to the base branch with a merge commit (–no-ff). Rebase and merge also keeps them and gives a linear history, but it always creates new commit SHAs and updates the committer information.

Rebase and merge refuses some cases, like conflicts, or a rebase GitHub considers unsafe because it would produce a different result than a merge.

Pros and cons of squashing

The upsides are mostly about a tidy history.

  • One commit to revert when a whole feature has to come out (GitLab)
  • A linear history with a single commit on the target (Microsoft Learn, Azure Repos)
  • Work-in-progress commits stay out of the main history (GitHub Docs)

The downsides show up later, in debugging and on shared work.

  • Bisect and blame lose the intermediate commits
  • Long-running branches hit the same conflicts after every squash merge
  • Squashing pushed commits locally needs a force push and leaves downstream clones to repair
  • Individual commit messages collapse into one description

Squash short-lived feature branches by default. Keep the commits when each one is a step someone reviewed.

Git Squash FAQ

Can the first commit in a branch be squashed?

Yes, with git rebase -i –root. The flag adds the root commit to the todo list, and later commits fold into it.

Without it, the first line of the list can’t be squashed, because nothing sits before it.

How do I get out of the editor during a squash?

Save and close the file. In Vim, press Esc, type :wq, and press Enter.

To abort instead, delete every line in the todo list, since Git stops the rebase when nothing is left. The sequence.editor setting changes which editor opens for that list.

Can merge commits be squashed with interactive rebase?

Not directly. By default the todo list leaves merge commits out and replays the remaining commits as one linear branch.

Adding --rebase-merges recreates the merges instead of flattening them. To collapse a whole branch that contains merges, git merge --squash handles the full range.

Does squashing keep credit for co-authors?

Only as text in the message. Git records one author per commit, so credit for the others lives in a trailer such as Co-authored-by.

That line survives only if its message does. Squash keeps messages for editing, while fixup drops them.

Setting a Team Default for Squash Merges

YouTube player

Teams should make squash the default for short-lived feature branches, with a documented exception for large changes.

Maintainers or admins hold the control. GitLab requires the Maintainer or Owner role, and Bitbucket gives admins the default strategy.

A rollout tends to go in this order.

  1. Allow squash without requiring it.
  2. Make it the default, still overridable.
  3. Require it only where one commit per change is policy.

GitLab’s Encourage setting selects squash by default but lets authors clear it, while Require removes that escape. Watch which branches opt out before locking anything.

A default has a cost. Without a written git workflow, engineers have no path to keep reviewed commits. Verified October 2, 2026 against Git 2.56.0 (released September 28, 2026) and current platform documentation.

Bogdan Sandu

Stay sharp. Ship better code.

Every week: one curated article, one tool worth knowing, one tip you can use tomorrow. No noise, no padding.