git switch and git checkout
git switch and git checkout are the two commands you use to move between branches in Git. git checkout is the original, do-everything command — it switches branches, restores files, and detaches HEAD onto arbitrary commits, all from one entry point. git switch is a newer, narrower command introduced specifically to make branch switching unambiguous and safer, and it is the one you should reach for by default. git checkout is still essential to know because it is everywhere in existing scripts, tutorials, and muscle memory.
Overview & How It Works
To understand branch switching you first need a clear picture of Git’s object model. Every commit Git creates is an object containing a pointer to a tree object (a snapshot of the directory structure at that point), pointers to zero or more parent commits, and metadata (author, committer, timestamp, message). A tree object in turn points to blob objects, which store the raw contents of files, and to other tree objects for subdirectories. Git does not copy everything per commit — it only stores new blobs and trees for what actually changed; anything unchanged is shared by reference, identified by its content hash.
A branch, such as main or feature/login-page, is nothing more than a small file under .git/refs/heads/ that contains a single commit hash — a lightweight, movable pointer to the tip commit of that line of work. HEAD is, in the normal case, a symbolic reference that points at one of these branch refs (you will see this written as HEAD -> refs/heads/main). When you commit, Git creates a new commit object and then moves the branch ref that HEAD points to forward, to that new commit. Switching branches does not change any history — instead, Git changes what HEAD points at and rewrites your working directory and staging area (the index) to match the snapshot recorded in the target branch’s tip commit.
That is the essence of what both git switch <branch> and git checkout <branch> do: read the target commit’s tree, compare it file by file against your working tree and index, update every file that differs, then repoint HEAD. If you have uncommitted changes that would be silently overwritten by this process, Git refuses and asks you to commit, stash, or discard them first.
It is also possible to point HEAD directly at a commit rather than at a branch. This is called detached HEAD state. In this state HEAD holds a raw commit SHA instead of a symbolic reference to a branch, so any new commits you make there are not tracked by a branch — they become unreachable and eligible for garbage collection the moment you switch away, unless you create a branch to hold onto them first.
Historically, git checkout handled all of this, plus restoring individual files from the index or another commit — a genuinely different operation that works on paths, not refs. That overload made checkout confusing: git checkout main could switch branches, but if a file happened to be named main too, Git had to guess your intent from context. Git 2.23 (released in 2019) introduced git switch and git restore to split checkout‘s responsibilities into two focused, less error-prone commands. git switch only ever operates on branches; git restore only ever operates on files. checkout remains fully supported, so you will keep encountering it, but this lesson teaches switch as the default for branch changes, with checkout explained alongside it.
Syntax
git switch
git switch [<branch>]
git switch -c <new-branch> [<start-point>]
git switch -d <commit>
git switch -
| Option | Meaning |
|---|---|
<branch> |
Switch to an existing local branch, updating the working tree, index, and HEAD. |
-c, --create <new-branch> |
Create <new-branch> and switch to it, starting from the current commit (or from <start-point> if given, e.g. another branch, tag, or commit). |
-C, --force-create <new-branch> |
Like -c, but resets the branch if it already exists, discarding its previous tip at that name. |
-d, --detach <commit> |
Check out a commit or tag directly, entering detached HEAD state instead of moving to a branch. |
- |
Switch back to whichever branch you were on before the current one. |
-f, --force |
Discard local changes in files that differ, instead of aborting the switch. |
-t, --track |
When creating a branch from a remote-tracking branch, set it up to track that remote branch. |
git checkout
git checkout <branch>
git checkout -b <new-branch>
git checkout <commit>
git checkout -- <path>
| Option | Meaning |
|---|---|
<branch> |
Switch to an existing branch (same effect as git switch <branch>). |
-b <new-branch> |
Create <new-branch> and switch to it (equivalent to git switch -c). |
-B <branch> |
Create or reset <branch> to the current commit and switch to it. |
<commit> |
Check out a specific commit or tag directly, entering detached HEAD state. |
-- <path> |
Restore <path> in the working tree from the index (or from a given commit), leaving HEAD and the current branch untouched. |
-f, --force |
Discard local changes instead of aborting. |
-m, --merge |
Attempt a three-way merge of local changes into the new branch instead of aborting. |
Examples
Example 1: Creating and switching to a new branch
Most of the time you want to start new work on its own branch. -c creates the branch and moves you onto it in a single step.
git branch
git switch -c feature/login-page
Output:
* main
Switched to a new branch 'feature/login-page'
git branch with no arguments lists local branches, marking the current one with *. git switch -c then creates feature/login-page pointing at the same commit as main and immediately repoints HEAD at it — the working tree does not change yet because both branches share the same tip commit.
Example 2: Git blocks a switch that would overwrite changes
Git will not silently discard uncommitted work when you switch branches.
echo '// work in progress' >> app.js
git switch main
Output:
error: Your local changes to the following files would be overwritten by checkout:
app.js
Please commit your changes or stash them before you switch branches.
Aborting
Because app.js differs between the current branch and main, applying main‘s version would destroy the uncommitted edit, so Git aborts. The safe fix is to stash the change, switch, and (later) restore it:
git stash
git switch main
Output:
Saved working directory and index state WIP on feature/login-page: 3a5c9f1 fix: add login form validation
Switched to branch 'main'
Example 3: Detached HEAD by checking out a specific commit
Sometimes you want to inspect (or branch from) an old commit rather than a branch tip.
git log --oneline -3
git checkout 3a5c9f1
Output:
3a5c9f1 fix: add login form validation
9f8e7d6 docs: fix typo in README
4c3b2a1 chore: initial commit
Note: switching to '3a5c9f1'.
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 3a5c9f1 fix: add login form validation
HEAD now points straight at commit 3a5c9f1 instead of at a branch ref. If you decide this old point in history is a good place to start real work — say, a hotfix — attach a branch to it before you do anything else:
git switch -c hotfix/login-typo
Output:
Switched to a new branch 'hotfix/login-typo'
hotfix/login-typo now points at 3a5c9f1, and HEAD is symbolic again, so any commits you make from here are safely reachable through the new branch.
Example 4: Restoring a single file
checkout‘s other historical job — discarding an unstaged edit to one file — is written with a -- before the path so Git treats what follows as a path, not a ref.
git status --short
git checkout -- app.js
Output:
M app.js
The first line of output is from git status --short, showing app.js modified but not staged. git checkout -- app.js replaces the working-tree copy of app.js with the version from the index, printing nothing on success; a follow-up git status --short would show a clean tree. The modern, unambiguous equivalent is:
git restore app.js
Prefer git restore for this task — it can never be mistaken for a branch switch.
How It Works Step by Step
When you run git switch <branch> (or git checkout <branch>), Git performs roughly these steps internally:
- Resolve
<branch>to a commit SHA by reading.git/refs/heads/<branch>. - Read that commit object to find the tree object representing its full directory snapshot.
- Compare that tree, file by file, against the current index and working tree. If any tracked file has uncommitted changes that the switch would overwrite, abort with an error (unless
--forceor--mergeis given). - Write the differing files into the working directory so it matches the target tree exactly.
- Update the index so it also matches the target tree — staged changes now reflect the new branch’s state.
- Move
HEAD: for a branch, rewriteHEADto the symbolic referencerefs/heads/<branch>; for a bare commit or tag, write the raw SHA intoHEADdirectly, producing detachedHEADstate.
Nothing about the commits themselves, their parents, or their trees is touched by any of this — switching branches only ever moves pointers and updates the two things that represent your current working state, the index and the working directory.
Common Mistakes
Mistake 1: Relying on checkout when a branch and a file share a name
git checkout main
If your repository has both a branch called main and a tracked file called main, git checkout main is ambiguous — older Git versions had to guess which one you meant, and could restore the file instead of switching branches. Fix: use git switch main when you mean the branch, and git restore main (or git checkout -- main) when you mean the file. Because each command only accepts one kind of target, the ambiguity disappears.
Mistake 2: Losing commits made in detached HEAD
git checkout 3a5c9f1
git commit --allow-empty -m 'fix: quick patch'
git switch main
Output:
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
8b2e4d0 fix: quick patch
If you want to keep it by creating a new branch, this may be a good time
to do so with:
git branch 8b2e4d0
Switched to branch 'main'
The commit made in detached HEAD is not deleted immediately, but it is unreachable from any branch, so it will eventually be garbage collected. Fix: before switching away, create a branch to hold the work with git switch -c <name>. If you already switched away, Git’s warning gives you the SHA, so you can still recover it:
git switch -c hotfix/quick-patch 8b2e4d0
Output:
Switched to a new branch 'hotfix/quick-patch'
Mistake 3: Discarding work with checkout — .
git checkout -- .
Run in a directory with unstaged edits you actually wanted to keep, this instantly replaces every changed tracked file with the version already in the index — with no confirmation and no easy undo, since the previous working-tree content was never committed anywhere. Fix: run git status and git diff first to see exactly what you are about to lose, restore specific paths instead of . when unsure, and use git stash if you might want the changes back later.
Best Practices
- Default to
git switchfor branches andgit restorefor files; readgit checkoutcomfortably, since it appears constantly in older material, but avoid writing new scripts around its overloaded behavior. - Run
git statusbefore switching branches so you know in advance whether uncommitted changes will block the switch. - Use
git switch -c <branch> <start-point>to branch off a specific commit, tag, or another branch, rather than always branching from whatever you happen to be on. - Name branches descriptively with a type prefix, such as
feature/login-pageorfix/nav-bar-overflow, mirroring the Conventional Commits style (feat:,fix:,chore:) you use in commit messages. - Before leaving a detached HEAD, create a branch with
git switch -c <name>if you made any commits there worth keeping. - Avoid reaching for
-f/--forceout of habit; understand that it discards local changes with no built-in undo, so commit or stash first. - Use
git switch -to jump back to whatever branch you were previously on. - Run
git fetchbefore switching onto a teammate’s branch, so you are not working from a stale remote-tracking tip.
Practice Exercises
- Starting from
main, create a branch calledchore/update-deps, make and commit a small change, then switch back tomainand confirm the change is not present there. (Hint:git switch -c, then compare withgit log --onelineon each branch.) - Check out an older commit from your log directly, so you land in detached HEAD, make an experimental commit, then decide you want to keep it. Attach a branch to it without losing the commit.
- Modify a tracked file without staging it, note its contents with
git diff, then discard the change withgit restore <file>. Confirm the working tree afterward exactly matches the last commit.
Summary
- A branch is a movable pointer to a commit;
HEADnormally points at a branch, and switching branches movesHEAD, not history. git switchandgit checkout <branch>both update the working tree and index to match the target commit’s tree, then repointHEAD.git switch -c <branch>(orgit checkout -b <branch>) creates and switches to a new branch in one step.- Checking out a raw commit or tag instead of a branch produces detached
HEADstate; commits made there need a branch to stay reachable. - Git 2.23 split
checkout‘s branch-switching and file-restoring roles intogit switchandgit restoreto remove ambiguity — prefer them for new work. - Git refuses to switch branches over uncommitted changes that would be overwritten; commit, stash, or use
--force/--mergedeliberately. git checkout -- <path>andgit restore <path>discard unstaged edits to a file immediately and without an easy undo.
