git
squash
commit

How do I squash my last N commits together?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

To squash the last N commits into one, use git reset --soft to move the branch pointer back N commits while keeping all changes staged, then create a new commit:

bash
git reset --soft HEAD~N
git commit -m "Your combined commit message"

This is the simplest and most reliable method. An interactive rebase (git rebase -i HEAD~N) also works and gives you more control over which commits to keep, squash, or reword, but the soft reset approach is faster when you just want everything collapsed into a single commit.

Method 1: Soft Reset (Simplest)

The soft reset moves the branch pointer backward while leaving the working tree and staging area untouched. All changes from the last N commits remain staged, ready for a new commit.

Example: Squash the Last 3 Commits

bash
1# Check your current log first
2git log --oneline -5
3
4# Output:
5# a1b2c3d Fix typo in README
6# e4f5g6h Add input validation
7# i7j8k9l Add user registration form
8# m0n1o2p Previous feature commit
9# q3r4s5t Older commit
10
11# Soft reset back 3 commits
12git reset --soft HEAD~3
13
14# All changes from the 3 commits are now staged
15git status
16# Changes to be committed:
17#   modified:   README.md
18#   new file:   src/register.js
19#   modified:   src/validation.js
20
21# Create one new commit with all the changes
22git commit -m "Add user registration with validation"

After this, the three separate commits are replaced by a single commit containing all their changes.

Why Soft Reset Works

The three reset modes clarify why --soft is the right choice:

FlagMoves HEADResets Staging AreaResets Working Tree
--softYesNoNo
--mixed (default)YesYesNo
--hardYesYesYes

With --soft, the changes from the "removed" commits stay staged. With --mixed, they become unstaged modifications. With --hard, they are discarded entirely. For squashing, --soft is what you want because you intend to recommit those exact changes.

Method 2: Interactive Rebase (More Control)

Interactive rebase gives you fine-grained control over each commit: keep, squash, reword, edit, reorder, or drop.

bash
git rebase -i HEAD~3

This opens your editor with a list like:

 
pick a1b2c3d Add user registration form
pick e4f5g6h Add input validation
pick i7j8k9l Fix typo in README

To squash the second and third commits into the first, change pick to squash (or s):

 
pick a1b2c3d Add user registration form
squash e4f5g6h Add input validation
squash i7j8k9l Fix typo in README

Save and close the editor. Git opens a second editor window where you can write the combined commit message. All three original messages are shown as a starting point.

Rebase Commands Reference

CommandShortEffect
pickpKeep the commit as-is
squashsMerge into the previous commit, combine messages
fixupfMerge into the previous commit, discard this message
rewordrKeep the commit but edit its message
editePause rebase to amend the commit
dropdRemove the commit entirely

Using fixup Instead of squash

If you do not want to edit the combined message and just want to keep the first commit's message:

 
pick a1b2c3d Add user registration form
fixup e4f5g6h Add input validation
fixup i7j8k9l Fix typo in README

This skips the message editing step entirely. The resulting commit has the message from the first (pick) commit.

Method 3: Merge with --squash (For Branches)

When squashing a feature branch into main, git merge --squash combines all branch commits into a single staged change:

bash
git checkout main
git merge --squash feature-branch
git commit -m "Add user registration feature"

This does not create a merge commit. It takes all the changes from the feature branch, stages them, and lets you commit them as a single commit on main. The feature branch's individual commits are not preserved in main's history.

Choosing the Right Method

ScenarioBest Method
Collapse last N commits into one, no fussgit reset --soft HEAD~N
Selectively squash some commits, keep othersgit rebase -i HEAD~N
Squash a feature branch into maingit merge --squash feature-branch
Rewrite commit messages during squashgit rebase -i HEAD~N with reword
Drop specific commits while squashing othersgit rebase -i HEAD~N with drop

Pushing After a Squash

Squashing rewrites history. The squashed commit has a different hash than the original commits. If you have already pushed the original commits to a remote, you need a force push:

bash
git push --force-with-lease origin feature-branch

Use --force-with-lease instead of --force. It refuses to push if someone else has pushed new commits to the remote branch since your last fetch, preventing you from accidentally overwriting their work.

Never force push to shared branches like main or develop without coordinating with your team. Squash before pushing, or squash on merge via the pull request UI.

Squash Workflows in Pull Requests

Most Git hosting platforms (GitHub, GitLab, Bitbucket) offer a "Squash and merge" button that squashes all PR commits into one when merging. This is the safest approach for teams because:

  • No one needs to rewrite history locally
  • No force pushes are needed
  • The PR retains its full commit history for review context
  • The target branch gets a clean, single commit

On GitHub, configure this in repository settings under "Merge button" to allow or require squash merging.

Recovering From a Bad Squash

If you squash and immediately realize you made a mistake, git reflog shows the previous state:

bash
1git reflog
2# a1b2c3d HEAD@{0}: commit: Add user registration with validation
3# i7j8k9l HEAD@{1}: reset: moving to HEAD~3
4# i7j8k9l HEAD@{2}: commit: Fix typo in README
5# ...
6
7# Restore the pre-squash state
8git reset --hard HEAD@{1}

The reflog keeps entries for about 90 days by default, so you have a safety net even after aggressive history rewriting.

Squashing Commits That Include Merge Commits

If the range of commits you want to squash includes merge commits, git rebase -i can produce conflicts or confusing results because rebase replays commits linearly.

For this situation, the soft reset approach is more reliable:

bash
git reset --soft HEAD~N
git commit -m "Squashed feature with merged changes"

The soft reset does not care about the topology of the commits being collapsed. It just moves the pointer and leaves all accumulated changes staged.

Common Pitfalls

Using HEAD~N with the wrong value of N is the most common mistake. Count carefully, or use git log --oneline first to visually identify which commit you want as the new base. If N includes commits from before your feature branch started, you will inadvertently squash commits that belong to other work.

Force pushing to a shared branch after squashing overwrites other people's commits. Always use --force-with-lease, and only force push to branches you own (feature branches). For shared branches, squash on merge via the hosting platform UI.

Forgetting to write a meaningful combined commit message wastes the opportunity that squashing creates. The whole point of squashing "fix typo," "WIP," and "addressing review comments" is to replace them with a clear, descriptive message.

Running git rebase -i when there are uncommitted changes in the working tree causes the rebase to fail or produces unexpected results. Commit or stash your changes before starting an interactive rebase.

Confusing squash and fixup in interactive rebase leads to losing commit messages you wanted to keep. squash combines messages and lets you edit. fixup silently discards the message. Choose based on whether you need the commit's message in the final result.

Squashing commits that have already been cherry-picked to another branch creates duplicate changes. The squashed commit has a different hash, so Git does not recognize it as the same change, leading to conflicts when the branches merge.

Summary

  • Use git reset --soft HEAD~N followed by git commit for the simplest squash. All changes stay staged.
  • Use git rebase -i HEAD~N when you need to selectively squash, reword, or drop individual commits.
  • Use git merge --squash to squash a feature branch into another branch.
  • Always use --force-with-lease instead of --force when pushing rewritten history.
  • Prefer "Squash and merge" in pull request UIs to avoid force pushes entirely.
  • Check git reflog to recover from a bad squash.

Course illustration
Course illustration

All Rights Reserved.