Git Checkout, Reset and Restore
I have always used the git checkout and git
reset commands to reset my working tree or index but since
Git 2.23 there has been a git restore command available
for these purposes. In this post, I record how some of the 'older'
commands I use map to the new ones. Well, the new commands aren't
exactly new since Git 2.23 was released in 2019, so this post is
perhaps six years too late. Even so, I want to write this down for
future reference. It is worth noting that the old and new commands
are not always equivalent. I'll talk more about this briefly as we
discuss the commands. However, they can be used to perform similar
tasks. Some of these tasks are discussed below.
Contents
Experimental Setup
To experiment quickly, we first create an example Git repository.
mkdir foo/; cd foo/; touch a b c
git init; git add a b c; git commit -m hello
Now we make changes to the files and stage some of the changes. We then add more unstaged changes to one of the staged files.
date | tee a b c d; git add a b d; echo > b
At this point, the working tree and index look like this:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: a
modified: b
new file: d
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: b
modified: c
File a has staged changes. File b has
both staged and unstaged changes. File c has only
unstaged changes. File d is a new staged file. In
each experiment below, we will work with this setup.
All results discussed in this post were obtained using Git 2.47.3 on Debian 13.2 (Trixie).
Reset the Working Tree
As a reminder, we will always use the following command between experiments to ensure that we restore the experimental setup each time:
date | tee a b c d; git add a b d; echo > b
To discard the changes in the working tree and reset the files in the working tree from the index, I typically run:
git checkout .
However, the modern way to do this is to use the following command:
git restore .
Both commands leave the working tree and the index in the following state:
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: a
modified: b
new file: d
Both commands operate only on the working tree. They do not alter the index. Therefore the staged changes remain intact in the index.
Reset the Index
Another common situation is when we have staged some changes but want to unstage them. First, we restore the experimental setup:
date | tee a b c d; git add a b d; echo > b
I normally run the following command to do so:
git reset
The modern way to do this is:
git restore -S .
Both commands leave the working tree and the index in the following state:
$ git status
On branch main
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: a
modified: b
modified: c
Untracked files:
(use "git add <file>..." to include in what will be committed)
d
no changes added to commit (use "git add" and/or "git commit -a")
The -S (--staged) option tells git
restore to operate on the index (not the working tree) and
reset the index entries for the specified files to match the
version in HEAD. The unstaged changes remain intact as
modified files in the working tree. With the -S
option, no changes are made to the working tree.
From the arguments we can see that the old and new commands are not
exactly equivalent. Without any arguments, the git
reset command resets the entire index to HEAD,
so all staged changes become unstaged. Similarly, when we run
git restore -S without specifying a commit, branch or
tag using the -s (--source) option, it
defaults to resetting the index from HEAD.
The . at the end ensures that all paths under the
current directory are affected. When we run the command at the
top-level directory of the repository, all paths are affected and
the entire index gets reset. As a result, both the old and the new
commands accomplish the same result.
Reset the Working Tree and Index
Once again, we restore the experimental setup.
date | tee a b c d; git add a b d; echo > b
This time we not only want to unstage the changes but also discard
the changes in the working tree. In other words, we want to reset
both the working tree and the index from HEAD. This is
a dangerous operation because any uncommitted changes discarded in
this manner cannot be restored using Git.
git reset --hard
The modern way to do this is:
git restore -WS .
The working tree is now clean:
$ git status On branch main nothing to commit, working tree clean
The -W (--worktree) option makes the
command operate on the working tree. The -S
(--staged) option resets the index as described in the
previous section. As a result, this command unstages any changes
and discards any modifications in the working tree.
Note that when neither of these options is specified,
-W is implied by default. That's why the
bare git restore . command in the previous section
discards the changes in the working tree.
Summary
The following table summarises how the three pairs of commands discussed above affect the working tree and the index, assuming the commands are run at the top-level directory of a repository.
| Old | New | Working Tree | Index |
|---|---|---|---|
git checkout . |
git restore . |
Reset to match the index. | No change. |
git reset |
git restore -S . |
No change. | Reset to match HEAD. |
git reset --hard |
git restore -SW . |
Reset to match HEAD. |
Reset to match HEAD. |
The git restore command is meant to provide a clearer
interface for resetting the working tree and the index. I still use
the older commands out of habit. Perhaps I will adopt the new ones
in another six years, but at least I have the mapping written down
now.