If you have worked with git for a while, you probably have already noticed that it is a powerful tool. Recently I needed to revert a change that has already been pushed to a remote branch, but I wanted to keep the history unchanged, and git gave me an option (or options) to do this as well. This short post will describe how that can be done.

The problem

Imagine that you have the following history on your master branch:

7f05a03 - (HEAD -> master) version 4 (10 seconds ago)
7c9b396 - version 3 (3 days ago)
75a8d4e - version 2 (3 days ago)
8d30133 - version 1 (3 days ago)

Unfortunately, it turns out that version 1 was the last one that was working correctly. Since you are a responsible team member, and your whole team relies on master branch (duh!), you want to keep the history as is and add some revert commits on top. This, by the way, also gives you and your team a possibility to learn from your mistakes maybe?

If you are using GitHub, and all the changes were made with Pull Requests, you could revert them one by one using graphical UI, but there are a few potential issues here. You'd rely on an external tool, you would need to do the changes one by one (with reviews each time maybe?), and you would not learn anything about git. I take learning over not learning any day of the week. So let's dive in!

Possible solutions

One by one

The simplest approach to reverting a few commits is to run... git revert command a few times:

$ git revert -n 7f05a03
$ git revert -n 7c9b396
$ git revert -n 75a8d4e

It's important to add -n flag there because otherwise you'd get prompted for a commit message after every step, and you'd end up with three revert commits. Since you want to get back to a specific version, one commit should be all you need at the moment. The downside here is that you need to perform revert N times (for N commits you are ahead of a working version).

Double reset

If you feel too lazy (or you have a lot of commits to revert), you can do the same thing with just two commands, regardless of the broken commit count. The first step would be to do a hard reset to the last working version, then soft one to the head of the remote branch.

$ git reset --hard 8d30133
$ git reset --soft ORIG_HEAD

The first reset changes the state to all your files to the way they looked on version 8d30133. The second reset keeps the files as they are, but marks the difference between their current look and the remote branch (ORIG_HEAD represents the last remote commit) as a local change. That marked difference allows you to create a commit that contains changes between these to versions, which represent a revert of all the commits in-between.

Checkout with a twist

I can already see you saying that making two commands to reset the changes is too much work. Being a lazy programmer myself, I spend some time to check if this is indeed possible, and git proved to be so powerful yet another time. Prepare yourself to be amazed:

$ git checkout -f 8d30133 -- .

What it does is resets (with force, so by overwriting any local changes) to a specific version. The true magic happens at the end of this command. You may know that -- is a separator after which you pass a pathspec where you ask git to replace the files in the index (the changes to be committed) with these coming from a version represented by that specific commit hash. It's maybe complicated to explain, but by running it you can check the outcome yourself. It works!

Summary

As you can see, there several ways to achieve the same thing in git. I recommend playing around a bit with each of the solutions and choose one as your go-to strategy in case of an emergency. Or maybe test your code better before pushing to a remote branch? The choice is yours :)