Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: February 6, 2024
When using Git, we might often find ourselves needing to undo or revert a commit. Whether it’s a rollback to a specific point in time or a revert to a particularly troublesome commit, undoing and reverting can help ensure stability.
In this tutorial, we’ll go through the most common commands and methods to undo and revert commits in Git. In addition, we’ll discuss and demonstrate the subtle differences in the way these commands function.
To start, we can review the state of a project at a particular commit by using the git checkout command. In particular, each commit has a unique SHA-1 identifying hash, which we can use with git checkout in order to revisit any commit in the timeline.
Furthermore, to get the identifier of a given commit, we can show the full history with the git log command:
$ git log --all --decorate --oneline --graph
In this case, we revisit a commit that has an identifying hash of e0390cd8d75dc0f1115ca9f350ac1a27fddba67d:
$ git checkout e0390cd8d75dc0f1115ca9f350ac1a27fddba67d
At this point, the working directory should match the exact state of the specified commit. Thus, we can view the project in its historical state and edit files without worrying about losing the current project state. Nothing we do here gets saved to the repository. We call this a detached HEAD state.
In fact, we can use git checkout on locally modified files to restore them to their working copy versions.
Now, let’s understand how we can change the current state of the repository.
To attempt to take back changes, we can use the git revert command. It’s important to remember that this command isn’t a traditional undo operation: it inverts changes introduced by the commit and generates a new commit with the inverse content.
In other words, the operation doesn’t revert to the previous state of a project by removing all subsequent commits, it simply undoes a single commit by applying its negative on the current state. This means that we should only use git revert if we want to apply the inverse of a particular commit.
Furthermore, git revert doesn’t move ref pointers to the commit that we’re reverting, which is in contrast to other undo commands such as git checkout and git reset. Instead, these commands move the HEAD ref pointer to the specified commit.
Let’s go through an example of revert.
First, we create a basic repository and populate the working directory with a single file:
$ mkdir git_revert_example
$ cd git_revert_example/
$ git init .
$ touch test_file
Next, we add content to the newly-created file and commit it:
$ echo "Test content" >> test_file
$ git add test_file
$ git commit -m "Adding content to test file"
After that, we add more content to have another commit:
$ echo "More test content" >> test_file
$ git add test_file
$ git commit -m "Adding more test content"
Once done, we check the current commit log:
$ git log --all --decorate --oneline --graph
* 617f2d8 (HEAD -> master) Adding more test content
* e0390cd Adding content to test file
We can see the HEAD is currently at 617f2d8, while the unique part of the older commit hash is e0390cd. Thus, we use the former with revert:
$ git revert 617f2d8
Now, we verify the contents of test_file:
$ cat test_file
Test content
In this instance, we reverted the most recent commit.
Reverting to a previous state in a Git repository can be achieved via the git reset command, which undoes more complex changes.
While git reset is similar in behavior to git checkout, it doesn’t move the HEAD ref pointer.
It has three primary forms of invocation that relate to Git’s internal state management system:
Understanding which invocation to use is crucial for using git revert correctly.
To understand the different invocations, we look at Git’s internal state management system, also known as the three trees of Git:
Now, let’s go through each invocation of reset.
Perhaps the most dangerous and frequently used option with this invocation is commit history, –hard updates ref pointers to the specified commit. After this, the staging index and working index reset to match that of the specified commit. Any previously pending changes to the staging index and working directory reset to match the state of the commit tree. Notably, we lose any pending or uncommitted work in the staging index and working index.
Adding on to the example above, let’s commit some more content to the original file:
$ echo "Text to be committed" >> test_file
$ git add test_file
Now, we add, populate, and commit a brand new file to the repository:
$ touch new_test_file
$ git add new_test_file
$ git commit -m "More text added to test_file, added new_test_file"
Let’s say we then decide to revert to the first commit in the repository. We achieve this by running a –hard reset:
$ git reset --hard e0390cd
Git should let us know that the HEAD is now at the specified commit hash. Looking at the contents of test_file shows us that the latest text additions aren’t present, and the new_test_file no longer exists. This data loss is irreversible, so it’s critical that we understand how –hard works with the three trees.
When invoking with –soft, reset updates the ref pointers and stops there. Thus, the staging index and working directory remain in the same state.
In the previous example, the changes we committed to the staging index wouldn’t have been deleted if we had used the –soft argument. We can still commit the changes in the staging index.
Without an argument, reset uses the –mixed flag, which offers a middle ground between the –soft and –hard invocations. The staging index resets to the state of the specified commit and ref pointers update. Any undone changes from the staging index move to the working directory.
Using –mixed in the example above means that local changes to the files aren’t deleted. Unlike with –soft, however, no changes remain within the staging index.
Commonly, reset is used with the HEAD~1 special reference that points to the most recent commit relative to the current HEAD.
This is a convenient way to target perhaps the most usual commit used with a reset:
$ git reset --soft HEAD~1
In this case, we perform a soft reset to HEAD~1, keeping the current work.
There are some potential caveats to this method:
Thus, although potentially useful, HEAD should be used with caution as a reference for reset.
In this article, we looked at ways to undo and revert Git commits.
A rudimentary way to summarize the methods discussed is that git revert is safe, while git reset is dangerous. As we saw in the example, there’s a possibility of losing work with git reset. Thus, with git revert, we should be able to safely undo a public commit, whereas git reset is tailored toward undoing local changes in the working directory and staging index.
Notably, git reset moves the HEAD ref pointer, whereas git revert simply reverts a commit and applies the undo via a new commit to the HEAD. As usual, it’s best to assume that other developers are reliant upon published commits.