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:
… so why not?
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?
So I did it…
$ git filter-branch -f --commit-filter 'git commit-tree -S "[email protected]"' HEAD ... Ref 'refs/heads/master' was rewritten
Great. Now let’s
git push -f…
and GitHub decides to close my pull requests:
No, my account wasn’t compromised. However, it took me a few minutes of panic first to figure that out.
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”):
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.
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 checkout, and
git checkout master, then ran
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.
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?
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.