Interactive rebase: tidy up before you share
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- read the plan from
git rebase -iand explain why the oldest commit is at the top understand - use reword, squash, fixup, drop and reordering to tidy a private branch, and check the result with
git diff ORIG_HEAD HEADapply - use
git commit --fixupandgit rebase -i --autosquashto meld a later fix into the right commit apply - decide whether commits may be tidied with interactive rebase or must be undone with revert because they are shared evaluate
Understand: The story you tell
Before you share a branch, read the story it tells. Here it says: add search box, fix typo, add debug output, and a commit just called results. That shows how you worked, not what changed. A reader would rather see two clean commits.
git rebase -i main opens a plan in the editor, with one line per commit on the branch. Every line starts with pick: replay this commit as it is. Which commit is at the top?
You change the word at the start of a line. Fixup melds the commit into the one above and throws away its own message. Squash does the same, but lets you combine the messages.
Drop removes the commit and its change, here the debug output. Reword keeps the change but lets you write a better message, here for results.
Git runs the plan from the top. The fix is melded into the search box, the debug commit is gone, and the last commit has a clear message. Like a plain rebase, these are new commits. The old ones are dashed: no branch points to them.
Often you find a mistake later, when other commits already sit on top. Then git commit --fixup makes a commit whose message starts with fixup and the name of the target. It is a note to Git: this belongs in that commit.
git rebase -i --autosquash main reads those notes. It moves each fixup line right under its target and marks it fixup, so the plan is ready. You only have to save.
Tidy only commits that are still private, before you push or open a pull request. And check the result. git diff ORIG_HEAD HEAD shows how the content changed. If you only rewrote messages, combined commits or changed the order, it should show nothing.
Watch: Tidy a search branch
We are on the private branch search. The log shows four commits on top of the snapshot, newest first.
$ git log --oneline
The plan opens with git rebase -i main. We put fixup on the typo fix, drop on the debug output and reword on results. Afterwards Git asks for the new message.
$ git rebase -i main
Two clean commits, both with new IDs. The old four lie dashed in the graph.
$ git log --oneline
Now we check what changed in the content. Only debug.txt is gone. The fixup and the reword did not touch the code. Had we dropped the wrong commit, git reset --hard ORIG_HEAD would have taken us back.
$ git diff --stat ORIG_HEAD HEAD
Later we improve the search box. The change belongs in the first commit, not in a new commit on top.
$ echo 'Search box with hint text' > search.txt && git add search.txt
git commit --fixup takes the commit it should fix. Here that is HEAD~1. Look at the message Git writes.
$ git commit --fixup HEAD~1
Now we run git rebase -i --autosquash main. The plan is already done: the fixup line sits right under its target. We just save it.
$ git rebase -i --autosquash main
Two commits again. The improvement now lives inside Add search box, and nobody can tell that it came later.
$ git log --oneline
Tasks
- Read the story the branch tells, with one line per commit.
- Tidy the branch: meld the typo fix into the search box commit, remove the debug commit and give
resultsthe messageAdd search results. - Compare the content before and after the tidy-up.
- Save the staged change as a fixup of the commit
Add search box. - Meld the fixup commit into its target.
Practise: Transfer: a report in five commits
You are on report, a private branch with five commits. No demo this time.
Tasks
- Combine
Add chartandAdd chart captioninto one commit with the messageAdd chart with caption. - Move
Write methodbeforeWrite results, so the report comes in a natural order. intro.txtcontains the typo Introductoin. Fix it, and meld the fix into the commitWrite intro, so that no extra commit is left.
Check yourself: Recall it
You combined two commits with squash and gave a third a new message with reword. What does git diff ORIG_HEAD HEAD show?
- Nothing, the content is the same as before
- The changes from the two commits you combined
- The old and the new message
You want to meld a commit into the one above, and you do not want to keep its message. Which word do you use?
- squash
- fixup
- drop
You have three commits and use reword on the middle one. Which commits get new IDs?
- Only the middle one, since it is the only one that changed
- The middle one and the one after it, while the first keeps its ID
- None, because the file content is the same
git log --oneline --decorate on main shows (HEAD -> main, origin/main) on the newest commit, and a debug commit two lines further down. How do you get rid of the debug commit?
- Interactive rebase with drop, then
git push --force-with-lease git reverton the debug commit, then an ordinarygit pushgit reset --hardto the commit before it, then force-push
Review from “Good commits”: you reword the commit stuff, which adds a price table. Which subject line is best?
- Added price table
- Add price table
- Update stuff
Common misconceptions
- “The plan shows the newest commit at the top, like
git log.” The oldest is at the top, because Git runs the plan from top to bottom. - “Interactive rebase edits the commits where they are.” From the first changed line downwards, every commit becomes a new commit with a new ID.
- “Squash and fixup are the same.” Both meld a commit into the one above. Squash lets you combine the messages, fixup keeps only the target's message.
Read more
The plan is a script
git rebase -i <base> writes a to-do list for the commits in base..HEAD, oldest first, and opens it in your editor. When you save and close, Git runs it from top to bottom. A real plan for a branch with three commits:
pick c6766f7 # Add search box
pick 00d10e3 # Rename button
pick 1218aec # Add search results
# Rebase a5ea1be..1218aec onto a5ea1be (3 commands)
The ID is what counts; the subject after it is only there to help you. Older Git versions show it without the #. The base can be a branch (git rebase -i main) or a count (git rebase -i HEAD~3 takes the last three commits). git rebase -i --root includes the very first commit.
| Word | Short | What it does |
|---|---|---|
| pick | p | Use the commit as it is |
| reword | r | Use the change, stop to edit the message |
| edit | e | Use the commit, then stop so you can amend or split it |
| squash | s | Meld into the commit above, combine both messages |
| fixup | f | Meld into the commit above, keep only the message above (fixup -C keeps this one's message instead) |
| exec | x | Run a shell command; the rebase stops if it fails |
| break | b | Stop here; git rebase --continue goes on |
| drop | d | Leave the commit out |
Deleting a line has the same effect as drop, and Git does not warn you. Set git config rebase.missingCommitsCheck error if you want Git to refuse a plan where a line has disappeared. You can change the remaining plan while a rebase is stopped with git rebase --edit-todo.
Stop and change a commit: edit
edit applies the commit and then stops, with that commit as HEAD. Change files, run git commit --amend, and finish with git rebase --continue.
You can also split a commit. Undo the commit but keep its changes with a mixed reset, then commit the parts separately:
$ git rebase -i main # mark "Add a and b" with edit
Stopped at fce6fb0... # Add a and b
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
$ git reset HEAD^
$ git add a.txt && git commit -m "Add a"
$ git add b.txt && git commit -m "Add b"
$ git rebase --continue
Successfully rebased and updated refs/heads/big.
$ git log --oneline
016f3d8 Add b
7170811 Add a
a5ea1be Initial snapshot
git reset HEAD^ without --hard moves only HEAD, which is detached during the rebase, and the index. Your files stay as they are.
Prepare fixes while you work
git commit can write the plan lines for you. The commit message tells --autosquash where the commit belongs:
| Command | Message | Later in the plan |
|---|---|---|
git commit --fixup=<commit> |
fixup! <subject> |
fixup: keep the target's message |
git commit --squash=<commit> |
squash! <subject> |
squash: combine the messages |
git commit --fixup=amend:<commit> |
amend! <subject> |
fixup -C: change and new message |
git commit --fixup=reword:<commit> |
amend! <subject> |
new message only, no change |
Then git rebase -i --autosquash main moves each line under its target. git config rebase.autoSquash true makes --autosquash the default for interactive rebases.
Check that you changed only what you meant to
git diff ORIG_HEAD HEAD compares the content before and after. It should be empty when you only reworded, reordered or combined commits. If it shows something you did not intend, look closer. To compare commit by commit, use git range-diff. Here Rename button was melded into Add search box:
$ git range-diff main ORIG_HEAD HEAD
1: c6766f7 ! 1: c463304 Add search box
@@ search.html (new)
+<form>
+ <label>Search</label>
+ <input name=q>
-+ <button>Go</button>
++ <button>Search</button>
+</form>
2: 00d10e3 < -: ------- Rename button
3: 1218aec = 2: 1bb4102 Add search results
! marks a commit whose change differs, with a diff of the two diffs below it. < means the old commit has no counterpart any more, and = means the change is identical.
Undo a tidy-up
While the rebase runs, git rebase --abort puts everything back. After it has finished, the branch's old tip is in the branch's reflog and usually in ORIG_HEAD, so git reset --hard ORIG_HEAD takes you back, as long as you have no uncommitted work. A reset during the rebase, as in the split example above, moves ORIG_HEAD. Then you find the old tip in the branch's reflog, as the lesson “Reflog” shows.
Rule of thumb
Tidy before you share. The lesson “Rebase” shows how to push a rewritten branch that is yours alone. Never tidy main or any other branch other people use.
Per-Arne's Git Good!