Portrait of me in my natural habitat

Use Interactive Rebases to Append Fixes to Old Commits

Posted 2021-03-30

tl;dr Using git’s interactive rebase you can easily go back a few commits to add a correction in the commit it logically belongs to.

I’ve recently tried to take better care of my commits. In pull requests previously just appended feedback onto the end of the branch.

3afac13 Fix typo in suprise dialog
e8cac49 Implement new suprise dialog
e9b67e2 Refactor dialogs to share a base component

Commit 3afac13 is example of one such commit. The SupriseDialog.tsx added in e8cac49 included some typo, so I pushed a new commit to fix it.

Now, if I wanted to fix this I could easily have just git commit --amend -—no-edit . But what if that typo is from a couple commits ago?

90654b0 Move all dialogs to same package
d949095 Remove unused dialogs
e8cac49 Implement new suprise dialog
e9b67e2 Refactor dialogs to share a base component

Before, what I would have done is git reset HEAD~1 to undo the commit at HEAD and then git stash to save it for each commit until I got to the one I needed to fix. This is both annoying and time-consuming.

A better way is git’s interactive rebase. Instead I could have done git rebase -i HEAD~3 . The -i flag says to make the rebase interactive, and HEAD~3 says to go back three commits from the current HEAD.

A view like this will then appear:

pick e8cac49 Implement new suprise dialog
pick d949095 Remove unused dialogs
pick 90654b0 Move all dialogs to same package

# Rebase e9b67e2..90654b0 onto e9b67e2 (3 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 <commit> = like "squash", but discard this commit's log message
# 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
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

This gives a ton of options. For this example, edit will let me amend onto the commit like I want, but if I just wanted to change the commit message I could use reword, or I could combine commits with squash. To use a command on a commit, change the work pick before the commit message at the top.

edit e8cac49 Implement new suprise dialog
pick d949095 Remove unused dialogs
pick 90654b0 Move all dialogs to same package

Then a message will appear telling me I’ve stopped at the appropriate commit.

Stopped at e8cac49...  Implement new suprise dialog
You can amend the commit now, with

  git commit --amend

Once you are satisfied with your changes, run

  git rebase --continue

I’ve done my best to become an interactive rebase wizard as it lets me easily produce a cleaner git history. You can find the documentation for the interactive rebase here!