Git Squash — Prevent Commit Loss with --force-with-lease
After a developer force-pushed a squashed commit to a shared branch, 3 commits vanished.
- git rebase -i HEAD~N: full control — pick, squash, fixup, reorder, reword
- git merge --squash: stages all branch changes as one set — no merge commit
- git reset --soft HEAD~N: moves HEAD back, changes stay staged — fastest for simple squash
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.
Squashing combines multiple commits into one clean commit. It is the standard pre-PR operation that transforms messy development history (WIP commits, typo fixes, debug logging) into a single meaningful commit that answers 'what changed and why'.
Three methods serve different use cases: interactive rebase for surgical control, merge --squash for branch-level squashing, and reset --soft for the fastest simple squash. The choice depends on how much control you need over which commits are combined and whether intermediate messages should be preserved.
Common misconceptions: that squashing is always required before merging (it is a team convention), that squashing loses code (it combines commits, the code stays), and that squashing is safe on shared branches (it rewrites SHAs, which breaks teammates).
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: folds commit into parent, opens editor to combine messages
- fixup: folds commit into parent, silently discards the folded message
- Use fixup for WIP, typo fixes, debug logging — messages that add no value
- Use squash when the intermediate message contains context worth preserving
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.
- merge --squash stages all branch changes without creating a merge commit
- You write the commit message manually — full control
- GitHub's Squash and Merge automates this with PR title/description as the message
- After squash-merge, delete the feature branch — it is no longer needed
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.
- --force-with-lease: fails if remote has commits you do not know about
- --force: blindly overwrites remote — can lose teammate's commits
- Always use --force-with-lease. Never use --force on any branch.
- If --force-with-lease fails: fetch first, then rebase your squash on top of the new remote state
When NOT to Squash
Squashing is not always the right choice. There are specific scenarios where preserving the commit history is more valuable than a clean linear log.
Large features with meaningful intermediate commits: If your feature has 15 commits but each one represents a logical, testable unit (e.g., 'add database migration', 'add API endpoint', 'add frontend component'), squashing them into one commit loses the ability to bisect within the feature. A reviewer who wants to understand the feature's evolution benefits from seeing the individual commits.
Shared branches where others have pulled your commits: Squashing rewrites SHAs. If teammates have branched off your commits, their branches diverge after you squash and force-push. Never squash on branches that others have pulled.
Release branches with audit requirements: In regulated industries, the commit history on release branches may be part of the audit trail. Squashing removes the individual commits that show when each change was made and by whom. Check your compliance requirements before squashing on release branches.
Commits that will be cherry-picked: If a commit needs to be cherry-picked to another branch (e.g., a hotfix), it must exist as a standalone commit. Squashing it into a larger commit makes cherry-picking impossible.
- --force-with-lease: fails if remote has commits you do not know about
- --force: blindly overwrites remote — can lose teammate's commits
- Always use --force-with-lease. Never use --force on any branch.
- If --force-with-lease fails: fetch first, then rebase your squash on top of the new remote state
Squash on Shared Feature Branch: 4 Developers Lose Work When --force Overwrites Commits
git rebase -i HEAD~8 and squashed all 8 commits into 1.
4. While Developer A was rebasing, Developer B pushed 3 new commits to the same branch.
5. Developer A ran git push --force origin feature/payment-v2.
6. The --force flag overwrote Developer B's 3 commits on the remote.
7. Developer B's next fetch showed the branch had diverged — their local had 3 commits the remote did not have.
8. Developer B's 3 commits were only in their local reflog, not on the remote.git push --force-with-lease — this would have failed because the remote had commits Developer A did not know about.
3. Team rule: never squash on a branch that multiple developers are actively pushing to. Squash only after all developers have finished pushing.
4. Team rule: always use --force-with-lease, never --force.
5. Added branch protection to require all developers to use separate feature branches, not a shared branch.- Squashing on a shared branch rewrites SHAs. If teammates have pulled the old commits, their branches diverge.
- Always use --force-with-lease instead of --force. --force-with-lease fails if the remote has commits you do not know about.
- Never squash on a branch that multiple developers are actively pushing to. Squash only after developer per feature branch is the industry standard. Shared feature branches create unnecessary coordination overhead.
git reflog — look for your commit hashes.
3. Cherry-pick onto the squashed branch: git cherry-pick <hash> for each lost commit.
4. Prevention: use --force-with-lease. It fails if the remote has commits you do not know about.git reset --hard ORIG_HEAD to return to pre-rebase state.
4. Correct base: git log --oneline -10 to find the right commit hash, then git rebase -i <correct-hash>.git reflog — find the dropped commit hash.
3. Cherry-pick it back: git cherry-pick <hash>.
4. Then re-run the interactive rebase with 'squash' instead of 'drop'.git fetch origin to see what the remote has.
4. If the remote commits should be kept: rebase your squash on top of them.
5. If the remote commits are duplicates: coordinate with the teammate who pushed them.git commit --amend to rewrite the last commit message.
4. Prevention: use 'fixup' instead of 'squash' for commits whose messages you want to discard.Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
That's Git. Mark it forged?
3 min read · try the examples if you haven't