If you’re like me and you’ve worked with Git for some time, you might have a couple of commands committed to your memory—from git commit
for recording your changes, to git log
for sensing “where” you are.
I have found git checkout
to be a command that I reach for pretty frequently, as it performs more than one operation. But a single command doing more than one thing might produce a suboptimal user experience for someone learning Git. I can almost picture an XKCD strip:
Learner: What do I run to change the branch I’m on?
You: Usegit checkout <branch>
.
Learner: What can I run to discard changes to a file?
You: Use…git checkout <file>
.
Learner: OK…
Even if you have the commands memorized, there have likely been times when you had to pause after typing a git checkout
command while you tried to match it with the operation you had in mind (e.g., “I just typed git checkout … to do X, but I thought git checkout does Y, does this really do what I want?")
Let’s take a look at what git checkout
can do, and an alternative (or two) that can make for a friendlier user experience in Git.
git checkout
do?Perhaps you were trying something out and made some changes to the files in your local Git repository, and you now want to discard those changes. You can do so by calling git checkout
with one file path or more:
$ git checkout app.js
The above sets the specified files paths to their content 1 in the index. If instead you’d like to set the files to their content in a tree, like a branch or a commit, specify it before the file paths. If it happens that the branch shares a name with the file, pass the --
to separate the two. 2
$ git checkout wip app.js
$ git checkout wip -- app.js
In other words, git checkout <filepath>
sets <filepath>
to its contents in the index; if <tree>
is provided, git checkout <tree> <filepath>
sets <filepath>
to its contents in <tree>
.
Say you want to return to a branch that you had been working on previously—wip
. You can run the below to set it to be the branch you’re on and “checkout” 3 its files:
$ git checkout wip
You might have encountered:
$ git checkout -
This checks out the last branch you were on, much like how cd -
in your shell changes you back to the last directory you were in.
Let’s add that our list of what git checkout
does:
git checkout <filepath>
sets <filepath>
to its contents in the index; if <tree>
is provided, git checkout <tree> <filepath>
sets <filepath>
to its contents in <tree>
.git checkout <branch>
sets the branch we’re on to <branch>
.However, instead of saying “setting the branch we’re on,” it’s more accurate to say that git checkout
sets HEAD
to point to <branch>
. As the concept of HEAD
is pretty important, let’s take a look at what HEAD
is before continuing our exploration of git checkout
.
One of Git’s roles is to track content, and it helps us to know what changes we have. But how does Git determine when a file has changed?
HEAD
plays a role in this. By setting HEAD
to, for example, a branch, as in the second operation we looked at, Git would report changes by comparing it against the contents of the branch that HEAD
points to 4. Both HEAD
and the branch would reference the same commit.
In addition to setting HEAD
to point to a named branch, you can also point it to a commit, which brings us to another git checkout
operation. For example, let’s say you see a page to be laid out weirdly, even though you remember it being pixel-perfect when you last worked on it about a week ago, with commit f7884
. To confirm your hypothesis, you can explore your project’s state as-of commit f7884
and set the contents of the files in your Git repository correspondingly via:
$ git checkout f7884
Apart from setting the contents of your files, it also sets HEAD
to point to the commit f7884
, unlike a branch in the second operation we looked at:
This is known as a detached HEAD state. (In fact, you can perform the equivalent operation by invoking git checkout
with the --detach
argument). If you were to make a new commit while in this state, HEAD
would advance accordingly, but these commits would not be reachable through the usual Git references, like branches and tags. For example, if you were in this state and made a new commit to add padding to a header, here’s what your Git history would look like:
If you were to switch away to another branch, and not point a reference to your new commit, there is a chance your new commit will be lost through garbage collection. 5
Phew, there are quite a few things that git checkout
can do!
git checkout <filepath>
sets <filepath>
to its contents in the index; if <tree>
is provided, git checkout <tree> <filepath>
sets <filepath>
to its contents in <tree>
.git checkout <branch>
sets HEAD
to point to <branch>
.git checkout <commit>
sets HEAD
to point to <commit>
.That’s not all it can do; there are other possible variations through its long/short options, perhaps as a result of Git’s growth from its open source contributors.6 But generally, we see that git checkout
deals with two aspects of the Git repository:
HEAD
to point to a branch or a commit, andGranted, these aspects are intertwined, with B being a corollary of A. For example, if you were switching a branch (aspect A), you’d probably also want Git to set the content of your files to reflect their state in the branch you were switching to (aspect B). But the business of changing the contents of files while leaving HEAD
unchanged, like in the first operation, does come across as distinct from the second and the third, where HEAD
gets changed to point to something else, like a branch or a commit. Having a Git command for “setting the contents of files” and a separate command for “changing HEAD
” would make for a better user experience, both to someone new to Git looking for a rule of thumb (“for X operation, use command X”), and to an experienced user of Git ("<types command X from heart and reads it>—yup, reads like what I want to do”).
Enter git restore
and git switch
.
Now let’s run through the three operations again to see how these two commands are used.
When given a file path, git checkout <filepath>
sets one or more <filepath>
to its contents in the index.
Use git restore
to set the contents of files, but not to change what HEAD
points to:
$ git restore <filepath>
As a mnemonic, think back to our example - we wanted to restore the contents of <filepath>
to the index and discard changes to those files.
For the variation where we’d set the files to their content in a tree (git checkout <tree> <filepath>
), use the --source
argument to git restore
:
$ git restore --source <tree> <filepath>
When given a branch, git checkout <branch>
sets HEAD
to point to <branch>
.
Use git switch
to set HEAD
to point to a branch:
$ git switch <filepath>
A useful mnemonic would be to think that we are switching to a branch.
When given a commit, git checkout <commit>
sets HEAD
to point to <commit>
.
Similarly use git switch
, but you have to specify --detach
. This helps to call out that you are putting your repository in detached HEAD
state.
$ git switch --detach <commit>
Both git switch
and git restore
were introduced in Git v2.23 released in August 2019, so you should be able to use them on a machine with an up-to-date installation of Git, without having to install an additional piece of software. Indeed, you may already have encountered references to git switch
and git restore
in the documentation for git checkout
, and in the advice printed by Git when entering detached HEAD
state, among others.
To help you get started with git switch
and git restore
, here’s a mapping from a git checkout
invocation you may already be using in your daily workflow to a git switch
or git restore
invocation:
git checkout | Change HEAD to: | Which files are changed? | git switch/restore |
---|---|---|---|
git checkout <filepath> git checkout -- <filepath> |
no change | Files listed in <filepath> |
git restore <filepath> |
git checkout <tree> <filepath> git checkout <tree> -- <filepath> |
no change | Files listed in <filepath> |
git restore --source <tree> <filepath> |
git checkout <branch> |
<branch> |
All files in repo | git switch <branch> |
git checkout <commit> git checkout --detach <commit> |
<commit> |
All files in repo | git switch --detach <commit> |
Here’s my challenge to you: start using git switch
and git restore
! To make things fun, once you’ve used them 12 times or more, post a screenshot as proof with the tag #12SwitchRestoresSansCheckOuts. Here’s my take on it.
If you’ve feedback on these commands, feel free to drop an email to the Git mailing list where development happens; see this note for details.
I hope this improved user experience will be a part of your daily workflow—better yet, of your muscle memory.
Many thanks to Dave Barnes and Liza for reading early versions of this post!
git checkout
has a full list of the options it takes; it’s also where detached HEAD is explained.I used “contents of files”, when it is more accurate to talk about the “working tree” as something separate from the index. The “Three Trees” section of the freely available Pro Git book explains what they are (with diagrams!) ↩︎
Note that the changes will be staged after running the command - or to use Git parlance, the index is overwritten. ↩︎
That the git checkout
command does a “checkout” of branches or files was in fact the description used in its documentation in earlier versions of Git, like in v1.7.0. ↩︎
When determining what has changed, HEAD
isn’t the only factor—it depends on how you ask Git for changes. For example, git diff
uses the index as the point of comparison, so even if your files didn’t match their content in HEAD
but had been staged, you’d get an empty output. It’s also important to note that Git doesn’t deal with changes or deltas; each commit is a complete snapshot of your files. ↩︎
The detached HEAD section of the git checkout
documentation gives some commands you can use to “recover” from this situation. ↩︎
In a 2011 interview on Geek Time by the Google Open Source Programs Office, Junio C Hamano, the maintainer of Git, responds to the criticism that Git is hard to use:
“Another thing is because the system wasn’t really designed, but grew organically. So somebody came up with an idea of doing one thing. ‘Oh, this is a good idea, a good feature; let’s add it to this command as this option name’. And the option name he chooses just gets stuck, but after a few months, somebody else notices, ‘Oh, this is a similar mode of operation with that existing command.'”
(This author bears some blame for expanding the plethora of options git checkout
takes, having contributed the -B
option, the “forced” counterpart to git checkout -b <branch>
.)
A summary of the interview can be found on the Google Open Source blog. (Via an InfoQ post also on the introduction of git switch and restore.) ↩︎