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:
git rebase -i HEAD~5That 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:
pick a1b2c3d Add login form
squash 4d5e6f7 Fix typo in label
squash 7g8h9i0 Tidy up the markupThat 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:
squashkeeps 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.fixupdiscards 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:
pick a1b2c3d Add password reset endpoint
squash 4d5e6f7 Add rate limiting to reset endpoint
fixup 7g8h9i0 Fix lint errorHere 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:
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:
git rebase --abortThis 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:
GIT_EDITOR=nano git rebase -i HEAD~5GIT_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:
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:
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:
git push --force-with-lease origin my-featureUse --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
See also
- How to undo things in Git: the hub for reset, restore, revert, and amend when a squash is not what you actually need.
- Git force push: --force vs --force-with-lease: the safe way to publish a rewritten branch after squashing.
- Essential Git branch commands: switch, delete, and inspect branches, the surrounding workflow for the branch you just flattened.
Sources
Authoritative references this article was fact-checked against.
- git-rebase (official Git documentation)git-scm.com
- git-merge (official Git documentation)git-scm.com





