Git reset and the three trees
The git reset command is a tool used to undo changes. It has three forms of invocation matching Git’s three internal state management systems called three trees of Git. These systems include HEAD (the commit history), the staging index and the working directory. We are going to look through each of these systems.
The working directory
The first tree is the working directory. It represents the files on the file system of your computer, that are available to the code editor for applying changes. The working directory is considered to be a specific commit of the checked out project. When the project is checked out, this means that its files have decompressed versions extracted from the Git repository.
echo 'hello git reset' > edited_file git status #On branch master #Changes not staged for commit: #(use "git add ..." to update what will be committed) #(use "git checkout -- ..." to discard changes in working directory) #modified: edited_file
The following tree is the staging index, which tracks the changes committed in the working directory. In general, the performance details of the staging area are hidden from users by Git. Sometimes, while talking about staging area, different expressions are used, like cache, directory cache, staged files, staging area etc..
Here we need git ls-files. command, which is considered to be a debug tool, that checks the state of the staging index.
git ls-files -s #543644 a32de29bb3c1d643328b29ae775ad8c2e48c3256 0 edited_file
The last tree is the commit history. The git commit command commits changes to a permanent snapshot placed of the staging index.
git commit -am "edit content of test_file" #[master ab23324] edit the content of edited_file #1 file changed, 1 insertion(+) git status #On branch master #nothing to commit, working tree clean
In the example above, you can see the new commit with a message "edit content of test_file".The changes are attached to the commit history.. At this stage, running git status shows no forthcoming changes to any of the trees. Invoking git log, you will see the commit history. Once the changes are made through the three trees, the git reset can be used.
How it works
At first sight, the git reset command has some similarities with the git checkout, as they both operate on HEAD. The git checkout command operates exclusively on the HEAD reference pointer, while the git reset command passes the HEAD reference pointer and the current branch reference pointer. You will make a better understanding of its behavior with the illustration below:
This illustration presents the sequence of commits on the master branch. As you can see, the HEAD ref and the master branch ref presently point to commit d. We will see how the image changes in case of git checkout b and git reset b.
git checkout b
When executing the git checkout command, the master ref is still pointing to the commit d. What comes to the HEAD ref, it has been moved and changed the pointer to the commit b. As a result, the repository is now in a 'detached HEAD' state.
git reset b
The git reset command switches both the HEAD and branch refs to the defined commit. Besides, it changes the state of the three trees. There are three command line arguments --soft, --mixed, and --hard direct that define the modification of the staging index, and working directory trees.
By default, the git reset command has constant arguments of --mixed and HEAD. So, invoking git reset is the same as invoking git reset --mixed HEAD. Here the HEAD is the stated commit. You can use any Git SHA-1 commit hash instead of it.
The most commonly used option is the --hard. Using it has however some risks. With --hard, the commit history ref pointers start pointing to the stated commit. After, the staging index and the working directory are reset to correspond to the stated commit. Changes that have been previously pending to the staging index and the working directory, are reset to match the commit tree state. Any pending commit in the staging index and working directory will be lost. The example below will demonstrate the above mentioned. First of all, execute the following commands:
echo 'test content' > test_file git add test_file echo 'modified content' >> edited_file
A new file named test_file has been created and added to the repository. Furthermore, the content of edited_file will be modified. Let us now check the state of the repository with these changes using the git status command.
git status #On branch master #Changes to be committed: #(use "git reset HEAD ..." to unstage) #new file: test_file #Changes not staged for commit: #(use "git add ..." to update what will be committed) #(use "git checkout -- ..." to discard changes in working directory) #modified: edited_file
As you can see, there are now some pending changes. The pending change for the staging index tree is the addition of test_file and the one for the working directory are modifications to edited_file. Now let us see the state of the staging index:
git ls-files -s #123126 7a32454a5477b1bf4765946147c49509a431f963 0 test_file #123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file
The test_file has been added to the index. The edited_file has been updated, but the staging index SHA (d7d77c1b04b5edd5acfc85de0b592449e5303770) stays the same. These changes are in the working directory. They are not promoted to the staging index as we have not used the git add command. At this point, we can execute git reset --hardand see the new state of the repo:
git reset --hard #HEAD is now at ab23324 update content of edited_file git status #On branch master #nothing to commit, working tree clean git ls-files -s #123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file
The --hard option executed a "hard reset". Git indicates that HEAD is pointing to the recent commit ab23324. Then, the state of the repo is checked with git status. Git indicates there are no pending changes. What comes to the state of the staging index, it has been reset to a point before adding the test_file. edited_file changes and the addition of test_file have been wiped out. This loss cannot be undone.
The operating mode is by default --mixed. It updates the ref pointers. The staging index is reset to the stated commit. Undone changes from the staging index are placed in the working directory.
echo 'new file content' > test_file git add test_file echo 'append content' >> edited_file git add edited_file git status #On branch master #Changes to be committed: #(use "git reset HEAD ..." to unstage) #new file: test_file #modified: edited_file git ls-files -s #123126 6a32154a5477b1bf4765946147c49509a4323d32 0 test_file #123126 3c3262db063f9e9426901092c00a3394b4bd3445 0 edited_file
In the example above, a test_file has been added and the contents of edited_file have been modified. Next, these changes are applied to the staging index with the help of the git status. With this state of the repository, now it’s time to invoke git reset.
git reset --mixed git status #On branch master #Changes not staged for commit: #(use "git add ..." to update what will be committed) #(use "git checkout -- ..." to discard changes in working directory) #modified: edited_file #Untracked files: #(use "git add ..." to include in what will be committed) #test_file #no changes added to commit (use "git add" and/or "git commit -a") git ls-files -s #123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file
The --mixed is the default mode. It has the same effect as git reset. The git status shows that there are changes to edited_file and that the test_file is an untracked file. This is the exact--mixed behavior. The staging index has been reset and the pending changes are moved to the working directory.
The --soft argument updates ref pointers and stops the reset. However, it doesn’t affect the staging index and the working directory.
git reset --soft git status #On branch master #Changes to be committed: #(use "git reset HEAD ..." to unstage) #modified: edited_file git ls-files -s #123126 32a252710639e5da6b515416fd779d0741e4561a 0 edited_file
A soft reset resets only the commit history. By default, it is invoked with HEAD as the target commit. Let’s now create a new commit to try a --soft with a target commit that is not HEAD:
git commit -m "add changes to edited_file"
Now our repository has three commits. In order to find the first one, we need to check its ID, which can be done by viewing the output from git log.
git log #commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df #Author: w3docs #Date: Fri Nov 1 14:02:07 2019 -0800 #add changes to edited_file #commit ab23324a6da9f0dec51ed16d3d8823f28e1a72a #Author: w3docs #Date: Fri Nov 1 11:31:58 2019 -0800 #change content of edited_file #commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 #Author: w3docs #Date: Thu Sep 31 18:40:29 2019 -0800 #initial commit
This is the ID of the initial commit. Now it will be used as the target for the soft reset. Before, we need to check the current state of the repository:
git status && git ls-files -s #On branch master #nothing to commit, working tree clean #123126 32a252710639e5da6b515416fd779d0741e4561a 0 edited_file
Now we can soft reset the first commit:
git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4 git status && git ls-files -s #On branch master #Changes to be committed: #(use "git reset HEAD ..." to unstage) #modified: edited_file #123126 32a252710639e5da6b515416fd779d0741e4561a 0 edited_file
In the example above, we had a soft reset and the git status and git ls-files combo command invoked, that outputs the state of the repository. The git status command shows that there are some changes to edited_file highlighting them as changes staged for the next commit. The git ls-files input shows that the staging index has remained unchanged and retains the SHA 32a252710639e5da6b515416fd779d0741e4561a . Let’s additionally examine the state of the repository after soft reset by the help of git log:
git log #commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 #Author: w3docs #Date: Thu Sep 31 18:40:29 2019 -0800 #initial commit
As we can see, the output above indicates that a single commit in the commit history. As with all git reset invocations, firstly --soft reset the commit tree.
Unlike --hard and --mixed that have both been against the HEAD, a soft reset took the commit tree back in time.
The difference between reset and revert commands
Git revert is considered to be a safer way of undoing changes than git reset. There is a great probability, that the work can be lost with it reset. Git resetdoesn’t delete a commit, but it can make the commit “orphaned”. This means, that there isn’t any direct way to access them. As a result, Git will delete all the orphaned commits, when it runs the internal trash collector. By default, Git runs the internal trash collector every 30 days. The orphaned commits are usually found with the help of git reflog command.
Another difference between these two commands is that git revert is configured to undo public commits, and git reset is configured to undo local changes to the working directory and staging index.
The inadmissibility of the reset of public history
Don’t use git reset <commit>, when there are snapshots after <commit>, which are moved to a public repository. When you publish a commit, take into account the fact that other developers rely on it too. Deleting commits that are being developed by other team members too, will cause lots of problems. Use git reset <commit> only on local changes. To fix public changes use git revert command.
Use the following for removing the specified file from the staging area, but not changing the working directory. It will unstage a file without overwriting changes:
git reset <file>
Use the following for resetting the staging area to correspond to the last commit, but leave the working directory unchanged. It will unstage all files without overwriting changes, giving you the possibility to rebuild the staged snapshot from scratch:
Use the following for resetting the staging area and the working directory to correspond to the last commit. It will unstage changes overwriting all changes in the working directory, too:
git reset --hard
Use the following for moving the branch tip back in time to commit, resetting the staging area to match, but not touching the working directory:
git reset <commit>
Use the following for moving the current branch tip backward to <commit> and resetting the staging area and the working directory to match:
git reset --hard <commit>
Removing local commits
As mentioned above, you can use git reset command for deleting commits on the local repository. The example below is a demonstration of such a usage of git reset. The git reset HEAD~2 command pushes the current branch backward by two other commits and removes the two recently created snapshots from the project history.
# Create a new file called `yourname.txt` and add some code to it # Commit it to the project history git add yourname.txt git commit -m "Start to develop a project" # Edit `yourname.txt` again and change some other tracked files, too # Commit another snapshot git commit -a -m "Continue developing" # Scrap the project and remove the related commits git reset --hard HEAD~2
The git resetcommand is usually used for making staged snapshots. In the example below we have 2 files called task.txt and index.txt. which have been added to the repository. Git reset lets us to unstage the changes that are not connected with the next commit.
# Edit task.txt and index.txt # Stage everything in the current directory git add . # Realize that the changes in task.txt and index.txt # should be committed in different snapshots # Unstage index.txt git reset common.txt # Commit only task.txt git commit -m "Edit task.txt" # Commit index.txt in a separate snapshot git add index.txt git commit -m "Edit index.txt"