As a developer you’ve been told many times not to force push, especially when you’re working with other people.

Turns out, rewriting commits on any repo can make your life a little tougher. But fortunately (spoiler alert) you’ll survive and write posts like this one.

The Happy Backstory

I didn’t know simply loving the idea of a portable GPG key for signing Git commits would be dangerous.

It must be known that I hate the idea of someone making Git commits under my name. It hasn’t happened, but still, GitHub’s automagic signing for commits created through the web interface has been dangling in front of my eyes for almost as long as I’ve been obsessed with OSS:

GitHub's GPG Key Commit Signing

… so why not?

The Problem

Yeesh.

All I wanted to do was sign my past commits, which apparently rewrites the IDs, which is tricky business: Source: Schwern’s well-explained Stack Overflow answer at https://stackoverflow.com/a/41883164

Here’s a quote: “If it’s a personal repository that nobody else is working on, then it’s not a problem. If it’s a repository with other collaborators, treat it like doing a major rebase.”

Note the words “other collaborators”, because I didn’t. I had opened two pull requests (which I thought would help with organization, although I realized in hindsight you don’t need to attach a PR to a feature branch to keep organized in a single-developer setting), but I was the only contributor. So, there’s no problem right? Right?

I did it!

$ git filter-branch -f --commit-filter 'git commit-tree -S "$@"' HEAD
...
Ref 'refs/heads/master' was rewritten

Great. Now let’s git push -f

and GitHub decides to close my pull requests:

hacks?

No, my account wasn’t compromised. However, it took me a few minutes of panic first to figure that out.

Why?

GitHub says this:

If you’re working in the shared repository model, we recommend that you use a topic branch for your pull request. While you can send pull requests from any branch or commit, with a topic branch you can push follow-up commits if you need to update your proposed changes. When pushing commits to a pull request, don’t force push. Force pushing can corrupt your pull request.

My emphasis; source: https://help.github.com/articles/about-pull-requests/

Perhaps this should be reworded to the following to make clear that the effects of force pushing on tracking are viral:

When pushing commits to a pull request or to a branch that a pull request depends on, don’t force push. Force pushing can corrupt your pull request.

The best example of this can be found on the actual comment screen for the PR (“No commit history in common”):

No commit history in common

That’s funny (strange), isn’t it? That’s not supposed to happen.

Wait a minute. We rewrote all the commits, and therefore the IDs, remember?

Here are the first two legacy commits, which are shared by both PR branches:

f4b2269: Code formatting in README
99677c6: Init commit

On the re-written (and signed) master, the commits look like this:

5929e26: Code formatting in README
51716b: Init commit

So I needed to revert all the commits master has in common to the commit IDs shared by the other branches.

The Solution

After hours of digging, I figured out a workaround.

Sure, I could have used one of my backups from the very tool I was rescuing, but I was sure there was a better way. Also, if I had gone that route, I would have had to recommit all of today’s changes, which sounds pretty tedious, to say the least.

No, we won’t be cherry-picking. I thought this was too simple to consider anything else. I only had to use git reflog, git checkout, and git log.

I did git checkout master, then ran git reflog:

$ git reflog
78e57c7 (HEAD -> master) HEAD@{0}: filter-branch: rewrite
78e7364 (origin/master) HEAD@{1}: filter-branch: rewrite
aedd81a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
aedd81a HEAD@{3}: rebase -i (pick)...
...

Eureka! git filter-branch was what caused all my problems to begin with. The last normal commit, aedd81a, will save me in just a moment.

Next I rewound my repo to how it was before the dreaded rewrite with:

$ git checkout aedd81a
Note: checking out 'aedd81a'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
...

Take note that last paragraph in the output output, which signals to the user that this fix could go down the drain without proper procedures in place.

I used git log to see what the checkout did:

...
commit f4b2269cbcbd7e9782b612c6a4bff26cb0cbfe2d
    Code formatting in README

commit 99677c63610a12f32a46da321b26de8dd02acd57
    Init commit

Horray! It’s as if I never rewrote the 99677c6 commit to 351716b in the first place, ruining my meticulously crafted PRs.

There was just one more step:

$ git push origin HEAD:master --force

Thanks to the git help screen for that command!

I know, I cringed at the --force flag as well, having gone through what I did. But this time the rewriting of history was to fix my ignorance, while not being ignorant of my ignorance. Could I use that word any more times in a sentence?

The Moral

I hope you enjoyed this somewhat funny story of me messing up and pulling through to reverse the mistake thanks to the power of git, and learned a lesson: don’t rewrite your commits (and therefore the SHA-1 IDs) without first taking care to merge any open PRs to not strand branches out in the wild.

Let me try that again. I learned this lesson: don’t force push, unless credentials are at stake. And even then, you can sometimes change those credentials (you anyway, because there’s no telling how many people pulled your project and therefore still have the old git history) and setup a honeypot!

But I want to force push… please?

Well, I’ll make an exception for you. If you have one branch, zero pull requests, one local repo, one developer, zero GitHub apps, and no need to worry about confusing a package registry, then congratulations, you can force push your typo fixes all day long.

For the rest of us, please don’t ever force push.