After a week of work on a feature branch, your commit history looks something like this:
fix typo
WIP
fix test
WIP again
fix linter
actually done now
These commits are useful while you’re working — but they’re noise in the main branch’s history. A squash merge collapses all of them into a single, descriptive commit before they land on main.
What squash merging actually does
A squash merge takes all the changes from a feature branch and stages them as a single commit on the target branch. The original branch’s commit history is discarded — only the diff survives.
The result: main stays readable, and every entry in git log represents a complete, shippable unit of work.
main
├── feat: add user authentication (#42)
├── feat: payment flow redesign (#38)
└── chore: update dependencies (#35)
Instead of:
main
├── fix test
├── WIP
├── fix typo
├── WIP
├── initial auth implementation
└── ...
The manual approach
Git supports squash merging natively. The standard workflow:
# Ensure main is up to date
git checkout main
git pull origin main
# Merge with squash — stages all changes, does not commit
git merge --squash feature/my-branch
# Write a single, descriptive commit message
git commit -m "feat: implement user authentication"
# Clean up the feature branch
git branch -d feature/my-branch
git push origin --delete feature/my-branch
The --squash flag is the key: it stages everything without creating a merge commit, giving you full control over the final commit message.
Automating it with a shell script
Doing this repeatedly across dozens of feature branches becomes tedious. A reusable script handles the mechanics:
#!/bin/bash
# git_squash.sh — merge a feature branch into a target branch with a squash commit
set -e
FEATURE_BRANCH=$1
TARGET_BRANCH=$2
if [ -z "$FEATURE_BRANCH" ] || [ -z "$TARGET_BRANCH" ]; then
echo "Usage: git-squash <feature-branch> <target-branch>"
exit 1
fi
echo "→ Squash merging $FEATURE_BRANCH into $TARGET_BRANCH"
# Backup the feature branch
git branch "${FEATURE_BRANCH}_backup"
# Switch to target and update
git checkout "$TARGET_BRANCH"
git pull origin "$TARGET_BRANCH"
# Squash merge
git merge --squash "$FEATURE_BRANCH"
# Commit (opens editor for the message)
git commit
# Push to remote
git push origin "$TARGET_BRANCH"
# Clean up
git branch -d "$FEATURE_BRANCH"
git branch -d "${FEATURE_BRANCH}_backup"
echo "✓ Done. $FEATURE_BRANCH merged into $TARGET_BRANCH."
Setting up the alias
Save the script somewhere permanent and make it executable:
chmod +x ~/scripts/git_squash.sh
Add an alias to your shell profile (.zshrc, .bashrc, or .profile):
alias git-squash="~/scripts/git_squash.sh"
Reload the profile:
source ~/.zshrc
Now you can squash-merge any branch with:
git-squash feature-123 main
When to use squash merges
Squash merging is the right default when:
- The feature branch has many small or fixup commits that don’t add historical value
- The team has agreed on a linear history strategy
- You’re enforcing conventional commits on
mainand want full control over the message
It’s the wrong choice when:
- You want to preserve the individual commit history for debugging (
git bisectbenefits from fine-grained commits) - Multiple people collaborated on the branch and attribution matters
- The commits are already clean and descriptive
The alternative: interactive rebase
If you want to selectively squash some commits but keep others, git rebase -i is more surgical:
git rebase -i main
This opens an editor where you mark commits as pick, squash, or fixup. More control, slightly more friction.
Key takeaways
git merge --squashcollapses all branch commits into a single staged diff — you write the final message- A clean
mainhistory makesgit log, code review, and incident debugging significantly easier - The shell script adds a
--backupbranch as a safety net before destructive operations - Squash merging is a team convention — agree on it upfront, and enforce it via branch protection rules in GitHub/GitLab