Introduction

Undoing Git commits can be perplexing and often requires a profound understanding of the Git system. Fortunately, Git provides several mechanisms to undo commits, allowing developers to correct mistakes or revert changes.

Plus, undoing commits is not as complex task as it seems and with the right knowledge, developers can confidently navigate through the process. This is why I've decided to write this guide to provide a step-by-step tutorial on how to undo commits in Git.

If you haven't read the previous article in this series "Git from Beginner to Advance", I highly recommend starting there as it covers the fundamental concepts of Git. Including staging, creating commits, and understanding the commit history.

Be Cautious

Undoing commits in Git is more advanced and requires careful consideration. Usually, it needs to be done when an unwanted commit has been made or when changes need to be reversed. Undoing commits in Git can be done using various methods.

Do you remember what Hermione said in the "Harry Potter and the Prisoner of Azkaban" book when she and Harry traveled back in time?

Don't you understand? We're breaking one of the most important wizarding laws! Nobody's supposed to change time, nobody!

Hermione
Hermione Granger from Harry Potter

This dialog from Hermione emphasizes the importance of being cautious when messing with the past, as it can have significant consequences. Similarly, undoing commits in Git should be approached with caution, as it can have consequences on the project's history and collaboration.

Of course, we are not in the wizarding world of Harry Potter, but the analogy serves as a reminder to exercise caution when undoing commits in Git. I'm not trying to scare you, just be mindful of the potential impact before proceeding with any undoing commits in Git.

Preparation before the Revert

To practice undoing commits, we need something to work with. I will initialize a new Git repository in a new directory with a "test.txt" file to demonstrate the steps for undoing a local Git commit. All this file contains is just the word "First". Let's commit this file to the repository using this command:

git add test.txt && git commit -m "Create test.txt file with 'First' text"

Now that we committed the file to the repo (repository), let's do a second commit by adding a word "Second" on the next line like this:

First
Second

And commit using the "Commit" command with the flag "-am" to add and commit the changes:

git commit -am "Add 'Second' text to test.txt file"

If I run the git log now, I will see both commits listed, with the most recent commit at the top.

"git log" command output with 2 commits

With two commits in our Git history, we can practice undoing the most recent commit. Let's do that.

Undo a Local Git commit

Undoing local commits in Git is relatively easy and can be done using the "git reset" command. Since commits are stored only on your machine and have not been pushed to a remote repository, you have more flexibility in undoing them without affecting other collaborators.

This is the command that I'm gonna use:

git reset HEAD~1

I get this output in my terminal:

"git reset HEAD~1" command output

We've already discussed in the previous article that "HEAD" in the concept of Git means the most recent commit on the current branch. The "~1" means to go back one commit from the current "HEAD" position. You can replace "1" with any other number, depending on how many commits you want to undo.

If we check our "git log" command after executing the "git reset HEAD~1" command, we can see that the commit we made earlier has been removed from the history. But there is still the word "Second" in the "test.txt" file.

There is a reason for that. We told Git to remove the last commit from the history but keep the changes. This is important to understand. We added the word "Second" and committed the changes.

Later, we realized that writing the word "Second" was a mistake and wanted to undo the operation, essentially bringing back the changes from the previous commit. But it never happened because we just removed the commit without undoing the change.

I can prove you by showing the current "git status" after we did the reset command:

"git status" command output shows that "test.txt" file is modified

The "test.txt" file is still marked as modified, and the changes we made to add the word "Second" are still present in the file. Let's do the same commit again:

git commit -am "Add 'Second' text to test.txt file"

Great! We are now back at the point where we run the reset command. Let's run the reset command again, but this time, we'll use the "--hard" flag to tell Git that we remove the commit and discard the changes.

git reset --hard HEAD~1

The output is now different from where we ran this command without the special flag. Take a look closely at the output:

"git reset --hard HEAD~1" command output

It says that HEAD (the last commit) is now pointing to the commit before the one we just removed. It even shows us the commit message, "Create test.txt file with 'First' text". Likewise, it means that this commit is now the last one in our history. Git Log will prove it if you take a look at it.

The "git status" command also shows that we don't have anything to commit. And opening the "test.txt" file will also show us that we only have the word "First" in the file.

There you go! Make sure to write this command somewhere down, as you will need it and might forget it.

Undo a Pushed Git Commit

We haven't talked about pushing commits to the remote repository yet, but it's worth mentioning that if you have already pushed a commit and want to undo it, it becomes a bit more complicated.

There are two ways of how you can undo a pushed commit in Git:

  1. Use git revert to create a new commit that undoes the changes made in the previous commit.
  2. Use git reset to remove the commit from the local and remote repositories.

Let's take a look at both approaches in more detail.

Reset the Pushed Commit

Resetting a pushed commit in Git involves removing the commit from both the local and remote repositories. After resetting the commit, it will no longer be available in the commit history and any changes made in that commit will be discarded.

This way of undoing commits is acceptable if you work on a private repository that is not shared with others. If you work on a project that is shared with others, it is generally not recommended to use git reset to undo pushed commits, as it can cause confusion and conflicts for other team members.

To demonstrate how it works, let's add the word "Second" to the "test.txt" file and commit the changes:

git commit -am "Add 'Second' text to test.txt file"

Now we should have two commits when executing the "git log" command. If we push this change to a remote repository and check GitHub, I see 2 commits in my repository.

Screenshot from GitHub shows 2 commits in the repository

Setting up a GitHub account and integrating it with local Git is not in a scope of this article. We will discuss integrating GitHub with Git in the fourth article of this series.

To reset the last commit that was pushed, we run the same command that we use for removing the previous commit locally.

git reset --hard HEAD~1

If we run "git log" we will see that we now have only one commit. To apply these changes to the remote repository, we need to force push using the command:

git push -f origin master

In my case, my default Git branch is called "master", but your branch might be called "main" or something else. In case if it's called "main", make sure to replace "master" with "main" when running the push command. If your upstream is already set to a remote repository, simply doing a force push with the command "git push -f" will update the remote repository with the changes you made locally.

This is the output that I have after force pushing:

"git push -f origin master" command output

This is a screenshot of my commits on the GitHub page after force pushing the reset commit:

Screenshot from GitHub shows 1 commit in repository

Now, it's time to check my preferred way of undoing commits, using the "git revert" command.

Revert the Pushed Commit

To show you how to revert the changes in Git history, you can use the "git revert" command. The main key of "git revert" is that it creates a new commit that undoes the changes made in the previous commit, effectively reverting the commit without deleting it from the history.

This is my preferred way of undoing commits when working with a remote repository, as it allows for a safer and more controlled approach. Especially when working on a collaborative project, using git revert ensures that the commit history remains intact and other team members are aware of the changes made.

To demonstrate how it works, I'm going to add the word "Second" to the "test.txt" file and commit the changes:

git commit -am "Add 'Second' text to test.txt file"

Now we have two commits when executing the "git log" command. If we push this change to a remote repository and check GitHub, I see 2 commits in my repository.

Screenshot from GitHub shows 2 commits in repository

To undo the changes in the second commit, we can create a third commit and will undo the changes in the second commit. To revert the changes, we need to run the following command:

git revert HEAD

My terminal window changes to the default terminal editor for creating a commit message.

Interactive commit window

This is the same mode that appears when you run a "git commit" command. As you can see, the commit message is already written by the Git program, and it explains that this new commit is reverting the changes made in the previous commit.

All I need to do is to save and exit the editor. To exit Vim editor, hold "Shift" and press the key "z" twice. It will save the file and exit.

If you run "git log" you will see that we now have 3 commits in the history, with the latest commit being the one that undoes the changes made in the second commit.

"git log" command output shows 3 commits in Git history

Now we can push changes to the remote repository using a regular "git push" command without forcing a push. This is what I see on GitHub after pushing the changes:

Screenshot from GitHub shows 3 commits in repository

And the nice thing is that if we open the "test.txt" file, we'll see that there is only a single line there with the word "First". Which is accurately what we intended to achieve by reverting the changes in the second commit.

To revert multiple commits, you should do move gradually to the desired changes by reverting changes one by one. As we saw with the "git reset" we could specify specific commits to revert with the "~" symbol. With "git revert", you would need to run the "git revert" command for each individual commit you want to revert. That way, you can precisely control which changes you want to undo, ensuring that the commit history remains accurate and transparent to all team members involved in the project.

Hiding files from Git

If you wish to hide certain files from Git, you can add them to the ".gitignore" file. You simply create a new ".gitignore" file in the root directory of your Git repository and list the files or patterns of files that you want to exclude from version control.

Here is the example of how to exclude a "node_modules" directory and an ".env" file from version control:

node_modules/
.env

If you create a ".gitignore" file with this content, it will ignore a "node_modules" directory and an ".env" file in your project. The slash after the directory name signifies that it refers to a directory rather than a file.

Ignore Files inside a Directory

There are different ways of ignoring files in the directory. One I just showed you when you ignore the whole directory. The second way, is to ignore all the files in the directory like this:

/storage/app/public/*

In this example, all files within the "/storage/app/public" directory will be ignored by Git.

Ignore Files with Specific Extensions

You can also ignore all files with specific extensions by including the file extension in the ".gitignore" file. Let's look at the example of how we can ignore specific file extensions:

/storage/public/*.jpg
/storage/public/*.png
/storage/public/*.jpeg
/storage/public/*.webp

In this example, all the JPG, PNG, JPEG, and WebP files will be ignored by Git within the "/storage/public" directory.

Exclude Files from Being Ignored

In some situations, you may want to exclude certain files from being ignored by Git, even if they match the patterns specified in the ".gitignore" file. The most popular example is a ".gitkeep" file.

The ".gitkeep" file is commonly used to keep empty directories under version control. Let's say I have a "cache" directory that I want to include in my Git repository, even if it is empty. But, at the same time, I would like to exclude all the files within that directory from version control because they are temporary files.

The truth about Git is that it only tracks directories that contain at least one tracked file. If your directory is empty, it will not be tracked by Git. To fix that, I'll create a file with a name ".gitkeep" in the "cache" directory that I want to keep under version control.

In my ".gitignore" file, I will write something like this:

/cache/*
!.gitkeep

It tells Git that I would like to exclude all files from the "cache" directory, except for ".gitkeep" files. The "!" before the name of the files informs Git that it should not ignore those specific files, even though they match the patterns specified in the ".gitignore" file.

If I run the "git status" command in a project with this ".gitignore", it will confirm what I explained.

"git status" command output shows a new file "cache/.gitkeep" in a staging state

If you write "!.gitkeep" in a ".gitignore" file, it will exclude ".gitkeep" files from being ignored by Git throughout the entire project, regardless of their location or specific directories. However, in the same way, you can write the path to the file to exclude them from being ignored by the Git like this: "!/cache/main.txt". In this example, the "main.txt" in the "/cache" directory will not be ignored by the Git.

Conclusion

In conclusion, the process of undoing commits in Git demands a cautious approach, emphasizing the importance of understanding the intricacies involved.

The guide covered undoing local Git commits using the "git reset" command, highlighting the significance of the "--hard" flag for discarding changes.

We also explored undoing pushed Git commits, presenting both the "git reset" and "git revert" approaches. The last one, "git revert" is a preferred method for collaborative projects, offering a safer and controlled approach while preserving the commit history.

Additionally, we addressed hiding files from Git using a ".gitignore" file, providing examples of excluding directories, specific extensions, and selectively including files with the use of the "!" symbol.

If you have any suggestions or questions about covered topics, please let me know in the comments below, and I'll be happy to address them.

Keywords: github, remote, commit, open-source, stage