Rewriting History with Git

by Gennie Harris

When I first joined the Singlebrook team, one of the things I was most excited to learn about was git; in fact, my first ever Singlebrook blog post was a brief-but-giddy overview of the program. In the months since then, I’ve spent a lot of time learning to use git to work effectively in various team scenarios. Luckily we have some git-masters in the Singlebrook team, so I’ve picked up a few tips and tricks that can elevate your git experience to a whole new level. Though there are myriad ways for git to improve your workflow, today I'm just going to focus on maintaining a clean commit history.

Patch Add

It can be incredibly easy to get caught up in some exciting new addition to a project and forget that there’s a world outside of this function or that controller...until you check the clock and somehow three hours have gone past. Maybe you were pair-programming or perhaps you were just in the zone, but suddenly you’ve accomplished six different commit-worthy changes, all mingled together across fourteen different files. I can’t claim that I’m a perfect git user who never lets this happen to me, but I can certainly make it seem that way in my history.

When changes get jumbled together like this, many people turn to GUI’s such as GitX to simplify things - yet it’s easy enough to untangle through the command line alone! By adding the --patch (or simply -p) option to your git add command, you can refine your git abilities from a broadsword to a veritable scalpel of staging changes. A patch add presents you each ‘hunk’ of changes individually to allow you to stage changes section-by-section instead of as entire files. Each hunk is accompanied by the prompt Stage this hunk [y,n,q,a,d,/,e,?]?

Responding with a question mark brings up the complete list of options, but the ones you’re most likely to use are:

  • [Y] - Yes, stage this hunk
  • [N] - No, do not stage it
  • [Q] - Quit, don’t stage this hunk or any of the rest
  • [S] - Split this hunk into smaller sections (which only works if your changes are separated by unchanged lines)
  • [E] - Edit the hunk manually - this option might be a little scary to use at first, but it’s perfect for when you have consecutive lines of change and only want to commit some of them

Using these and the rest of the patch add commands, you can easily add snippets of your changes and break that swath of edits into manageable commits.

Interactive Rebase

When it comes to maintaining a clean commit history, git’s interactive rebase tool is an ideal companion for patch add. Interactive rebase allows you to literally rewrite your history - combining or reordering commits, updating commit messages, or even stopping to amend commits.

You can initialize an interactive rebase with git rebase -i and then the parent of the last commit you’d like to edit (for example, git rebase -i HEAD~3 will allow you to edit your last three commits). You can also specify a branch to rebase against to edit only the commits unique to your branch. If you don’t specify a commit, interactive rebase will default to those you’ve made since last pushing to your remote - this is especially useful since you should generally avoid editing commits that other developers have already pulled.

But what actually happens when you run an interactive rebase, you ask? Let’s say you create a new branch off of master, make six commits, and then run git rebase -i master. Your editor will show you a git-rebase-todo that looks something like this:

pick cdf4578 Update the readme
pick 74a6bb9 Update the readme again (whoops)
pick 4dbe6a4 Make an actual change
pick 6892209 Make a related change
pick 51b8354 Implement another change that should probably actually happen before all of that
pick 5bae0e3 Add this perfect chnage excetp for the typos in the commit mesagge

Below the commits you’ve specified, you’ll also see your available 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
# These lines can be re-ordered; they are executed from top to bottom.

Most of the commands are pretty straightforward; in our case we’ll probably edit the file to look something like this:

pick 51b8354 Implement another change that should probably actually happen before all of that
pick cdf4578 Update the readme
fixup 74a6bb9 Update the readme again (whoops)
pick 4dbe6a4 Make an actual change
squash 6892209 Make a related change
reword 5bae0e3 Add this perfect chnage excetp for the typos in the commit mesagge

Here we moved one of our commits backward in history so it looks like we completed it before all the other changes; we then specified that 74a6bb9 should actually be included with the previous commit (while using the first commit’s message as is). We then decided that 4dbe6a4 and 6892209 should be ‘squashed’ together, but with a new message. Finally, we want to change the wording of the last commit’s message to get rid of those pesky typos.

Git will now replay each commit one by one, pausing as necessary. In our case, it will pause once so that we can edit the commit message for our squashed commits, showing us something like this:

# This is a combination of 2 commits.

# The first commit's message is:
Make an actual change

# This is the 2nd commit message:
Make a related change

It will pause one more time so that we can reword our sloppily-written commit message. It will also pause if there are any merge conflicts because of our reordering and allow us to resolve them before continuing on. Using interactive rebase, we can then take that sloppy history and turn it into this:

51b8354 Implement another change that should probably actually happen before all of that
cdf4578 Update the readme
4dbe6a4 Make these two related changes
5bae0e3 Add this perfect change with a typo-free message

Not only does this make us look like a coding whiz who never makes a single mistake and always commits changes perfectly, but it can make pull-requests much less painful for team members who are reviewing your changes.

It doesn’t always seem worth it to take time out of actually, y’know, gettin’ stuff done just to play around with your git history, but it’s important to remember that your commits tell a story. Just as no one wants to listen to a storyteller ramble on about insignificant details and go off on tangents, a messy git history can be confusing or misleading to teammates or future developers on a project. By simply integrating some of git’s awesome features like patch adding and interactive rebasing, you can let your project history tell its story cleanly and concisely, and make it easier and more enjoyable to work on for everyone involved.