5.
Rebasing to Rewrite History
Written by Chris Belanger
As you saw in the previous chapter, rebasing provides you with an excellent alternative to merging. But rebasing also gives you the ability to reorganize your repository’s history. You can reorder, rewrite commit messages and even squash multiple commits into a single, tidy commit if you like.
Just as you’d tidy up your code before pushing your local branch to a remote repository, rebasing lets you clean up your commit history before you push to remote. This gives you the freedom to commit locally as you see fit, then rearrange and combine your commits into a handful of semantically-meaningful commits. These will have much more value to someone (even yourself!) who has to comb through the repository history at some point in the future.
Note: Again, a warning: Rebasing in this manner is best used for branches that you haven’t shared with anyone else. If you must rebase a branch that you’ve shared with others, then you must work out an arrangement with everyone who’s cloned that repository to ensure that they all get the rebased copy of your branch. Otherwise, you’re going to end up with a very complicated repository cleanup exercise at the end of the day.
To start, extract the compressed repository from the starter directory to a convenient location on your machine then navigate into that directory from the command line.
Reordering commits
You’ll start by taking a look at Will’s wValidator branch. Execute the following to see what the current history looks like:
git log --all --decorate --oneline --graph
You’ll see the following at the top of your history graph:
* 45f5b4f (HEAD -> wValidator) Updated team acronym
* 15233a5 Added new maintainer to README.md
* 783031e Refactoring the main check function
* 6396aa8 Removing TODO
* 8e39599 check04: Checking diagonal sums
* 199e71d util06: Adding a function to check diagonals
* a28b9e3 check03: Checking row and column sums
* bdc8bc7 util05: Fixing comment indentation
* a4d6221 util04: Adding a function to check column sums
* 59fd06e util03: Adding function to check row sums
* 5f53302 check02: Checking the array contains the correct values
* 136dc26 Refactoring the range checking function
* 665575c util02: Adding function to check the range of values
* 0fc1a91 check01: checking that the 2D array is square
* 5ec1ccf util01: Adding the checkSqaure function
It’s not terrible, but this could definitely use some cleaning up. Your task is to combine those two trivial updates to README.md into one commit. You’ll then reorder the util* commits and the check* commits together and, finally, to combine those related commits into two separate, tidy commits.
Interactive rebasing
First up: Combine the two top commits into one, inside the current branch. You’re familiar with rebasing branches on top of other branches, but in this chapter, you’ll rebase commits on top of other commits in the same branch.
In fact, since a branch is simply a label to a commit, rebasing branches on top of other branches really is just rebasing commits on top of one another.
But since you want to manipulate your repository’s history along the way, you don’t want Git to just replay commits on top of other commits. Instead, you’ll use interactive rebase to get the job done.
First, get your game plan together. You want to combine, or squash those top two commits into one commit, give that new commit a clear message, and rebase that new squashed commit on top of the ancestor of the original commits. So your plan looks a little like the following:
- Squash
45f5b4fand15233a5. - Create a new commit message for this squashed commit.
- Rebase the resulting new commit on top of
783031e.
To start an interactive rebase, you need to use the -i (--interactive) flag. Just as before, you need to tell Git where you want to rebase on top of; in this case, 783031e.
So, execute the following to start your first Git interactive rebase:
git rebase -i 783031e
Git opens up the default editor on your system, likely Vim, and shows you the following:
pick 15233a5 Added new maintainer to README.md
pick 45f5b4f Updated team acronym
# Rebase 783031e..45f5b4f onto 783031e (2 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
Well, that’s different! You’ve seen Vim in action before to create commit messages, but this is something new. What’s going on?
Squashing in an interactive rebase
Here, Git’s taken all of the commits past your rebase point, 15233a5 and 45f5b4f, and put them at the top of the file with some rather helpful comments down below.
What you’re doing at this step is effectively creating a script of commands for Git to follow when it rebases. Git will start at the top of this file and work downwards, applying each action it finds, in order.
To perform a squash of commits, you simply put the squash command on the line with the commit you wish to squash into the previous one. In this case, you want to squash 45f5b4f, the last commit, into 15233a5.
Note: Git interactive rebase shows all commits in ascending commit order. This is a different order than what you’re used to seeing in with
git log, so be careful that you’re squashing things in the correct direction!
Since you’re back in Vim, you’ll have to use Vim commands to edit the file. Cursor to the start of the 45f5b4f line and press the C key, followed by the W key — this is the “change word” command, and it essentially deletes the word your cursor is on and puts you into insert mode.
So type squash right there. The top few lines of your file should now look as follows:
pick 15233a5 Added new maintainer to README.md
squash 45f5b4f Updated team acronym
That’s all you need to do, so write your changes and quit with the familiar Escape + :wq + Enter combination.
Git then throws you straight back into another Vim editor, this one a little more familiar:
# This is a combination of 2 commits.
# This is the 1st commit message:
Added new maintainer to README.md
# This is the commit message #2:
Updated team acronym
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Jun 9 07:28:08 2019 -0300
#
# interactive rebase in progress; onto 783031e
# Last commands done (2 commands done):
# pick 15233a5 Added new maintainer to README.md
# squash 45f5b4f Updated team acronym
# No commands remaining.
# You are currently rebasing branch 'wValidator' on '783031e'.
#
# Changes to be committed:
# modified: README.md
#
Okay, this is a commit message editor, which you’ve seen before. Here, Git helpfully shares the messages of all commits affected by this rebase operation. You can choose to keep or edit any one of those commit messages, or you can choose to create your own.
Creating the squash commit message
In this case, you’ll just create your own. Clear this file as follows:
- Type gg to ensure you’re on the first line of the file.
- Type dG (that’s a capital “G”) to delete all of the following lines from the file.
You now have a nice, clean file for a commit message. Press i to enter insert mode and then add the following message:
Updates to README.md
Then save your changes with Escape + :wq + Enter to continue the rebase operation.
Git carries on, emitting a little output with the success message at the end:
Successfully rebased and updated refs/heads/wValidator.
Execute the following to see what the repository history looks like now:
git log --all --decorate --oneline --graph
Look at the top two lines and you’ll see the following (your hashes will be different, of course):
* 2492536 (HEAD -> wValidator) Updates to README.md
* 783031e Refactoring the main check function
Git has done just what you asked; it’s created a new commit from the two old commits and rebased that new commit on top of the ancestor. To see the combined effect of squashing those two commits into one, check out the patch Git created for 2492536 with the following command:
git log -p -1
Take a look at the bottom of that output and you’ll see the following:
-This project is maintained by teamWYXZ:
+This project is maintained by teamWYXZC:
- Will
- Yasmin
- Xanthe
- Zack
+- Chris
There’s the combined effect of merging those two patches into one and rebasing that change on top of the ancestor commit.
Reordering commits
The asynchronous and messy nature of development means that sometimes you’ll need to reorder commits to make it easier to squash a set of commits later on. Interactive rebase lets you literally rearrange the order of commits within a branch. You can do this as often as you need, to keep your repository history clean.
Execute the following to see the latest commits in your repository:
git log --oneline
Take a look at the order of the last dozen commits or so:
2492536 (HEAD -> wValidator) Updates to README.md
783031e Refactoring the main check function
6396aa8 Removing TODO
8e39599 check04: Checking diagonal sums
199e71d util06: Adding a function to check diagonals
a28b9e3 check03: Checking row and column sums
bdc8bc7 util05: Fixing comment indentation
a4d6221 util04: Adding a function to check column sums
59fd06e util03: Adding function to check row sums
5f53302 check02: Checking the array contains the correct values
136dc26 Refactoring the range checking function
665575c util02: Adding function to check the range of values
0fc1a91 check01: checking that the 2D array is square
5ec1ccf util01: Adding the checkSqaure function
69670e7 Adding a new secret
There’s a collection of commits there that would make more sense if you arranged them contiguously. There’s one set of check functions commits (the check0x commits) and another set of utility functions (the util0x commits). Before you merge these to master, you’d like to squash these related sets of commits into two commits to keep your repository history neat and tidy.
First, you’ll need to start with the common ancestor of all of these commits. In this case, the base ancestor commit of the commits you’re concerned with is 69670e7. That commit will be the base for your interactive rebase.
Execute the following to start the interactive rebase on top of that base commit:
git rebase -i 69670e7
Once again, you’ll be launched into Vim to edit the rebase script for the rebase operation:
pick 5ec1ccf util01: Adding the checkSqaure function
pick 0fc1a91 check01: checking that the 2D array is square
pick 665575c util02: Adding function to check the range of values
pick 136dc26 Refactoring the range checking function
pick 5f53302 check02: Checking the array contains the correct values
pick 59fd06e util03: Adding function to check row sums
pick a4d6221 util04: Adding a function to check column sums
pick bdc8bc7 util05: Fixing comment indentation
pick a28b9e3 check03: Checking row and column sums
pick 199e71d util06: Adding a function to check diagonals
pick 8e39599 check04: Checking diagonal sums
pick 6396aa8 Removing TODO
pick 783031e Refactoring the main check function
pick 2492536 Updates to README.md
# Rebase 69670e7..2492536 onto 69670e7 (14 commands)
Since Git starts at the top of the file and works its way down in order, you simply need to rearrange the lines in this file in contiguous order to rearrange the commits.
Since you’re in Vim, you might as well use the handy Vim shortcuts to move lines around:
- To “cut” a line into the clipboard buffer, type dd.
- To “paste” a line into the edit buffer underneath the current line, type p.
Use these two key combinations to do the following:
- Move the
util01line to just above theutil02line. - Leave the
Refactoring the range checking functionafterutil02. - Move the
util03throughutil06lines, in order, to follow theRefactoring the range checking functioncommit.
When you’re done, your rebase script should look as follows:
pick 0fc1a91 check01: checking that the 2D array is square
pick 5ec1ccf util01: Adding the checkSqaure function
pick 665575c util02: Adding function to check the range of values
pick 136dc26 Refactoring the range checking function
pick 59fd06e util03: Adding function to check row sums
pick a4d6221 util04: Adding a function to check column sums
pick bdc8bc7 util05: Fixing comment indentation
pick 199e71d util06: Adding a function to check diagonals
pick 5f53302 check02: Checking the array contains the correct values
pick a28b9e3 check03: Checking row and column sums
pick 8e39599 check04: Checking diagonal sums
pick 6396aa8 Removing TODO
pick 783031e Refactoring the main check function
pick 2492536 Updates to README.md
Then, save your changes with Escape + :wq + Enter to continue the rebase operation.
Git continues with a little bit of output to let you know things have succeeded:
Successfully rebased and updated refs/heads/wValidator.
Now, take a look at the log with git log --oneline and you’ll see that Git has neatly reordered your commits, and added new hashes as well:
35aab2b (HEAD -> wValidator) Updates to README.md
3899829 Refactoring the main check function
c8d5335 Removing TODO
5d16107 check04: Checking diagonal sums
5c9e64d check03: Checking row and column sums
4018013 check02: Checking the array contains the correct values
f7a31a0 util06: Adding a function to check diagonals
851663d util05: Fixing comment indentation
6c857e4 util04: Adding a function to check column sums
5ad299c util03: Adding function to check row sums
2575920 Refactoring the range checking function
96fb378 util02: Adding function to check the range of values
55d4ded util01: Adding the checkSqaure function
ded7caa check01: checking that the 2D array is square
69670e7 Adding a new secret
I want to stress once again that these are new commits, not simply the old commits moved around. And it’s not just the commits you moved around inside the instruction file that have new hashes: Every single commit from your rebase script has a new hash — because they are new commits.
Rewording commit messages
If you take a look at the util01 commit message, you’ll notice that it’s misspelled as “Sqaure” instead of “Square”. As a word nerd, I can’t leave that the way it is. But I can quickly use interactive rebase to change that commit message.
Note: In my repo, the commit has the hash of
55d4ded, while in your system, it will likely be different. Simply replace the hash below with the hash of the commit you want to rebase on top of — that is, the commit just before the one you want to change, and things will work just fine.
Execute the following to start another interactive rebase, indicating the commit you want to rebase on top of. In this instance, you want to rebase on top of the check01: checking that the 2D array is square commit:
git rebase -i ded7caa
When Vim comes up, you’ll see the commit you’d like to change at the top of the list:
pick 55d4ded util01: Adding the checkSqaure function
Ensure your cursor is on that line, and type cw to cut the word pick and change to insert mode in Vim. In place of pick, type reword there, which tells Git to prompt you to reword this commit as it runs the rebase script.
When you’re done, the very first line in the script should look as follows:
reword 55d4ded util01: Adding the checkSqaure function
Note: You’re not fixing the commit message in this step; rather, you’ll wait for Git to prompt you to do it when the rebase script runs.
Save your work with Escape + :wq + Enter and you’ll immediately be put back into Vim. This time, Git’s asking you to actually modify the commit message.
Press i to enter insert mode, cursor over to that egregious misspelling, change the word checkSqaure to checkSquare, and save your work with Escape + :wq + Enter.
Git completes the rebase and drops you back at the command line.
You can see that Git has changed the commit message for you by executing git log --oneline and scrolling down to find your new, rebased commit:
4f4e308 util01: Adding the checkSquare function
It’s a small thing, to be sure, but it’s a nice thing.
Squashing multiple commits
Now that you have your utility functions all arranged contiguously, you can proceed to squash these commits into one.
Again, you’ll launch an interactive rebase session with the hash of the commit you want to rebase on top of. You want to rebase on top of the Adding a new secret commit, which is still 69670e7. Remember: When you rebase on top of a commit, that commit doesn’t change, so it still has the same hash as before. It’s just the commits that follow that will get new hashes as each is rebased.
To start your adventure in squashing, execute the following to kick off another interactive rebase:
git rebase -i 69670e7
Once you’re back in Vim, find the list of contiguous commits for the utility functions.
To squash a list of commits, find the first commit in the sequence you’d like to squash and leave that commit as it is. Then, on every subsequent line, change pick to squash. As Git executes this rebase script, each time it encounters squash, it will meld that commit with the commit on the previous line.
That’s why you need to leave that first line unchanged: Otherwise, Git will squash that first commit into the previous commit, which isn’t what you want. You want to squash this set of changes relevant to the utility functions as a nice tidy unit, not squash them into some random commit preceding them.
Note: You can use a bit of Vim-fu to speed things along here.
- Type cw on the first commit you want to squash (the
util02one) and changepicktosquash. - Then press Escape to get back to command mode.
- Cursor down to the start of the next commit you want to squash, and type . - a period. This tells Git “Do that same thing again, only on this line instead.”
Continue on this way for all of the utility function commits. When you’re done, your rebase script should look like the following:
pick ded7caa check01: checking that the 2D array is square
pick 4f4e308 util01: Adding the checkSquare function
squash 421c298 util02: Adding function to check the range of values
squash 96dc840 Refactoring the range checking function
squash 19e90e9 util03: Adding function to check row sums
squash c9d8aa3 util04: Adding a function to check column sums
squash 30f164a util05: Fixing comment indentation
squash 0bda95b util06: Adding a function to check diagonals
pick d34c59b check02: Checking the array contains the correct values
pick d235bf9 check03: Checking row and column sums
pick 00212f3 check04: Checking diagonal sums
pick ca6f8df Removing TODO
pick a4a05c0 Refactoring the main check function
pick a351e8a Updates to README.md
Save your changes with Escape + :wq + Enter and you’ll be brought into another instance of Vim. This is your chance to provide a single, clean commit message for your squash operation.
Vim helpfully gives you a bit of context here, as it lists the collection of commit messages from the squash operation for context:
# This is a combination of 7 commits.
# This is the 1st commit message:
util01: Adding the checkSquare function
# This is the commit message #2:
util02: Adding function to check the range of values
# This is the commit message #3:
Refactoring the range checking function
# This is the commit message #4:
util03: Adding function to check row sums
# This is the commit message #5:
util04: Adding a function to check column sums
# This is the commit message #6:
util05: Fixing comment indentation
# This is the commit message #7:
util06: Adding a function to check diagonals
You could choose to reuse some of the above content for the squash commit message, but in this case, simply type gg to ensure you’re on the first line and dG to clear the edit buffer entirely.
Press i to enter insert mode, and add the following commit message, to sum up your squash effort:
Creating utility functions for Magic Square validation
Save your changes with Escape + :wq + Enter and Git will respond with a bit of output to let you know it’s done. Execute git log --oneline to see the result of your actions:
858e215 (HEAD -> wValidator) Updates to README.md
d42dd03 Refactoring the main check function
499c6ac Removing TODO
7530a8f check04: Checking diagonal sums
c98bb17 check03: Checking row and column sums
eec2df9 check02: Checking the array contains the correct values
2207949 Creating utility functions for magic square validation
ded7caa check01: checking that the 2D array is square
69670e7 Adding a new secret
Nice! You’ve now squashed all of the util commits into a single commit with a concise message.
But there’s still a bit of work to do here: You also want to rearrange and squash the check0x commits in the same manner. And that, dear reader, is the challenge for this chapter!
Challenges
Challenge 1: More squashing
You’d like to squash all of the check0x commits into one tidy commit. And you could follow the pattern above, where you first rearrange the commits in one rebase and then perform the squash in a separate rebase.
But you can do this all in one rebase pass:
-
Figure out what your base ancestor is for the rebase.
-
Start an interactive rebase operation.
-
Reorder the
check0xcommits. -
Change the
pickrebase script command forsquashon all commits from thecheck02commit, down to and including theRefactoring the main check functioncommit. -
Save your work in Vim and exit.
-
Create a commit message in Vim for the squash operation.
-
Take a look at your Git log to see the changes you’ve made.
Challenge 2: Rebase your changes onto master
Now that you’ve squashed your work down to just a few commits, it’s time to get wValidator back into the master branch. It’s likely your first instinct is to merge wValidator back to master. However, you’re a rebase guru by this point, so you’ll rebase those commits on top of master instead:
- Ensure you’re on the wValidator branch.
- Execute
git rebasewithmasteras your rebase target. - Crud — a conflict. Open README.md and resolve the conflict to preserve your changes, and move the changes to the
## Contactsection. - Save your work.
- Stage those changes with
git add README.md. - Continue the rebase with
git rebase --continue. - Check the log to see where
masterpoints and wherewValidatorpoints. - Check out the
masterbranch. - Execute
git mergeforwValidator. What’s special about this merge that lets you avoid a merge commit? - Delete the
wValidatorbranch.
If you get stuck or need any assistance, you can find the solution for these challenges inside the challenge folder for this chapter.
Key points
-
git rebase -i <hash>starts an interactive rebase operation. - Interactive rebases in Git let you create a “script” to tell Git how to perform the rebase operation
- The
pickcommand means to keep a commit in the rebase. - The
squashcommand means to merge this commit with the previous one in the rebase. - The
rewordcommand lets you reword a particular commit message. - You can move lines around in the rebase script to reorder commits.
- Rebasing creates new commits for each original commit in the rebase script.
- Squashing lets you combine multiple commits into a single commit with a new commit message. This helps keep your commit history clean.
Where to go from here?
Interactive rebase is one of the most powerful features of Git because it forces you to think logically about the changes you’ve made, and how those changes appear to others. Just as you’d appreciate cloning a repo and seeing a nice, illustrative history of the project, so will the developers that come after you.
In the following chapter, you’ll continue to use rebase to solve a terribly common problem: What do you do when you’ve already committed files that you want Git to ignore? If you haven’t hit this situation in your development career yet, trust me, you will. And it’s a beast to solve without knowing how to rebase!