Overcoming Git disasters (Gitsasters) Part 2: Git cherry-pick, Git revert, and Git reflog
In the previous post in this series, we learned about the command
git reset, together with its three modes -
--hard. We then applied our knowledge to a few “gitsasters,” and saw that by understanding git and how it works, we can feel confident fixing cases when we need to rewrite history.
In this post, you’ll acquire additional tools to rewrite history and deal with “gitsasters.” Be sure to read Part 1 first, so you have the relevant background for overcoming git disasters.
Getting started with a repo
To get started, let’s start by creating a small repository to play with:
As you can see, I’ve created a repository and created a file called
1.txt, with the content
hello. I then added it to the
index, and created a commit. This commit was added to the
main branch, as we can verify using
So this is our current state:
Committing to the wrong branch
Great, now let’s create another branch…
As I explained in the post about creating a repository from scratch, this creates the branch
feature_branch, and also changes
HEAD to point to that branch.
So this is the current state:
Next, create a new file, and commit this addition to this feature branch:
As a result, this is the state:
Now, switch back to the
main branch using
git checkout, make changes to the file
1.txt, and record these changes on
This is the result:
Oh, wait! What if you actually wanted this change to be introduced in the
feature_branch, and not in
main? We dealt with a similar case where we accidentally committed to the
main branch, but actually wanted the commit to be recorded on a new branch. Similar to that instance, we now want the change introduced in this commit on an already existing branch.
A reminder, it’s useful to draw the current state versus the desired state:
There are technically a few changes between the current “gitsaster” state and the desired state:
- The commit which
mainis pointing to.
- The commit which “Commit 3” is pointing to.
- The commit which
feature_branchis pointing to.
Start by adding the changes introduced in “Commit 3” to
feature_branch. Note the changes this commit introduced using
git diff as follows:
How to use
If you want to apply these changes to the last commit on
feature_branch, we need to be on
feature_branch so we can use
git checkout again. Now use the
git cherry-pick command:
This basically takes the changes in “Commit 3” (with the SHA-1 value of
c067afe7a50a54b1137aef0ed3b63f611b4ee8c7). In this case, the changes to the file
1.txt generate a new commit including these specific changes, and then changes the pointer of the active branch. Now the
feature_branch points to the new commit.
The log now looks like this:
You can see the new commit, and the changes it introduced using
This is exactly what we wanted: we applied the same changes that we had seen earlier, and this time, we applied them on
feature_branch. In other words, the new commit introduced the same changes as “Commit 3”, and it also has the same commit message - that is “Commit 3.” But this is a different commit object introduced with a different timestamp and thus it has a different SHA-1 value. At this point, let’s call it “Commit 3.1”.
So now, our newly created commit points to “Commit 2” and
feature_branch points to this new commit:
What’s left for us to do is to change
main to point to “Commit 1”. For that, we can simply switch to
main branch again using
git checkout main. Then we can undo the last commit on this branch using
Awesome! So we’ve successfully achieved the desired state with the power of the well-known
git reset command together with the new command we introduced,
Continuing with another example…
Pushing a faulty commit
Let’s create another file
2.txt, stage and commit it:
Now our reachable git history looks like this:
Next, add some content to the newly created file, and then stage and commit the change:
And now, let’s say I
push this to the remote repository.
And… Oh, oops, I actually had a typo there:I wrote
tezt instead of
text. I want to fix it. Remember you can use
git reset of some sort to undo this last commit. The problem is that it has already been
pushed to the remote. This means that someone might have already
pulled this commit, and we don’t want to rewrite history so that it is inconsistent between our copy of the repository and someone else’s copy of the repository.
Just to clarify, I recommend not using
git reset to rewrite commits that have already been pushed to the remote unless you are completely sure that no one else has pulled the relevant commits.
If you are not sure if someone has
pulled the relevant commits, I recommend creating another commit that will simply undo the last commit. Obviously in this case, you can also just change the content of
2.txt, but we want to clarify that we are undoing the last commit as it was a mistake.
Next, take the changes introduced in the last commit, which you can see by using
Apply them in reverse like this:
So in this case, add a new line to erase it. By introducing a commit in which we remove a line, reversing it means adding the line again to undo the faulty commit.
How to use
To undo the faulty commit, use
git revert. This command reverses the changes that previous commits introduced, and records them in new commits.
In this example, you can just use
git revert HEAD which tells git to take the changes introduced at
HEAD (which before running this command pointed to “Commit 5”) and reverse those changes.
Here's how it looks using
Note that we now have a new commit. Let’s take a closer look at the changes this commit has introduced:
You can see that this commit did exactly the reverse of the previous commit.
To summarize, our repo’s history looks like this:
An interesting note by the way: you can also use
git show to show the changes introduced in a specific commit. So instead of using
git diff HEAD~1 HEAD, just use
git show HEAD:
Indeed, this is the reverse of the previous commit.
And what if we lost the commit?
We’ve now seen two new commands -
git revert and
git cherry-pick. Let’s move on to a more advanced scenario.
Let’s say, for example, you introduced a commit that took a really long time to create.
So obviously, this wasn’t really a lot of work. But imagine that it was an important commit that took a long time to create, and you hastily, by mistake, executed this command:
Oh my! What a gitsaster! 😨😱
git reset first goes through the “soft” step, changing whatever
HEAD was pointing to. In our case,
git reset changes the
main branch to point to what used to be
HEAD~1. That is, the commit that reverted “Commit 5”. This is where
git reset --soft would stop.
Here you can see this is indeed the state by using
git reset updates the index to be the same as what
HEAD currently points to, after the first step. So in our example, the index no longer includes changes introduced to
1.txt. That’s where
git reset --mixed would stop.
In the third step,
git reset updates the working dir to be like the index. In this case, it means the working dir will no longer include the changes we introduced to the file
1.txt. To verify, use
git status and look directly at the file system:
So, our precious changes are gone!
Or… are they? 🤔 The question is whether they are actually gone.
As we know, the commit object representing “Commit 6” was introduced to the system, so the git object is recorded within git’s internal object database. This commit object references a tree object which will in turn have a reference to the blob with the updated contents of
Below you can see the commit object is there - it is just not reachable from any of the branches.
So if you have the SHA-1 of “Commit 6”, you can actually look at it!
To validate, assuming you’ve stored the SHA-1 of the created commit, look at this commit object by using
And the commit is there!
And actually, its parent is the commit that reverted “Commit 5”. So basically, all we need to do is just to have HEAD point to “Commit 6” again. And for that, use the same command:
Again, this command first updates whatever
HEAD is pointing to. In this case, it is the branch
main, now pointing to “Commit 6”. To verify:
You can see that indeed
main points to “Commit 6”.
git reset updates the index to resemble the state of “Commit 6” and then also updates the working dir.
Be sure to verify:
Awesome, now, let’s remove this commit from the history again:
At this point, we’ve completely undone “Commit 6”, so that
main points to the commit that reverted “Commit 5.0”. This can be easily verified:
Also, note that the staging area and the working dir match the state of the commit that reverted “Commit 5”:
Now, let’s say we don’t have the SHA-1 value of “Commit 6.” Are we lost?
How to use
Luckily, no, and this is an opportunity to introduce yet another important tool:
git reflog 🎉
Basically, without needing to do anything, git silently records what
HEAD points to every time you change it. So whenever we change the active branch, introduce a new commit or use
git reset, git updates its
Note the SHA-1 of our lost commit, “Commit 6.”.To get a better view of these commits, use
git log -g which gives us a normal
git log output, but for our reflog.
Here’s how it looks:
You can now use
git reset --hard with that SHA-1 value. Another option is to create a branch pointing to this SHA-1 value:
So this is our state:
To remove our commit again, let’s use
git reset --hard HEAD~1 here:
If you look closely at the
reflog, you can see that it also uses “nicknames” for commits. Just as we could use
HEAD~1 to reference the parent of
git reflog uses
HEAD@1 to reference the last thing
HEAD pointed to. So instead of using
git reset --hard <SHA-1>, reference this commit by its nickname:
As you can see, we’ve gotten the same result. That is, we are back at “Commit 6”:
Nice, and the clear benefits of
In my previous post, we covered the command
git reset, together with its three modes -
--soft, that moves whatever
HEAD is pointing to,
--mixed, that goes on to update the index to what
HEAD is pointing to after the
soft step, and last -
--hard, which updates the working dir to match the state of the index.
In this post, we’ve added three new tools to our toolbox. First,
git cherry-pick, which applies the changes introduced in a commit or a range of commits. Second,
git revert, which reverses the changes introduced in a commit or a range of commits. And last,
git reflog, which shows us the history of what
HEAD points to, and comes in handy when you’ve lost a commit. We’ve also applied our knowledge to various cases, including committing to the wrong branch, pushing faulty commits to the remote, and losing a commit from our reachable history.
All in all, my hope is that you now feel confident when facing a wide range of gitsasters. The important thing is to understand that these are all pointers to commit objects. When you are not sure, just draw the current state as well as the desired state, go over the tools we have covered here, and you will be on your way to success.
Piggybacking on the theme of success, I encourage you to check out Swimm’s free platform. GitHub's State of the Octoverse report showed how developers see about a 50% productivity boost with easy-to-source documentation. Swimm’s community of users is growing at an astounding rate, all benefiting from code-coupled documentation. Development work requires collaboration, and Continuous Documentation is the game changer.
Sign up for a 1:1 Swimm demo and see Swimm's knowledge management tool for code up close.