Sometimes we do a merge and realize that our program stopped working and we want to undo it. Here we have two options: 1) undoing the commit as if it had never been there (called resetting), 2) create a new commit that cancels out all the changes in our merge commit (called reverting).
When resetting we have to be very careful because we are re-writing history. If the changes only exist on our local repository that is OK. But if we have already shared the changes with the rest of our team we should NEVER re-write history.
The diagrams below explain the two situations. In both of them, we begin from the same starting point: a merger commit has taken place, merging a branch called new-feature into master. The difference is that when we use reset we move the master pointer back to its parent commit (the commit with a red border that is before the merge commit, and in the master branch). Because there are now no commits or pointers pointing to our merger commit, Git will Garbage Collect this commit and throw it away.
On the other hand, when we use a revert we are creating a new commit that comes after the merge commit, and moving the HEAD pointer onto this new commit (shown in blue with white border on the diagram). This commit makes all the opposite changes as those that were done on the merge commit. One thing to consider when reverting mergers is that the merge commit actually has two parent commits: one on master and one on new-feature. We'll have to specify Git which commit to revert to and, since we'll do it from the master branch (because it's the branch that was modified with the merger), we'll revert to the parent commit on this branch.
Before we start with an example, let's take a look at our log. We'll do both operations on commit 3a485a4, which is our last merge commit. Before starting an operation like this, it's always recommended that you write down the ID for the commit you are going to revert or reset. You can delete it once done.
We'll start with the reset option. But before we do we need to clarify some terminology. The git reset command has three options:
- --soft: Git will have our repository point to a different commit, but our index and working tree will not be affected.
- --mixed: Git will take the new snapshot and put it in the index as well. This is the default option so we don't need to specify it.
- --hard: Git will take the new snapshot and copy it to both the index and the working tree. This means that all environments will look the same. It also means that all local changes in our working-tree will be lost, so make sure to stash them before resetting.
To reset our repository, index, and working tree in the master branch to the commit previous to the merge we run
git reset --hard HEAD~1
We can see that both HEAD and master are now at commit b03190d, and the merge commit has disappeared from our tree. The commit is still in our repository thou, and we can recover it (until the Garbage Collector takes it away for good).
If we want to reset to the point before resetting (i.e. a point where the merge is still there) we run
git reset --hard 3a485a4
We can now check our log and see that our merge commit is back. Keep in mind that if you close Git Bash, the Garbage Collector will be run and the commit will be lost forever.
Now let's take a look at reverting. To do so we run
git revert -m 1 HEAD
Here we are telling Git that it should revert the HEAD pointer to the previous commit on the master branch. The -m 1 means first parent commit.
Git will open VS Code so that we can edit our message, but here we'll accept the default and close it. When we do, Git will complete the reverting process.
We can check our log now and see that a new commit has been added and its message says that it has reverted the merge.