Git Reset



The git reset command is a tool for undoing changes. It points to the current state of HEAD back to a previous commit.

The git reset has three core forms of invocation that are Soft, Mixed, and Hard. These three forms correspond to Git's three internal state management systems, The commit History (HEAD), The Staging Area (INDEX), and The Working directory.


Git reset and the three trees of git

To properly understand how Git is working, we must understand Git's internal state management systems called the three trees of Git. These trees are just different collections of files.

The three trees of git architecture are:

  1. The Working Directory: It holds the actual files
  2. The Index or Staging Area: It is where files are getting prepared to be included on a commit.
  3. The HEAD or The Commit History: It is a place where commits are saved and retrieved. The HEAD point to the last commit.

The proper way to demonstrate the git three trees architecture is by creating a new repository and following files through the tree trees of git. Let us get started by creating a new repository by following the bellow commands:

$ mkdir git_reset_demo
$ cd git_reset_demo/
$ git init
Initialized empty Git repository in /git_reset_demo/.git/
$ touch reset_file_demo
$ git add reset_file_demo
$ git commit -m "initial commit"
[master (root-commit) 1a03d4b] initial commit
   1 file changed, 0 insertions(+), 0 deletions(-)
   create mode 100644 reset_file_demo

In the above example, we create a new git repository with one empty file reset_file_demo. The commit history has one commit (1a03d4b) from adding reset_demo_file.


The Working Directory or The Working Tree

The first tree is the working directory. It represents the files on your local file system. Any change to files will be viewed in the Working Tree. Git recognizes any change made to files in the Working Tree, but until you ask Git to track these files, it will not save any modification made to these files.

We will continue in our example by adding some change to the Working Tree.

$ echo "adding some text for git reset demo" >> reset_file_demo
$ git status
On branch master
Changes not staged for commit:
   (use "git add <file>..." to update what will be committed)
   (use "git restore <file>..." to discard changes in working directory)
         modified:   reset_file_demo

no changes added to commit (use "git add" and/or "git commit -a")

In the above example, we added some text to reset_file_demo. After, we run the git status command that shows that Git is aware of the file's changes. These changes are apart of the Working Tree.


The Staging Area (Index)

The next tree is the Staging Index. On this tree, Git starts tracking and saving changes that occur in files. These changes are prepared to be included on the next commit.

Different terms can come while talking about the Staging Index like staging area, index, staged files, cache, directory cache, local cache, etc ...

To display the state of the Staging Index, we need use git ls-files command as follows:

$ git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0       reset_file_demo

The git ls-files show information about files in the Staging Index and the Working Tree. To filter just for staged files, we add -s or --stage option. The -s option shows staged contents' mode bits, object name, and stage number.

In the above example, we are interested in the object name 'reset_file_demo' and the stage number which is the Staging object SHA-1 hash (e69de29bb2d1d6434b8b29ae775ad8c2e48c5391). The staging Index has its own object hash that is different from those of the commit history.

The next step is to add the modified file 'reset_file_demo' to the Staging Index.

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   reset_file_demo

no changes added to commit (use "git add" and/or "git commit -a")

$ git add reset_file_demo

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   reset_file_demo

In the above, we used the git add command to move the modified 'reset_file_demo' file from the Working Tree to the Staging Index.

Let's display the Staging Index content.

$ git ls-files -s 
100644 1ec4e63e067938156e289bc9e797b562456dd209 0       reset_file_demo

In the above output, we see that the object SHA-1 hash for 'reset_file_demo' has been changed from e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 to 1ec4e63e067938156e289bc9e797b562456dd209.


The Commit History or the Local Repository

The last tree is the Commit History which is everything in the .git directory.

To add items from the Staging Index to the Commit History, we will use the git commit command that takes all Staging Index changes, wraps them together, and add them to the Commit History.

$ git commit -m "modified content of rest_file_demo"
[master 90bb007] modified content of reset_file_demo
   1 file changed, 1 insertion(+)

$ git status
On branch master
nothing to commit, working tree clean

In the example above, we created a new commit with the message "modified content of rest_file_demo". The changes are saved to the Commit History. After running the git status command, we can see that we now have a clean Working tree and nothing to commit.

Once the changes went through the three trees, the git reset command can be invoked.


How git reset works

The git reset command seems to be similar to the git checkout command, as they both operate on HEAD. However, the git checkout command operates only on the HEAD reference pointer, while the git reset command will move the HEAD pointer reference and the current branch reference. To explain this behavior, consider the following illustration:

git sequence commits

The illustration above presents a sequence of commits on the master branch. The HEAD ref and the Master branch ref point to the last commit (the commit D).

We will see how the illustration change when we run git checkout B and git reset B.

git checkout B

git checkout B

After executing the git checkout B command, the Master ref did not move, still pointing to the last commit (commit D) while the HEAD ref has been moved and now is pointing to the commit B. The repository is now in a "detached HEAD" state.

git reset B

git reset B

As we can see in the illustration above, after executing the git reset B command, both the HEAD and branch refs moved to the specified commit.

Apart from updating the HEAD and the branch ref, the git reset command will change the state of all three trees. There is always a change happening to the third three, the Commit Tree. This change is about the move of the HEAD and the branch ref to the specified commit. On the other hand, there are command-line arguments for the Working Tree and the Staging Index --soft, --mixed, and --hard, which controls how the modification will occur.


Main Options

The git reset command has implicit arguments of --mixed and HEAD. Invoking the git reset is equal to running git reset --mixed HEAD. In this case, HEAD is the specified commit. Rather than using HEAD, any Git SHA-1 commit hash can be passed.

git reset main options

--hard

The most frequently used option is the --hard. However, it is risky to use. When the --hard option is used, The Commit History ref pointers are changed to point to the specified commit. Then it resets (clears out) The Working Directory and The Commit History and overwrites their contents with the specified commit's content. Any modifications that have been previously pending to The Working Directory or The Staging Index are cleared out to match the specified commit. This indicates any work in progress that lives in The Working Directory and The Staging Index will be lost.

To explain this, let us continue with the 'git_reset_demo' repository that we created before. First, we will make some change to the 'git_reset_demo' repository by executing the following commands:

$ echo 'new file demo content' >> new_file_demo
$ git add new_file_demo
$ echo "modified content" >> reset_file_demo

In the above commands, a new file named 'new_file_demo' is created and added to the Staging Index. Also, the content of the 'reset_file_demo' file has been updated. Now let us verify the repository state using the git status command.

$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   new_file_demo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   reset_file_demo

The above output shows that there are some pending changes in the repository. The Staging Index tree has a pending modification by adding the 'new_file_demo' file, and the Working Directory has a pending modification by updating the 'reset_file_demo' file.

Now let us verify the state of the Staging Index :

$ git ls-files -s
100644 dfaf1227cbd021fde5b0dfff42f9e7dcb1c382ed 0       new_file_demo
100644 1ec4e63e067938156e289bc9e797b562456dd209 0       reset_file_demo

The output shows that the 'new_file_demo' has been added to the Staging Index, and the 'reset_file_dem' has been updated. However, its staging SHA-1 hash (1ec4e63e067938156e289bc9e797b562456dd209) stays the same. This unchanged SHA-1 hash of the 'reset_file_demo' file is expected because we did not move the change from the Working Directory to the Staging Index.

Now let us run the ``git rest --hard` command and check the repository's new state.

$ git reset --hard
HEAD is now at 90bb007 modified content of reset_file_demo

$ git status
On branch master
nothing to commit, working tree clean

$ git ls-files -s
100644 1ec4e63e067938156e289bc9e797b562456dd209 0       reset_file_demo

In the above, after running the git reset command using the --hard option. The output shows that the HEAD ref is pointing now to the recent commit '90bb007'. Then, we verify the state of the repository with the git status command. The output shows that there are no pending changes. Finally, we verify the Staging Index state and found that it has been moved to a point before 'new_file_demo' was created. The modification to the 'reset_file_demo' file and the addition of the 'new_file_demo' file has been lost. This loss cannot be undone.

Note: Before using the --hard option, be sure what you really want to do since the git rest --hard command overwrites any uncommitted changes. This means any pending work will be lost.

--mixed

The --mixed option is the default operation mode for the git reset command. The HEAD and Branch refs are moved to the specified commit. The Staging Index is reset to the state of the specified commit. Any modifications that have been pending in the Staging Index are moved to the Working Directory.

Let us continue with our 'git_reset_demo' example :

$ echo 'new file demo content' > new_file_demo
$ git add new_file_demo
$ echo 'more content' >> reset_file_demo
$ git add reset_file_demo
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:     new_file_demo
        modified:   reset_file_demo

$ git ls-files -s
100644 dfaf1227cbd021fde5b0dfff42f9e7dcb1c382ed 0       new_file_demo
100644 237994aadf20ee6b9bc688b3f18bb8b95af43e3c 0       reset_file_demo

In the example above, a 'new_file_demo' has been added, and the contents of 'reset_file_demo' have been updated. After, these modifications are moved to the Staging Index using git add. Under the repository's current state, it is time to execute the git reset command.

$ git reset --mixed
Unstaged changes after reset:
M       reset_file_demo

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   reset_file_demo

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        new_file_demo

no changes added to commit (use "git add" and/or "git commit -a")

$ git ls-files -s
100644 1ec4e63e067938156e289bc9e797b562456dd209 0       reset_file_demo

The --mixed option is the default mode. It is equivalent to executing git reset without any options. The output of git status indicates that modifications to 'reset_file_demo' and 'new_file_demo' is an untracked file. The Staging Index has been reset, and the pending modifications are moved to the Working Directory.

--soft

The --soft option updates HEAD and Branch refs to the specified commit. The reset stops on the modification of the ref pointers. The Staging Index and the Working directory are not affected.

Let us continue with our demo repository:

$ git add reset_file_demo
$ git ls-files -s
100644 237994aadf20ee6b9bc688b3f18bb8b95af43e3c 0       reset_file_demo

$ rm new_file_demo
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   reset_file_demo

In the example above, the change of 'reset_file_demo' has been promoted to the Staging Index. We checked the update of the Index with the git ls-files -s output. We removed 'new_file_demo using the rm new_file_demo because we do not need it anymore. The git status shows changes in the file 'reset_file_demo' to be committed.

With the current repository state, we can now invoke a git reset --soft.

$ git reset --soft 
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   reset_file_demo

$ git ls-files -s
100644 237994aadf20ee6b9bc688b3f18bb8b95af43e3c 0       reset_file_demo

In the above, we have executed a soft reset. Verifying the repository state using git status and git ls-files -s reveals that nothing has changed. A soft reset will only reset the Commit History. By default, git reset is invoked with HEAD as the target commit. So in our example, our Commit History was already on the HEAD, and we implicitly reset to HEAD, so nothing really happened.

To execute a soft reset with a target that is not HEAD. Let us create a new commit. We already have 'reset_file_demo' waiting in the Staging Index to be committed.

$ git commit -m"more content to reset_file_demo" 

At this stage, our demo repository has three commits. We will move back in time like a time machine to the first commit. To do this, we need the first commit ID, which can be done by looking at the output from git log.

$ git log
commit c6db2c83f28a54e5281e4b9bdefb84e3a904273f (HEAD -> master)
Author: yassere dahbi <dahbi.yassere@gmail.com>
Date:   Fri Oct 25 12:41:49 2019 +0100

    more content to reset_file_demo

commit 90bb007caabbd7c87752046a5ab754cbf44ad4c8
Author: author_name <author_email@pincode.com>
Date:   Wed Oct 23 14:50:20 2019 +0100

    modified content of reset_file_demo

commit 1a03d4bb1604fd7958d328909faafd90347403cf
Author: author_name <author_email@pincode.com>
Date:   Wed Oct 23 11:47:49 2019 +0100

    initial commit

We are interested in the first commit ID, for this example is 1a03d4bb1604fd7958d328909faafd90347403cf. So we will use this commit ID as the target for our soft reset.

Before using our time machine (git reset) to travel back in time, let us first verify our repository's current state.

$ git status && git ls-files -s
On branch master
nothing to commit, working tree clean
100644 237994aadf20ee6b9bc688b3f18bb8b95af43e3c 0       reset_file_demo

Here the combo git status and git ls-files -s outputs that we have a clean Working Directory and there are no pending changes in the Staging Index. The version of 'reset_file_demo' file in the Staging Index is 237994aadf20ee6b9bc688b3f18bb8b95af43e3c. So now we can invoke our time machine git reset --soft to soft reset to the first commit.

$ git reset --soft 1a03d4bb1604fd7958d328909faafd90347403cf
$ git status && git ls-files -s
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   reset_file_demo

100644 237994aadf20ee6b9bc688b3f18bb8b95af43e3c 0       reset_file_demo

In the example above, we had a soft reset to the first commit and also executed the git status and git ls-files -s combo command that shows us the state of our repository. As we can see, git status indicates that there are some modifications to 'reset_file_demo' in the Staging Index waiting to be committed. For git ls-files -s shows that the Staging Index has not changed, and we still have the same SHA 237994aadf20ee6b9bc688b3f18bb8b95af43e3c that we have before.

To understand more what happened after a soft reset. Let us verify the state of the repository using git log:

$ git log
commit 1a03d4bb1604fd7958d328909faafd90347403cf (HEAD -> master)
Author: author_name <author_email@pincode.com>
Date:   Wed Oct 23 11:47:49 2019 +0100

    initial commit

As we can see, the git log output shows that we have a single commit in the commit history. This proves that git reset --soft move back time to the specified commit. As with all git reset invocations, the first action reset make is to reset the Commit tree. However, unlike --hard and --mixed that have both been against the HEAD and have not moved the Commit History back in time. A soft reset has only moved the Commit Tree back in time to the specified commit.

A soft reset does not touch the Working Directory or the Staging Index. It is only work in the Commit Tree. It moves back time to the specified commit.


The difference between Reset and Revert commands

The git reset command is a safe way to undoing changes comparing to the git reset. There is a high probability that work can be lost using git reset. Git reset does not delete a commit. However, it can make the commit "orphaned", which means that there is no direct access using a ref to attain them. Git will delete any orphaned commits when the internal garbage collector is triggered. By default, Git executes the internal garbage collector every 30 days. The orphaned commits can be found and restored using the git reflog.

Another face of difference between these two commands is that git revert is suitable to undo public commits, while git reset is suitable to undo local changes to the Working Directory and the Staging Index.


Try to avoid to Reset Public History

You should never use git reset <your-sha-commit> command when there are snapshots after that are already pushed to a remote public repository. When you publish a commit, take into consideration that other developers depend on it too. Deleting commits that are being developed by other developers will cause a lot of problems in a project.

It is recommended practice to use git reset <your-sha-commit> only on local changes, while for public changes, use the git revert command instead.


Examples of using git reset

$ git reset <file_name>

The command above will remove the specified file from the Staging Index area without changing the Working Directory. It will unstage the specified file without overwriting any changes.


$ git reset

The command above will reset the Staging area to match the last commit. However, it will leave the Working Directory untouched. It will unstage all files from the Staging area without overwriting changes, offering the possibility to rebuild the staged snapshot from scratch.


$ git reset --hard

The command above will reset the Staging area and the Working Directory to match the last commit (The commit that the HEAD ref point). It will unstage all the pending work in the Staging area, and it will overwrite all changes in the Working Directory.

Be careful when using --hard because you can lose pending work in the Staging area and the Working Directory.


$ git reset <commit_sha>

The command above will move the Commit History back in time to the specified commit. It will unstage all the pending work from the Staging area and move it to the Working Directory, and it will reset the Staging area to match the state of the specified commit. But it will not touch the Working Directory.


$ git reset --hard <commit_sha>

The command above will move the Commit History back in time and reset the Staging area and the Working Directory to match the specified commit.


Removing Local Commits

The example below presents a use case when you are working in a local git repository, and you have done different commits, but you decide that you want to get rid of them.

$ echo "initital content" >> demo_file
$ git add demo_file 
$ git commit -m "initial commit for demo_file"
$ echo "more content" >> demo_file
$ git commit -am "adding content to demo_file" 
$ echo "last content" >> demo_file
$ git commit -am "adding last content to demo_file"
$ git reset --hard HEAD^3

In the example above, we created a new file named "demo_file," and we committed the change of the file in three different commits. The git reset HEAD^3 moves the current branch backward by three commits.

This kind of reset should only be used for commits that are not published to a shared repository.


Unstaging files

The git reset is usually used for preparing staged snapshots. In the example below, we have 2 files named "index.html" and "home.html" which have been already added to the repository. The git reset command will help us to unstage the changes that are not related to the next commit.

git add .
git reset home.html
git commit -m "edit index.html"
git add .html
git commit -m "edit home.html"

In the above commands, we staged the two files, "home.html" and "index.html" using git add. Then we realized that the change in "home.html" and "index.html" should be committed in different snapshots. So we used git reset to unstage "home.html" so the first commit will just include "include.html".



ExpectoCode is optimized for learning. Tutorials and examples are constantly reviewed to avoid errors, but we cannot warrant full correctness of all content. While using this site, you agree to have read and accepted our terms of use, cookie and privacy policy.
Copyright 2020-2021 by ExpectoCode. All Rights Reserved.