Intermediate 5 min · March 30, 2026
Git Squash Commits: Combine Multiple Commits into One

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 & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is Git Squash Commits?

Git squash is the process of combining multiple commits into a single commit, effectively rewriting your branch's history to present a cleaner, more logical sequence of changes. You'd do this to eliminate noisy 'fix typo' or 'wip' commits before merging into a shared branch like main, making the project history easier to review, bisect, and understand.

Squashing commits is like editing a draft before you submit it.

The core problem squash solves is the tension between frequent local commits (good for saving progress) and a clean, linear history for collaboration (good for everyone else). Without squash, you either force reviewers to wade through every intermediate mistake, or you never commit often enough and risk losing work.

There are three main ways to squash in Git: interactive rebase (git rebase -i), git merge --squash, and git reset --soft followed by a new commit. Interactive rebase is the most flexible—you pick which commits to fold together, reorder them, or even drop them entirely. git merge --squash is a one-shot operation that takes all commits from a feature branch and creates a single squashed commit on the target branch, but it discards the original branch's commit metadata entirely.

The git reset method is the simplest for small, local cases: you soft-reset to an earlier commit, then re-commit all changes as one. Each approach has trade-offs in control, traceability, and safety.

Where this gets dangerous is the 'squash-merge trap' that the official docs gloss over. When you squash commits and force-push, you're rewriting history that others may have based work on. The --force-with-lease flag is your safety net—it refuses the push if someone else has pushed new commits to the same branch, preventing you from silently overwriting their work.

Without it, a plain git push --force can destroy commits your teammates rely on, leading to painful recovery sessions. The rule of thumb: never squash commits that have been pushed to a shared branch unless you're the only person working on it, and always use --force-with-lease to protect against race conditions.

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).

What Git Squash Actually Does to Your Commit History

Git squash is a rebase operation that combines multiple commits into a single commit. The core mechanic: during an interactive rebase (git rebase -i), you mark commits with 'squash' or 'fixup' to fold their changes into the preceding commit. Squash preserves the commit message for editing; fixup discards it entirely. This rewrites history by creating new commit objects with combined diffs and timestamps.

Squashing does not merge changes — it replays them. Git takes the diff from each squashed commit and applies it sequentially, then creates one new commit with the accumulated changes. The original commits become unreachable from any branch reference, though they remain in the object store until garbage collection. This means squashing is a destructive rewrite: any other branch or collaborator referencing the original commits will diverge.

Use squash to clean up a feature branch before merging into main — turning a dozen 'fix typo' and 'WIP' commits into one logical change. It's essential for maintaining a readable, bisectable history. In CI/CD systems, squashing prevents noisy commit logs from triggering unnecessary build steps and makes rollbacks atomic. Never squash commits that have already been pushed to a shared branch unless you coordinate with the entire team.

Squash ≠ Safe by Default
Squashing rewrites history — a force push after squash will orphan commits other developers may have based work on, causing painful rebase conflicts.
Production Insight
A team squashed a shared feature branch before merging, then force-pushed — three developers lost 2 days of work because their local branches referenced the now-orphaned commits.
Symptom: 'fatal: refusing to merge unrelated histories' or 'error: failed to push some refs' after a force push.
Rule: Never squash commits that have been pushed to a shared branch; if you must, use --force-with-lease and notify the team to rebase immediately.
Key Takeaway
Squash rewrites history — original commits become unreachable and are eventually garbage collected.
Use squash only on local or feature branches before merging into a shared branch.
Always prefer --force-with-lease over --force to prevent overwriting others' work.
Git Squash Workflow and Risks THECODEFORGE.IO Git Squash Workflow and Risks Three squash methods and the force-with-lease trap Interactive Rebase pick/squash commits in editor git merge --squash flatten feature branch into one commit git reset --soft undo commits, stage all, recommit Squash-Merge Trap loses branch context and co-author info force-with-lease push safely after local rewrite ⚠ Squash-merge discards per-commit metadata Use interactive rebase + force-with-lease instead THECODEFORGE.IO
thecodeforge.io
Git Squash Workflow and Risks
Git Squash Commits

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).

The Squash-Merge Trap: What the Docs Don't Tell You

Every new dev discovers git merge --squash and thinks they've found a cheat code. They haven't. They've found a landmine wrapped in convenience.

Here's what happens: git merge --squash feature-branch takes all commits from your feature branch, smashes them into your working directory as uncommitted changes, and leaves you hanging. No commit. No merge. No branch history link. You're holding a grenade with the pin pulled.

The docs call this "clean history." I call it losing the ability to trace which commits actually broke production. When your deployment fails at 3 AM, you want git bisect, not a single monolithic commit that says "Add billing system."

Use --squash only when the feature branch is truly atomic — one logical change, one commit worth of work. If you can't describe the entire branch's purpose in a single tweet, you're not squashing. You're bulldozing evidence.

SquashMergeExample.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — devops tutorial

# Step 1: Checkout target branch
git checkout main
# Output: Switched to branch 'main'

# Step 2: Squash-merge feature branch
git merge --squash payment-gateway-v2
# Output: Automatic merge went well; stopped before committing as requested
# Notice: no commit created, no merge link preserved

# Step 3: Commit the squashed changes
git commit -m "feat: integrate Stripe payment gateway v2"
# Output: [main 7a9d4c3] feat: integrate Stripe payment gateway v2
#  1 file changed, 342 insertions(+)
Output
Switched to branch 'main'
Automatic merge went well; stopped before committing as requested
[main 7a9d4c3] feat: integrate Stripe payment gateway v2
1 file changed, 342 insertions(+)
Production Trap:
After squash-merge, you can't git revert just one feature commit. You're stuck reverting the entire monolith or crafting a manual reverse commit. Your CI/CD pipeline just lost granular rollback capability.
Key Takeaway
Squash-merge only works if the feature is truly one logical unit. If your branch has 12 commits fixing edge cases, you're not squashing — you're destroying forensic evidence.

Squashing the Last Two Commits: The Incantation You'll Use Daily

You've shipped a hotfix. Then you realized you forgot to bump the version. Then you noticed a typo in the README. Now you have three commits that should be one.

git rebase -i HEAD~2

That -i flag opens your default editor with a list of the last two commits. Change pick to squash (or just s) on the second commit. Save. Exit. Done.

What happened under the hood? Git took your commit messages, combined the diffs, and opened a new editor to merge the messages. Write something descriptive like "fix: resolve race condition in auth module" — not "fix bug #4812 fix bug #4813."

One warning: if you've already pushed those commits, force-pushing a squashed history will make your teammates want to strangle you. Only squash local commits or commits on branches you own exclusively.

SquashLastTwo.ymlYAML
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
// io.thecodeforge — devops tutorial

# Starting state — two commits that should be one
git log --oneline -3
# Output:
# e7b9f31 (HEAD) fix typo in auth error message
# 4c2a9fb fix: resolve race condition in auth module
# 1d3e8b5 feat: implement JWT authentication

# Squash tip commit into the previous one
git rebase -i HEAD~2

# In the editor, change:
# pick 4c2a9fb fix: resolve race condition in auth module
# squash e7b9f31 fix typo in auth error message

# Save & exit, then in the commit message editor:
# Keep: "fix: resolve race condition in auth module"
# Save & exit again

# Result:
git log --oneline -3
# Output:
# 8f1b2c3 (HEAD) fix: resolve race condition in auth module
# 1d3e8b5 feat: implement JWT authentication
Output
e7b9f31 (HEAD) fix typo in auth error message
4c2a9fb fix: resolve race condition in auth module
1d3e8b5 feat: implement JWT authentication
After rebase:
8f1b2c3 (HEAD) fix: resolve race condition in auth module
1d3e8b5 feat: implement JWT authentication
Senior Shortcut:
Use git rebase -i @~N instead of HEAD~N. The @ is a shorthand for HEAD that saves exactly two keystrokes each time — and saves you from typing HEAD 500 times over your career.
Key Takeaway
Squash the last two commits with git rebase -i HEAD~2. Change pick to squash on the second commit. Never force-push squashed commits to shared branches.

Tips for a Smooth Squash

Most devs butcher a squash because they don't prep the battlefield. You're rewriting history — one misstep and you've nuked a coworker's branch. The rule: squash only commits that have never been pushed to a shared remote. If they have, you're asking for merge-conflict hell.

Before you rebase, run git log --oneline and count your commits. Know exactly how many you're squashing. Then git rebase -i HEAD~N where N is that count. In the editor, change every line after the first from 'pick' to 'squash' (or 's'). Save and exit — Git will prompt you for a new combined message. Write something meaningful, not 'fix stuff'.

Pro tip: if your branch is behind main after the squash, force-push is inevitable. Use git push --force-with-lease instead of --force. It checks if your remote branch has moved since you last fetched. If it has, Git aborts. That safety net has saved my ass more times than I'll admit.

SquashPrep.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — devops tutorial

# Step 1: Count your commits
git log --oneline HEAD~5..HEAD
# Output:
# a1b2c3d fix: typo in login
# e4f5g6h feat: add rate limiter
# i7j8k9l chore: bump deps
# m0n1o2p fix: lint errors
# q3r4s5t feat: wire up API

# Step 2: Interactive rebase last 5 commits
git rebase -i HEAD~5
# In editor: change picks 2-5 to 's'

# Step 3: Force push with safety net
git push --force-with-lease origin my-branch
Senior Shortcut:
If you always squash before merging, alias git squash-head to git reset --soft HEAD~1 && git commit --amend. Squashes current commit into previous one — no interactive editor, no rebase.
Key Takeaway
Always squash before pushing to shared remotes. Use --force-with-lease, not --force.
● 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?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
May 23, 2026
last updated
1,663
articles · all by Naren
🔥

That's Git. Mark it forged?

5 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