Moving HEAD: reset, checkout, switch, restore, revert
Mid / SeniorWhat each command does to HEAD, the branch ref, the index, and the working tree — reset soft/mixed/hard, why checkout was split into switch and restore, revert vs reset, and stash.
Created 15 October 2026
8 concepts · detail hidden
01Glossary — terms used in this page
#c-glossary-terms-used-in-this-page- HEAD — the ref for “what is checked out”; normally points to a branch, which points to a commit.
- Index / staging area — the snapshot that the next commit will contain.
- Working tree — the files on disk.
- Detached HEAD — HEAD pointing directly at a commit hash instead of at a branch.
- ORIG_HEAD — a ref Git sets before dangerous operations (reset, merge, rebase) to the previous HEAD, so you can go back.
- Reflog — the local log of every move of HEAD and of each branch tip.
- Stash — a stack of saved, uncommitted changes, stored as special commits under
refs/stash. - Worktree — a working directory attached to a repository; one repo can have several, each with its own checked-out branch, HEAD, and index.
02The mental model — which of the four things does a command touch?
#c-the-mental-model-which-of-the-four-things-does-a-command-touEvery “undo-ish” command is defined by which of these it changes:
① HEAD (what is checked out) ② the branch ref HEAD points to ③ the index ④ the working treeCommand ① HEAD ② branch ref ③ index ④ working tree Safe for uncommitted work? git reset --soft Cmoves with branch moves to C — — yes git reset C(--mixed)moves with branch moves to C = C — yes git reset --hard Cmoves with branch moves to C = C = C no — discards edits git switch B/git checkout B→ branch B — = B = B (keeps compatible local edits) yes (refuses on conflict) git restore f— — — f = index no — discards f’s edits git restore --staged f— — f = HEAD — yes git revert Cmoves with branch new commit appended updated updated needs clean tree The single most important distinction:
resetmoves the branch;switch/checkout <branch>moves HEAD to a different branch.git reset --hard B (on main) git switch feature before: A───B───C ← main ← HEAD before: A───B───C ← main ← HEAD └───D ← feature after: A───B ← main ← HEAD after: A───B───C ← main └───C (unreferenced; └───D ← feature ← HEAD reflog still has it) the BRANCH moved only HEAD moved; no branch changed03`git reset` — soft, mixed, hard
#c-git-reset-soft-mixed-hardgit reset <commit>always does step 1, and optionally 2 and 3:- Move the branch that HEAD points to to
<commit>(HEAD comes along). →--softstops here. - Make the index match that commit. →
--mixed(the default) stops here. - Make the working tree match too. →
--hard.
Before resetting, Git saves the old tip in
ORIG_HEAD.Worked example
A───B───C ← main ← HEAD (C added "feature.py", you also have unstaged edits in app.py)git reset --soft HEAD~1main→ B. The index still contains C’s snapshot, sofeature.pyshows as staged; unstagedapp.pyedits are untouched. Commit C’s changes are “un-committed but staged”. Classic use — squash the last N commits:git reset --soft HEAD~3 git commit -m "Add login flow" # one commit replaces threegit reset HEAD~1 # --mixedmain→ B, index = B. C’s changes are now unstaged modifications in the working tree. Use: “I committed too early / want to split the commit into several.”git reset --hard HEAD~1main→ B, index = B, working tree = B. C’s changes and your uncommittedapp.pyedits are gone from disk. Commit C is still recoverable from the reflog (git reset --hard ORIG_HEADorHEAD@{1}); uncommitted edits are not — they never became objects (unless they weregit added, in which case the blobs are dangling and findable withgit fsck --lost-found).Reset with a path — a different operation
git reset HEAD -- config.yaml # or: git restore --staged config.yamlWith paths,
resetdoes not move any branch. It only copies that file’s version from the commit into the index — i.e. unstages it. (--soft/--hardare not allowed with paths.)Common recipes
Goal Command Undo last commit, keep changes staged git reset --soft HEAD~1Undo last commit, keep changes unstaged git reset HEAD~1Throw away everything back to the remote git fetch && git reset --hard origin/mainUndo a reset / bad merge / bad rebase git reset --hard ORIG_HEAD(or aHEAD@{n}from the reflog)Unstage a file git restore --staged fPitfalls
reset --hardalso leaves untracked files alone; to delete those usegit clean -fd(preview with-n).- Resetting a branch that is already pushed rewrites shared history — you would need to force-push. On shared branches use
revertinstead.
- Move the branch that HEAD points to to
05`git switch` and `git restore` — checkout split in two
#c-git-switch-and-git-restore-checkout-split-in-twoGit 2.23 (2019) introduced two commands, each doing one of checkout’s jobs:
git switch— change branches onlygit switch main # switch to an existing branch git switch -c feature/x # create from HEAD and switch git switch -c hotfix origin/main # create from a start point git switch - # back to the previous branch git switch --detach v1.4.0 # inspect a tag — detaching must be explicit git switch feature/remote-only # auto-creates a local branch tracking origin/feature/remote-only git switch -C feature/x origin/main # (re)create, resetting it if it existsIt never touches individual files, and refuses to detach HEAD unless you say
--detach— guarding the classic “I committed on a detached HEAD and lost it” mistake.git restore— change files onlygit restore app.py # working tree ← index (discard unstaged edits) git restore --staged app.py # index ← HEAD (unstage, keep edits) git restore --staged --worktree app.py # both ← HEAD (fully revert the file) git restore --source=main~2 app.py # working tree ← an old version git restore -p app.py # interactively pick hunks to discardThe default source is the index for the working tree, and HEAD for
--staged. It never moves HEAD or a branch.Mapping old → new
Old New git checkout Bgit switch Bgit checkout -b Bgit switch -c Bgit checkout <hash>git switch --detach <hash>git checkout -- fgit restore fgit reset HEAD fgit restore --staged fgit checkout <rev> -- fgit restore --source=<rev> --staged --worktree fcheckoutstill works and will not be removed;switch/restoreare simply clearer and safer.06`git revert` — undo by adding, not by rewriting
#c-git-revert-undo-by-adding-not-by-rewritingrevertcomputes the inverse diff of a commit and records it as a new commit. History only grows, so it is safe on shared/pushed branches.A───B───C───D main git revert C ⇒ A───B───C───D───C̄ (C̄ = "Revert 'C'", undoes C's changes)git revert a1b2c3d # opens editor with "Revert ..." message git revert --no-edit HEAD # undo the latest commit git revert HEAD~3..HEAD # revert a range (one revert commit each) git revert -n HEAD~3..HEAD && git commit -m "Revert login feature" # one combined commitReverting a merge commit
A merge has two parents, so Git needs to know which side is “mainline”:
git revert -m 1 <merge-hash> # keep parent 1 (the branch you merged INTO), undo the merged-in sideGotcha: after reverting a merge, the feature branch’s commits are still in the history of main. If you later fix the branch and merge it again, Git considers those old commits already merged and brings in only the new ones. The usual fix is to first revert the revert (
git revert <revert-hash>), then merge the fixes.reset vs revert
resetrevertChanges history yes — moves branch backwards no — adds a commit Safe on pushed/shared branches no (requires force push) yes Leaves a trace no (reflog only, local) yes, a visible revert commit Typical use clean up local, unpushed work undo something already on main/ in production07`git stash` — a temporary shelf made of commits
#c-git-stash-a-temporary-shelf-made-of-commitsgit stashsaves uncommitted changes and resets the working tree and index to HEAD, so you can switch context.git stash push -m "wip: login form" # tracked changes (staged + unstaged) git stash push -u # also untracked files git stash push -p # choose hunks git stash list # stash@{0}: On feature/login: wip: login form git switch main && ... # do something else git switch feature/login git stash pop # re-apply and drop (keeps the entry if conflicts occur) git stash apply stash@{1} # re-apply, keep in the list git stash branch fix-from-stash # new branch from the stash's base commit, apply, dropHow it is stored
A stash is a small set of commits (shown here for
git stash push -u):W ← refs/stash (working-tree state; parents: HEAD, I, U) /|\ / | \ HEAD I U I = index state, U = untracked files (only with -u)stash@{n}is just the reflog ofrefs/stash. That means a dropped stash is a dangling commit, recoverable for a while withgit fsck --unreachable | grep commit.Pitfalls
- Stashes are local and easy to forget; prefer a WIP commit on a branch for anything longer than a few minutes.
popwith conflicts does not drop the entry — resolve, thengit stash drop.- Untracked files are not stashed without
-uand follow you to the other branch.
08Worktrees — several branches checked out at once
#c-worktrees-several-branches-checked-out-at-onceNormally a repository has one working tree, so only one branch can be checked out at a time. To look at another branch you have to commit or stash, switch, and wait for files (and often build caches) to churn.
git worktreeremoves that limit: it attaches additional working directories to the same repository, each with its own checked-out branch.# main checkout in ~/code/app on feature/search, half-finished, build running... git worktree add ../app-hotfix release/1.4 # existing branch git worktree add -b fix/login-npe ../app-fix origin/main # new branch from a start point git worktree add --detach ../app-review a1b2c3d # detached HEAD, e.g. to inspect a commit cd ../app-hotfix # a full checkout of release/1.4 — edit, test, commit, push git worktree list # /home/ana/code/app 9f8e7d6 [feature/search] # /home/ana/code/app-hotfix 3c4d5e6 [release/1.4] # /home/ana/code/app-fix 5d4c3b2 [fix/login-npe]What is shared and what is per-worktree
~/code/app/ main worktree ├── .git/ THE repository │ ├── objects/ shared: all commits, trees, blobs │ ├── refs/heads/, refs/tags/ shared: branches and tags │ ├── config, hooks/ shared │ ├── HEAD, index main worktree's own HEAD and index │ └── worktrees/ │ └── app-hotfix/ │ ├── HEAD "ref: refs/heads/release/1.4" │ ├── index its own staging area │ ├── logs/HEAD its own HEAD reflog │ └── gitdir path back to ../app-hotfix/.git └── src/ ... ~/code/app-hotfix/ linked worktree ├── .git a FILE: "gitdir: ~/code/app/.git/worktrees/app-hotfix" └── src/ ...Shared across all worktrees Per worktree Object database (commits, blobs) HEAD(which branch / commit is checked out)Branches, tags, remote-tracking refs Index (staging area) refs/stash, config, hooksWorking files, untracked and ignored files Reflogs of branches HEAD’s reflog, in-progress merge/rebase/bisect state Consequences:
- No duplication. A worktree costs only the checked-out files — no second copy of history, unlike a second
git clone. - Commits are instantly visible everywhere. Commit in
app-hotfix, and from~/code/appyou can immediatelygit cherry-pick release/1.4— no push/fetch round trip, because both directories read the same refs. - One branch, one worktree. Git refuses to check out a branch that is already checked out elsewhere (
fatal: 'release/1.4' is already used by worktree at ...): two worktrees moving the same branch ref would corrupt each other’s view of it. Use a new branch or--detach(--forceoverrides, but avoid it). - A merge, rebase, or bisect in progress in one worktree does not affect the others.
Managing worktrees
git worktree list # all worktrees, their HEAD and branch git worktree remove ../app-hotfix # delete dir + metadata (refuses if dirty; --force to override) git worktree prune # clean metadata for worktrees whose dir was deleted manually git worktree move ../app-fix ../fix # relocate git worktree lock ../app-usb --reason "on removable drive" # protect from prune git worktree repair # fix links after moving the main repoRemoving a worktree does not delete its branch — the commits are safe on the branch; delete it separately with
git branch -dif it is merged.When to use worktrees
- Urgent hotfix or review mid-task — no stash, no lost build/IDE state on your main checkout.
- Running long things in parallel — a test suite or benchmark on one branch while you keep coding on another.
- Comparing behaviour — run the old and new version side by side (
../app-v1on a tag,.on your branch). - Parallel AI coding agents — each agent gets its own worktree and branch, so their edits never collide in the same files, and results come back as ordinary branches to review and merge. (Claude Code’s worktree isolation works this way.)
- Bare repo + worktrees layout — some people
git clone --bareand keep every branch as a worktree folder (main/,feature-x/).
Pitfalls
- Untracked and ignored files are not shared.
node_modules/,.venv/,.env, build outputs do not exist in a new worktree — reinstall dependencies and copy local config. This is the most common surprise. - Disk and IDE cost. Each worktree is a full checkout of files; IDEs index each one separately.
- Deleting the directory by hand (
rm -rf ../app-hotfix) leaves stale metadata, and the branch still counts as “checked out” untilgit worktree prune. - Shared stash and config.
git stash listshows stashes from all worktrees; agit configchange applies everywhere (per-worktree config needsextensions.worktreeConfigandgit config --worktree). - Submodules are only partially supported in linked worktrees — test before relying on it.
- No duplication. A worktree costs only the checked-out files — no second copy of history, unlike a second