Fixing Stupid Mistakes in Git Pull Requests

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:

Simple wording mistake in a pull request

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?

Git rebasing

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.

Seriously though, 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.

Anyways. 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:

Simple grammatical mistake in a commit

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:

Failed commit history

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 master.

So head to your terminal, make sure you’re on your feature branch (“stupid-mistake” in this case), and type git rebase -i master:

Running git rebase from the command line

The -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.

The UI of an interactive git rebase

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.

Instructions for squashing a git commit

Git will then give you the opportunity to change the commit message of your new, melded commit.

Change the commit message of a squashed git 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.

Showing which part to remove when crafting a squashed git commit

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.

A forced git push being rejected

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.

A forced git push succeeding

Hooray!

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

Comments

  • Nino

    if you use f (fixup) instead of s (squash) you do not have to remove the second commit message, since it isn’t there…

    Which, if you would read the exaplanations, is also explained in your very own screenshot…

    • Ah, indeed. I’ve been doing this for years and somehow never noticed 😀 Thanks!

  • Andrew

    Another alternative is `git commit –amend` if you’re just modifying the most recent commit.

  • Tikitu de Jager

    For a slight (very slight) safety blanket under that `–force`, try `–force-with-lease` instead. It works like `–force` but fails if there are commits on the remote branch that you haven’t fetched yet. In other words: “force but only if my picture of the remote branch is up to date.”

    • Oh wow, that’s actually really cool. Like a lot of things in git I had no idea that existed. Thanks for the tip!

      • Tikitu de Jager

        You’re welcome! Do beware the thinness of the safety blanket though! If you `git fetch` then without looking immediately `git push –force-with-lease` it’s just as destructive as a straight `–force`. It doesn’t protect you from losing work, only from losing work *that you haven’t fetched yet*.

  • Pingback: Dew Drop – October 30, 2015 (#2123) | Morning Dew()

  • William Manley

    I like to use `git commit –fixup` and `git rebase -i –autosquash` for this.

    In this instance you’d run `git commit –fixup=8ad1093` which would create a commit for you named “fixup! Adding a very useful sentence”. The advantages are that you don’t need to come up with a commit message yourself and it is clear from the commit logs exactly which commit your fixup is for.

    I’d then run `git rebase -i –autosquash master`. This will pop up the rebase editor as before but it would already say:

    pick 8ad1093 Adding a very useful sentence
    fixup abcdef1 fixup! Adding a very useful sentence

    so you don’t need to change anything, just exit the editor and you’re done.

    This technique is particularly useful when you have many commit on a branch that you want to fix up. It removes ambiguity and thinking at rebase time which already requires some care.

  • J. Brad Harris

    “how [to] absolve” – FTFY. 🙂