In addition to a branching strategy, which seems to get a lot of attention, there is also a strategy for git revert. I suppose this is more of a set of good practices and training than a strategy. Whatever you want to call it let’s take a little time to talk about some options for undoing changes. But first, let’s clear up a few things on git revert itself by describing exactly what happens when someone does a git revert
First, we’ll create a simple repository. As I learn and internalize best by doing. I think others do as well.

mkdir revert-project
cd revert-project
git init
echo "First line of first file." | cat >> file1.txt
git add .
git commit -a -m "First commit of first line of first file."
echo "Second line of first file." | cat >> file1.txt
git add .
git commit -a -m "Second commit of second line of first file."
echo "Third line of first file." | cat >> file1.txt
git add .
git commit -a -m "Third commit of third line of first file."

With this series of commands we have a simple repository with one file that contains:

First line of first file.
Second line of first file.
Third line of first file.

And we have a repository that has 1 commit for each line in the file.
The question some people have is what exactly happens when a commit is reverted? Let’s revert the second commit. What we’re trying to do is undo what happened for the second line in the file. There are a couple of logical possibilities.

  • A checkout of the first commit effectively happens. That is we revert and anything done in that commit, and after, is undone.
  • Only the changes in the second commit are undone and the rest is unaffected. That is line 3 will be preserved.

The hope is that Git is awesome enough that we can do the second operation because we already have a mechanism to checkout and branch from an earlier commit. And it turns out this is the case. But in making the modification Git understands there is a problem. The issues lies in the fact that Git stores objects in a content addressable file system. And the second and third commits are going to modify that same content by making this revert, that is line 2 of the file. The result of the revert commit is to change line 2 to what was in line 3. And the developer must resolve this conflict manually by making the edit, adding it to the index, and making a new commit.
To see this for yourself, you can do a git log and find the SHA-1 of the commit and use it to do the revert. My SHA-1 is used in the example but you’ll have to substitute your own.

git revert 06ae78aa6113f9b3f0f7d853f2a203fe1e70018f

Git will tell you there are conflicts and I’ll assume you resolved them with your editor and saved the changes. When you’re done, line 2 should contain the contents of line 3 and the original content of line 2 will be gone.

git add .
git commit -a

When you’re done a “git log” will show the revert and the conflicts. This will be sharable with others who are using a shared repository.
Let’s roll the repository back a bit and try another scenario.

git reset --hard HEAD~3

At this point I’m expecting only the first commit to be in the logs. We will now add a couple more.

echo "First line of second file." | cat >> file2.txt
git add .
git commit -a -m "Second commit of first line of second file."
echo "First line of third file." | cat >> file3.txt
git add .
git commit -a -m "Third commit of first line of third file."

Let’s try the git revert action on this scenario.

git revert 38a4b9064176eb9e61319381fe85839625cbc0c6

Ahhh, yes. The revert happened without any conflicts. Only the second commit was undone. Subsequent commits are still in tact. At this point only file1.txt and file3.txt exist. The content address for the object involved in this revert was different so that’s why there were no conflicts.
One more scenario. We can revert a range of commits. Let’s wheel back the repository one more time to remove the last revert:

git reset --hard HEAD~1

If you specify a range of commits or several commits at once, an individual revert will be created for each. This makes sense because those commits were created as individual pieces of work. So keeping them separate means you can choose a single revert in the future to “undo” the “undo”, that is put an individual commit back. There is no limit to the number of times a unit of work. that is a commit, can be reverted. I’ve seen it done as many as 6 times in a particularly problematic area!
Let’s try reverting range of commits and squashing the revert. I generally never squash anything myself because I don’t like to lose information. But it might make sense to squash everything in a revert. In order to only undo part of the squashed revert, you’ll have to revert the revert and then create a new revert with only the parts you want to revert. More effort for that scenario. But if you believe the commits should be grouped so a revert of the revert will be simpler, it might make sense to squash it so that undoing all of the undo is simpler in the future. Confused yet?
One more thing to note in our contrived example. You can’t specify the first commit in the range, although you can specify it in a list of reverts. I consider this a bug but most people in the real world will never come across this 😉 Because of this, I’ll specify without the range first to undo the first and second commit. And then we’ll do an example of the second and third commits with a range.

git revert HEAD~2 HEAD~1

Only file3.txt should exist. So, again we have 2 reverts and we want to squash them.

git rebase -i HEAD~2

This is the tricky part but I’ll show you the interactive part and my edits as an example:

pick b546298 Revert "First commit of first line and first file."
squash 00f982d Revert "Second commit of first line of second file."

The key here is the second part started as “pick” and I changed it to “squash”. The first one has to be “pick” and the second and subsequent lines should begin with “squash” to bring those 2 commits together. Then Git will show you the results of your squash. At this point, you can complete the commit. Check that file1.txt and file2.txt are removed and that file3.txt exists. Now revert the squashed revert.

git revert HEAD

“file1.txt”, “file2.txt”, and “file3.txt” should all exist because we reverted the squashed revert.
Wheel back the last 2 commits for our final example:

git reset --hard HEAD~2

As promised, here is another example of specifying a range. This will revert the second and third commits

git revert HEAD~2..HEAD

Again we have 2 individual revert commits. You can practice squashing those as we mentioned above. When you’re done only file1.txt should exist. And finally revert the squashed revert to bring all 3 files back to the way they were.
That got a little crazy in the previous examples. But it will allow you to share your code with others safely and effectively. It is never a good idea to rewrite history that has already been shared. Everyone will learn to hate working with you because they will keep having to fix their repositories whenever you are involved. Git revert gives us a formal way to share repository rollbacks with others that won’t make them have to fix up their own repositories to start doing their real work.
One more thing I should mention. It is possible to checkout a previous version of the code and recommit it so as to avoid having to use “git revert”. Changes will be properly sharable. This will work, but there are some pitfalls to doing this. See my post on Git – Full History for details of why this can trip people up and may not be something you want to do.