← Back to DevBytes

Git Squash Commits

What is Git Squash?

Squashing in Git is the process of condensing multiple commits into a single, cohesive commit. Instead of pushing a messy trail of work-in-progress commits like fix typo, wip, or maybe this works?, you combine them into one clean commit that tells a clear story about the change. Think of it as editing your commit history — taking the rough draft of your development journey and polishing it into a publish-ready narrative.

Technically, squashing rewrites history. It doesn't simply hide intermediate commits; it creates a brand new commit that encompasses all the changes from the selected range of commits, with a single commit message that you provide. The original commits are discarded (or more precisely, they become unreachable and eventually garbage-collected by Git).

Why Squashing Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Squashing isn't just about aesthetics — it has real practical benefits for both individual developers and teams:

Cleaner Project History

A Git log full of half-baked commits makes it harder to understand the evolution of a codebase. When every feature lands as a single, well-described commit, git log --oneline becomes a readable changelog rather than a stream of consciousness.

Simpler Code Reviews

Reviewing a pull request with 20 tiny commits is exhausting. Each commit might fix something from three commits ago, forcing reviewers to mentally reconstruct the final state. A squashed branch presents the net change cleanly, making review more efficient and accurate.

Easier Bisecting and Debugging

When you use git bisect to find a regression, you want each commit to represent a working, logical state. Intermediate commits that break the build or represent incomplete features make bisecting useless. Squashing ensures each commit on main is a stable checkpoint.

Reduced Noise in Blame and Logs

git blame shows the last commit that touched a line. If your history is full of formatting fixes and renames, blame becomes cluttered. Squashed commits keep the attribution meaningful.

How to Squash Commits

There are two primary ways to squash commits in Git, each suited to different scenarios. Let's explore both in detail with practical examples.

Method 1: Interactive Rebase (For Feature Branches)

Interactive rebase is the most common and flexible method. It lets you reorder, squash, edit, or drop commits in your local branch before sharing it. Here's the typical workflow.

Imagine you've been working on a feature branch and your commit history looks like this:

# Current branch: feature/new-login
# Commit history:
a1b2c3d Add login form validation (latest)
d4e5f6g Fix validation regex bug
g7h8i9j Temporary debug logging
j0k1l2m Add login form component
m3n4o5p Initial login page setup (oldest)

You want to squash the last four commits into one meaningful commit. Here's how:

Step 1: Start the Interactive Rebase

Run the following command, where HEAD~4 refers to the last 4 commits:

git rebase -i HEAD~4

Alternatively, if you want to squash all commits since branching off main:

git rebase -i main

Step 2: The Rebase Editor Opens

Git opens your default text editor with a list of the commits, each prefixed with the word pick:

pick m3n4o5p Initial login page setup
pick j0k1l2m Add login form component
pick g7h8i9j Temporary debug logging
pick d4e5f6g Fix validation regex bug
pick a1b2c3d Add login form validation

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like squash, but discard commit message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit

Step 3: Mark Commits for Squashing

Change the word pick to squash (or just s) for all commits except the oldest one you want to keep as the base. The oldest commit stays as pick:

pick m3n4o5p Initial login page setup
squash j0k1l2m Add login form component
squash g7h8i9j Temporary debug logging
squash d4e5f6g Fix validation regex bug
squash a1b2c3d Add login form validation

Save and close the editor. Git now combines all those commits into one.

Step 4: Write the Final Commit Message

After saving, Git opens another editor showing all the commit messages from the squashed commits. You can keep them all, delete some, or write an entirely new message. A good practice is to delete everything and write a single, descriptive message:

# This is a combination of 5 commits.
# This is the 1st commit message:

Initial login page setup

# This is the commit message #2:

Add login form component

# This is the commit message #3:

Temporary debug logging

# This is the commit message #4:

Fix validation regex bug

# This is the commit message #5:

Add login form validation

Replace all of that with:

Implement login form with client-side validation

- Create login page layout with responsive design
- Add form component with email and password fields
- Implement real-time validation for email format and password length
- Display inline error messages for invalid inputs

Save and close. Your branch now has a single, polished commit.

Using Fixup Instead of Squash

If you want to discard the commit message of a squashed commit entirely (no multi-message editor), use fixup (or f) instead of squash. This is great for trivial commits like "typo fix" or "lint fix":

pick m3n4o5p Initial login page setup
fixup j0k1l2m typo
fixup g7h8i9j lint fix

With fixup, Git automatically uses only the pick commit's message — no second editor appears.

Quick Squash with Autosquash

You can also prepare fixup commits ahead of time using git commit --fixup:

# Make a fixup commit targeting a specific earlier commit
git commit --fixup=a1b2c3d -m "fixup! Add login form validation"

Then run an interactive rebase with autosquash enabled:

git rebase -i --autosquash HEAD~5

Git automatically reorders the fixup commits next to their targets and marks them as fixup, saving you the manual editing step.

Method 2: Merge with --squash (For Pull Requests)

The second method is git merge --squash, which is often used when integrating a feature branch into main. Instead of rewriting the feature branch history, it squashes at merge time — the feature branch retains its original commits, but main gets a single squashed commit.

# On the main branch, merge the feature branch as a single commit
git checkout main
git merge --squash feature/new-login

This stages all the changes from feature/new-login as a single set of modifications but does not create a commit yet. You then manually commit:

git commit -m "Implement login form with client-side validation"

The key difference from interactive rebase: the feature branch history remains intact. The squashing only affects how the changes appear on main. This is particularly useful in pull request workflows where you want to keep the granular history on the branch for reference but present a clean commit on main.

Here's a complete example combining both approaches in a typical GitHub workflow:

# 1. Create feature branch
git checkout -b feature/add-dark-mode
# ... make several messy commits ...
git add src/theme.css && git commit -m "wip dark mode"
git add src/theme.css && git commit -m "fix colors"
git add src/components && git commit -m "update components for dark mode"
git add src/theme.css && git commit -m "oops forgot variables"

# 2. Before creating PR, clean up with interactive rebase
git rebase -i main
# In editor: squash the 4 commits into 1 with a clean message

# 3. Push the cleaned branch
git push origin feature/add-dark-mode

# 4. After PR approval, merge with squash on GitHub UI
# OR manually:
git checkout main
git merge --squash feature/add-dark-mode
git commit -m "Add dark mode support across all components"
git push origin main

Handling Conflicts During Squash Rebase

When squashing commits, Git applies changes sequentially. If intermediate commits conflict with later ones in the sequence, you'll need to resolve conflicts during the rebase. This is actually a good thing — it means your squashed history had internal inconsistencies that needed fixing anyway.

Here's how to handle it:

# During interactive rebase, Git pauses on conflict:
# Applying: Add login form component
# ...
# CONFLICT (content): Merge conflict in src/login.js

# 1. Resolve the conflict in your editor
# 2. Stage the resolved file
git add src/login.js

# 3. Continue the rebase
git rebase --continue

# If things go wrong, abort entirely
git rebase --abort

After resolving all conflicts, Git continues combining the commits and eventually presents the commit message editor as normal.

Best Practices for Squashing Commits

1. Squash Before Sharing, Not After

Never squash commits that have been pushed to a shared branch that others might have pulled. Squashing rewrites history, and if someone else has based work on those commits, you'll cause painful merge conflicts and confusion. Squash only on your local branch or on a branch you're certain no one else uses.

# Safe: local branch, not yet pushed
git rebase -i HEAD~5

# Dangerous: branch already pushed and shared
# Avoid unless you coordinate with all collaborators
git rebase -i main && git push --force origin feature/shared-branch

2. Keep the Squashed Commit Atomic

Don't squash unrelated changes together just to reduce commit count. A commit should represent a single logical change. If your branch touches three completely separate areas of the codebase, consider splitting it into multiple well-described commits rather than one monolithic blob.

# Good: all changes are part of one feature
squash commits for "add dark mode"

# Better if changes are unrelated:
# Keep separate commits for:
#   - "Update ESLint config"
#   - "Fix broken unit tests"
#   - "Add dark mode feature"

3. Write Meaningful Commit Messages

The squashed commit message should explain what the change does and why. Use the conventional commits format or a structured template. Don't just concatenate old messages — write a fresh, intentional summary.

# Poor squashed commit message:
"Initial login page setup + Add login form component + Temporary debug logging + Fix validation regex bug + Add login form validation"

# Good squashed commit message:
"feat(auth): implement login form with client-side validation

Validate email format and password length before submission.
Display inline errors for invalid inputs. Supports screen readers
via aria-live regions."

4. Use Fixup for Trivial Corrections

Reserve squash for commits that have meaningful messages you might want to reference when crafting the final message. For commits that are purely noise ("fix typo", "lint", "formatting"), use fixup to automatically discard their messages without an editor prompt.

pick abc1234 Implement caching layer
fixup def5678 fix typo in cache key
fixup ghi9012 run prettier
squash jkl3456 Add cache invalidation tests

5. Don't Over-Squash

Not every branch needs to be squashed to a single commit. If your feature naturally breaks into 3-4 logical steps, keep those separate commits. The goal is clarity, not a commit count of one. A good rule of thumb: if you'd be comfortable explaining each commit to a colleague six months from now, keep them separate.

6. Leverage GitHub/GitLab Squash Merge

Many hosting platforms offer "Squash and Merge" as a merge option in pull requests. This automates the merge --squash workflow and lets you edit the final commit message in the UI. It's a great compromise — developers keep their granular branch history, and main stays clean.

7. Verify After Squashing

After a complex squash rebase, always verify your branch before pushing:

# Check the new commit history
git log --oneline --graph

# Verify the diff against main is exactly what you expect
git diff main..HEAD

# Run tests to make sure the squashed commit is functional
npm test

Conclusion

Git squash is a powerful history-editing tool that transforms messy development logs into a clean, professional commit narrative. Whether you use interactive rebase to polish your feature branch before sharing or git merge --squash to integrate changes cleanly into main, squashing helps your team maintain a project history that's actually useful for understanding, debugging, and collaborating. The key is balance — squash noise, not substance. Keep commits atomic, write intentional messages, and never rewrite shared history without coordination. When used thoughtfully, squashing elevates your Git workflow from a chronological dump of keystrokes to a deliberate record of meaningful progress.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles