Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to squash commits with interactive rebase
Tools

How to squash commits with interactive rebase

Squashing collapses several commits into one. It rewrites them, so every hash changes and pushed history is off limits.

By Max Arthur
Co-Founder & Content Marketer·September 23, 2026·8 Min read
White text reading 'How to squash Git commits' on a dark purple dotted background

Squashing combines several commits into one. The usual reason is a branch full of "fix typo", "actually fix it" and "wip" commits that nobody needs to read.

git rebase -i HEAD~3

That opens an editor listing the last three commits. Change pick to squash on all but the first, save, and they collapse into one.

Measured on git 2.50.0:

before:  8865559 commit 4
         a7f9a25 commit 3
         88a25ae commit 2
         e78be7e first

after:   150c029 commit 2
         e78be7e first

Four commits became two, the file contents were preserved exactly, and the hash changed from 8865559 to 150c029. That last point governs everything else on this page.

The editor

git rebase -i HEAD~3 opens something like:

pick 88a25ae commit 2
pick a7f9a25 commit 3
pick 8865559 commit 4

# Commands:
# p, pick   = use commit
# r, reword = use commit, but edit the message
# s, squash = use commit, but meld into previous commit
# f, fixup  = like squash, but discard this commit's message
# d, drop   = remove commit

Two things about this screen surprise people.

The order is oldest first, which is the opposite of git log. The top line is the oldest of the three.

You never mark the first line. Squash means "meld into the commit above", so the top commit is the one everything else melds into. Marking it produces an error, because there is nothing above it in the selection.

So the edit looks like:

pick 88a25ae commit 2
squash a7f9a25 commit 3
squash 8865559 commit 4

Save and close. Git then opens a second editor with all three messages combined, for you to write the final one.

squash versus fixup

squash keeps the commit's message and asks you to combine them. fixup discards it silently.

Use fixup for the commits that were never meant to be read: "typo", "oops", "wip". Use squash when each commit says something worth folding into the final message.

Mixing them in one rebase is normal:

pick   88a25ae add login form
fixup  a7f9a25 typo
squash 8865559 add validation

Choosing how many commits

HEAD~3 means "the last three". Counting wrong is the usual mistake, so check first:

git log --oneline -5

Better, when you are squashing a feature branch, is to name the base rather than count:

git rebase -i main

That selects every commit on your branch since it diverged from main, which is almost always what you actually mean and cannot be miscounted.

The rule about pushed commits

Squashing rewrites commits, so every hash after the squash point changes. Anyone who already has the old commits now has a history that disagrees with yours.

Squash before you push, not after.

If you must squash something already pushed, and you are certain nobody else has it:

git push --force-with-lease

--force-with-lease refuses if the remote has commits you have not seen, which makes it meaningfully safer than --force. It protects against the case where a colleague pushed while you were rebasing, and it is the only force push worth typing by default.

On a shared branch such as main, do not do this. The recovery involves everyone who has the branch, and the fix for a bad commit on shared history is revert, which adds a commit rather than rewriting one. This is the same constraint described in merge versus rebase.

The alternatives that need no rebase

Squash merging. Most of the time, the reason to squash is to keep a tidy main, and the merge can do it for you:

git switch main
git merge --squash feature
git commit

That applies every change from feature as one uncommitted lump, which you then commit. The branch's own history stays as it was. GitHub, GitLab and Bitbucket all offer this as a merge button setting, and turning it on removes the need for most manual squashing.

Note the side effect: the branch will not show as merged in git branch --merged, because none of its commits ended up in main. That is covered in deleting a branch.

Amending the last commit. When there is only one commit to fix:

git commit --amend

No rebase required. It replaces the most recent commit, which is still a rewrite, so the same pushed-history rule applies.

The other things interactive rebase can do

Squashing is one line in a menu that does considerably more. The same git rebase -i screen supports:

Command Effect
pick Keep the commit as it is
reword Keep the changes, edit the message
squash Merge into the commit above, combine messages
fixup Merge into the commit above, discard this message
drop Remove the commit entirely
edit Stop here so you can amend the commit

Two of those are worth knowing beyond squashing.

reword fixes a bad commit message without touching anything else. It is the multi-commit version of git commit --amend.

Reordering. You can move the lines themselves, and git replays the commits in the order you leave them. That is how you group related commits together before squashing, or move an unrelated fix to the front so it can be split off.

edit pauses the rebase at that commit with it checked out, so you can change the files, git commit --amend, and then git rebase --continue. This is how you split one commit into two, by amending it down and committing the remainder separately.

All of these carry the same rewriting caveat as squashing.

A worked example: cleaning a branch before review

The typical state of a feature branch after a few days:

a1b2c3 wip
d4e5f6 add validation
7890ab typo
cdef01 fix the test I broke
234567 add login form

Read bottom-up, the story is: add the form, add validation. Everything else is noise from how the work happened.

git rebase -i main
pick   234567 add login form
fixup  cdef01 fix the test I broke
pick   d4e5f6 add validation
fixup  7890ab typo
fixup  a1b2c3 wip

Two pick lines and three fixups, with the lines reordered so each fixup sits under the commit it belongs to. The result is two clean commits, each of which is a change a reviewer can read on its own.

That is the outcome worth aiming for, and it is why "squash everything into one" is a habit rather than a rule. The goal is a history that explains the change, not the shortest possible history.

When it goes wrong

A conflict during the rebase. Each squashed commit is replayed in turn, so a conflict can appear partway through:

# fix the files, then
git add .
git rebase --continue

Or back out entirely:

git rebase --abort

--abort returns the branch exactly as it was. It is completely safe, and it is the right move whenever a rebase becomes confusing.

You squashed the wrong commits. The reflog has the old position:

git reflog
git reset --hard HEAD@{1}

The pre-rebase commits still exist and are reachable through the reflog, which is the same recovery route described in detached HEAD.

How much to squash

Squashing everything into one commit is common and not always right. A commit should be a change that makes sense on its own, so a branch that genuinely did three things reads better as three commits than as one.

The rule worth applying: squash the commits that exist because of how you worked ("wip", "typo", "fix review comment"), and keep the ones that exist because of what changed. A reviewer reading history in six months cares about the second kind.

Marking fixups as you go

The tidy version of this workflow, for when you already know a commit is a fix to an earlier one:

git commit --fixup a1b2c3

That creates a commit whose message is fixup! <the original message>. It sits in your history looking obvious, and later:

git rebase -i --autosquash main

--autosquash reads those markers, moves each fixup commit directly under the commit it belongs to, and pre-sets it to fixup. The editor opens with the whole plan already correct, and you save and close.

This removes the two fiddliest parts of the manual process: reordering lines and remembering which commit each fix belonged to. Turn it on permanently:

git config --global rebase.autosquash true

Three common mistakes

Marking the first line as squash. There is nothing above it to meld into, so git stops with an error. The top commit in the list is always the one everything else folds into.

Counting the wrong number of commits. HEAD~3 when you meant four leaves a stray commit behind; when you meant two it pulls in one you did not intend to touch. Naming the base branch (git rebase -i main) removes the arithmetic entirely.

Squashing after pushing, then using --force. Plain --force overwrites whatever is on the remote, including commits a colleague pushed while you were rebasing. --force-with-lease refuses in exactly that case, and there is no good reason to type the other one.

Quick reference

git rebase -i HEAD~3        # last three commits
git rebase -i main          # everything since main
#   pick / squash / fixup / reword / drop

git rebase --continue       # after fixing a conflict
git rebase --abort          # cancel, restore the branch

git merge --squash feature  # squash without rebasing
git commit --amend          # fix just the last commit
git push --force-with-lease # after rewriting your own branch
git reflog                  # undo a rebase

Want to reshape history somewhere mistakes cost nothing? Work through Git basics in a real repository in the browser.

Keep reading

More from the blog

White text reading 'Objects are not valid as a React child' on a dark purple dotted background
October 7, 2026·7 min readTools

Objects are not valid as a React child: what to render

Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.

Read more
White text reading 'How to fix npm install errors' on a dark purple dotted background
October 6, 2026·8 min readTools

npm install errors: read the code, not the stack

Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons