Manage Git work tree and index using “git restore” command

Alexey Samoshkin
5 min readJul 19, 2021

--

If you’re working with Git from a command line, as I used to in my everyday work, most likely you find it tough to remember all those commands, that are related to managing working tree and index.

Old school way

Let’s take a look at each typical scenario, along with a command that handles it.

I have a bunch of new files/changes in a working tree. I want to split them in several commits. So I need to stage several files first, commit, then stage the next couple of files, commit, and so on. This is obvious and straightforward. Just use “git add” command. Nothing new here.

$ git add <file1> <file2>

I have accidentally staged a wrong file, which I’d like to postpone for the next commit, and now I want to un-stage it.

$ git reset —- <path>

Note, that “git reset” command comes with two completely different responsibilities:
1. Un-stage files from index. It takes file from HEAD and put them to the index, effectively un-staging them. You still have the same file in working tree. You’re working on a file-level, and you’re not allowed to specify commit (it always uses HEAD). It does not move branch pointer to another commit — your HEAD is left intact.

2. Move a branch pointer to another commit and overwrite the working tree and/or index to completely match the given commit. You a working at a commit-level, and cannot specify a single file. All files are affected.

The latter case comes in following forms:

$ git reset —-soft <commit>
$ git reset —-hard <commit>
$ git reset —-mixed <commit> # — mixed is the default

Next scenario. I’ve changed several files in my working tree, but then I realised it’s all crap, and I want to revert one or more files.
This command takes file from Git index and puts it to the working tree.

$ git checkout -— <path>

Alternatively, you might want to revert file to one of your previous commit. Note, that this command would also update file in both index and working tree to match the given commit.

$ git checkout <commit> -- <path>

Note, that same to git reset, git checkout command is overloaded with two responsibilities: switching branches and restoring files.

To sum up all these commands’ zoo, I’ve created this neat diagram.

Manage work tree and index in an old school way

Index stages and merge conflicts

During a merge, in case of merge conflict, Git uses index to keep the different revisions of conflicting files from a three-way merge. To be more correct, it has a concept of different index stages. Think of them like the copies of regular Git index, with each copy given a dedicated number.

#1. Keeps the files from common merge-base commit in a three-way merge
#2. Ours. Files from our current branch (e.g master)
#3. Theirs. Files from the branch we’re going to merge (e.g feature)

There’re several commands, that let you interact with index stages.

Suppose, when a merge conflict happens, I don’t want to resolve the merge, but just take “ours” version of the given file (e.g from a master) and completely discard theirs one (from a feature branch).

$ git checkout --theirs -- <path>

Or vice versa, take “theirs” version of a file and completely discard “ours” one.

$ git checkout --ours -- <path>

Or, another use case. What if you messed up while trying to resolve the conflict within a single file. Now, you’re completely lost and start to panic. Don’t run “git merge— abort” just to repeat the whole merge process rerunning “git merge” again. Just restore merge conflict markers using following command:

$ git checkout --merge -- <path>

There’s no built-in command to peek into the file version from a common merge-base, but you can do it with some low-level Git plumbing:

$ git cat-file -p :1:[file] > [file]

New shiny way with “git restore”

Notice, how we used several commands with subtle differences in arguments passed to manage our working tree and index. That’s a cumbersome experience. I constantly forget those commands/arguments variations. To tackle the issue, I’ve added a bunch of shell aliases, but besides usability benefits, they just hide the root problem — inconsistency and complexity of the basic Git commands.

alias giA='git add -A'
alias gia="git add"
alias gir="git reset --"
alias gwu="git checkout --"
# etc

Let’s look at how “git restore” command brings more consistency handling those use cases.

I have accidentally staged a wrong file, which I’d like to postpone for the next commit, and now I want to un-stage it.

$ git restore --staged <path>

I’ve changed several files in my working tree, but then I realised it’s all crap, and I want to revert one or more files. This command takes file from Git index and puts it to the working tree.

$ git restore <path>

You want to revert file to the version from HEAD. Note, that this command updates file in both index and working tree to match the given commit.

git restore --staged --worktree <path>

If you want to revert a file to the version from one of your previous commit:

git restore --staged --worktree -s <commit> <path>

Moreover, you can even take file from the given commit, but update working tree only, without touching index. It’s something, which was not possible in an “old school” way.

git restore --worktree -s <commit> <path>

To work with index stages during merge conflict, use the same “git restore” command.

git restore --theirs <path>
git restore --ours <path>
git restore --merge <path>

To sum up, let’s look at the diagram again, now updated with a “git restore” usage.

Manage work tree and Git index with “git restore” command

Conclusion

Notice, how “git restore” command gives you the unified solution to manage your working tree and index.

It makes your life a bit easier, since you don’t need to remember all those git reset, git checkout commands, don’t need to recall to delimit command from “pathspecs” using -—, don’t keep in mind the difference between git reset <path> and git reset --hard|soft|mixed <commit> command flavours.

That’s great. Hope you enjoy my new post.

--

--