Using rebase to ensure a clean commit history
Git’s rebase feature is a powerful and advanced tool that can help you convert a messy commit history into useful notes for future developers. Sometimes that future developer might even be you!
More precisely, the interactive rebase feature can be used to re-write history to make it appear like you never made a mistake and wrote all of your perfect code in one try. Yes, it is sorcery. No, you do not need a Hogwarts degree perform it.
Starting at a messy commit history
So you’ve done your work in your feature branch and, hopefully, have been generating frequent, isolated commits for your work in progress. When you run
$ git log your history may look something like this:
commit 28f0fed0fbefab20436e9d0927d0e07241b1db05 Author: Matt Langan <email@example.com> Date: Wed Jun 28 13:25:05 2017 -0500 wip workflow commit 88c7675ef37489056ff0550cd3216d6c7599a99f Author: Matt Langan <firstname.lastname@example.org> Date: Wed Jun 28 13:24:51 2017 -0500 wip setup commit cdf3bf1fde1c3c138eb73a020c375b70c7a22b65 Author: Matt Langan <email@example.com> Date: Wed Jun 28 13:00:27 2017 -0500 wip local setup commit 70adbee56a3b0a8e766e0eba120dc390813895ee Author: Matt Langan <firstname.lastname@example.org> Date: Wed Jun 28 13:00:03 2017 -0500 wip unified guides commit 69bcb81dd6e1749560a51775e66a7befd6c6fd5e Author: Matt Langan <email@example.com> Date: Wed Jun 28 08:35:43 2017 -0500 wip aggregate all guides outside of workflow
Initiating the interactive rebase
So you have a bunch of “wip” (work-in-progress) commits that you are now ready to do 2 things with:
- Combine them into logical clusters of commits. In this example, all the commits that changed
workflow.mdshould be grouped into one single commit that tells the story of one cohesive re-write of the file.
- Provide thoughtful commit titles and messages to help future developers who might have questions about the changes and why they were made.
First, you’ll probably need to get out of your log history by typing
q in front of the
: at the bottom of your shell. It will look something like this:
commit 610e2a16ffca9665aabe9f58055f3b2bc64a4895 Author: Matt Langan <firstname.lastname@example.org> Date: Wed Jul 5 10:25:58 2017 -0500 wip workflow :
Now that you’re back in your command prompt you need to tell Git 2 things:
- You want to perform an interactive rebase
- How far back in the history you want to go
The first step is handled by a simple Git command
$ git rebase -i, and the second part is handled by appending additional context to that command. There are 2 ways to perform that second step.
If you know exactly which commit you want to go back to you can copy its hash and append it to the rebase command, so your whole command would look like:
$ git rebase -i 610e2a16ffca9665aabe9f58055f3b2bc64a4895
That will tell Git that you’re interested in rewriting the history starting at your most recent commit, going all the way back to the commit for the hash you provided.
The other option is simply tell Git that you want to go a set number of commits back from your latest commit. Let’s say you only wanted to rewrite your most recent commit. In that case you would run:
$ git rebase -i HEAD~1
Want to rewrite your last 7 commits?
$ git rebase -i HEAD~7
Performing the rebase
If your machine is properly configured then your default Git editor should open up a window that consists of your commit log at the top and rebase instructions at the bottom.
pick 6c6fd5e wip aggregate all guides outside of workflow pick 13895ee wip unified guides pick 7a22b65 wip local setup pick 599a99f wip setup pick 1b1db05 wip workflow # Rebase 1b1db05..6c6fd5e onto e77065b (5 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # 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
Note that the display order is the opposite from what you see when you run
$ git log. The most recent commit is at the bottom, and the oldest is at the top.
It’s critical that you treat this file with caution, as the work done here will be permanent. For example, if you delete one of the commit lines that commit will be lost. For this reason it is recommended that your local copy be pushed to the remote before performing the rebase. That way, if you make a mistake you can’t recover from during the rebase, you can always delete your local feature branch, re-pull it from the remote, and start over.
You’ll notice the list of commands available, starting with
pick and ending with
drop. By default, all of the commits are set to
pick which, as you can see, simply tells Git to use the commit as-is. This retains the code change, the title, the commit body, and its order in the log. If you leave all actions as
pick and close this window then the rebase will run and the history will not be changed.
Once you’ve selected your commands you need to save the file and close the window. If you’re using Atom you need to close the the entire Atom application window. Once you’ve done that you should be redirected to your shell, where you will see messages from Git about its attempt to run your commands.
If the rebase fails or hits a conflict then you will see an error message. You can always run
$ git rebase —abort to cancel the rebase and return to the commit history you had before attempting the rebase.
The simplest change to make in a rebase is the
reword. This lets you edit a commit’s title and body, but makes no other changes to the history or code. To reword a commit, replace
r beside the commit hash.
When Git runs your
reword command it will once again launch your default editor, this time with a file open for just that commit. Supply your commit title and message, then save and close the file to apply the commit. Remember to insert an empty line between your title and commit body!
Refer to writing-a-commit for guidelines on how to author your title and body.
As with the rebase log, if you’re using Atom then both the file and application windows must be closed in order for Git to continue performing the rebase.
This is the command to use to combine multiple sets of changes into a single commit, while only retaining the commit title and message of the parent (top) commit. For example, if we applied the following changes to the rebase file…
pick 6c6fd5e wip aggregate all guides outside of workflow f 13895ee wip unified guides pick 7a22b65 wip local setup pick 599a99f wip setup pick 1b1db05 wip workflow
… then the code changes in
13895ee would be combined into the commit above it (
6c6fd5e), and the commit title and body from
13895ee would be destroyed. If that rebase were to be completed successfully then the next time you brought up the rebase window you would see
pick 6c6fd5e wip aggregate all guides outside of workflow pick 7a22b65 wip local setup pick 599a99f wip setup pick 1b1db05 wip workflow
This will remove the commit and your history will appear as though it was never created in the first place.
Combining commits that weren’t made in succession
You can move lines around to change the order in which they will be reflected in the log. This is also useful when combined with a fixup. Let’s take another look at our messy history and work through a reorder and fixup strategy. In this case, let’s say that the we wanted to combine our most recent commit
1b1db05 with the first commit in our rebase history
Our original commit history looks like this:
pick 6c6fd5e wip aggregate all guides outside of workflow pick 13895ee wip unified guides pick 7a22b65 wip local setup pick 599a99f wip setup pick 1b1db05 wip workflow
First, we would move
pick 1b1db05 wip workflow from line 5 to line 2, resulting in:
pick 6c6fd5e wip aggregate all guides outside of workflow pick 1b1db05 wip workflow pick 13895ee wip unified guides pick 7a22b65 wip local setup pick 599a99f wip setup
Next, we apply the
fixup to the more recent commit in order to combine it with its intended parent commit:
pick 6c6fd5e wip aggregate all guides outside of workflow f 1b1db05 wip workflow pick 13895ee wip unified guides pick 7a22b65 wip local setup pick 599a99f wip setup
Now when we save and close the commit file Git will apply the code changes from