Homeβ€Ί DevOpsβ€Ί Git Squash Commits: Combine Multiple Commits into One

Git Squash Commits: Combine Multiple Commits into One

Where developers are forged. Β· Structured learning Β· Free forever.
πŸ“ Part of: Git β†’ Topic 14 of 19
Learn how to squash commits in Git using interactive rebase, git merge --squash, and git reset.
βš™οΈ Intermediate β€” basic DevOps knowledge assumed
In this tutorial, you'll learn:
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
⚑ Quick Answer
Squashing commits is like editing a draft before you submit it. During development you commit dozens of times β€” 'fix', 'fix again', 'actually fix'. Before merging, you collapse those into one clean, meaningful commit that tells the story of what changed, not how many times you changed direction. The individual drafts disappear; the final version survives.

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.

git_squash_rebase.sh Β· BASH
12345678910111213141516171819202122232425
# 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
β–Ά Output
Successfully rebased and updated refs/heads/feature/payment-retry.

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.

git_squash_merge.sh Β· BASH
123456789101112131415
# 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
β–Ά Output
Squash commit -- not updating HEAD
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.

git_squash_reset.sh Β· BASH
1234567891011121314151617
# 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
β–Ά Output
HEAD is now at a3f9c2e Add PaymentRetryService skeleton
# All changes staged, ready for single commit
πŸ”₯
Always use --force-with-lease, not --forceWhen pushing squashed commits to a remote branch, use git push --force-with-lease instead of --force. Force-with-lease checks that no one else has pushed to the branch since you last fetched. Plain --force overwrites whatever is there, including commits from a teammate who pushed while you were rebasing. I've seen this cause real data loss on shared feature branches.
MethodToolBest ForKeeps Intermediate Messages?
Interactive rebasegit rebase -iSelective squashing, reorderingOptional β€” you choose
git merge --squashgit merge --squashSquashing entire branch on mergeNo β€” write one new message
git reset --softgit resetQuick squash of last N commitsNo β€” write one new message
GitHub Squash & MergeGitHub UITeam standardisation on PR mergeNo β€” 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.

πŸ”₯
Naren Founder & Author

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.

← PreviousGit Delete Local and Remote BranchNext β†’Git Stash: Save and Restore Work in Progress
Forged with πŸ”₯ at TheCodeForge.io β€” Where Developers Are Forged