git checkout
and git reset
are heavily overloaded
commands that can be hard to understand because many similar-looking commands
do subtly different things. This is my attempt to describe the different
variants as simply and concisely as possible.
Target audience: people who understand the concepts of index vs. working tree, history graph, refs etc.
The most important distinction is that both commands have two forms: one with a path argument and one without. Here are some examples of commands without a path argument:
git checkout git checkout next git checkout - git reset git reset --hard git reset --hard HEAD~5And here are some with paths:
git checkout . git checkout HEAD test git reset . git reset HEAD~5 foo bar baz
Without path | With path | |
---|---|---|
checkout | Switch branches | Overwrite working tree file(s) from index or commit |
reset | Overwrite a branch and/or the full index/working tree with a specific version | Overwrite index entries with a specific version |
Let's get the easy part out of the way. Whenever you specify paths,
obviously only the paths in question are affected (so this will never fully
switch a branch, nor change what commit a branch points to), but exactly what
is affected depends on the command and which options are provided. Whenever I
say <commit>
, it could also mean a branch name or e.g.
HEAD
. The --
is an optional separator to
disambiguate (e.g. if you have a path that has the same name as a ref. It's a
good habit to always use the separator).
Command | Take versions from | Update the index | Update the working tree | Change/switch current branch |
---|---|---|---|---|
git checkout [--] <path> | index | no* | YES | no |
git checkout <commit> [--] <path> | commit | YES | YES | |
git reset [--] <path> | HEAD | YES | no | |
git reset <commit> [--] <path> | commit | YES | no |
(*) Technically you could say that the index entry is updated with itself, but effectively nothing changes.
So, a simple takeaway is that git checkout
with path
always updates the working tree and git reset
with path
never updates the working tree. Another interesting takeaway is that
there is no straightforward command to overwrite a working tree file with a
version from a specific commit without also overwriting the index entry. You
have to resort to trickery to achieve that.
Example: git checkout next
This switches your working tree over to a different branch. If you have any
uncommitted changes, they are kept (i.e. you'll be on the new branch and still
have the same uncommitted changes). If this is not possible without making
potentially destructive changes, checkout
will refuse to do
anything. You can use -f
to discard all uncommitted changes, or
-m
to try and apply the uncommitted changes to the new branch
using a file merge operation, potentially creating conflict markers etc.
This is the hardest to explain, because it has two main purposes rolled into one: changing what a branch points to, or doing a blanket rollback of changes in the index and/or the working tree. Or both...
Whether the current branch's target commit is changed (which is most often
used to change it to an older commit to effectively remove commits from the
branch) depends on whether a <commit>
argument is provided.
If this is not given, it defaults to HEAD
– that's
precisely what the branch is currently pointing to, so by default the branch
doesn't really change.
So, whenever you don't actually want to touch the branch pointer, you leave
out the <commit>
argument, at which point the options
become more interesting. If you use both the argument and the options,
reset
does both: first it overwrites the branch pointer, then it
does whatever the options tell it to do.
A typical example where the second purpose (rolling back changes in the
index/working tree) is what we care about is git reset --hard
.
Here, the branch pointer does not change, but --hard
tells
reset
to rollback all changes in the index and the working tree.
Effectively, this means "remove all uncommitted changes from the index and
working tree".
Here's the full overview:
Command | Change branch pointer | Roll back index | Roll back working tree | Source version |
---|---|---|---|---|
git reset --soft | no* | no | no | HEAD |
git reset git reset --mixed | no* | YES | no | HEAD |
git reset --hard | no* | YES | YES | HEAD |
git reset --soft <commit> | YES | no | no | commit |
git reset <commit> git reset --mixed <commit> | YES | YES | no | commit |
git reset --hard <commit> | YES | YES | YES | commit |
(*) Technically you could say that the branch pointer is updated with its current value, but effectively nothing changes.
If you've been paying attention, you might have noticed that git
reset --soft
without a <commit>
argument does...
nothing. That's right! --soft
is fairly useless in that
situation.