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
|Overwrite working tree file(s) from index or commit
|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
<commit>, it could also mean a branch name or e.g.
-- 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).
|Take versions from
|Update the index
|Update the working tree
|Change/switch current branch
git checkout [--] <path>
git checkout <commit> [--] <path>
git reset [--] <path>
git reset <commit> [--] <path>
(*) 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.
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
<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
reset to rollback all changes in the index and the working tree.
Effectively, this means "remove all uncommitted changes from the index and
Here's the full overview:
|Change branch pointer
|Roll back index
|Roll back working tree
git reset --soft
git reset --mixed
git reset --hard
git reset --soft <commit>
git reset <commit>
git reset --mixed <commit>
git reset --hard <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
reset --soft without a
<commit> argument does...
nothing. That's right!
--soft is fairly useless in that