Interactive Rebase in Git: Reshape Your Commit History
Interactive rebase is an ordinary rebase where you get to edit the instructions first. Git shows you the list of commits it is about to replay; you change the list; Git executes what you wrote.
That is the entire concept. Everything people do with it — squashing, rewording, reordering, splitting, dropping — is a matter of changing lines in a text file.
Starting one
Section titled “Starting one”git rebase -i HEAD~4HEAD~4 names the commit before the range you want to edit — the four commits after it are the ones
listed. To include the very first commit in the repository, use --root; HEAD~N where N is the total
number of commits refers to the root’s parent, which does not exist:
fatal: invalid upstream 'HEAD~4'You can also specify a branch, which is usually clearer:
git rebase -i mainThat lists every commit on your branch that is not on main — almost always exactly the set you want.
The todo list
Section titled “The todo list”Git opens your editor with something like this:
pick de4df70 Add parserpick 1aff95b fix typopick c413def Add validationpick f195a48 WIP debug output
# Rebase 9fc05fd..f195a48 onto 9fc05fd (4 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, in which case# keep only this commit's message; -c is same as -C but# opens the editor# x, exec <command> = run command (the rest of the line) using shell# b, break = stop here (continue rebase later with 'git rebase --continue')# d, drop <commit> = remove commit# ...# These lines can be re-ordered; they are executed from top to bottom.Two things to notice immediately:
Oldest first. This is the opposite of git log. The commit at the top is replayed first.
The lines are instructions. Change the verb at the start of a line to change what happens to that commit. Reorder the lines to reorder the commits. Delete a line to drop that commit entirely.
The commands
Section titled “The commands”| Command | Short | Effect |
|---|---|---|
pick | p | Use the commit unchanged |
reword | r | Use it, but open an editor for the message |
edit | e | Stop after applying it, so you can amend the content |
squash | s | Combine into the previous commit; both messages offered for editing |
fixup | f | Combine into the previous commit; discard this one’s message |
drop | d | Remove the commit |
exec | x | Run a shell command at this point |
break | b | Stop here; resume with --continue |
label / reset / merge | l / t / m | Reconstruct branch topology, used by --rebase-merges |
update-ref | u | Move another branch ref to this point, used by --update-refs |
Deleting a line does the same thing as drop. Being explicit is safer: an accidentally deleted line is
indistinguishable from a deliberate one, and Git will happily discard a commit you meant to keep.
A worked cleanup
Section titled “A worked cleanup”A branch with four commits, two of which should not survive review:
git log --onelinef195a48 WIP debug outputc413def Add validation1aff95b fix typode4df70 Add parser9fc05fd Initial commitThe intent: fold the typo fix into “Add parser”, drop the debug commit, and give the validation commit a better message.
git rebase -i HEAD~4Edit the list to:
pick de4df70 Add parserfixup 1aff95b fix typoreword c413def Add validationdrop f195a48 WIP debug outputSave and close. Git replays, stops to let you reword, and finishes:
Successfully rebased and updated refs/heads/main.git log --oneline00039a8 Add input validationb02ce4b Add parser9fc05fd Initial commitTwo commits instead of four, both meaningful. The typo fix is folded into the parser commit — verify it is genuinely there rather than lost:
git show HEAD~1:parser.pyptypo fixUsing edit to change a commit’s content
Section titled “Using edit to change a commit’s content”reword changes a message. edit stops the rebase so you can change the commit itself.
edit b02ce4b Add parserpick 00039a8 Add input validationGit applies the commit and then stops:
Stopped at b02ce4b... Add parserYou can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continuegit status confirms where you are:
interactive rebase in progress; onto 9fc05fdLast command done (1 command done): edit b02ce4b Add parserNext command to do (1 remaining command):Now make changes and fold them in:
echo "more" >> parser.pygit add parser.pygit commit --amend --no-editgit rebase --continueSuccessfully rebased and updated refs/heads/main.Splitting a commit
Section titled “Splitting a commit”A commit that does two unrelated things can be divided. Mark it edit, then undo the commit while keeping
its changes:
-
Mark it for editing in the todo list.
-
When Git stops, reset the commit but keep its changes unstaged:
Terminal window git reset HEAD^A plain
reset— no--hard— moves the branch back one commit and leaves the working tree alone. The changes are now uncommitted. -
Stage and commit the pieces separately:
Terminal window git add parser.pygit commit -m "Add parser"git add validate.pygit commit -m "Add validation"git add -pis useful here if the two changes are in the same file. -
Continue:
Terminal window git rebase --continue
Reordering
Section titled “Reordering”Reordering is just moving lines. To make the validation commit come first:
pick c413def Add validationpick de4df70 Add parserGit replays them in the new order. Whether that works depends on whether the commits are independent — if the validation commit modifies code the parser commit introduces, replaying it first will conflict or fail. Reordering Commits covers dependency handling.
exec: running commands between commits
Section titled “exec: running commands between commits”exec runs a shell command after the commit above it. The rebase stops if the command exits non-zero.
pick de4df70 Add parserexec npm testpick c413def Add validationexec npm testThis is the practical way to verify that every commit on a branch builds — which matters if your team
relies on git bisect, or integrates with rebase-and-merge so individual commits land on main.
There is a shortcut that adds an exec after every commit automatically:
git rebase -i --exec "npm test" mainAutosquash: marking fixups as you go
Section titled “Autosquash: marking fixups as you go”Deciding which commit a fix belongs to is easier at the time than later. --fixup records the intent in
the commit itself.
git commit --fixup=1d51084This creates a commit whose message is fixup! Add parser — a marker naming its target.
Later, --autosquash reorders the todo list automatically:
git rebase -i --autosquash HEAD~4pick 1d51084 Add parserfixup 3b54d21 fixup! Add parserpick febcef5 Add validationpick f347f23 Add testsGit moved the fixup commit next to its target and changed pick to fixup. Save without editing and the
history collapses correctly.
Enable it permanently:
git config --global rebase.autoSquash truegit commit --squash=<commit> is the equivalent for squash rather than fixup, keeping both messages.
Reaching a commit deep in history
Section titled “Reaching a commit deep in history”git commit --amend only reaches the most recent commit. Interactive rebase reaches any commit, as long as
you include it in the range.
To fix the third commit back:
git rebase -i HEAD~3Mark it edit, make the change, git commit --amend, then --continue. Every commit after it is replayed
on top, so they all get new IDs too — a change to one commit necessarily rewrites all its descendants.
Two shortcuts avoid counting commits:
git rebase -i <commit>^ # edit <commit> and everything after itgit rebase -i main # edit every commit not on mainThe first is the useful one when you know which commit is wrong: append ^ to name its parent, which is
where the range must start.
The todo list is a file you can edit mid-rebase
Section titled “The todo list is a file you can edit mid-rebase”While a rebase is paused, the remaining plan lives in the repository and can be changed:
git rebase --edit-todoThis reopens the editor on the remaining commands. Useful when a conflict reveals that your original
plan was wrong — you can drop a commit you now know is redundant, or convert a pick into an edit so
you can fix something at that point.
After editing, continue as normal:
git rebase --continueYou can also see where you are without changing anything. During a rebase Git provides two extra refs:
| Ref | Names |
|---|---|
REBASE_HEAD | The commit currently being applied |
ORIG_HEAD | Where your branch pointed before the rebase started |
git show REBASE_HEAD # what am I resolving?git log --oneline ORIG_HEAD # what did the branch look like before?git show REBASE_HEAD is the command to run before considering --skip — it tells you precisely which
commit’s work you would be discarding.
Rebasing onto main versus reshaping in place
Section titled “Rebasing onto main versus reshaping in place”Two different jobs use the same command, and mixing them makes conflicts harder to reason about.
Reshaping in place — squashing, rewording, reordering — with no change of base:
git rebase -i HEAD~5The commits replay onto the same parent they already had. Conflicts here are only between your own commits, which is unusual and usually means the reorder was invalid.
Rebasing onto a newer base, optionally with reshaping:
git rebase -i mainNow you are doing both at once: moving onto main’s current tip and editing the plan. Conflicts may come
from either source, and telling them apart mid-rebase is harder.
When things go wrong
Section titled “When things go wrong”| Situation | Command |
|---|---|
| Cancel the whole rebase | git rebase --abort |
| Change the remaining plan mid-rebase | git rebase --edit-todo |
| A commit is now empty | git rebase --skip |
| Finished, but the result is wrong | git reset --hard <pre-rebase-id> |
git rebase --abort restores the branch exactly. It always works while the rebase is in progress.
Once the rebase has finished, the reflog is the way back:
git reflogeee8f01 HEAD@{0}: rebase (finish): returning to refs/heads/feature84b5d12 HEAD@{1}: rebase (pick): D: first feature commit68cbf80 HEAD@{2}: rebase (start): checkout maina1d07c2 HEAD@{3}: commit: E: second feature commitHEAD@{3} is the pre-rebase tip:
git reset --hard HEAD@{3}A practical cleanup routine
Section titled “A practical cleanup routine”Before opening a branch for review:
-
See what you have.
Terminal window git log --oneline main..HEAD -
Back it up.
Terminal window git branch backup-$(git rev-parse --abbrev-ref HEAD) -
Reshape.
Terminal window git rebase -i mainFold fixups into their targets, drop debugging commits, reword anything unclear, and order the commits so they read as a sequence of deliberate steps.
-
Verify every commit builds.
Terminal window git rebase --exec "make test" main -
Check the net change is unaltered.
Terminal window git diff backup-feature HEADThis should print nothing. Reshaping history must not change the code.
-
Push.
Terminal window git push --force-with-lease -
Delete the backup once review is under way.
Step 5 is the safety check that makes the whole routine trustworthy: an empty diff proves you reorganised the history without altering the outcome.
What makes a good final history
Section titled “What makes a good final history”Interactive rebase gives you the ability to reshape a branch. It does not tell you what shape to aim for.
A useful target: each commit should be a change a reviewer could evaluate on its own, and the sequence should read as the order in which someone would sensibly have done the work.
That usually means:
Separate mechanical changes from behavioural ones. A commit that renames a function across forty files is trivially reviewable. The same rename mixed with a logic change is not, because the reviewer has to find the real change among the noise.
Introduce before you use. Add the new helper in one commit, call it in the next. A reviewer reading forwards never encounters something undefined.
Keep refactors reversible. A commit that only moves code should produce no behaviour change, and it is worth saying so in the message so a reviewer knows what to check.
Delete in its own commit. Removing the old implementation after the new one is in place makes the switch obvious and the revert easy.
One reason per commit. If the message needs “and”, consider splitting.
A branch reshaped this way is faster to review, and its commits remain useful years later when someone runs
git blame on a line and wants to know why it looks like that.
Common mistakes
Section titled “Common mistakes”Rebasing a shared branch. The one with the widest impact.
Deleting a line by accident. Deleting drops the commit. Use drop explicitly so intent is visible.
Squashing in the wrong direction. squash and fixup fold into the commit above. A fixup on the
first line has nothing to fold into and Git will refuse.
Using git reset --hard HEAD^ when splitting. Discards the changes you were splitting.
Forgetting the range excludes its endpoint. git rebase -i HEAD~3 edits three commits, starting from
the one after HEAD~3.
Rewriting during review. Comments lose their anchors.
Not verifying afterwards. Run step 5. An empty diff against your backup is cheap certainty.
Reaching for --skip on a conflict. It drops the commit. Only use it when the change is genuinely
already applied.
Mental Model
Section titled “Mental Model”Interactive rebase is a script Git writes for you and lets you edit.
Git lists the commands it intended to run —
pickeach commit in order. You rewrite the script: change verbs, reorder lines, delete some. Then Git runs your version instead of its own.The commits it produces are all new. You are not editing history; you are writing a new history that tells a better story about the same changes.
What You Learned
Section titled “What You Learned”- Interactive rebase shows the replay plan as an editable todo list, oldest commit first.
pick,reword,edit,squash,fixupanddropcover almost all real use.squashcombines messages;fixupdiscards the second one.editstops so you can amend, add commits, or split one commit into several withgit reset HEAD^.- Reordering is moving lines, and only works when the commits are independent.
execruns commands between commits;--execadds one after every commit.--fixupplus--autosquashrecords fold-in intent at commit time and applies it later.--abortcancels; the reflog or a backup branch recovers after completion.
Try It Yourself
Section titled “Try It Yourself”Build a messy branch and clean it up. Disposable repository only.
-
Create the repository and four commits, deliberately messy:
Terminal window mkdir ~/rebase-lab && cd ~/rebase-lab && git initecho "# Demo" > README.md && git add . && git commit -m "Initial commit"echo p > parser.py && git add . && git commit -m "Add parser"echo "typo fix" >> parser.py && git commit -am "fix typo"echo v > validate.py && git add . && git commit -m "Add validation"echo debug >> validate.py && git commit -am "WIP debug output" -
Back it up:
git branch backup. -
Look at the plan without changing anything:
git rebase -i HEAD~4, then quit your editor without saving. Confirmgit log --onelineis unchanged. -
Now reshape. Run it again and set the list to:
pick <id> Add parserfixup <id> fix typoreword <id> Add validationdrop <id> WIP debug output -
Verify the fixup landed:
git show HEAD~1:parser.pyshould contain both lines. -
Verify nothing else changed.
git diff backup HEAD— predict the output before running it. -
Practise recovery:
git reset --hard backupand confirm all four commits return. -
Try
edit: rebase again, mark the first commitedit, add a line,git commit --amend --no-edit, thengit rebase --continue.
Step 6 should print nothing. If it does not, the reshape changed the code as well as the history — which is exactly the mistake this check catches.
Next Lesson
Section titled “Next Lesson”Reordering deserves its own treatment, because commit dependencies make it the operation most likely to conflict.