Back
blog

Overcoming Git Disasters (Gitsasters) Part 2: Git Cherry-Pick, Git Revert, and Git Reflog

Overcoming Git Disasters (Gitsasters) Part 2: Git Cherry-Pick, Git Revert, and Git Reflog  cover image

In the previous post in this series, we learned about the command git reset, together with its three modes - --soft, --mixed, and --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 git log:

So this is our current state:

Source: Brief

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 main branch:

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:

  1. The commit which main is pointing to.
  2. The commit which “Commit 3” is pointing to.
  3. The commit which feature_branch is 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 git cherry-pick

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 git diff:

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 git reset:

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, git cherry-pick.

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 git diff:

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 git revert

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 git log:

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! 😨😱

Remember, 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 log:

Next, 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 1.txt.

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 git show:

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”.

Next, 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 git reflog

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 reflog.

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 HEAD, 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 git reflog!

Bottom Line

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.