Undo your breakfast (Meet Git #8)
How to revert changes in Git
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).
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.
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.
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.
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
.
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”.
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.
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.
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.
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
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.
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 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
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