Git Squash Commits: Combine Multiple Commits into One
- git rebase -i HEAD~N gives you full surgical control β pick which commits to squash, reorder them, or edit messages.
- git merge --squash collapses an entire branch into staged changes without a merge commit β equivalent to what GitHub's 'Squash and merge' does.
- git reset --soft HEAD~N is the fastest approach for squashing the last N commits when you just want one clean commit.
PR review culture has made squashing a daily operation for most teams. The commits you make during development are breadcrumbs for yourself. The commits that land on main are documentation for everyone who comes after you β including yourself in six months when you're running git blame on a production bug.
I enforce a policy on every team I lead: every commit that lands on main must have a message that answers 'what changed and why', not 'what I was doing at 3pm'. Squashing is how you get there without rewriting history while others are working on the same branch.
Squash with Interactive Rebase
Interactive rebase is the surgical tool. git rebase -i HEAD~N opens an editor showing the last N commits. Change pick to squash (or s) on any commit to fold it into the one above it. fixup (or f) squashes without keeping the commit message β useful for minor fixes.
The mental model: the topmost commit in the editor is the oldest. Commits marked squash get folded into the commit immediately above them in the list.
# Squash last 4 commits git rebase -i HEAD~4 # Editor opens: # pick a3f9c2e Add PaymentRetryService skeleton # pick 7b2d4f1 Add exponential backoff logic # pick 9c3e8a2 Fix null check in retry handler # pick d4f8b3c Add unit tests for retry logic # Change to: # pick a3f9c2e Add PaymentRetryService skeleton # squash 7b2d4f1 Add exponential backoff logic # squash 9c3e8a2 Fix null check in retry handler # squash d4f8b3c Add unit tests for retry logic # Save and close. Git opens another editor to combine commit messages. # Write the final message: # feat(payment): Add PaymentRetryService with exponential backoff # # Implements retry logic for failed payment API calls. # Uses exponential backoff with jitter to avoid thundering herd. # Covers null payment reference edge case. # Force push to update remote (only on your own feature branch) git push origin feature/payment-retry --force-with-lease
Squash with git merge --squash
When merging a feature branch, git merge --squash feature/branch takes all commits from the branch and stages them as a single set of changes without creating a merge commit. You then write one commit message for the whole thing.
This is what GitHub does when you press 'Squash and merge'. The difference: GitHub creates the squashed commit for you; git merge --squash stages the changes and makes you commit manually.
# On main, squash-merge a feature branch git checkout main git merge --squash feature/payment-retry # All changes are now staged but not committed git status # Changes to be committed: # modified: src/main/java/io/thecodeforge/payment/PaymentRetryService.java # Write the single commit message git commit -m "feat(payment): Add PaymentRetryService with exponential backoff" # Clean up β feature branch is no longer needed git branch -D feature/payment-retry git push origin --delete feature/payment-retry
Automatic merge went well; stopped before committing as requested
Squash with git reset (Simplest for Small Cases)
For squashing a small number of commits on your current branch without opening an editor, git reset is the fastest path. Move HEAD back N commits while keeping the working directory changes staged.
# You have 3 commits to squash on feature/payment-retry git log --oneline -3 # d4f8b3c Add unit tests # 9c3e8a2 Fix null check # 7b2d4f1 Add backoff logic # Soft reset: moves HEAD back 3 commits, keeps changes staged git reset --soft HEAD~3 # All 3 commits' changes are now staged git status # Write one clean commit git commit -m "feat(payment): Add retry logic with exponential backoff and tests" # Force push git push origin feature/payment-retry --force-with-lease
# All changes staged, ready for single commit
| Method | Tool | Best For | Keeps Intermediate Messages? |
|---|---|---|---|
| Interactive rebase | git rebase -i | Selective squashing, reordering | Optional β you choose |
| git merge --squash | git merge --squash | Squashing entire branch on merge | No β write one new message |
| git reset --soft | git reset | Quick squash of last N commits | No β write one new message |
| GitHub Squash & Merge | GitHub UI | Team standardisation on PR merge | No β GitHub writes the message |
π― Key Takeaways
- git rebase -i HEAD~N gives you full surgical control β pick which commits to squash, reorder them, or edit messages.
- git merge --squash collapses an entire branch into staged changes without a merge commit β equivalent to what GitHub's 'Squash and merge' does.
- git reset --soft HEAD~N is the fastest approach for squashing the last N commits when you just want one clean commit.
- Always use --force-with-lease instead of --force when pushing squashed commits β it prevents accidentally overwriting a teammate's push.
- Never squash commits on a branch other people are actively working on β rewriting hashes breaks their local history.
β Common Mistakes to Avoid
- βSquashing commits that are already on a shared branch β rebasing rewrites commit hashes. If teammates have built on those commits, you'll create diverged histories that are painful to reconcile.
- βUsing --force instead of --force-with-lease on push β plain force overwrites whatever is on the remote, potentially discarding teammates' commits.
- βSquashing without writing a proper commit message β the point of squashing is a clean history. 'squashed commits' is not a commit message.
- βRunning git rebase -i on main or develop β interactive rebase rewrites public history. Only do this on your own feature branches.
Interview Questions on This Topic
- QWalk me through squashing the last 5 commits on a feature branch using interactive rebase.
- QWhat is the difference between git merge --squash and git rebase -i for combining commits?
- QWhy should you use --force-with-lease instead of --force when pushing after a rebase?
Frequently Asked Questions
What does squashing commits in Git mean?
Squashing combines multiple consecutive commits into one. Instead of 'fix', 'fix again', 'really fix this time' appearing in git log, you get one commit with a meaningful message describing all the changes. It cleans up messy development history before merging to main.
Can I squash commits that are already pushed to a remote?
Yes, but you must force push afterward (git push --force-with-lease), which rewrites the remote branch history. Only do this on your own feature branches, never on shared branches like main or develop.
What is the difference between squash and fixup in git rebase -i?
Both squash and fixup fold a commit into the one above it. squash opens an editor asking you to combine the commit messages. fixup silently discards the folded commit's message and keeps only the top commit's message. Use fixup for minor corrections where the message adds no value.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.