Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Squash Commits the Right Way

By Kokil Thapa | Last reviewed: September 2026

You opened a pull request with fourteen commits titled "fix typo", "wip", and "actually fix it". Reviewers skim past the noise. CI logs tie failures to the wrong SHA. That is the moment you need to squash commits the right way—not as a habit on every branch, but as a deliberate cleanup step before merge. On teams I work with through web development in Nepal and remote client repos, squashing is a workflow choice with real trade-offs. This guide shows the commands, the safety rules, and the cases where you should leave history alone.

What does it mean to squash commits the right way?

Squashing merges multiple commits into one. The diff stays the same. The history becomes shorter. "The right way" means three things together: one commit equals one logical change, the message explains why—not only what—and you do not destroy audit trails on branches other people use.

Git offers several squash paths. They are not interchangeable. Pick based on who shares the branch and whether you need per-commit SHAs later for git bisect or signed attestation.

Squash Commits the Right WayBefore: WIP noisefix typowip checkoutmore fixesactually fix it14 commits, hard to reviewAfter: one logical unitfeat(cart): validatedelivery zones1 commit, clear intent
Squash commits the right way turns noisy WIP history into one reviewable, bisect-friendly unit of work.

A squashed commit still points to a parent. Git creates a new SHA. Anyone who based work on the old SHAs must reconcile. That is why squashing belongs on feature branches you own—not on main after others have pulled intermediate commits.

The three squash mechanisms teams actually use

  • Interactive rebase (git rebase -i): Rewrites local commits before push. Best control over message and ordering.
  • Squash merge on a pull request: Platform combines all PR commits at merge time. No force push on the feature branch required.
  • Soft reset + recommit: git reset --soft to a base, then one new commit. Fast on small branches; easy to mess up on large ones.

On production Laravel apps I maintain with Deployer 7 and GitLab CI, we squash feature work at merge. We keep granular commits on the feature branch while iterating. Reviewers can still see the progression in the PR timeline even when main gets one commit.

When should you squash commits instead of keeping them separate?

Squash when commits are implementation noise—not when each commit is a independently revertible unit. The decision is contextual. A law-firm portal fix and a trek booking platform deploy both benefit from clean main, but for different reasons.

ScenarioSquash?Why
Feature branch with WIP commitsYesReviewers read one diff; changelog stays readable
Each commit passes CI and maps to a ticketOften noGranular revert and bisect matter more than brevity
Open-source contributionUsually yesMaintainers prefer one commit per PR on main
Shared long-lived branch others pullNoRewriting forces painful rebases for teammates
Release branch with tagged versionsNoTags and SHAs are contractual for deployments
Pairing with Conventional CommitsSquash to one typeOne feat: or fix: per merged PR

Squashing also pairs well with signed commits. One squashed commit means one signature to verify. That aligns with supply-chain practices covered in signing Git commits with GPG or SSH. If every WIP commit was signed, squashing produces a new commit that needs a fresh signature after rebase.

Do not squash just to hide mistakes. A bad merge or leaked secret needs revert or reflog recovery—not burial under a squash. Security fixes deserve their own visible commit message on main.

How do you squash commits locally with interactive rebase?

Interactive rebase is the precision tool. You edit a todo list. Git replays commits. You mark which ones to squash. Follow this sequence on a branch only you have pushed—or before first push.

  1. Ensure your working tree is clean: git status.
  2. Find the base commit—the point before your feature work: git log --oneline main..HEAD.
  3. Start interactive rebase against that base: git rebase -i main.
  4. In the editor, leave the first commit as pick. Change later commits to squash or s.
  5. Save and close. Git opens a second editor for the combined message.
  6. Write a message that matches your team standard. Push with care if the branch was already remote.

Example rebase todo list

pick a1b2c3d feat(cart): add zone validation schema
squash d4e5f6a fix typo in zone label
squash 7g8h9i0 wip tests
squash j1k2l3m address review comments

After save, Git combines all four into one commit. The resulting message should not paste four subject lines blindly. Write something a release note could quote:

feat(cart): validate delivery zones before checkout

Add server-side zone rules matching NPR delivery bands.
Reject out-of-area addresses with a clear user message.
Includes PHPUnit coverage for edge postcodes.

Squash the last N commits without counting back to main

For quick cleanup before your first push:

git reset --soft HEAD~4
git commit -m "feat(cart): validate delivery zones before checkout"

--soft keeps staged changes. You lose individual commit messages unless you copied them. Prefer rebase -i when messages contain ticket IDs you need to preserve.

Interactive Rebase Squash FlowFeaturebranchgit rebase -ipick + squashOne commitnew SHASafe push checklistBranch only you use?--force-with-leaseNever force push shared main or release tags
Local squash via interactive rebase finishes with a safe push—force-with-lease only on branches you own.

If you already pushed the messy commits, update the remote carefully:

git push --force-with-lease origin feature/delivery-zones

--force-with-lease refuses to push if someone else added commits you have not seen. Plain --force overwrites blindly. On shared feature branches, coordinate in chat before any force push. I have seen CI pipelines and cron jobs break because a teammate force-pushed while another was rebasing.

How do you squash commits on a pull request without rewriting shared history?

Pull request squash merge is the lowest-friction option for teams that forbid force push on protected branches. GitHub, GitLab, and Bitbucket all expose it. The platform creates one merge commit on the target branch containing the full PR diff. Individual commits remain visible in the PR UI for archaeology—but not on main.

GitHub squash merge

Enable "Allow squash merging" in repository settings. At merge time, choose "Squash and merge". Edit the default title. GitHub often concatenates commit subjects—delete the noise. Prefer a single line matching Conventional Commits if your changelog tooling expects it.

GitLab squash at merge

Set Squash commits when merge request is accepted in project settings or per MR. GitLab can enforce squash for all merges into main. That matches how several sister sites on my shared EC2 deploy pipeline behave—one commit per MR keeps Deployer release notes predictable.

When PR squash beats local rebase

  • Branch protection blocks force push.
  • Multiple authors contributed; rewriting reassigns blame awkwardly.
  • Reviewers already approved specific SHAs—you should not rewrite after approval without re-review.
  • You want zero risk to the source branch; delete it after merge anyway.

PR squash does not replace local hygiene. Smaller commits during development still help reviewers comment on incremental diffs. Squash at the boundary—when work enters shared history.

PR Merge Strategy OutcomesMerge commitAll commits on mainFull history preservedSquash mergeOne commit on mainPR timeline keeps detailRebase mergeLinear main historyEach commit replayedSquash commits the right way at PR mergeClean main + no force push + review trail in PRBest default for small teams on protected main
Squash merge on pull requests delivers clean main-branch history without local interactive rebase on protected branches.

Reference: GitHub documents merge options at docs.github.com pull request merges. Git's own rebase manual lives at git-scm.com/docs/git-rebase.

What are the common mistakes when squashing Git commits?

Most squash accidents are social, not syntactic. The commands work. Teams skip the rules about shared state. These failures show up repeatedly on client projects and on support and maintenance engagements when a new developer joins mid-sprint.

Force pushing main or a release tag

Never rewrite main, production, or tagged releases others deploy. Deployer symlink swaps and GitLab CI jobs reference SHAs in logs. A force-pushed main breaks clone scripts, open PR bases, and automated changelogs. If you already did—stop pushing and read recover lost commits with git reflog before anyone runs git pull.

Squashing unrelated changes together

One commit should revert cleanly. Do not squash a refactor with a bug fix. If production needs the fix without the refactor, you want two merges or two commits. Use git rebase -i to reorder first—group related picks, squash within groups only.

Losing authorship and co-author trailers

Squash rewrites the committer metadata. Add co-authored-by trailers when pairing:

feat(api): add rate limit headers

Co-authored-by: Name <email@example.com>

GitHub and GitLab attribute co-authors from the trailer line. Without it, squash merge can hide contributor credit in the blame view on main.

Breaking signed commit chains

GPG or SSH signing applies to the new squashed commit only after you re-sign. Configure commit signing before squash, or sign again post-rebase. Teams enforcing verified commits via branch rules will block unsigned squashed pushes.

Automating without hooks

Pre-push hooks catch accidental force push to protected refs. Combine with CI lint on commit messages. See Git hooks to automate checks for a practical setup. A regex tester helps validate Conventional Commit patterns in hook scripts before you enforce them repo-wide.

Should You Squash?Ready to merge?Others pull branch?Use PR squash mergeSolo branch?rebase -i locallyProtected main?No force push everEach commit CI-clean?Maybe keep separateSquash commits the right way = one logical unit
Decision flow: shared branches favour PR squash merge; solo branches allow interactive rebase before push.

Deploy and CI gotchas after squash

On Laravel 12 or 13 projects, a squashed commit still triggers the same pipeline. Verify these after your first squash merge:

  • CI caches keyed by commit SHA invalidate—first build may run slower.
  • Deployer release names derived from short SHA change; note the new ID in deploy logs.
  • Changelog generators reading conventional commits on main see one entry—ensure the squash message carries the ticket reference.
  • Open dependabot or renovate PRs rebased onto old SHAs may need a fresh rebase onto new main.

For infrastructure-heavy repos, Linux system administration teams sometimes tag server config separately from app code. Do not squash across unrelated concerns—keep infra commits out of application feature squashes.

Key Takeaways

  • Squash WIP commits into one logical unit with a message that explains why the change exists—not a pile of subject lines.
  • Use git rebase -i on private feature branches; use PR squash merge when branch protection blocks force push.
  • Always push with --force-with-lease, never plain --force, and never rewrite main or release tags.
  • Keep fixes and refactors in separate squashes so production can revert one without the other.
  • Re-sign commits and add co-author trailers after squash so blame and verified-commit rules stay intact.
  • Pair squash policy with Conventional Commits and pre-push hooks so CI and changelogs stay predictable.

People Also Ask

Is squashing commits the same as rebasing?

No. Rebasing replays commits onto a new base and can reorder or edit each one. Squashing combines multiple commits into one. You often squash during an interactive rebase, but rebase alone does not merge commits unless you mark them squash in the todo list.

Does squash merge delete commit history permanently?

On the target branch, yes—the individual commits become one. On GitHub and GitLab, the pull request page still lists the original commits for review. Cloning main after squash merge shows only the squashed commit. Clone the merged PR URL if you need the granular SHAs later.

When should you never squash commits?

Never squash on branches others have pulled when you plan to force push. Never squash tagged releases. Never combine unrelated fixes that might need independent revert. Open-source maintainers sometimes reject squashing when each commit in a PR is a logically separate patch.

What is the difference between squash and fixup in interactive rebase?

squash merges commits and opens an editor to combine messages. fixup merges commits but discards the secondary commit message entirely. Use fixup for "address review" commits where the first message already describes the final intent.

Build a Git workflow your team can trust

Squash commits the right way is a team policy—not a personal preference. Pick PR squash merge for protected main, interactive rebase for solo cleanup, and document when granular history beats brevity. Your reviewers, CI logs, and future self during a 2 a.m. incident will thank you. If you want help standardising Git workflow, CI, and zero-downtime deploys on a production app, see the services overview or Notary Nepal portfolio case for how clean merge history supports maintainable legal-tech portals. Ready to tighten your pipeline? Contact us to talk through branch rules, hooks, and deploy automation—or browse more guides on the blog and the home page.

Frequently Asked Questions

Squashing merges multiple commits into one while keeping the diff unchanged. Doing it the right way means one commit equals one logical change, the message explains why—not only what—and you never rewrite history on branches others have already pulled. Pick interactive rebase on private branches, squash merge on shared pull requests, or soft reset plus recommit for small solo branches. The goal is reviewable main history without destroying audit trails teammates depend on.

Squash when commits are implementation noise—WIP titles, typos, review tweaks—not when each commit passes CI and maps cleanly to a ticket you might revert independently. Feature branches with messy iteration benefit before merge. Open-source PRs often squash to one commit per contribution. Do not squash shared long-lived branches others pull, release branches with tagged versions, or unrelated fixes and refactors you may need to revert separately on production Laravel apps.

On a branch only you use, ensure a clean working tree with git status. Find your base with git log --oneline main..HEAD, then run git rebase -i main. Leave the first commit as pick; mark later ones squash or s. Save, then write one combined message in the second editor—not four pasted subject lines. If already pushed, update with git push --force-with-lease origin your-branch, never plain --force, and coordinate on shared feature branches first.

Use platform squash merge when branch protection blocks force push. On GitHub, enable Allow squash merging, choose Squash and merge, and edit the default title—GitHub often concatenates noisy subjects. On GitLab, set Squash commits when merge request is accepted, optionally enforced for all merges into main. Individual commits stay visible in the PR timeline; main gets one commit. This suits multi-author PRs and post-approval merges where rewriting SHAs would require re-review.

No. Rebasing replays commits onto a new base; squashing combines multiple commits into one. You often squash during interactive rebase, but rebase alone does not merge commits unless you mark them squash in the todo list.

On the target branch, yes—individual commits become one. GitHub and GitLab PR pages still list originals for review. Cloning main after squash shows only the squashed commit.

Never squash on branches others have pulled if you plan to force push. Never rewrite main, production branches, or tagged releases—Deployer symlink swaps and GitLab CI logs reference SHAs. Never combine unrelated fixes and refactors that production might need to revert independently. Do not squash to hide bad merges or leaked secrets; use revert or reflog recovery instead. Open-source maintainers sometimes reject squashing when each PR commit is a logically separate patch.

squash merges commits and opens an editor to combine messages—you choose what survives in the final subject and body. fixup merges commits but discards secondary commit messages entirely. Use fixup for address-review or typo commits where the first pick already describes the final intent. Use squash when secondary messages contain ticket IDs, test notes, or context worth folding into one release-note-ready message.

Before your first push, run git reset --soft HEAD~N where N is the commit count, then git commit -m with one clear message. Soft reset keeps staged changes but drops individual commit messages unless you copied them first. This is fast on small branches but easy to mess up on large ones. Prefer git rebase -i when messages contain ticket IDs or review context you need to preserve in the combined message.

Force pushing main or release tags breaks clone scripts, open PR bases, and automated changelogs—recover via git reflog if needed. Squashing unrelated changes together blocks clean reverts; reorder with rebase -i and squash within groups only. Squash rewrites committer metadata, so add Co-authored-by trailers when pairing. Squashing breaks signed commit chains until you re-sign the new commit. Automating checks with pre-push hooks and CI lint on commit messages catches accidental force pushes to protected refs before damage spreads.

Interactive rebase rewrites local commits before or after push, giving full control over ordering, messages, and which commits combine—best on branches you own. Squash merge happens at PR merge time on GitHub, GitLab, or Bitbucket; the platform creates one commit on the target branch without force push on the feature branch. PR squash wins when protection blocks force push, multiple authors contributed, reviewers approved specific SHAs, or you will delete the source branch anyway. Both deliver clean main; only rebase rewrites the feature branch itself.

--force-with-lease refuses to push if someone else added commits you have not seen, preventing blind overwrites of a teammate's work. Plain --force overwrites the remote regardless. After interactive rebase on a pushed feature branch, always use --force-with-lease origin your-branch. On shared feature branches, coordinate in chat before any force push—I have seen CI pipelines and cron jobs break when one developer force-pushed while another was rebasing the same branch.

On Laravel 12 or 13 projects with Deployer 7 and GitLab CI, the pipeline still runs but caches keyed by commit SHA invalidate, so the first build may be slower. Deployer release names derived from short SHA change—note the new ID in deploy logs. Changelog generators reading Conventional Commits on main see one entry; ensure the squash message carries the ticket reference. Open Dependabot or Renovate PRs rebased onto old SHAs may need a fresh rebase onto new main. Keep infrastructure commits out of application feature squashes.

Do not squash to hide mistakes—a bad merge or leaked secret needs revert or reflog recovery, not burial under a squash. Security fixes deserve their own visible commit message on main so auditors, incident responders, and changelog readers can identify them quickly. Squashing WIP noise around a fix is fine; merging the fix with unrelated refactors or burying it in a generic feat commit is not. One logical security fix with a clear message beats a tidy history that obscures what changed and when.

GPG or SSH signing applies to the new squashed commit only after you re-sign—squashing produces a new SHA that needs a fresh signature. Teams enforcing verified commits via branch rules will block unsigned squashed pushes, so configure signing before squash or sign again post-rebase. Squash rewrites committer metadata; add Co-authored-by trailers when pairing so GitHub and GitLab attribute contributors in blame view. Squashing also pairs well with signed commits in supply-chain terms: one squashed commit means one signature to verify instead of many WIP signatures.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: