TechEarl

How to Squash Git Commits (Interactive Rebase and Friends)

Squash Git commits into one with interactive rebase: change pick to squash or fixup, fold into the line above, abort cleanly, or flatten a whole branch with git merge --squash. The whole workflow, including the force-push step you cannot skip.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
Squash Git commits into one with interactive rebase: change pick to squash or fixup, fold commits into the line above, or flatten a branch with git merge --squash.

To squash the last few Git commits into one, run an interactive rebase, then change pick to squash (or fixup) on the commits you want to fold:

bash
git rebase -i HEAD~5

That opens your editor with the last five commits listed oldest-first. Each line starts with pick. Leave the first line as pick, change the lines below it to squash or fixup, save, and Git collapses them into the commit above. If those commits were already pushed, you then have to force push the rewritten branch. That is the whole job. The rest of this page is the detail: the squash-vs-fixup difference, the one rule people get backwards, how to bail out, and the non-interactive shortcuts for flattening an entire branch.

If you arrived here from a wider "I made a mess, get me back" search, the undo things in Git guide is the hub for resets, reverts, and amends; this page is specifically about collapsing several commits into one.

The one rule: you squash into the line above

This trips up almost everyone the first time. squash and fixup do not act on the line they sit on as a standalone instruction. They fold that commit into the commit on the line above it. So the editor buffer looks like this:

bash
pick   a1b2c3d  Add login form
squash 4d5e6f7  Fix typo in label
squash 7g8h9i0  Tidy up the markup

That produces one commit. The first line stays pick because there is nothing above it to fold into; the two squash lines below collapse upward into it. If you mark the first line squash, Git errors with "cannot squash without a previous commit" because there is no parent in the range to absorb it. The mental model is "the topmost pick is the keeper, everything below it gets melted into it."

The list is oldest at the top, newest at the bottom, the reverse of git log. That ordering catches people too, so read the commit subjects, not the positions, before you change anything.

squash vs fixup: keep the messages or drop them

The only difference between the two is what happens to the commit messages:

  • squash keeps the message. After you save, Git opens a second editor with all the squashed commit messages concatenated so you can edit them down into one combined message.
  • fixup discards the message. The folded commit's log message is thrown away and only the message of the commit above survives. No second editor opens.

Use fixup when the lower commits are noise ("fix typo", "oops", "wip") and you only want the top commit's message. Use squash when each commit said something worth merging into the final message. You can mix them in one rebase:

bash
pick   a1b2c3d  Add password reset endpoint
squash 4d5e6f7  Add rate limiting to reset endpoint
fixup  7g8h9i0  Fix lint error

Here the rate-limiting message gets folded into the combined message you edit, and the lint fix is silently absorbed.

There is a faster path for the fixup case. If you commit a fix and immediately know which earlier commit it belongs to, tag it at commit time and let Git wire up the rebase for you:

bash
git commit --fixup=a1b2c3d
git rebase -i --autosquash HEAD~5

--autosquash reorders the fixup! commit directly under its target and pre-marks it fixup, so the editor buffer is already correct and you just save. Set git config --global rebase.autosquash true to make it the default for every interactive rebase.

If it goes wrong, abort

A rebase that hits a conflict, or one where you realize the plan was wrong, is fully reversible until you finish it. To throw the whole thing away and put the branch back exactly where it was:

bash
git rebase --abort

This returns HEAD and the working tree to the pre-rebase state. Nothing is lost. If instead you hit a conflict you do want to resolve, fix the files, git add them, and run git rebase --continue. Reach for --abort whenever you are unsure: it is the safe exit, and the original commits are still in the reflog even after a rebase completes, so a botched squash is recoverable with git reflog either way.

The editor that opens for the rebase plan is whatever Git is configured to use. In some non-interactive shells, cron jobs, or minimal containers there is no editor and the rebase fails to start. Set one for that invocation:

bash
GIT_EDITOR=nano git rebase -i HEAD~5

GIT_EDITOR overrides the configured editor for that one command. For a permanent setting use git config --global core.editor "nano" (or "code --wait", "vim", whatever you actually use).

Squash a whole feature branch in one move

If the goal is "turn my entire feature branch into a single commit on main," you do not need the interactive editor at all. git merge --squash stages all the branch's changes as one uncommitted set, which you then commit yourself:

bash
git switch main
git merge --squash my-feature
git commit -m "Add the feature, squashed"

This takes every commit on my-feature and flattens it into one staged changeset on main, no rebase, no per-line editing. The difference from interactive rebase is scope: rebase -i lets you squash a subset and keep some commits separate, whereas merge --squash collapses the whole branch into one. It also leaves my-feature untouched, so the original commits still exist on that branch if you want them. This is the option to reach for when you do not care about the intermediate history at all.

Or just reset to a base and recommit

A third approach skips both rebase and merge. Move the branch pointer back to where it diverged, keeping all the work staged, then make one fresh commit:

bash
git reset --soft main
git commit -m "All my feature work in one commit"

git reset --soft moves HEAD to main but leaves the index and working tree exactly as they are, so every change from every commit since the branch point is sitting staged, ready for a single commit. This is the bluntest way to flatten a branch and the easiest to reason about: no plan file, no conflicts to walk through. The trade-off is that it is all-or-nothing, like merge --squash, so use it when you want one commit and nothing else.

After squashing a pushed branch, you must force push

Squashing rewrites history: the commits you folded no longer exist, replaced by new ones with new SHAs. If the branch was only ever local, a plain git push works. But if you had already pushed it, the remote still has the old commits, and a normal push is rejected because your local history is no longer a fast-forward of the remote. You have to overwrite the remote branch:

bash
git push --force-with-lease origin my-feature

Use --force-with-lease, not a bare --force. The lease refuses the push if someone else pushed to the branch since you last fetched, which protects you from silently clobbering a teammate's commits. The full safety story, including when even --force-with-lease is not enough, is in the safe force-push guide. And the rule that goes with it: squashing already-pushed commits on a shared branch (especially main) rewrites history other people have built on, which is antisocial and breaks their clones. Squash freely on your own feature or PR branches; leave shared history alone.

FAQ

No. squash and fixup fold a commit into the line above it, so the commit you want to keep stays pick (it has to be the top line of the group), and you mark the commits below it squash or fixup. Marking the first line squash fails, because there is no commit above it to fold into.

squash keeps the commit message (Git opens a second editor so you can combine all the messages into one), while fixup discards the folded commit's message and keeps only the message of the commit above. Use fixup for "wip" and "fix typo" noise; use squash when the message is worth merging in.

If you are still inside the rebase, run git rebase --abort to put the branch back untouched. If the rebase already finished, the old commits are still in the reflog: run git reflog, find the entry from before the rebase, and git reset --hard to that SHA. Nothing is truly gone until garbage collection runs.

Use git merge --squash. From your target branch, run git merge --squash my-feature then git commit. It stages all of the branch's changes as one uncommitted set, no interactive plan and no per-commit editing. git reset --soft main followed by a single commit does the same thing from the feature branch itself.

Squashing rewrites history, so your local commits have new SHAs and no longer fast-forward the remote. You have to overwrite the remote branch with git push --force-with-lease. See force push safely for why --force-with-lease is the right form. Only do this on branches you own, never on shared history.

See also

Sources

Authoritative references this article was fact-checked against.

Tagsgitsquash commitsinteractive rebasegit rebasefixupgit merge --squashDevOpsCLI

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Use find -type d -empty to list empty directories and find -type f -empty for empty files. The -depth trap for deleting nested empty trees, the hidden-file gotcha, the safe two-pass cleanup, and BSD vs GNU find notes.

How to Find (and Delete) Empty Directories and Files

find . -type d -empty lists every empty directory; find . -type f -empty lists every empty file. The catch is what 'empty' means (a hidden file makes a directory not empty) and the -depth trap that lets find -delete collapse whole nested empty trees in one pass. The flag reference, the safe two-pass cleanup, the BSD vs GNU notes, and the mistakes that bite.

Three patterns for counting ACF Repeater rows: count() on the raw field, get_field_count, and a fast meta-only count without loading the rows.

How to Count Rows in an ACF Repeater Field

Counting ACF Repeater rows is three short patterns: count() on the raw field, get_field_count() inside a loop, and a faster meta-only count that skips loading the rows. Each has its right use case.