This post is about getting a working knowledge of git revert and how/why it may be used. The target will be to gain a working knowledge of how to use git revert to roll back unwanted changes to a branch.
The documentation for git revert can be found HERE . We will look at some of the different flags and sub commands outlined in the documentation. For a training ground, I have created a GitHub repository that to use as the example project that needs the roll back work to be done in. It's better to break something that does not matter before we need to depend on these commands in a real project. The example project can be found HERE.
Before we start I am doing this on a Linux operating system but as git is the command line tool I don't see why these commands will not work across different platforms. All the same I just want to put that warning out there.
Example 1 Single commit revert
Now lets get started. The first thing that is done is we create a the sample files and commits on the main branch. For each of these commits we add a new file which contains a line stating which commit it was added in and with add this line to the last file create. All the files are created in a samples directory.
# Sample contains of a file
--> cat samples/commit_3.txt
commit 3
commit 4# Structure of the project
--> tree
.
├── README.md
└── samples
├── commit_1.txt
├── commit_2.txt
├── commit_3.txt
├── commit_4.txt
└── commit_5.txt
1 directory, 6 files# Current commits in the project
--> git log --oneline
c216f8e (HEAD -> origin/main, origin/HEAD, main) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit
Now that we have a bases to start with, what is going to be the example use case that we will use. Lets say that the change created in "commit 3" are unwanted and need to be removed. As I may (most likely) will make a mistake the first time I try this, I will create two branches to safely work with. The first branch will be pre-git-revert and the second is going to be post-git-revert-example-1. This allows use to keep the main free of any unwanted changes while also letting us compare the changes at the end.
# View current branches
--> git branch
* main# Create new pre-git-revert branch
--> git checkout -b pre-git-revert
Switched to a new branch 'pre-git-revert'# Create new post-git-revert-example-13
--> git checkout -b post-git-revert-example-1
Switched to a new branch 'post-git-revert-example-1'# View current branches
--> git branch
main
* post-git-revert-example-1
pre-git-revert
First we need to get the commit id for the commit we wish to revert. This can be done using git log. Then we add the id to the revert command, which should now revert the changes as required.
# Get the commit id for the 3rd commit
--> git log --oneline
c216f8e (HEAD -> post-git-revert-example-1, origin/main, ...) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit# Use git revert with the commit id
--> git revert 19896c0
CONFLICT (modify/delete): samples/commit_3.txt deleted in parent of 19896c0 (commit 3) and modified in HEAD. Version HEAD of samples/commit_3.txt left in tree.
error: could not revert 19896c0... commit 3
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
From the returned output we can tell that the revert did not happen cleanly and this is way we create separated branches just in case. Conflicts can be common with using git and should not be a thing to worry about. It's all about how we approach fixing the issue but first we need to known whats worry. Simplest way to start is to use git status.
--> git status
On branch post-git-revert-example-1
You are currently reverting commit 19896c0.
(fix conflicts and run "git revert --continue")
(use "git revert --skip" to skip this patch)
(use "git revert --abort" to cancel the revert operation)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: samples/commit_2.txt
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add/rm <file>..." as appropriate to mark resolution)
deleted by them: samples/commit_3.txt
Now we can see that the issue lies with "samples/commit_.txt", which makes sense. If you remember when we created the files we added a line to the previous commit file so "samples/commit_3.txt" will look as below. It is up to use to decide what the correct action to fix this problem should be.
# Contains of "samples/commit_3.txt"
--> cat samples/commit_3.txt
commit 3
commit 4
To fix the issue we have really three choices that can be made.
The file is correct so we don't remove the file or the line "commit 3" from the file.
The file is required but we need to remove the line that says "commit 3".
The file is no longer need and it's safe to remove. This also means removing the "commit 4" .
The next steps is to edit "samples.commit_3.txt" and remove the "commit 3" line. Of course this can be done using any editor but when on the command line I like to use vim (There is some really usefully shortcuts in vim)
# Open the file to edit
--> vim samples/commit_3.txt# Press "d" twice on the line to be removed
# Save and quit the file with ":wq"
That should be the file edited. In the real world this change could be more complex and require some testing. For peace of mind we will do the git status again just to insure with did not break anything else and also get what the next commands should be. We should still be seen the same error message as earlier. This time we know we want to keep the file by using git add. Then once the file has been added we can run git revert --continue.
# Check the status to ensure nothing new has a issue
--> git status
On branch post-git-revert-example-1
You are currently reverting commit 19896c0.
(fix conflicts and run "git revert --continue")
(use "git revert --skip" to skip this patch)
(use "git revert --abort" to cancel the revert operation)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: samples/commit_2.txt
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add/rm <file>..." as appropriate to mark resolution)
deleted by them: samples/commit_3.txt# Add the updated file
--> git add samples/commit_3.txt# Continue with the revert
--> git revert --continue# Opens the commit message
Revert "commit 3"
This reverts commit 19896c05177ff6e7d61e1463bce79c6037f7720e.
# Conflicts:
# samples/commit_3.txt
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch post-git-revert-example-1
# You are currently reverting commit 19896c0.
#
Changes to be committed:
modified: samples/commit_2.txt
modified: samples/commit_3.txt
## Up date the message as required and save the the changes with ":wq"
When rolling back changes I feeling adding the "Changes to be committed" block, it's could be usefully at a later stage. But that is just me. As this is only an example revert I have not put much effort into the commit message but you really should. The reason why the this commit was been revert should have been include or at least links to where more information can be found such as a JIRA for the work.
So now what? If you do a git status you will find that you are on a clean working tree. So what does the commit log look like.
--> git log --oneline
ec18660 (HEAD -> post-git-revert-example-1) Revert "commit 3"
c216f8e (origin/main, origin/HEAD, pre-git-revert, main) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit
This is where what I was expecting did not happen. I was not expecting to see "19896c0 commit 3" still in the log but I guess it makes sense that it is still there. You want to keep history as much as can. We can also check what has changed by doing a diff against the pre-git-revert branch,
We can see that the only changes made is the removal of "commit 3" from two files which is what we expected.# Compare both revert branches
--> git diff pre-git-revert
diff --git a/samples/commit_2.txt b/samples/commit_2.txt
index 19ea448..c1acd67 100644
--- a/samples/commit_2.txt
+++ b/samples/commit_2.txt
@@ -1,2 +1 @@
commit 2
-commit 3
diff --git a/samples/commit_3.txt b/samples/commit_3.txt
index b1f2612..bd7b200 100644
--- a/samples/commit_3.txt
+++ b/samples/commit_3.txt
@@ -1,2 +1 @@
-commit 3
commit 4
Example 2 Multi commit revert
In this example we will look at reverting more than one commit at a time. We will set up the branches as before make a clean branch off of the pre-git-revert branch. This time we will call the new branch post-git-revert-example-2. See the example above to know how to do this. A lot of this example will be the same as the first example. The project structure is now the same as the start of the last example.
Now in this example we are going to remove commits 2 and 3. As before we will get the Id's for the commits using git log and then start the revert using git revert.
# Getting commit id's for 2nd and 3rd commit
--> git log --oneline
c216f8e (HEAD -> post-git-revert-example-2, origin/main, origin/HEAD, pre-git-revert, main) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit# Using git revert with the commit Id's as you think it would be done
--> git revert -n dd89729..19896c0# Nope That does not work :(
So that does not work as expected which is ok that's why we are doing these examples before needing to do this for real. The example in the docs is git revert -n master~5..master~2 which I convert to what I hoped would work for me using the commit id's. The problem seems to be there is no clean way to undo commit 3 as commit 4 affects the file which was created. Of course I turn to the internet for answers and every example I found is revert back from the Head of the branch. That is not what we are trying to do. We still need to find an answer to this problem, so lets make one up.
We know we can revert one commit at a time, so lets do that. This will be slower but works. The next issue with doing this is there will be multiple revert commits but this we can tidy up by squashing the commits together. So lets start the slow process of revert each commit, start with the newest and working backwards. I am getting the Id's from the git log command earlier.
# Reverting commit 3
--> git revert 19896c0
CONFLICT (modify/delete): samples/commit_3.txt deleted in parent of 19896c0 (commit 3) and modified in HEAD. Version HEAD of samples/commit_3.txt left in tree.
error: could not revert 19896c0... commit 3
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'# Checking where the issue is
--> git status
On branch post-git-revert-example-2
You are currently reverting commit 19896c0.
(fix conflicts and run "git revert --continue")
(use "git revert --skip" to skip this patch)
(use "git revert --abort" to cancel the revert operation)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: samples/commit_2.txt
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add/rm <file>..." as appropriate to mark resolution)
deleted by them: samples/commit_3.txt
# The issue is in samples.commit_3.txt
# This due to commit 4 adding a line in that file.
# I am going to say its safe to remove that file
--> git rm samples/commit_3.txt
rm 'samples/commit_3.txt'# git status show we are ok to continue
--> git revert --continue# Edit the commit message as required
Next we follow the very same steps to revert the commit 2. And with there been no conflicts in the any files we are asked for the commit message straight away. This we edit as require. now when we look at the git log we can see the entries for the two reverts that was just carried out.
--> git log --oneline
ca32e18 (HEAD -> post-git-revert-example-2) Revert "commit 2"
b78b033 Revert "commit 3"
c216f8e (origin/main, origin/HEAD, pre-git-revert, main) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit
But this is a bit mess and we can tidy it up a bit which would usefully for the next person coming along. This we will do using the rebase command.
# Rebase interactively on the first two commits
--> git rebase -i HEAD~2-------------------------------------------------------------------------------------------------------------
pick b78b033 Revert "commit 3"
pick ca32e18 Revert "commit 2"
# Rebase c216f8e..ca32e18 onto c216f8e (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
----------------------------------------------------------------------------------------------------------------------# We want to edit the first commit message or at least check that its correct
# and squash the second commit
edit b78b033 Revert "commit 3"
squash ca32e18 Revert "commit 2"
After saving the file we are now given the choice to amend the commit message or continue with the rebase. This choice was give by adding "edit" to the first commit. We will continue editing the commits as required. Now if we want to view the git log we can see the two commits have been replaced with one. This is what we were aiming for.
--> git log --oneline
1c57b69 (HEAD -> post-git-revert-example-2) Reverting Commits 2 & 3
c216f8e (origin/main, origin/HEAD, pre-git-revert, main) commit 5
027e48a commit 4
19896c0 commit 3
dd89729 commit 2
e3f1d68 commit 1
4b54946 Initial commit
Wrapping up with the Why
Why is git revert such a usefully command to know about. It allows us to undo changes in a clean manner. We could have done this by going around and manually deleting the different components but in a large repo your going to miss something. Revert will also show what parts of a system is been affect by the change.
The is a second reason you may want to use the revert and that's so you can revert the revert. Take this example you are working on a product that performs interactions with other system. The release is made your feature is in production. The third party system has a manager bug and needs to roll back which now breaks your feature. Rather than rolling back your release, with git revert you can make a new release that reverts the broken feature but also allows the easy reverting of the revert later when the other system is back in a working state.
Some last words. I did think do the multi commit revert would have been easier and if you you how please leave a commit below. Knowing the correct way to do this before I need to do it will be super usefully.