Squashing in Git means combining several commits into one. It’s mostly done before a pull request: instead of ten commits like “fix”, “now it works” and “typo”, your branch ends up with a single commit that describes the whole change. The history is easier to read, and it’s easier to revert an entire feature if you need to.
In this note I’ll show you three ways to squash commits, when to use each one, and how to undo it if something goes wrong.
Before you start #
Say you’re on a feature branch created from main, with a few commits:
git log --oneline a1b2c3d fix: typo
d4e5f6a feat: form validation
9f8e7d6 feat: contact form
b6adbbc (origin/main, main) chore: initial version You want the top three commits to become one, on top of main.
Method 1: git rebase -i (the most common) #
An interactive rebase lets you choose which commits to combine.
1. Start the rebase from the point where your branch left main:
git rebase -i main If you only want to squash the last few commits, you can also say how many:
git rebase -i HEAD~3 2. Mark the commits to squash. Git opens your editor with a list, oldest first:
pick 9f8e7d6 feat: contact form
pick d4e5f6a feat: form validation
pick a1b2c3d fix: typo Leave the first one as pick and change the rest to squash (or just s):
pick 9f8e7d6 feat: contact form
squash d4e5f6a feat: form validation
squash a1b2c3d fix: typo If you use fixup (or f) instead of squash, Git combines the commit but drops its message. Handy for “typo” commits.
3. Save and close the editor. Git asks for the message of the combined commit. Write it, save and close.
If Vim opened: press
ito type,Escto leave insert mode, then type:wqandEnterto save and quit. To use VS Code as Git’s editor instead:git config --global core.editor "code --wait".
Done. Check with git log --oneline that you now have a single commit on top of main.
Method 2: git reset —soft (the fastest) #
If you want to squash all the commits on your branch into one, without editing a list:
git reset --soft main
git commit -m "feat: contact form with validation" git reset --soft moves the branch back to main but keeps all your changes staged. Then you make a single commit with everything.
Use
--soft, never--hard.git reset --hardthrows your changes away.
If your local main is out of date, use the exact point where your branch split off:
git reset --soft $(git merge-base HEAD origin/main) Method 3: git merge —squash (when merging into main) #
If you’d rather leave your branch alone and squash when you merge it:
git switch main
git merge --squash feature
git commit -m "feat: contact form with validation" All the changes from feature land on main as one commit. Your feature branch stays as it was.
On GitHub, this is the “Squash and merge” button on a pull request. If your team uses it, you don’t need to squash by hand.
How to push a squash to GitHub #
Methods 1 and 2 rewrite your branch’s history, so a regular git push will be rejected. Use:
git push --force-with-lease --force-with-lease is safer than --force: if someone pushed something to your branch that you don’t have, the push fails instead of overwriting it.
Never squash commits that are already on a shared branch like
main. You’d rewrite everyone’s history.
How to undo a squash #
Git remembers where your branch was before the change. Find it in the reflog:
git reflog 3c4d5e6 HEAD@{0}: rebase (finish): returning to refs/heads/feature
...
a1b2c3d HEAD@{4}: commit: fix: typo And go back to that point:
git reset --hard HEAD@{4} Here --hard is exactly what you want: you’re going back to the previous state.
Which one should I use? #
| Situation | Method |
|---|---|
| I want to choose which commits to combine | git rebase -i |
| I want my whole branch as one commit | git reset --soft |
| I want to squash on merge, without touching my branch | git merge --squash or “Squash and merge” on GitHub |
One last tip: when you write the message for the combined commit, use Conventional Commits. I explain how to enforce it in your project in this note on husky and commitlint.