- 1. Getting geeky with Git #1. Remotes and upstream branches
- 2. Getting geeky with Git #2. Building blocks of a commit
- 3. Getting geeky with Git #3. The branch is a reference
- 4. Getting geeky with Git #4. Fast-forward merge and merge strategies
- 5. Getting geeky with Git #5. Improving merge workflow with rebase
- 6. Getting geeky with Git #6. Interactive Rebase
- 7. Getting geeky with Git #7. Cherry Pick with Reflog
- 8. Getting geeky with Git #8. Improving our debugging flow with Bisect and Worktree
- 9. Getting geeky with Git #9. Understanding the revert feature
- 10. Getting geeky with Git #10. The overview of Git hooks with Husky
- 11. Getting geeky with Git #11. Keeping our Git history clean with fixup commits
Sometimes, when working on our project with Git, we might commit some code that we are not happy with. One of the solutions that Git provides to undo that is the revert feature. This article looks into how it works and what we need to look out for when using it.
The idea behind Git Revert
While we might be tempted to think of the revert feature as a means of undoing our work, this is not the complete picture. When reverting, Git analyzes the changes we want to undo. Then, it creates a commit on top of them that reverses the changes.
1 2 3 4 |
touch index.js echo "console.log('Hello world')" > index.js git add ./index.js git commint -m "Added the index.js file" |
Above, we’ve created the index.js file add committed it to the repository. Let’s take a look at the log.
1 |
git log |
commit 075913625d998dbccd74613f90dd21f74d725ca6 (HEAD -> master)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 17:23:53 2021 +0200Added the index.js file
We can see the hash of the commit above. It serves as an identifier generated based on the contents of the commit. We can use it to refer to this commit when attempting a revert.
If you want to know more about commit hashes, check out Getting geeky with Git #2. Building blocks of a commit
1 |
git revert 0759136 |
Usually just a few first characters of the hash is enought to uniquely identify a commit.
Running the above command opens a text editor.
Above, we can specify the commit message that will be attached when reverting the changes. The crucial thing here is that Git keeps this operation in history.
1 |
git log |
commit 9c3b233aa064a1d12436d869ed8ecc0e4ce9b39b (HEAD -> master)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 17:36:57 2021 +0200Revert “Added the index.js file”
This reverts commit 075913625d998dbccd74613f90dd21f74d725ca6.
commit 075913625d998dbccd74613f90dd21f74d725ca6
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 17:23:53 2021 +0200Added the index.js file
Let’s look closer into the revert commit.
1 |
git show 9c3b23 |
We can see that the revert commit deletes the index.js file.
Reverting a merge
So far, we’ve only reverted a simple commit. Reverting merges requires us to dig a little deeper into the revert feature.
1 2 3 4 |
git checkout -b feature-a echo "console.log('Feature A')" > feature-a.js git add ./feature-a.js git commit -m "Implemented feature A" |
1 2 3 4 5 |
git checkout master git checkout -b feature-b echo "console.log('Feature B')" > feature-b.js git add ./feature-b.js git commit -m "Implemented feature B" |
Above, we’ve created the feature-a and feature-b branches. Let’s now merge it to the master branch.
1 2 3 |
git checkout master git merge feature-a git merge feature-b |
Let’s look into the logs to figure out what happened when we’ve merged two branches into our master branch.
1 |
git log |
commit 2e1ca724207ee83814c7593c7f992bba58abdd3a (HEAD -> master)
Merge: 924a7a3 e3416e7
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 18:26:22 2021 +0200Merge branch ‘feature-b’
commit e3416e72e78761f40a880050427764b262cd05d5 (feature-b)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 18:26:06 2021 +0200Implemented feature B
commit 924a7a3a93d9f0d26372b55cb26b9d76622f378a (feature-a)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 18:12:00 2021 +0200Implemented feature A
In the logs, we can see that we only have one merge commit. This is because Git performed a fast forward merge when merging feature-a into master.
A fast forward merge can occur when there is a linear path from the current branch tip to the target branch. It wasn’t the case when merging feature-b into master, and therefore, Git created a merge commit.
If you want to know more about merging, check out Getting geeky with Git #4. Fast-forward merge and merge strategies
Since Git didn’t create a merge commit for feature-a, performing a revert is as simple as reverting the commit that implements feature A.
1 |
git revert 924a7a |
Setting a mainline
It gets a little bit more complicated when we want to revert feature-b. To understand it better, let’s take a closer look at the merge commit.
1 |
git cat-file -p 2e1ca72 |
parent 924a7a3a93d9f0d26372b55cb26b9d76622f378a
parent e3416e72e78761f40a880050427764b262cd05d5
author marcin <wanago.marcin@gmail.com> 1621787182 +0200
committer marcin <wanago.marcin@gmail.com> 1621787182 +0200Merge branch ‘feature-b’
In the second part of this series, we’ve learned that a commit’s parent is the previous commit. When Git creates a merge commit, it has multiple parents. Each parent is the tip of the branch involved in the merging process.
When we perform a revert on a merge commit, git doesn’t automatically know which branch we merged the changes into. Git refers to it as the mainline branch. When reverting the above merge, we have two possibilities:
1 |
git revert --mainline 1 2e1ca72 |
1 |
git revert --mainline 2 2e1ca72 |
When we set the mainline with a number, we indicate which parent is mainline. To be able to do that, let’s use git log.
1 |
git log -1 924a7a3 |
commit 924a7a3a93d9f0d26372b55cb26b9d76622f378a (feature-a)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 18:12:00 2021 +0200Implemented feature A
1 |
git log -1 e3416e7 |
commit e3416e72e78761f40a880050427764b262cd05d5 (feature-b)
Author: marcin <wanago.marcin@gmail.com>
Date: Sun May 23 18:26:06 2021 +0200Implemented feature B
Above, we can see, that the last commit on the first parent implements feature A. Therefore, we know that it is the master branch. To perform a successful revert, we need to set the mainline to 1.
1 |
git revert --mainline 1 2e1ca72 |
Issues to consider
The most important thing to grasp from this article is that reverting commits doesn’t erase them from Git history. Instead, it creates new commits that aim to reverse the changes. The above can cause some unforeseen consequences.
Let’s once again create a branch feature-a with a commit.
1 2 3 4 |
git checkout -b feature-a echo "console.log('Feature A')" > feature-a.js git add ./feature-a.js git commit -m "Implemented feature A" |
Imagine a situation in which you merge feature-a into master by accident and you want to revert it.
1 2 3 |
git checkout master git merge feature-a git revert HEAD |
The HEAD keyword is the pointer to the commit our repository is checked out on. In this case, it is the commit we’ve merged from feature-a. If you want to know more, check out Getting geeky with Git #3. The branch is a reference
The crucial thing about the above commands is that now the master branch contains a history of deleting the feature-a.js file. This can prove to be quite an issue later.
Now, let’s say that we’ve continued working on the feature-a branch. Meanwhile, our team pushed some important work to the master branch that we now need. Let’s merge master to feature-a.
1 2 |
git checkout feature-a git merge master |
Because the master branch contains a history of deleting the feature-a.js file, merging it causes the file to disappear in the feature-a branch also.
Summary
The crucial thing to understand about reverting is that it doesn’t erase the changes from Git history. This can lead to issues similar to the one described above. Therefore, it might be a good idea to avoid reverting. If we are not aiming to revert from branches used by other developers, a suitable alternative would be to perform an interactive rebase. If you want to know more, check out the sixth part of this series.