Interactive Rebase: Clean Up Your Commits Before You Share Them
Turn a messy trail of WIP commits into a clean, reviewable history with git rebase -i. Squash, reword, reorder, split and drop commits, and let fixup commits do the cleanup for you.
On this page
Real work is messy. You commit a half-finished form, then "WIP", then "fix typo", then a debug print you forgot to remove. That's fine while you're working, but it's a poor history to hand to a reviewer, and an even worse one to find six months later in git log or git blame.
Interactive rebase lets you rewrite your own branch before you share it: combine commits, rewrite their messages, change their order, split them up or delete them. It's one of the most useful Git skills to learn once you're comfortable with branches. If you aren't yet, read A Practical Git Branching Workflow for Beginners first.
The messy branch#
Here's a feature branch with five commits on top of main:
$ git log --oneline main..
746aec7 debug print
451c410 fix typo
63d1135 Add password validation
0ce6ff7 WIP
4430f61 Add login formmain.. means "commits on this branch that aren't on main". What we'd like to share is just two commits: Add login form (including the WIP and the typo fix, which both touched the form) and Add password validation, without the debug print.
Starting an interactive rebase#
git rebase -i mainGit opens your editor with a todo list, one line per commit:
pick 4430f61 Add login form
pick 0ce6ff7 WIP
pick 63d1135 Add password validation
pick 451c410 fix typo
pick 746aec7 debug print
# Rebase 1c893f9..746aec7 onto 1c893f9 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, ...
# d, drop <commit> = remove commit
# ...The todo list is upside down compared to git log
git log shows the newest commit first. The todo list shows the oldest first, because that's the order Git will replay them in. Read it from top to bottom like a script.
Git will start from main and run each line in turn. Change the word at the start of a line to change what happens to that commit:
| Command | What it does |
|---|---|
pick |
Keep the commit as it is |
reword |
Keep the changes, but stop to edit the message |
edit |
Apply the commit, then pause so you can change it (amend, split…) |
squash |
Merge into the commit above, and combine both messages |
fixup |
Merge into the commit above, and throw this commit's message away |
drop |
Delete the commit (removing the line does the same) |
exec |
Run a shell command at this point, and stop if it fails |
Squash, reorder and drop#
Edit the todo list like this, then save and close the editor:
pick 4430f61 Add login form
fixup 0ce6ff7 WIP
fixup 451c410 fix typo
pick 63d1135 Add password validation
drop 746aec7 debug printWe changed three things: the two small commits become fixups of Add login form, fix typo moved up so that it sits right under the commit it fixes, and the debug print is dropped.
$ git rebase -i main
Successfully rebased and updated refs/heads/login-form.
$ git log --oneline main..
486c6b2 Add password validation
24c6dfe Add login formNotice the hashes changed, even for Add password validation, whose content we didn't touch. Rebase never edits commits: it creates new ones with the same changes. That detail matters when we push, below.
squash or fixup?
Use squash when the second message has something worth keeping: Git opens the editor with both messages so you can merge them. Use fixup for "oops" commits whose message you'd delete anyway.
Fixup commits: clean up as you go#
Reordering lines by hand gets tedious. If you already know which commit a change belongs to, tell Git when you commit:
# Spotted a bug in "Add password validation"? Fix it, then:
git commit -a --fixup=486c6b2Git creates a commit called fixup! Add password validation. Later, --autosquash moves every such commit under its target and marks it fixup for you:
$ git rebase -i --autosquash mainpick 24c6dfe Add login form
pick 486c6b2 Add password validation
fixup 4eb2adb fixup! Add password validationJust save and close. To make this the default, run git config --global rebase.autoSquash true.
Splitting a commit with edit#
Sometimes one commit does two unrelated things. Mark it edit, and Git pauses right after applying it:
edit 9a1c2d3 Add login form and fix footer links
pick 3f7e8b1 Add password validationgit reset HEAD^ # undo the commit, keep its changes as unstaged edits
git add login.html
git commit -m "Add login form"
git add footer.html
git commit -m "Fix footer links"
git rebase --continue # carry on with the rest of the todo listgit add -p is handy here when both changes are in the same file: it asks about each block of changes one at a time.
When there's a conflict#
Reordering or dropping commits can produce conflicts, exactly like a merge. Git stops and tells you which files conflict. You have three options:
# fix the files, then mark them as resolved and continue
git add login.html
git rebase --continue
git rebase --skip # drop the commit that conflicts and move on
git rebase --abort # give up: put the branch back exactly as it was--abort is your safety net. As long as the rebase hasn't finished, nothing has changed for real.
Run your tests on every commit
git rebase -i main --exec "npm test" (or -x) adds an exec line after every commit. If the tests fail anywhere, the rebase stops on the commit that broke them. It's a great final check that each commit works on its own, which is what makes git bisect useful later.
Rebase without moving your branch#
git rebase -i main also moves your branch on top of the latest main. Usually that's what you want, but if main has moved a lot, you'll get conflicts that have nothing to do with your cleanup. To tidy commits in place, keeping the same starting point:
git rebase -i --keep-base mainYou can also name the starting point directly: git rebase -i HEAD~3 lets you edit the last three commits.
Pushing a rewritten branch#
Because the commits are new, a branch you pushed before the rebase no longer matches the remote, and a normal git push is rejected. Use:
git push --force-with-leaseUnlike --force, --force-with-lease refuses to overwrite the remote branch if someone else pushed to it since you last fetched, so you can't wipe out a teammate's work by accident.
The golden rule of rebasing
Only rewrite commits that nobody else has built on. Cleaning up your own feature branch before (or during) a pull request is fine. Rebasing main, or a branch your teammates have pulled, leaves them with copies of the old commits and a painful merge.
Undoing a rebase#
Finished a rebase and regret it? Git keeps the old position of your branch in ORIG_HEAD:
git reset --hard ORIG_HEADThis only works right after the rebase, because other commands also update ORIG_HEAD. Further back, the reflog remembers every place your branch has been (git reflog). How Git Really Works explains why the old commits are still there.
Recap#
git rebase -i mainopens a todo list of your branch's commits, oldest first.- Change
picktoreword,edit,squash,fixupordrop, and move lines to reorder commits. git commit --fixup=<commit>plus--autosquashmakes cleanup almost automatic.--abortstops a rebase in progress;ORIG_HEADand the reflog undo a finished one.- Rebasing creates new commits, so push with
--force-with-lease, and never rebase commits other people have built on.