Intermediate 3 min · March 30, 2026

Git Squash — Prevent Commit Loss with --force-with-lease

After a developer force-pushed a squashed commit to a shared branch, 3 commits vanished.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
Plain-English First

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.

io/thecodeforge/git/SquashRebase.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# io.thecodeforge — Squash with Interactive Rebase

# ─────────────────────────────────────────────────────────────
# 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.

# ─────────────────────────────────────────────────────────────
# Use fixup instead of squash for commits whose messages you want to discard
# ─────────────────────────────────────────────────────────────
# pick a3f9c2e Add PaymentRetryService skeleton
# fixup 7b2d4f1 WIP: saving progress
# fixup 9c3e8a2 Fix typo
# fixup d4f8b3c Add debug logging (remove before merge)
# fixup does NOT open a message editor — the intermediate messages are discarded.

# ─────────────────────────────────────────────────────────────
# Force push to update remote (only on your own feature branch)
# ─────────────────────────────────────────────────────────────
git push origin feature/payment-retry --force-with-lease
# --force-with-lease fails if the remote has commits you do not know about.
# --force blindly overwrites — never use it.
Output
Successfully rebased and updated refs/heads/feature/payment-retry.
squash vs fixup: Keep or Discard the Message
  • 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
Production Insight
Interactive rebase is the only method that gives you control over individual commits. You can squash some, fixup others, reorder them, and reword messages — all in one session. This is why it is the preferred method for pre-PR cleanup. The trade-off: it is more complex than reset --soft and requires understanding the editor commands. For teams where only one developer works on a branch, reset --soft is often faster. For teams where commit message quality is enforced, interactive rebase is essential.
Key Takeaway
Interactive rebase gives surgical control: pick, squash, fixup, reorder, reword. squash combines messages, fixup discards them. Always use --force-with-lease after pushing squashed commits. Never squash on shared branches.

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.

io/thecodeforge/git/SquashMerge.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# io.thecodeforge — Squash with git merge --squash

# ─────────────────────────────────────────────────────────────
# 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
#   new file:   src/test/java/io/thecodeforge/payment/PaymentRetryServiceTest.java

# Write the single commit message
git commit -m "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."

# ─────────────────────────────────────────────────────────────
# 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
merge --squash Is What GitHub's Squash and Merge Button Does
  • 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
Production Insight
merge --squash is the cleanest method for integrating a feature branch into main. It produces exactly one commit with all the feature's changes, no merge commit, and no intermediate history. The trade-off: you lose the feature's internal refactors, multi-phase implementations), consider a regular merge instead. For most features, squash-merge is the right default.
Key Takeaway
merge --squash stages all branch changes as one set without a merge commit. You write the commit message manually. This is what GitHub's Squash and Merge button does. After squash-merge, delete the feature branch.

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.

io/thecodeforge/git/SquashReset.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# io.thecodeforge — Squash with git reset --soft

# ─────────────────────────────────────────────────────────────
# 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 with --force-with-lease (never --force)
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 --force
  • --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
Production Insight
reset --soft is the fastest method for squashing the last N commits when you do not need to selectively choose which commits to combine. It is two commands: reset --soft HEAD~N then commit. No editor, no interactive mode, no learning curve. The trade-off: you cannot selectively squash some commits while keeping others separate. For that, use interactive rebase. For the common case of 'I have 5 WIP commits, squash them all', reset --soft is the right tool.
Key Takeaway
reset --soft HEAD~N is the fastest squash method — two commands, no editor. Changes stay staged, ready for one clean commit. Use it for simple squashes. Use interactive rebase when you need selective control. Always --force-with-lease after pushing.

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.

Always Use --force-with-lease, Not --force
  • --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
Production Insight
The squash-everything culture creates a specific failure mode: when a production bug is introduced by a feature that was squashed into one commit, the only way to revert is to revert the entire feature. If the feature had 10 logical commits, you could revert just the one that introduced the bug. Squashing removes this surgical revert capability. The trade-off is between clean history and revert granularity.
Key Takeaway
Do not squash when: commits are meaningful logical units, others have pulled your commits, the branch has audit requirements, or commits will be cherry-picked. Squash noise (WIP, typos). Preserve signal (migrations, endpoints, tests).
● Production incidentPOST-MORTEMseverity: high

Squash on Shared Feature Branch: 4 Developers Lose Work When --force Overwrites Commits

Symptom
A teammate reported that their 3 commits were missing from the remote feature branch. Their local branch showed 'Your branch and origin/feature/payment-v2 have diverged'. The remote branch had 1 squashed commit instead of the expected 11 commits (8 original + 3 new). The teammate's 3 commits were not in the squashed commit.
Assumption
The teammate assumed a GitHub issue or a corrupted repository. They checked GitHub status and repository settings. Nobody suspected the squash — the developer who squashed did not announce it and used --force instead of --force-with-lease.
Root cause
1. Four developers were working on the same feature branch (feature/payment-v2). 2. Developer A decided to squash their 8 commits before the PR review. 3. They ran 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.
Fix
1. Immediate: Developer B found their 3 commits in their local reflog and cherry-picked them onto the squashed branch. 2. Developer A switched to 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.
Key lesson
  • 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.
Production debug guideSystematic recovery paths for lost commits, diverged branches, and failed squashes.5 entries
Symptom · 01
Commits lost after teammate squashed and force-pushed to shared branch
Fix
1. Your commits are in your local reflog, not on the remote. 2. Find them: 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.
Symptom · 02
git rebase -i opens wrong commits — squashed the wrong ones
Fix
1. You passed the wrong base commit to rebase -i. 2. If the rebase is still in the editor: abort — close the editor without saving. 3. If the rebase completed: 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>.
Symptom · 03
Interactive rebase dropped a commit instead of squashing it
Fix
1. You used 'drop' instead of 'squash' during interactive rebase. 2. The commit is still in reflog: 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'.
Symptom · 04
git push --force-with-lease rejected after squash — 'non-fast-forward'
Fix
1. The remote has commits you do not know about (someone pushed while you were squashing). 2. This is --force-with-lease protecting you. Do NOT switch to --force. 3. Fetch: 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.
Symptom · 05
Squashed commit has wrong message — intermediate messages leaked in
Fix
1. During interactive rebase, you chose 'squash' which opens an editor to combine messages. 2. The editor showed all messages concatenated. You may have saved without editing. 3. Fix: git commit --amend to rewrite the last commit message. 4. Prevention: use 'fixup' instead of 'squash' for commits whose messages you want to discard.
★ Git Squash Triage Cheat SheetFast recovery for squash-related failures, lost commits, and diverged branches.
Commits lost after teammate squashed and force-pushed
Immediate action
Find lost commits in your local reflog before they expire (30 days).
Commands
git reflog | grep 'commit' (find your lost commit hashes)
git cherry-pick <hash> (recover each lost commit onto the squashed branch)
Fix now
Prevention: team rule to use --force-with-lease. Never --force on shared branches.
git rebase -i squashed wrong commits+
Immediate action
Undo with ORIG_HEAD if the rebase completed.
Commands
git reset --hard ORIG_HEAD (return to pre-rebase state)
git log --oneline -10 (find the correct base commit hash)
Fix now
Re-run: git rebase -i <correct-base-hash>. Verify the commit list before saving.
Interactive rebase dropped a commit — used 'drop' instead of 'squash'+
Immediate action
Recover the dropped commit from reflog.
Commands
git reflog (find the dropped commit hash)
git cherry-pick <hash> (recover the commit)
Fix now
Re-run interactive rebase with 'squash' instead of 'drop'.
git push --force-with-lease rejected after squash+
Immediate action
The remote has commits you do not know about. Do not use --force.
Commands
git fetch origin (see what the remote has)
git log --oneline origin/feature-branch..HEAD (compare your squash vs remote)
Fix now
If remote commits should be kept: rebase your squash on top of them. Coordinate with teammate.
Squashed commit message is wrong — intermediate messages leaked in+
Immediate action
Amend the last commit message.
Commands
git log --oneline -1 (verify the current message)
git commit --amend (rewrite the last commit message)
Fix now
If already pushed: git commit --amend && git push --force-with-lease. Prevention: use 'fixup' instead of 'squash'.
Squash Methods Compared
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

1
git re~N gives you into staged changes without a merge commit
equivalent to what GitHub's 'Squash and merge' does.
2
git reset --soft HEAD~N is the fastest approach for squashing the last N commits when you just want one clean commit.
3
Always use --force-with-lease instead of --force when pushing squashed commits
it prevents accidentally overwriting a teammate's push.
4
Never squash commits on a branch other people are actively working on
rewriting hashes breaks their local history.
5
Do not squash commits that will be cherry-picked to other branches
squashing makes cherry-picking impossible.
6
squash combines messages, fixup discards them. Use fixup for WIP commits and typo fixes where the message adds no value.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What does squashing commits in Git mean?
02
Can I squash commits that are already pushed to a remote?
03
What is the difference between squash and fixup in git rebase -i?
04
When should I NOT squash commits?
05
What is the fastest way to squash the last 5 commits?
🔥

That's Git. Mark it forged?

3 min read · try the examples if you haven't

Previous
Git Delete Local and Remote Branch
14 / 19 · Git
Next
Git Stash: Save and Restore Work in Progress