Learn how to use git commit --fixup with interactive rebase to automatically squash commits and clean up your commit history with real-world examples.
Introduction
We've all been there. You're working on a feature branch, making small incremental commits with messages like "wip", "fix typo", "update imports". Before you know it, you have 15 commits for what should logically be one clean PR. Squashing these commits manually is tedious and error-prone.
Enter git commit --fixup—a powerful tool that lets you amend commits in place and use interactive rebase to automatically squash them. In this guide, you'll learn how to use --fixup with rebase to efficiently clean up your commit history and produce professional, easy-to-review PRs.
What is git commit --fixup?
git commit --fixup is a flag that allows you to amend the most recent commit without creating a new commit. Unlike a traditional amend, --fixup doesn't create a replacement commit but instead modifies the current commit directly in the git history.
Key difference:
git commit --amend: Creates a new commit, invalidates the old onegit commit --fixup: Modifies the commit in-place, preserves history
When combined with git rebase -i, --fixup becomes incredibly powerful because you can:
- Fix typos in commit messages
- Add forgotten files to commits
- Squash multiple related commits into one
- Reorder commits logically
- All while maintaining a clean, linear history
The Traditional Problem
Consider this common scenario:
# You're working on a feature and make these commits:
git commit -m "wip: add authentication"
git commit -m "fix: typo in auth"
git commit -m "add tests for auth"
git commit -m "update auth logic"
git commit -m "fix off-by-one error"
git commit -m "wip: add authorization"
git commit -m "refactor auth module"
Seven commits for what should be one cohesive "Add authentication" feature. This is where --fixup with interactive rebase shines.
Another common example: You're debugging an issue and make rapid commits:
git commit -m "wip: trying solution A"
git commit -m "didn't work, revert"
git commit -m "wip: trying solution B"
git commit -m "close but need tweak"
git commit -m "fix: final adjustment"
Five commits that should be one "Fix authentication bug" commit. Without --fixup, you'd need to manually interactively rebase each time.
Using git commit --fixup
Basic Fixup Workflow
When you need to clean up commits:
- Identify the commit range you want to rebase
- Use interactive rebase to start the process
- Mark commits to squash with
fixup - Let rebase auto-squash them
Creating Fixup Commits
First, create fixup commits as you work:
# Original commit
git commit -m "feat: add user authentication"
# Oops, forgot to add the password reset
# Add the file and create a fixup commit
git add password-reset.ts
git commit --fixup=HEAD
This creates a new commit marked as a fixup for HEAD. When you run git rebase --autosquash, Git will automatically combine them.
Step-by-Step Example
Let's say you have this commit history:
$ git log --oneline
a1b2c3d (HEAD) refactor auth module
e4f5g6h add tests for auth
h7i8j9k fix typo in auth
1a2b3c4 add authorization
You want to squash the last three commits into one. Here's how:
# Start interactive rebase
git rebase -i HEAD~3
# The editor opens showing:
pick a1b2c3d refactor auth module
fixup e4f5g6h add tests for auth
fixup h7i8j9k fix typo in auth
Change fixup commits to squash (or leave them as fixup—they'll be auto-squashed):
pick a1b2c3d refactor auth module
squash e4f5g6h add tests for auth
squash h7i8j9k fix typo in auth
When you save and close, git automatically squashes the marked commits into the first commit.
The Auto-Squash Behavior
Here's the key insight: any commit marked with fixup (or squash) after the first pick will be automatically combined with the pick commit.
For example, with this configuration:
pick 1a2b3c4 add authorization
fixup h7i8j9k fix typo in auth
fixup e4f5g6h add tests for auth
pick a1b2c3d refactor auth module
Git produces:
- Commit 1: Contains changes from
1a2b3c4,h7i8j9k, ande4f5g6h(squashed) - Commit 2: Contains changes from
a1b2c3d
Example with fixup workflow:
# While working on a feature
git commit -m "feat: implement login"
# Later, you find a bug
vim login.ts
git add login.ts
git commit --fixup=HEAD
# Even later, you remember to add tests
git add login.test.ts
git commit --fixup=HEAD
# Now clean up with autosquash
git rebase -i --autosquash HEAD~3
# Git automatically arranges:
pick abc1234 feat: implement login
fixup def5678 Fix login bug
fixup ghi9012 Add login tests
# All combined into one clean commit!
Real-World Example: Adding Authentication
Let's walk through a practical scenario. You're adding user authentication to your app and have these commits:
$ git log --oneline
f47ac8 (HEAD) refactor: clean up auth flow
ba3d92 wip: add logout
8c7e1a wip: add token refresh
4a5b6c fix: handle edge case
9c8d7e wip: add login form
2a3b4c wip: setup auth context
You want the final history to look like:
- "setup auth context" with everything
- "refactor: clean up auth flow"
Step 1: Start Rebase
git rebase -i HEAD~5
Step 2: Mark Commits
Change the configuration to:
squash 2a3b4c wip: setup auth context
squash 9c8d7e wip: add login form
fixup 4a5b6c fix: handle edge case
fixup 8c7e1a wip: add token refresh
squash ba3d92 wip: add logout
pick f47ac8 refactor: clean up auth flow
Step 3: Save and Continue
Git will:
- Squash
2a3b4cwith9c8d7e,4a5b6c,8c7e1a, andba3d92 - Keep
f47ac8as a separate commit - Open your text editor to refine the combined commit message
Step 4: Refine the Message
The editor opens with the combined commit message:
# This is a combination of 5 commits.
# This is the 1st commit message:
wip: setup auth context
# This is the 2nd commit message:
wip: add login form
...
Edit it to:
feat: add authentication with login form, token refresh, and logout functionality
Real-World Example: Fixing a Bug Rapidly
# Initial bug fix
git commit -m "fix: handle null pointer in user service"
# Test and find another case
vim user.service.ts
git add user.service.ts
git commit --fixup=HEAD
# Add test for the fix
git add user.test.ts
git commit --fixup=HEAD
# Clean up
git log --oneline
# abc1234 fix: handle null pointer in user service
# def5678 fixup! fix: handle null pointer...
# ghi9012 fixup! fix: handle null pointer...
git rebase -i --autosquash HEAD~3
# Git automatically combines them into one commit!
Best Practices and Common Pitfalls
DO: Plan Your Rebase Strategy
Before starting, think about your desired outcome:
- Single feature? Squash all related commits into one
- Multiple features? Group commits logically, then squash within each group
- Bug fixes? Keep bug fixes separate from feature commits
# Plan: 3 logical commits
# 1. Setup (commits 1-5)
# 2. Bug fix (commit 6)
# 3. Refactor (commit 7)
git rebase -i HEAD~7
Example: Good planning prevents mess:
# Bad: Everything in one commit
git rebase -i HEAD~10 # Squash all into one monolithic commit
# Good: Logical groupings
git rebase -i HEAD~10 # Create 3-4 focused commits
# - Feature implementation (commits 1-6)
# - Bug fix (commit 7-8)
# - Refactoring (commit 9-10)
DO: Use Descriptive Commit Messages
Since you're combining commits, write comprehensive messages:
# Bad
git commit -m "fix"
# Good
git commit -m "fix: handle null pointer in auth service when user is not found"
This makes the --fixup workflow more efficient because you won't need to edit messages later.
Example: Good messages save time:
# While working
git commit -m "feat(auth): add login component with form validation"
# Oops, missed something
git commit --fixup=HEAD -m "forgot to include error handling"
# When squashing, the message is already clear!
DO: Use git rebase --autosquash
Git has a powerful --autosquash flag that automatically organizes fixup commits:
# Create fixup commits as you work
git commit -m "feat: add authentication"
git add forgotten-file.ts
git commit --fixup=HEAD # Marks it as a fixup
# Later, clean up everything automatically
git rebase -i --autosquash HEAD~5
Git will automatically arrange fixup commits right after their target commits, saving you from manually reordering them.
Example: Autosquash in action:
# Your history looks messy
git log --oneline
abc1234 feat: add authentication
def5678 fixup! feat: add authentication
ghi9012 wip: update docs
jkl3456 fixup! feat: add authentication
# Run autosquash
git rebase -i --autosquash HEAD~4
# Git automatically produces:
pick abc1234 feat: add authentication
fixup def5678 fixup! feat: add authentication
fixup jkl3456 fixup! feat: add authentication
pick ghi9012 wip: update docs
This is especially useful when you have multiple fixup commits scattered throughout your history.
DO: Test Between Rebases
After squashing, verify your code still works:
# After rebase, run tests
npm test
# If tests fail, you can still use git reflog to recover
git reflog
git reset --hard HEAD@{1}
Example: Catching bugs early:
# Rebase and squash
git rebase -i HEAD~5
# Immediately test
npm test
# ✅ All passing - safe to continue
# ❌ Failing - use reflog to recover
# If tests fail
git reflog
# abc1234 HEAD@{0}: rebase -i: Fast-forward
# def5678 HEAD@{1}: commit: Fix typo
git reset --hard HEAD@{1} # Back to safety
DON'T: Rebase Public History
Never rebase commits that have been pushed to a shared repository. This creates duplicate commits and confuses your team.
# Check if commits are pushed
git log origin/main..HEAD
# If output is empty, safe to rebase
# If commits show up, don't rebase—use merge instead
Example: Checking before rebasing:
# Before rebasing, check if pushed
git log origin/main..HEAD --oneline
# abc1234 New feature
# def5678 Fix typo
# These commits haven't been pushed - safe to rebase
# If you see:
git log origin/main..HEAD --oneline
# (empty)
# Already pushed - DO NOT rebase!
# Use merge instead or coordinate with team
Common Mistakes and How to Avoid Them
Mistake 1: Forgetting to Force Push
After rebasing, you need to force push:
git push --force-with-lease
Use --force-with-lease instead of --force to prevent overwriting others' work.
Example: Safe force push:
# Rebase complete
git rebase -i HEAD~5
# Push with lease (safer)
git push --force-with-lease origin feature-branch
# If someone else pushed, this will fail and warn you
# Instead of overwriting their work
Mistake 2: Conflicts During Rebase
When squashing commits with merge conflicts:
git rebase -i HEAD~5
# Conflict arises in commit 3
Auto-merging failed for "src/auth.ts"
Fix conflicts and mark as resolved:
git add src/auth.ts
git rebase --continue
Example: Handling conflicts gracefully:
# Conflict during rebase
git rebase -i HEAD~5
# CONFLICT: content merge conflict in auth.ts
# Edit file, resolve conflict
vim auth.ts
# <<<<<<< HEAD
# Your changes
# =======
# Their changes
# >>>>>>> fixup-combined
# Save and stage
git add auth.ts
git rebase --continue
# If too complex, abort and try different strategy
git rebase --abort
Mistake 3: Losing Work in Fixup Commits
Remember: fixup modifies commits in-place. If you need the original:
# Before rebasing, create a backup branch
git branch backup-branch
# If something goes wrong, recover
git reset --hard backup-branch
Example: Safety first:
# Before major rebase
git branch backup-before-cleanup
# Attempt the rebase
git rebase -i HEAD~10
# Oops, something went wrong
git reset --hard backup-before-cleanup
# Safe to try again
Mistake 4: Squashing Too Much
Resist the urge to squash everything into one giant commit. Good granularity:
# Good: Logical grouping
- "setup authentication" (commits 1-5)
- "fix authentication bug" (commit 6)
- "refactor authentication module" (commit 7)
# Bad: Monolithic commit
- "everything related to auth" (all commits combined)
Example: Finding the right balance:
# Too granular (10 commits for one feature)
# Hard to review, noisy history
# Too coarse (1 giant commit)
# Impossible to code review, loses context
# Just right (3-4 commits)
# feat: add authentication system
# fix: handle edge cases in auth
# refactor: improve auth code structure
# Each commit is focused and reviewable
Troubleshooting Common Issues
Issue: Git Doesn't Recognize --fixup
Ensure you're using Git 2.24 or later:
git --version
# Should be 2.24.0 or higher
Issue: Rebase Shows Confusing Merge Conflicts
Sometimes rebase produces unexpected conflicts:
# Strategy: Reset and start fresh
git rebase --abort
# Check what went wrong
git status
# Try again with clearer planning
git rebase -i HEAD~5
Issue: Squashed Commit Has Wrong Message
You can edit the message during the squash, but if you need to change it later:
# Amend the squashed commit
git commit --amend
# If it's not the most recent commit
git rebase -i HEAD~2 # interactively change the second commit
Comparison: Manual vs. Fixup Workflow
| Aspect | Fixup Approach | Manual Interactive Rebase | |