Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Interactive rebase: tidy up before you share

Module 4: Undo and rewrite · 13 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • read the plan from git rebase -i and 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 HEAD apply
  • use git commit --fixup and git rebase -i --autosquash to 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

  1. Read the story the branch tells, with one line per commit.
  2. Tidy the branch: meld the typo fix into the search box commit, remove the debug commit and give results the message Add search results.
  3. Compare the content before and after the tidy-up.
  4. Save the staged change as a fixup of the commit Add search box.
  5. 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

  1. Combine Add chart and Add chart caption into one commit with the message Add chart with caption.
  2. Move Write method before Write results, so the report comes in a natural order.
  3. intro.txt contains the typo Introductoin. Fix it, and meld the fix into the commit Write 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 revert on the debug commit, then an ordinary git push
  • git reset --hard to 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.

Official documentation