Git Checkout: Navigating the HEAD Pointer, Index, and Working Directory
TL;DR: The git checkout command is Git’s primary navigation system. It updates the files in your working directory to match a specific commit or branch, updates the index (staging area) to match that snapshot, and points the .git/HEAD reference to the new active target. Because it historically handled two distinct actions—switching branches and discarding local file changes—modern Git introduces git switch and git restore as clean replacements.
What Does git checkout Actually Do?
To understand how git checkout works, we must first look at Git’s Three Trees architecture. Git manages your project using three distinct states, or “trees” (which are actually data structures, not physical directories):
- The Working Directory (Tree 1): The actual sandbox on your computer’s filesystem. These are the physical files you open, edit, and save in your IDE.
- The Index / Staging Area (Tree 2): A binary file located at
.git/indexthat stores a list of file paths, their hashes, and metadata. It represents the pre-compiled snapshot of the next commit you plan to make. - The HEAD (Tree 3): A pointer (stored at
.git/HEAD) that refers to the commit snapshot representing your current location in history.
+-------------------+ +-------------------+ +----------------------+
| HEAD (Tree) | ---> | Index (Staging) | ---> | Working Directory |
| (Current Commit) | | (.git/index) | | (Physical Files) |
+-------------------+ +-------------------+ +----------------------+
When you use git checkout, its behavior depends on whether you pass it a commit-like reference (a branch, commit hash, or tag) or a file path.
1. Branch Switching (Checkout a Commit)
When you run git checkout <branch-name>:
- Git reads the target commit object.
- It updates the index (
.git/index) to match the list of files in that commit. - It alters your Working Directory: it creates files that exist in the target commit but not your current state, deletes files that don’t exist in the target commit, and overwrites modified files.
- It writes the path of the branch ref to
.git/HEAD(e.g.,ref: refs/heads/feature).
2. File Restoring (Checkout a Path)
When you run git checkout -- <file-path>:
- Git does not change
HEAD. You stay on the same branch. - Instead, it bypasses the commit history entirely. It reads the version of that file currently in the index (Tree 2) and copies its contents to overwrite your Working Directory (Tree 1).
- Any uncommitted changes in that file are instantly lost.
The Film Projector Metaphor
Think of Git as a film projector:
- The Commit History is the roll of film. Each frame is a commit.
- HEAD is the lens that focuses on a single frame.
- The Working Directory is the image projected on the screen. When you checkout a branch, you roll the film to focus on a different frame, updating the projected image. When you checkout a file, you’re not changing the frame; you’re just cleaning a dirty spot on the screen by resetting it to match what the current frame prescribes.
What Happens Inside .git/ (Hands-on exploration)
Let us watch what happens under the hood of .git/ by setting up a repository, switching branches, triggering a detached HEAD state, and restoring files.
Step 1: Initialize and Add Commits
First, let’s create a repository and add two commits on two separate branches.
$ mkdir git-checkout-demo
$ cd git-checkout-demo
$ git init
Initialized empty Git repository in /users/admin/git-checkout-demo/.git/
$ echo "Version 1" > app.txt
$ git add app.txt
$ git commit -m "First commit"
[main (root-commit) 5a1b2c3] First commit
Let’s create a new branch named feature (refer to /git-branch for details) but stay on main:
$ git branch feature
At this moment, both branches point to commit 5a1b2c3. Let’s verify .git/HEAD:
$ cat .git/HEAD
ref: refs/heads/main
Step 2: Switch Branch
Now let’s switch to the feature branch:
$ git checkout feature
Switched to branch 'feature'
Let’s check the .git/HEAD file again:
$ cat .git/HEAD
ref: refs/heads/feature
Git updated the reference target inside the .git/HEAD file to refs/heads/feature.
Now let’s add a new commit on feature to diverge from main:
$ echo "Feature Code" > feature.txt
$ git add feature.txt
$ git commit -m "Add feature script"
[feature c7d8e9f] Add feature script
Let’s check where the pointers are:
mainpoints to5a1b2c3featurepoints toc7d8e9fHEADpoints torefs/heads/feature
Now, let’s switch back to main:
$ git checkout main
Switched to branch 'main'
If we run ls in the working directory:
$ ls
app.txt
Notice that feature.txt is gone. Git deleted it because it is not present in the main commit 5a1b2c3. It also updated the .git/HEAD pointer back to ref: refs/heads/main. If we check the index contents using plumbing commands:
$ git ls-files --stage
100644 19a4e0a6d0c2e68449c25f464016a28bb0fbf2de 0 app.txt
Only app.txt remains staged in the index.
Step 3: Enter a “Detached HEAD” State
What happens if we checkout a specific commit hash instead of a branch name? Let’s check out the feature commit hash c7d8e9f directly:
$ git checkout c7d8e9f
Note: switching to 'c7d8e9f'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
HEAD is now at c7d8e9f Add feature script
Git outputs a long warning. Let’s inspect .git/HEAD:
$ cat .git/HEAD
c7d8e9fc8084a9e5251d5c58bb0e56e07a33bb55
Unlike before, .git/HEAD does not say ref: refs/heads/.... It contains the raw SHA-1 hash of the commit!
This is what “Detached HEAD” means: HEAD is pointing directly to a commit object rather than a branch reference.
If you make a new commit here, Git will create the commit object and update .git/HEAD to point to it. However, because no branch pointer references that new commit, if you run git checkout main, the new commit will become a dangling reference. It won’t appear in the /git-log of any branch, and eventually, it will be deleted.
Let’s resolve this. If we made changes and want to save them, we can attach a branch to our current position:
$ git checkout -b feature-recovery
Switched to a new branch 'feature-recovery'
Let’s check .git/HEAD again:
$ cat .git/HEAD
ref: refs/heads/feature-recovery
We are safely attached to a branch once again!
Step 4: Checking out a File
Let’s switch back to main and modify our app.txt file without staging or committing it:
$ git checkout main
$ echo "Dirty Uncommitted Line" >> app.txt
Let’s run /git-status to see where this modification lives:
$ 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: app.txt
The changes are only in our Working Directory. The index still contains the clean version of app.txt from our last commit.
Let’s restore the file to match the index:
$ git checkout -- app.txt
Let’s check the contents of app.txt:
$ cat app.txt
Version 1
The dirty line is gone! Git read the index hash for app.txt, pulled that blob from the .git/objects folder, and overwrote the file in our working directory.
Detailed Command Options & Advanced Usage
The git checkout command is incredibly powerful, with options that let you control how files are merged or restored.
1. Relative Commits and Tag Navigation
You can checkout tags, which are permanent commit references, or navigate using relative commit notation.
- Checkout a tag:
Like checking out a commit hash, this will put you in a detached HEAD state because tags are read-only references.$ git checkout v1.0.0 - Checkout using
HEADoffsets:HEAD~1orHEAD~goes back 1 commit to the first parent.HEAD~~orHEAD~2goes back 2 commits.HEAD^goes to the first parent of a merge commit, whileHEAD^2goes to the second parent.
2. Restore Files from a Specific Commit
If you want to restore a file, you don’t have to use the index. You can pull the file directly from another commit or branch.
$ git checkout c7d8e9f -- app.txt
Crucial Internal Detail: Unlike checking out a file from the index, checking out a file from a specific commit updates both your Working Directory and stages the file in your index. You do not need to run git add app.txt afterward; it is ready to commit.
3. The Modern Alternatives: git switch and git restore
Because of the UX confusion between branch switching and file restoring, Git version 2.23 introduced two specialized commands designed to split the responsibility of checkout.
git switch(for branch navigation):- Switch to an existing branch:
git switch <branch-name> - Create and switch to a new branch:
git switch -c <new-branch-name> - Return to the previous branch:
git switch -
- Switch to an existing branch:
git restore(for file modification discarding):- Discard working directory modifications:
git restore <file>(equivalent togit checkout -- <file>) - Unstage a staged file:
git restore --staged <file>(previouslygit reset HEAD <file>) - Restore a file from a specific commit:
git restore --source=<commit-hash> <file>
- Discard working directory modifications:
Real-World Scenario: The Dirty Working Directory Conflict
Suppose you are working on the branch main and have modified a file named config.json. You haven’t committed these changes yet. Suddenly, a production bug appears on the hotfix branch, and you need to switch immediately.
You run:
$ git checkout hotfix
If the modifications to config.json do not conflict with the version in hotfix, Git will allow you to switch and will carry your uncommitted modifications with you to the hotfix branch.
However, if config.json was changed in hotfix as well, Git will block you to prevent loss of work:
error: Your local changes to the following files would be overwritten by checkout:
config.json
Please commit your changes or stash them before you switch branches.
Aborting
How to Resolve This:
- Stash the changes (Recommended):
Save your work temporarily without committing it:
For more details, see our guide on /git-stash.$ git stash $ git checkout hotfix # Work on hotfix... $ git checkout main $ git stash pop - Force the checkout (Destructive):
If you don’t care about your changes and want to wipe them out:
$ git checkout -f hotfix - Perform a Merge Switch:
If you want to attempt to merge your local changes into the target branch during the switch:
This triggers a three-way merge. If there are conflicts, Git will put you on$ git checkout -m hotfixhotfixbut mark the conflict regions insideconfig.jsonfor you to resolve.
Common Pitfalls and Mistakes
1. Running git checkout -- <file> Accidentally
If you run this on a file, your local changes are immediately deleted and overwritten by the index. There is no undo history for untracked or unstaged edits. They never entered the Git object database, so they are unrecoverable.
2. Forgetting to Resolve Detached HEAD before Switching
If you checkout an old commit, write several new commits, and then type git checkout main without creating a branch first, those new commits are orphaned.
- Fix: Use
git reflogto locate the hash of the last commit you made in the detached state, checkout that hash, and create a branch usinggit checkout -b <branch-name>.
3. Namespace Clashes with --
If you have a file named main and a branch named main, typing git checkout main is ambiguous. Git will default to checking out the branch.
- Fix: Always use the double dash (
--) to clarify that you are targeting a file path:
This forces Git to interpret$ git checkout -- mainmainas a file rather than a branch.
Command Quick Reference
| Legacy Command | Modern Command | Action | Internals Affected |
|---|---|---|---|
git checkout <branch> | git switch <branch> | Switch active branch | Updates .git/HEAD, index, and working tree |
git checkout -b <name> | git switch -c <name> | Create and switch branch | Creates branch ref file, updates .git/HEAD |
git checkout -- <file> | git restore <file> | Discard local modifications | Overwrites working tree file with index blob |
git checkout <hash> -- <file> | git restore --source=<hash> <file> | Copy file from specific commit | Overwrites index and working tree with commit blob |
git checkout -f <branch> | git switch -f <branch> | Force branch switch | Overwrites working tree, discards modifications |
git checkout -m <branch> | git switch -m <branch> | Switch branch with merge | Executes three-way merge on conflicted files |
FAQ (People Also Ask)
What is the difference between git checkout and git switch?
Under the hood, they do the exact same thing when switching branches. The difference is purely user interface safety. git switch was introduced in Git 2.23 to prevent developers from accidentally losing uncommitted work in their files, which can happen when using git checkout for file restoration. git switch only handles branch operations.
How do I check out a remote branch that I don’t have locally?
If the remote branch is named origin/feature-auth, simply fetch the latest updates from your remote and checkout the branch name:
$ git fetch
$ git checkout feature-auth
Git will recognize that a remote branch matches that name, create a local branch called feature-auth pointing to the remote commit, and set up upstream tracking.
Does git checkout delete files?
Yes. If you switch to a branch where certain files do not exist (for example, files created on your old branch after the branches diverged), Git will delete those files from your working directory. If you switch back, Git will recreate them.
Test Your Knowledge
Loading quiz…