2 AM. Code review done, about to open a PR when your reviewer pings back: “Your commit history looks like garbage. You fixed stuff and tacked on 5 more commits — ‘fix typo’, ‘fix again’, ‘ok done now’… clean it up.”
Sound familiar? I’ve been in that situation more times than I’d like to admit. The usual approach is to open git rebase -i HEAD~10 and manually squash each line — easy to mix up the order, especially at 2 AM when your brain isn’t firing on all cylinders.
Fortunately, Git has a pair of underrated features built right in: --fixup and --autosquash. Combined, they automatically handle the squashing you’d otherwise do by hand — right commit, right order, no need to eyeball hashes.
The Problem: A Commit History That Looks Like Spaghetti
Say you’re working on the “Add user authentication” feature. After a few hours of coding and a handful of small fixes, git log looks like this:
$ git log --oneline
a3f9c12 ok done
b7e2d45 fix typo in login form
c1a8b33 forgot to add validation
d4f7e22 Add user authentication
e9b1c55 Update README
The top three commits are actually minor fixes for d4f7e22. Logically they belong together, but history is scattered. Reviewers see a mess, git bisect becomes harder down the line, and git blame points to the “ok done” commit instead of anything meaningful.
The manual fix: git rebase -i HEAD~4, changing pick to squash line by line. Time-consuming, error-prone, and when you’re tired, you’re more likely to get it wrong than right.
Core Concepts
What is git commit –fixup?
git commit --fixup <commit-hash> creates a new commit with a message automatically formatted as fixup! <original message>. Think of it as a sticky label that tells Git this commit needs to be folded into the original when you rebase.
For example, to mark a fix as belonging to commit d4f7e22 (message: “Add user authentication”):
$ git add auth/login.py
$ git commit --fixup d4f7e22
# Git automatically creates a commit with message: "fixup! Add user authentication"
You don’t have to think of a message, and you don’t need to remember the original commit name later — the fixup! prefix already carries all the information Git needs.
What does git rebase –autosquash do?
git rebase -i --autosquash automatically reorders commits with the fixup! prefix to appear right after their target commit in the rebase list, and changes their action to fixup. When you open the editor, everything is already in the right place — just save and quit.
Hands-On Walkthrough
Step 1: Normal Commits
I’m working on a payment feature, starting with two commits:
$ git add payment/checkout.py
$ git commit -m "Add checkout flow"
# Hash: abc1234
$ git add payment/cart.py
$ git commit -m "Add cart calculation"
# Hash: def5678
Step 2: Spotted a Bug — Use –fixup Instead of a Throwaway Commit
During testing, I notice checkout.py is missing validation. After fixing it:
# Instead of: git commit -m "fix validation in checkout" ← adds more noise
# Use:
$ git add payment/checkout.py
$ git commit --fixup abc1234
# git log --oneline:
# f9e3a12 fixup! Add checkout flow ← name set automatically
# def5678 Add cart calculation
# abc1234 Add checkout flow
Then I find another bug in cart — same approach:
$ git add payment/cart.py
$ git commit --fixup def5678
# git log --oneline:
# 7b2c891 fixup! Add cart calculation
# f9e3a12 fixup! Add checkout flow
# def5678 Add cart calculation
# abc1234 Add checkout flow
Step 3: Clean Up Before Pushing
$ git rebase -i --autosquash HEAD~4
Git automatically opens the editor with the list already reordered — no changes needed:
pick abc1234 Add checkout flow
fixup f9e3a12 fixup! Add checkout flow
pick def5678 Add cart calculation
fixup 7b2c891 fixup! Add cart calculation
Save and quit the editor. Git squashes automatically. Result:
$ git log --oneline
a8d3f21 Add cart calculation
c4b7e92 Add checkout flow
Clean history — 2 tidy commits instead of 4 messy ones. Reviewer is happy, and I can actually sleep.
Set It Once, Use It Forever
Instead of typing --autosquash every time, enable it in your global config:
$ git config --global rebase.autoSquash true
After that, just run git rebase -i HEAD~N and autosquash kicks in automatically.
–squash vs –fixup: Which One Should You Use?
--fixup squashes the commit and discards the child commit’s message, keeping only the original. --squash squashes but preserves both messages so you can edit them in the interactive rebase editor.
# Use --squash when you want to merge additional message content into the original commit
$ git commit --squash abc1234
# Creates: "squash! Add checkout flow" — Git opens the editor to let you edit the message during rebase
For 95% of “small fix after the fact” cases, --fixup is all you need. --squash is only necessary when you want to add more context to the original commit message.
Fixup Commits Deep in History
The beauty of --autosquash is that it finds the right target commit no matter where it sits in history. Made 10 commits and want to fixup the 3rd one? Just do it normally, then run rebase -i HEAD~11. Git automatically places the fixup! right after its target commit — no counting required.
Real-World Team Usage
On an 8-person team I worked with, I proposed this flow for the review process: all members use --fixup for any post-review fixes, then autosquash before merging into main. Merge conflicts dropped noticeably because history was cleaner, and reviewers could actually track changes without wading through 7 “fix” commits just to understand what a feature does.
Our team rules:
- During development: commit freely, use
--fixupto mark fixes - Before pushing to remote: run
rebase --autosquashto clean up locally - Never rebase commits already on a shared branch (it creates divergence for others)
Check Before Rebasing
If you’re not sure which commits have already been pushed:
# See which commits haven't been pushed to remote yet
$ git log origin/feature-branch..HEAD --oneline
# Only rebase these commits — don't touch anything already pushed
Conclusion
git commit --fixup + git rebase -i --autosquash is the most time-saving combo I know for keeping commit history clean. Instead of manually squashing in interactive rebase when you’re tired and error-prone, just mark fixes at commit time, then run one rebase command before pushing — and you’re done.
Add rebase.autoSquash true to your global config once. From then on, your PRs won’t be littered with “fix typo”, “ok done now”, “actually fixed now” commits — and your reviewer won’t be pinging you at 2 AM.

