Skip to main content

Squashing Commits

What is Squashing?

Squashing combines multiple commits into a single commit. This is used to clean up a messy feature branch before merging — turning a series of wip, fix typo, and temp commits into one or a few clean, meaningful commits that tell a story.

Squashing is performed via git rebase -i (interactive rebase). It is a history rewrite, so only squash commits that have not been pushed to a shared branch, or use --force-with-lease if you need to push afterward.


When to Squash

Good use caseNot ideal
Collapsing WIP / "fix typo" / "temp" commits before a PRSquashing logically separate concerns into one giant commit
Creating a clean single commit per ticket/storySquashing after teammates have based work on your branch
Reducing noise in a long-running feature branchWhen each commit is already clean and meaningful

Squashing with Interactive Rebase

Example: Clean up the last 5 commits

git rebase -i HEAD~5

Git opens your editor:

pick a3f9bc2 feat(transactions): add domain model
pick 7d1e4f0 wip: controller in progress
pick 2c8a1b3 fix typo in service
pick 9f3e2d1 add missing null check
pick 1a4b5c6 feat(transactions): add controller endpoint

Change pick to squash (or s) for commits you want to fold upward:

pick a3f9bc2 feat(transactions): add domain model
squash 7d1e4f0 wip: controller in progress
squash 2c8a1b3 fix typo in service
squash 9f3e2d1 add missing null check
squash 1a4b5c6 feat(transactions): add controller endpoint

Git will pause and open the message editor, combining all messages:

# This is a combination of 5 commits.
# The first commit's message is:
feat(transactions): add domain model

# This is the commit message #2:
wip: controller in progress

# This is the commit message #3:
fix typo in service
...

Edit it down to one clean message:

feat(transactions): add transaction listing with date range filter

- Domain model and repository for transaction lookup
- Service layer with date range validation
- REST controller with pagination support

Closes JIRA-123

Save and close — Git produces a single clean commit.


Squashing into Multiple Logical Commits

You do not have to squash everything into one. Group related commits logically:

pick a3f9bc2 feat(transactions): add repository and domain model
squash 7d1e4f0 wip: missing index annotations
squash 2c8a1b3 add missing validation in repository
pick 9f3e2d1 feat(transactions): add service and controller
squash 1a4b5c6 fix: missing error handling in controller
squash b3e9f1a test: add unit tests for service

This produces two clean commits — one for the data layer, one for the service/API layer.


Squash Merge (via git merge --squash)

When merging a feature branch, --squash collapses all feature commits into staged changes and lets you write one clean commit:

git switch main
git merge --squash feature/JIRA-123
git commit -m "feat(transactions): add transaction listing with date range filter (JIRA-123)"

The feature branch's individual commits do not appear in main's history. The feature branch itself is not deleted and is not "merged" in Git's eyes — you must delete it manually.

This is the approach used by GitHub's "Squash and merge" button in pull requests.


After Squashing — Force Push

After squashing commits that were already pushed, force-push to update the remote branch:

git push --force-with-lease origin feature/JIRA-123

Squash vs Fixup

squashfixup
Keeps this commit's message?✅ Yes — adds to combined message❌ No — discards message
When to useMeaningful message worth keepingTrivial fix, message is noise
pick a3f9bc2 feat(transactions): add service logic
squash 7d1e4f0 add missing validation ← message included
fixup 2c8a1b3 fix typo ← message discarded

See Fixup for the --fixup / --autosquash workflow.


Squash at PR Review Time

A good habit: keep whatever commits you need locally while working (safety nets), then squash before requesting review. Reviewers see clean history, and git log on main stays meaningful.