I have a penchant for submitting pull requests with silly grammatical and/or technical errors, like this gem I submitted the other day with a simple wording flub:
In this commit I talk about a repo's "master" branch, and then link to its “end” branch. Nice one TJ. Nailed it.
Luckily, my grammar-savvy colleague caught my blunder before this wording was merged in. But now what?
I could add another commit that fixes the wording, but that would mean two things:
- 1) I would clutter this repo’s history with a trivial commit.
- 2) My stupid mistake would live in git history until the end of time. It would metaphorically be on my permanent record. And Git doesn't forget.
Now, you could argue that a cluttered history is no big deal, because over time a distributed git repository with a large number of contributors inevitably turns into a cluster of pain and sorrow, and that searching through this nebulous void is an exercise in futility. You definitely could argue that.
But I would argue against you, in favor of a clean git history that's reasonably easy to search through. You never know which files you'll need to
git blame in the future in search of answers.
So if you’re not adding a new commit, how do you solve this problem?
Given that we’re talking about Git, there are probably 27 different ways you could go about fixing this problem, but I’ll explain what works for me: rebasing.
If you’re not familiar with
git rebase it’s an incredibly powerful command that lets you rewrite history, rearrange commits, and probably time travel too.
git rebase has 20+ options and 10+ merge strategies. Three of the merge strategies are named patience, resolution-dependent, and octopus — and I only made one of those up.
git rebase can seem overwhelming, but the basics actually aren't all that hard to grok. Let’s say you have a feature branch named "stupid-mistake," in which you’ve committed the single biggest grammatical atrocity I know of:
If you’re not catching the mistake, it’s paramount that you take a moment to read up on its versus it’s. I can wait.
If this commit lives on it will surely shame your family for generations to come, so let’s look at how absolve you of your sin against the grammar gods.
The first thing you'll want to do is add a new commit that fixes the problem. Yes, yes, I just told you a new commit wasn’t the answer, but stick with me for a moment. You should now have a git history that looks a little something like this:
This is where
git rebase comes in. You can think of a git rebase as a sort of redo for your entire branch. Git will replay the branch’s history one commit at a time, and during that replay process you have the chance to alter history on the fly.
To perform a git rebase, you first need to know the branch your feature/topic branch is based off of. This is usually
master, but you may have a more custom git workflow that you're using. (And unfortunately, if you don’t know your branch’s parent, determining it is shockingly hard.) For this article I'll assume your branch's parent is
So head to your terminal, make sure you're on your feature branch ("stupid-mistake" in this case), and type
git rebase -i master:
-i flag tells git to do an interactive rebase, which means your editor will open with a display that may give your UX designer nightmares.
This screen is pretty self explanatory so let’s move on — lol just kidding. I'm not sure which is more scary here: the lack of clear instructions for what in the world to do with this UI, or the giant scary text that tells you that if you remove a line your "COMMIT WILL BE LOST."
As scary as this screen seems (and quite frankly is), once you learn the basics it’s pretty easy to use. As git reapplies each commit during a rebase, this UI is how you change how each individual commit is applied.
See the word "pick" on the first two lines? Pick is the default behavior, and basically means to just apply the commit without changing anything. But you can also edit commits on the fly, reword commit messages, and a whole lot more. In this case we want to use the "squash" command, as it takes one commit and, to use git’s own wording, melds it into the previous commit. How do you actually apply the squash? You just change the word "pick" to "squash" (or "s") in your editor.
And uh yeah, I’m not kidding. In your editor, change the "pick" on line 2 to "s". Save. And close your editor.
Git will then give you the opportunity to change the commit message of your new, melded commit.
I usually just remove the second commit message, but you’re free to do whatever you want here. Save and close your editor when you have a commit message you’re comfortable with.
Your stupid mistake is now gone from your local git history, but if you’ve pushed your branch to a remote repository (e.g. GitHub), you’ve got one more step. If you naively attempt to push your branch at this point git will stop you in your tracks.
Git is complaining because your local branch is not in sync with its remote counterpart. You can override git and push anyways with what's called a force push, but before I show you how to do that, I have to get one obligatory warning out of the way:
NEVER FORCE PUSH TO A SHARED BRANCH ON A SHARED REPOSITORY
This technique isn't something you want to use on
master, or any other branch that others might be working on. If you do force push, your co-contributors (upon running
git pull), will be sent to an out-of-sync state I personally refer to as "git hell" — where reflogs are sentient and Linus says condescending things about your family.
For a good explanation of why force pushes can be bad, read Will Anderson's discussion on the topic. There are Star Wars gifs.
Despite this warning, in my experience most pull requests, especially those done on public GitHub repos, are done on simple feature branches where only one person has commits, and therefore force pushing is quite safe. And as long as you’re there by your lonesome, you can bust out the git version of
sudo by adding the
--force flag (or
-f) to the
git push command.
With this your task is now complete. You’ve successfully removed your silly mistake from history.
Although there are a decent number of steps involved in removing a commit from a branch you’ve already pushed, over time I think you’ll find the process to be fairly quick and easy. Enjoy rewriting history!
Header image courtesy of Tuomas Puikkonen