Undo your breakfast (Meet Git #8)

How to revert changes in Git

Krystian Szpiczakowski
CodeX
9 min readDec 29, 2022

--

Photo by Nick Fewings on Unsplash

Introduction

Hello and welcome to the next article of a series devoted to the Git basics (not only) for programmers.

Leveraging Git in your workflow, you can store changes made within your files safely and effectively. Sometimes, however, you may realize that you told Git to keep track of something that in fact should not be recorded.

Luckily, Git offers a few ways to undo changes in history, and each of these can be used in different situations, which I’m going to explain in this article.

Let’s get started!

Possible scenarios of undoing changes

There are a few possible reasons and stages at which you would want to revert changes in Git — consider the following examples below:

  • unstage changes if they are not committed yet
  • undo changes that have already been committed

You should be already familiar with terms such as staging area/index and commit. If not, I encourage you to read my previous articles in this series.

Undo changes if they are not committed

Let’s imagine this scenario:

You have introduced changes to the famous Royal Breakfast¹, you saved the file in your favorite text editor, and you closed your laptop because it’s time to go sleep. You decided not to create a commit of your work yet, though, because you know it’s just a draft version of your new idea.

The next day, you woke up with a fresh mind, and you came to the conclusion the new idea is crap, and it’s better not to change anything, because the Royal Breakfast is already perfect.

It turns out it’s too late to revert changes in the recipe, because the file has already been saved, and the undo operation in your text editor most likely does not work, since you closed the editor earlier.

Oh, did I say that it’s too late? With Git, not at all, just use git reset, git checkout, or both to bring your modified file to the original state (the state represented by your last commit). Let’s find out which command is relevant in what situation.

Changes are not staged

Which command are you supposed to use to revert a file, given you haven’t staged changes yet? Use git checkout -- <file>, which replaces the selected file with its version from the latest commit.

For example, I modified the recipe saved in royal_breakfast.txt, so I want to use git checkout -- royal_breakfast.txt. You can use also git checkout -- ., if you’d like to revert all tracked files in the current directory (and subdirectories too).

The history consists of four commits
Decorate the plate with ketchup? Hmm…
The recipe was modified, but changes are not staged yet
With “git checkout”, You can easily align your working directory with the last commit
There is no ketchup at all after git checkout

You may be wondering why have I used git checkout instead of git restore, even if Git suggests using git restore after I typed git status. Well, the official Git’s documentation states clearly that this is an experimental feature that may change in the future.

Some commands in Git are experimental, such as “git restore”

Changes are already staged

In case you have already staged changes with git add, but not committed them yet, you’ve got to perform just one additional step compared to the previous scenario.

First, you need to remove changes from the staging area by using git reset -- <file>, in my case this would be git reset -- royal_breakfast.txt. Then, you need to perform the same operation as described in the previous paragraph, that is git checkout -- royal_breakfast.txt.

And again, although Git suggests using git restore --staged, I will stick to the old method, unless git restore is no longer experimental.

The change with ketchup is staged this time
First, move changes away from the staging area
Then, restore your working directory from your last commit

Undo changes that have already been committed

However, sometimes it happens that unwanted changes have already made their way to the repository. Even though, Git offers a few ways to deal with it, so let’s check them out.

If your mistaken commit should stay in history (git revert)

The git revert command is very useful and straightforward to use. Moreover, this command provides a safe and clean solution to revert an unwanted commit — just type git revert <commit_id>.

What’s so special about git revert? It preserves the faulty commit, which means you’ll end up with Git history containing two commits: the faulty one, and the second that reverts the former.

First, let’s create two commits.

The first change contains ketchup decoration
Ketchup decoration committed
The second change contains artichokes
Commit with artichokes created
History with two latest commits

Suddenly, you found out that the change coming along with the first commit should not be stored in the repository, but the second commit is fine and should stay as is. Namely, the commit de2df73 with ketchup decoration is something we want to get rid of, while the commit ab6091f should stay.

For this kind of scenarios, git revert is a perfect solution. To make things right again, I’ll just type git revert de2d.

The revert operation introduced a new commit on top of history

You may ask a question: “How do I know if the commit I want to get rid of should stay in history or not?”

To provide a simple answer for a not-so-trivial question, for now, assume you should favor git revert over other methods if:

  • the change you want to undo is not the latest commit in the current branch, or
  • the commit is already pushed to a remote repository, and was used by someone else

Don’t worry if you don’t know what pushing means — it’s a concept used when you work with remote repositories, and for now I only work with the local repository.

If you think you don’t need the last commit (or the last few) in history, go for git reset, which I explained below.

If the faulty commit can be deleted from history

Let’s assume the last three commits in Git history are to be removed. Since I’ve been working on a local repository, I don’t share these commits with anyone, so I can safely delete them.

To delete three commits from history, usegit reset --hard HEAD~3. This command can be translated to something like this:

“Move the HEAD pointer three steps backward, and recreate the working directory from the current commit”.

From now on, the current commit will be 27a28ac
After performing “git reset”, Git is telling you what is the current commit
Removing those three commits was successful

When we work with Git, we always point somewhere in the history using the HEAD reference.

If you move HEAD backward, you lose access to the history that’s ahead, which means the history looks like it never contained our faulty commit, that is de2d….

This command is useful, but make sure you know what you’re doing, otherwise you may lose a part of your history. If you want to remove a commit from history, but as a safety net you want changes related to that commit to keep in the working directory, use git reset HEAD~3, or git reset --soft HEAD~3. By using either of these two, you still have to manually revert changes from the working directory.

Of course, Git includes a safety button called git reflog that allows you to restore commits even after using git reset --hard, but that is definitely a topic for another article.

If the faulty commit requires just an amendment

Another super useful command that I use regularly is git commit --amend. With the command above, you can bring additional changes to an existing commit.

Say, you decided to add artichokes to the breakfast (I do love artichokes). Let’s introduce them to the recipe, and create a commit.

Added artichokes to the recipe
Artichokes are mentioned in the recipe

You read this recipe once, twice and notice that the information about artichokes is not very accurate. Will the reader know that you meant pickled artichokes? And will they know that they should drain the artichokes from the brine?

Let’s modify the recipe to include these details, then.

Normally, when changes are staged, you would create a brand-new commit that appears on top of history, like this below.

Fixing the last commit by introducing another one works, but we can do better

Instead of creating a new commit, we can simply edit the existing one, given the commit we want to amend is the last one in history. To do this, simply type git commit --amend, and Git will incorporate your changes from the staging area to the existing commit.

Make sure changes for amending are staged
Amending of the commit was successful
After amending, commit’s checksum was changed from 3353b7e to 2129442
From now on, one commit contains all required information

Watch out for commits that might have been already pushed, though, because the checksum of the amended commit was changed, and this can lead to nasty problems with keeping the history synchronized (and your colleagues will stop liking you).

Bonus: resolving conflicts

A conflict has appeared that must be resolved

If you’re reverting a commit, and you’re seeing something like this, don’t panic. This message means that we’ve got a conflict that has to be resolved. I haven’t explained to you what conflicts are yet, but in short, conflicts occur when there are changes overlapping each other within a single file.

The good thing about Git is that it won’t leave you stranded with a mess — if you don’t know what to do, simply obey what Git is telling you, and type git revert --abort. This will bring you to the state before the conflict occurred.

The only thing left after this stressful situation is a set of files that you can safely delete. Before you do it, however, it might be useful to skim through them and see what’s the cause of this conflict.

If a conflict occurs, you can always step back and think over it

Because I aborted the conflict resolution with git revert --abort, let’s try again to revert the change by typing git revert de2d, and resolve the conflict.

When I opened the conflicted file, I can see markers that help me understand what’s going on.

Basically, the text between <<<<<<< HEAD and ======= caused the conflict, and Git needs my help to decide what to do.

As you can see, I’ve added ketchup and artichokes side by side in the ingredients section, so Git isn’t sure what to keep and what to remove. We want to delete everything ketchup-related, so let’s tidy up this recipe a bit.

The recipe after conflict resolution

The recipe is fixed now — note, I also deleted markers that pointed to the conflicting spot.

Once I edited the file and saved it, I can move on to Git Bash and finalize the revert operation. To do it:

  • add the conflicted file to the index
  • let Git continue reverting our changes by typing git revert --continue
The resolved conflict still has to be added to the index
Resolved conflicts have to be moved to the index/staging area
All conflicts have been resolved — let’s proceed with the revert operation
The revert operation introduced a new commit on top of history
The revert operation was successful — there is no ketchup in the recipe

In general, there are nice tools out there for resolving conflicts, but the presented conflict was quite simple, so the old good notepad was enough to fix it.

References

[1] Krystian Szpiczakowski, How to start cooking with Git (Meet Git #4) https://medium.com/codex/how-to-start-cooking-with-git-meet-git-4-a286e4e08109

--

--

Krystian Szpiczakowski
CodeX

The dev who enjoys writing tests / Home-grown barista