If you’re using git as an integral part of your development, source control and deployment workflow like we are, chances are you’re following some kind of policy such as git-flow, or at the very least branching and merging. Generally speaking, you should never get into a situation where you need to revert a merge once you’ve completed it. You should have tests in place to ensure code works before it gets anywhere near the master branch. However, oversights can happen, so here’s what you can do to help rectify the situation.
The basic scenario is thus; you branch off master into a separate feature branch, and you commit your work there as commits A, B, C. You then decide you’re ready to deploy that branch, so you merge into master and you push the code out to production. Everything seems to be running smoothly, as the tests predicted, until you notice some horrific production-specific bugs happening and you’re forced to roll back. At this point some readers will be thinking “aha! That’s why I use a deployment tool like Capistrano to maintain build history and allow me to quickly roll back” but most of the time you’ll also want to ensure that your git repository has stable code on the master branch at all times. This is to prevent team members and other agents (such as automated CI tools) working on and potentially deploying faulty code.
If you can, use your deployment tool to immediately roll back, and that’ll buy you some time to figure out what to do next. First port of call is debugging. Can you figure out if it’s a trivial bug that can be fixed quickly so you can redeploy the new stuff? It’s worth spending a few minutes at this point to decide this. If it turns out it’s a trickier issue, you’re best off humbly prying your merge out of master, reverting master back to a functional state, and getting the problem sussed out on your feature branch.
But wait. If you did a clean, straight up git merge feature-branch into the master branch, then commits from your feature branch are likely to be intertwined with other commits in the master branch’s history. Blast. That means we can’t just do a straight up git reset –hard to a point before we merged our feature-branch.
I uncovered a detailed document explaining the process of reverting a faulty merge, fixing up the branch, re-merging the branch and the resultant implications. I encourage you read and understand it before doing anything. The long and short of it is – you can nullify a faulty merge by running git revert -m 1 merge-sha1. This creates an inverse mirror commit that negates all of the changes introduced into master by the merge from your feature branch. Note that this does not “undo” the merge. The merge and its reversion both exist in the git history, it’s just the data that’s been returned to it’s original state. As Linus Torvalds explains in that document “Yes, it undoes the data, but no, it doesn’t undo history”.
So now you have a commit in your master branch history which nullifies your changes and signposts the fact you reverted the merge. You can push this to your remote repository, the code will return to its original functional state and other developers shouldn’t trip up on any nasty conflicts. You can then continue fixing up your branch (or if you’ve already deleted it, just branch off from the commit before your revert commit) until everything works. Then when you’re ready to merge back to master, you revert your revert commit, and merge in the additional changes on your branch.
This may seem clunky and sure, it’s sub-optimal if you’re working alone on the branch in a small repo. If that’s the case then you’d probably prefer pristine history. That said, the methods outlined in that article involve compromising bisectability through the use of –no-ff, or rewriting the git history via git rebase, which requires a forced push update on the remote repo and causes conflicts when other developers try to pull the latest down. The trade-offs we’re making here are geared towards bigger teams so we avoid conflicts and elucidate a fully transparent project history.