Skip to content

Moving HEAD: reset, checkout, switch, restore, revert

Mid / Senior

What 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

  1. 01Glossary — 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.
    #c-glossary-terms-used-in-this-page
  2. 02The mental model — which of the four things does a command touch?

    Every “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 tree
    
    Command ① HEAD ② branch ref ③ index ④ working tree Safe for uncommitted work?
    git reset --soft C moves with branch moves to C — — yes
    git reset C (--mixed) moves with branch moves to C = C — yes
    git reset --hard C moves 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 C moves with branch new commit appended updated updated needs clean tree

    The single most important distinction: reset moves 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 changed
    
    #c-the-mental-model-which-of-the-four-things-does-a-command-tou
  3. 03`git reset` — soft, mixed, hard

    git reset <commit> always does step 1, and optionally 2 and 3:

    1. Move the branch that HEAD points to to <commit> (HEAD comes along). → --soft stops here.
    2. Make the index match that commit. → --mixed (the default) stops here.
    3. 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~1
    

    main → B. The index still contains C’s snapshot, so feature.py shows as staged; unstaged app.py edits 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 three
    
    git reset HEAD~1         # --mixed
    

    main → 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~1
    

    main → B, index = B, working tree = B. C’s changes and your uncommitted app.py edits are gone from disk. Commit C is still recoverable from the reflog (git reset --hard ORIG_HEAD or HEAD@{1}); uncommitted edits are not — they never became objects (unless they were git added, in which case the blobs are dangling and findable with git fsck --lost-found).

    Reset with a path — a different operation

    git reset HEAD -- config.yaml       # or: git restore --staged config.yaml
    

    With paths, reset does not move any branch. It only copies that file’s version from the commit into the index — i.e. unstages it. (--soft/--hard are not allowed with paths.)

    Common recipes

    Goal Command
    Undo last commit, keep changes staged git reset --soft HEAD~1
    Undo last commit, keep changes unstaged git reset HEAD~1
    Throw away everything back to the remote git fetch && git reset --hard origin/main
    Undo a reset / bad merge / bad rebase git reset --hard ORIG_HEAD (or a HEAD@{n} from the reflog)
    Unstage a file git restore --staged f

    Pitfalls

    • reset --hard also leaves untracked files alone; to delete those use git 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 revert instead.
    #c-git-reset-soft-mixed-hard
  4. 04`git checkout` — one command, two unrelated jobs

    Historically checkout did two very different things depending on its arguments:

    git checkout feature          # 1. switch branches: move HEAD, update index + working tree
    git checkout -b feature       #    create and switch
    git checkout a1b2c3d          #    detach HEAD at a commit
    
    git checkout -- app.py        # 2. overwrite app.py in the working tree from the index
    git checkout main -- app.py   #    overwrite app.py (index AND working tree) from main
    

    The second form is destructive and silent — it discards uncommitted edits to the file with no confirmation. And it is ambiguous: git checkout dev switches to branch dev if it exists, but restores a file named dev if it does not. The -- separator resolves that, but people forget it.

    What switching branches actually does

    git checkout B / git switch B:

    1. Compute the difference between the current HEAD tree and B’s tree.
    2. For each file that differs, update the index and working tree — but only if you have no uncommitted changes to that file. Uncommitted changes to files that are the same in both branches are carried over.
    3. If a local change would be overwritten, refuse: error: Your local changes to the following files would be overwritten by checkout. (Stash or commit first, or --merge to attempt a three-way merge.)
    4. Rewrite .git/HEAD to ref: refs/heads/B and log it in the reflog.

    No branch ref moves. No commits are created or destroyed.

    #c-git-checkout-one-command-two-unrelated-jobs
  5. 05`git switch` and `git restore` — checkout split in two

    Git 2.23 (2019) introduced two commands, each doing one of checkout’s jobs:

    git switch — change branches only

    git 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 exists
    

    It 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 only

    git 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 discard
    

    The 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 B git switch B
    git checkout -b B git switch -c B
    git checkout <hash> git switch --detach <hash>
    git checkout -- f git restore f
    git reset HEAD f git restore --staged f
    git checkout <rev> -- f git restore --source=<rev> --staged --worktree f

    checkout still works and will not be removed; switch/restore are simply clearer and safer.

    #c-git-switch-and-git-restore-checkout-split-in-two
  6. 06`git revert` — undo by adding, not by rewriting

    revert computes 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 commit
    

    Reverting 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 side
    

    Gotcha: 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

    reset revert
    Changes 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 production
    #c-git-revert-undo-by-adding-not-by-rewriting
  7. 07`git stash` — a temporary shelf made of commits

    git stash saves 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, drop
    

    How 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 of refs/stash. That means a dropped stash is a dangling commit, recoverable for a while with git 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.
    • pop with conflicts does not drop the entry — resolve, then git stash drop.
    • Untracked files are not stashed without -u and follow you to the other branch.
    #c-git-stash-a-temporary-shelf-made-of-commits
  8. 08Worktrees — several branches checked out at once

    Normally 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 worktree removes 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, hooks Working 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/app you can immediately git 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 (--force overrides, 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 repo
    

    Removing a worktree does not delete its branch — the commits are safe on the branch; delete it separately with git branch -d if 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-v1 on 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 --bare and 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” until git worktree prune.
    • Shared stash and config. git stash list shows stashes from all worktrees; a git config change applies everywhere (per-worktree config needs extensions.worktreeConfig and git config --worktree).
    • Submodules are only partially supported in linked worktrees — test before relying on it.
    #c-worktrees-several-branches-checked-out-at-once