Git Course 0%

Interactive rebase

Expert Git CLI ≈ 14 min

What you will learn

  • What each word in the todo list does
  • How to squash, reword, reorder and split commits
  • Autosquash, which removes most of the manual editing

After this lesson you can

  • I can tidy a branch's history before review, and escape if it goes wrong

Why this matters

A branch after a real afternoon contains "wip", "fix typo" and "actually fix typo". Interactive rebase turns that into the two or three commits you would have made if you had known where you were going, which makes review easier and history readable.

It also rewrites history, so the rule from lesson 12.3 applies without exception: only on commits nobody else has.

The todo list

Terminal
$ git rebase -i main

Git opens an editor containing one line per commit, oldest first:

Text
pick 68c4a9d feat: add the difficulty filter
pick a41d0c2 wip
pick 12ae4d1 fix typo
pick 9c8987f docs: document the filter

# Rebase d3b92ec..9c8987f onto d3b92ec (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

You edit the first word of each line, save, and close. Git then replays the commits according to your instructions.

Interactive rebase: the todo list and what it produces On the left, four commits on a branch: add the difficulty filter, wip, fix typo, and document the filter. In the middle, the todo list Git opens in your editor, with each commit on a line beginning with a command: pick for the first, fixup for the second and third, and pick for the fourth. On the right, the result: two commits, because the two fixups were absorbed into the first. Below, the command words are listed: pick keeps the commit, reword changes only its message, edit stops so you can amend, squash merges it into the previous one and asks for a combined message, fixup merges it and discards its message, and drop removes it. A caption warns that this rewrites history, so it is only for commits that have not been pushed. BEFORE 68c4a9d add filter a41d0c2 wip 12ae4d1 fix typo 9c8987f document it THE TODO LIST GIT OPENS pick 68c4a9d add filter fixup a41d0c2 wip fixup 12ae4d1 fix typo pick 9c8987f document it edit the first word of each line AFTER a8b40cb add filter 5a1c15c document it two commits, new hashes, same files The words you can put at the start of a line pick keep this commit as it is reword keep the change, edit the message edit stop here so you can change the commit itself squash merge into the one above, combine messages fixup merge into the one above, discard its message drop remove the commit entirely Reordering the lines reorders the commits. Deleting a line is the same as drop , which is why care is needed. Every commit after the one you touch gets a new hash, so interactive rebase is for branches nobody else has. Escape at any point with git rebase --abort.
The four commits, the todo list with two lines changed to fixup, and the two commits that result.

The words

Word Does
pick Keep the commit as it is
reword Keep the change, edit the message
edit Stop at this commit so you can amend it or split it
squash Merge into the commit above, and combine the messages
fixup Merge into the commit above, discarding this message
drop Remove the commit entirely

Two behaviours beyond the words: reordering the lines reorders the commits, and deleting a line is the same as drop, which is why an accidental deletion is worth knowing about.

Squashing, in practice

Change the second and third lines to fixup:

Text
pick   68c4a9d feat: add the difficulty filter
fixup  a41d0c2 wip
fixup  12ae4d1 fix typo
pick   9c8987f docs: document the filter

Output
Successfully rebased and updated refs/heads/feature/21-difficulty-filter.
Terminal
$ git log --oneline -3
5a1c15c docs: document the filter
a8b40cb feat: add the difficulty filter
d3b92ec chore: add CI configuration and contributing guide

Four commits became two, with the same files. Both remaining commits have new hashes, because their content or their parent changed (lesson 18.1).

Autosquash: the version worth learning

Editing the todo by hand is fine for four commits and tedious for fifteen. Autosquash automates the common case: you notice a mistake in an earlier commit and want the fix folded into it.

Terminal
$ git commit --fixup 68c4a9d

That makes an ordinary commit whose message is fixup! feat: add the difficulty filter. Then:

Terminal
$ git rebase -i --autosquash main

Git pre-arranges the todo for you:

Text
pick   68c4a9d feat: add the difficulty filter
fixup  12ae4d1 fixup! feat: add the difficulty filter
pick   9c8987f docs: document the filter

Save and close without editing anything, and the fix is absorbed into the commit it belongs to. Set git config --global rebase.autosquash true and --autosquash becomes the default.

This is the workflow to adopt: as you work, --fixup the commit each fix belongs to, then one autosquash rebase before opening the merge request.

Splitting a commit

The one that needs edit:

  1. Mark the commit edit and save. Git stops there.
  2. git reset HEAD~1 — the commit is undone, its changes back in your files (lesson 12.4).
  3. Stage and commit the first part, then the second.
  4. git rebase --continue.

Getting out

Situation Command
A conflict, and you want to finish Resolve, git add, git rebase --continue
A conflict on a commit that no longer matters git rebase --skip
Anything at all going wrong git rebase --abort
It finished and you regret it git reset --hard ORIG_HEAD, or the reflog (lesson 12.1)

Nothing here is lost: the original commits are unreferenced but present, and the reflog names them.

Publishing afterwards

Rewriting a pushed branch means the next push is rejected, and the answer is a lease-checked force (lesson 12.8):

Terminal
$ git push --force-with-lease

Your own feature branch: routine. A branch someone else has checked out: agree it first. A shared branch: no.

How to do it

Terminal
$ git rebase -i main
$ git commit --fixup <hash>
$ git rebase -i --autosquash main
$ git rebase --continue | --skip | --abort

Set your editor first, or the todo opens in something unexpected (lesson 3.3).

Common mistakes

  • Rebasing a shared branch. The rule has no exceptions.
  • Deleting a line by accident, which drops the commit. Abort and start again.
  • Squashing everything into one commit when the branch genuinely did three things.
  • Forgetting the branch is pushed, then being surprised by the rejection.
  • Editing the todo's comment lines, which do nothing.
  • Using --skip to make a conflict disappear, which drops that commit's change (lesson 12.6).

Try it yourself

Goal: tidy a messy branch two ways.

  1. On a branch, make four commits: a real one, then "wip", "fix typo", and another real one.
  2. Run git rebase -i main, mark the two middle lines fixup, and confirm you end with two commits.
  3. Undo it all with git reset --hard ORIG_HEAD and check the four commits are back.
  4. Now do it with autosquash: make the same mess, but commit the fixes with git commit --fixup <hash of the commit they belong to>.
  5. Run git rebase -i --autosquash main, save without editing, and compare the result with step 2.

Expected result: the same tidy history twice, once by editing the todo and once with no editing at all.

Show solution

Step 3 is the reassurance that this is reversible: ORIG_HEAD points at the branch as it was before the rebase, so a rebase you dislike costs one command. Step 5 is the workflow to keep: --fixup as you go, one autosquash before review, and the todo list never needs editing by hand.

Check yourself

1. What is the difference between squash and fixup in the todo list?
2. What does git commit --fixup <hash> do?
3. You are halfway through an interactive rebase and it is going badly. What do you run?

Key terms

Rebase Squash Rewriting history Commit