Git is an incredibly powerful and useful tool for anyone developing software (as are its less popular cousins like CVS, SVN, Mercurial and Perforce). Version Control enables safer, more sensible software development; it provides visibility and traceability for changes; and it is absolutely incredible for smoothing out the workflow when multiple people are working on the same project. However, there are still times when version control can run into problems that are difficult to tackle with the normal set of tools.
Let’s imagine, for a moment, that there’s been a bad merge. Perhaps someone absent-mindedly deleted an important file and that change ended up being merged to your master branch. Once you've copy-pasted the first 20 commands off stack overflow, your branch is worse than before, and your boss is looking at your history with disgust; It's time to give these options a try. The first step, of course, is to revert the commit and allow Git to sort out how to make the issue go away. This is generally a sufficient measure for simpler networks of branches, but I’ve personally come across situations where this caused terrible complications. For reference, the situation in question had an accidental merge to the master branch for the project from a feature branch, which was branched off of a secondary trunk where a larger set of connected features was being developed before being included in the master branch. The merge went through; we spotted the issue; we reverted the merge commit. The problem came in when we tried to merge the secondary trunk branch into the master branch - despite us reverting the bad commit, the master branch was under the impression that it was ahead of most of the changes in the secondary trunk branch.
Fortunately Git does provide some very powerful tools to help manage more complicated situations like the one I mentioned above. The article should hopefully give you some hints as to how to apply those tools (and hopefully some copy-pasteable commands to speed the process up). Most of the strategies and hints noted here are applicable to any Git-based repositories, hosted or not, but may require certain permissions and the use of terminal commands.
The branch is a mess, what now?
Diagnosing with Bisect
If you have a remote repository you need to push whatever fix you make to, please refer to the next section after using one of the following methods.
Diagnostics for this kind of problem should start with the simplest possible solution: can you fix this in whatever feature branch the issue comes from, and do another merge to replace the problem with the fixed version? This is ideal, if possible, as this is typically the least likely to cause issues down the line and also fits into your usual git flow.
If you are not sure where the big problem came from (i.e. which commit), this is the step to use Git’s bisection tool (otherwise just get the ID of the bad merge commit using git log). Use:
git log
git log will find a commit to the master branch at which the issue was not yet present; this does not need to be the last commit before the issue, just whatever commit you find first that did not yet contain the issue.
Next, start the bisection process:
git bisect start git bisect good <commit ID for commit without the issue> git bisect bad <commit ID for most recent commit>
At this point, Git will select a commit to look at, and check it out for you. Check whether the issue is present; if it is,
git bisect bad
if not,
git bisect good
Repeat the above until Git prints a line in the form: <commit ID for bad merge> is the first bad commit
Once you have the relevant Commit ID on hand, you know where you need to go back to.
Reverting
If no simple fix is available to merge in (perhaps it’s something that affects a lot of files and is hard to reverse), can you simply revert the commit and have the problem go away? If it was an unintentional delete or a bad change on a feature branch being merged back into the root it was branched from, this is usually a fairly safe option:
git revert <commit ID>
to revert the changes (or use the appropriate buttons on the UI you use, if available). Do be aware that this can still cause issues if you do need to merge anything into that root branch from the offending feature branch later on, as the commit plus reverting commit restores the content of the root branch, but leaves the root branch believing it already contains the changes from the offending feature branch up to the bad commit. My best advice after any git revert is to move any changes that you need from the relevant feature branch to a new branch (manually, not with a merge) to avoid odd conflicts during later merges.
Rebasing
Ok, what if a git revert is not going to work for your situation? The next solution is to use
git rebase -i <safe commit ID> The safe commit ID above should be for the commit before the issue was introduced. Git will then show a list of commits (much like git log would show) in the format: pick a2a2a2a Bad commit pick b1b1b1b More recent commit(s)
Remove the line(s) in the list that represent the bad commit(s) - please note, though, that the rebasing process may run into conflicts and trouble if the removal of the bad merge commit conflicts with later changes. If the conflicts are easily fixable, follow the instructions provided by Git and complete the rebase; if not, abort the process with:
git rebase --abort
and move on to the next method.
Rewinding and Restoring Manually
The next solution, while likely the most reliable and safe solution, is unfortunately both a little labor-intensive and requires permission to change the commit history:
git checkout <root branch> git pull Run git log
and note down all the commits after the one you want to fix. The next step is to move the branch pointer for the root branch to the commit before the issue was introduced (get this from git log if needed):
git update-ref -m "reset: Reset <root branch name> to before <problem description>" refs/heads/<root branch name> <last safe commit ID>
The branches you need to re-merge to the root branch will be one of two types: ones that were branched off before the bad merge and didn’t merge in the root branch after the bad merge. Those you should be able to just merge in again (keep a careful eye on this, though, in case one of the branches that seem to be this type is actually the second type).
The second type of branch will have the bad commit as part of their history. Unfortunately running git rebase <root branch>on these branches will not be sufficient. You can try the interactive rebase method noted above using git rebase -i <safe commit ID> on these branches, but if the problem is complex or extensive, this may be more labour-intensive than manually transferring the required changes from these branches to one or more branches branched off of the root branch after the git update-ref step.
Branches that had not yet been merged but had been branched off of the broken root branch will need similar treatment to the second type of branch mentioned above.
The branch has been revived, but what about the remote repository?
The methods above all show you how to fix the issue on your local repository, but for most software developers there is an upstream/remote repository that also needs those exact changes to ensure everyone is on the same (repaired) page. The tricky part here is not the command, but to ensure that you have the required permissions and, if need be, temporarily disable protections that prevent you from pushing directly on the root branch. Then run:
git push -f
Do note: forcing a push (as with that command) is considered a destructive operation (makes changes and removes previous state in a non-reversible way) and should be done with caution and after providing plenty of warning to other people working on that repository to ensure that they know to be careful and ensure their current branches are repaired as required to avoid merge conflicts later on.
