Skip to main content
RunBook Academy

Git, CI/CD & GitOpsVI · Index / Staging AreaIndex

Resetting and restore on the index — git reset, git restore, and choosing the right undo

Intermediate⏱ ~22 mingit

What you'll learn

  • Choose between git reset and git restore for a given state transition
  • Trace the effect of git reset --soft, --mixed, and --hard on each tree
  • Use git restore --staged to unstage a file without losing working tree changes
  • Use git restore to discard working tree changes without touching the index
  • Recognise why git reset --hard is destructive and choose git revert for shared branches

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Mistakes in staging are inevitable: a wrong file added, a partial stage that needs to be redone, a working tree edit that should never have been made. Git has two commands for moving state back from the index and the working tree: git reset and git restore. They overlap but they are not the same, and the wrong choice is a common source of lost work. This lesson maps every common mistake to the command that fixes it without losing data, and warns about the forms that are destructive on shared branches.

The two commands and their domains

git reset is the older command. Its three modes describe how far back to unwind:

  • git reset --soft <ref> moves only HEAD. The index and working tree are untouched. Committed changes become staged for re-commit.
  • git reset --mixed <ref> (the default) moves HEAD and resets the index to match. The working tree is untouched. Committed changes become unstaged edits.
  • git reset --hard <ref> moves HEAD, resets the index, and resets the working tree. Committed changes are gone from all three trees (recoverable via git reflog for the standard 90-day window).

git restore was added in Git 2.23 (August 2019) to give the common operations clearer names:

  • git restore <path> discards working tree changes for the named paths; the file is replaced with the version from the index.
  • git restore --staged <path> resets the named paths in the index to match HEAD; the working tree is untouched.
  • git restore --source=<ref> <path> writes the named paths into the working tree from the named ref, leaving the index untouched.

The two commands overlap: git restore --staged <path> and git reset HEAD <path> are equivalent, as are git restore <path> and git checkout -- <path>.

flowchart LR
    A["HEAD"] -- "git reset --soft" --> B["Index unchanged"]
    A -- "git reset --mixed" --> C["Index matches HEAD"]
    A -- "git reset --hard" --> D["Index + WT match HEAD"]
    B -- "git restore --staged" --> C
    C -- "git restore" --> D

Case 1: unstage a single file

The most common staging mistake is adding a file that does not belong in the next commit. The fix is to drop it from the index while leaving the working tree bytes intact:

git restore --staged terraform/backend.tf
# equivalent to: git reset HEAD terraform/backend.tf

After this command, terraform/backend.tf is back in the “Changes not staged for commit” bucket. The bytes on disk are unchanged. The engineer can re-stage with git add if the addition was a mistake, or leave the file unstaged if the bytes should be discarded separately.

Case 2: unstage everything

The second-most-common mistake is to add everything (git add -A or git add .) and then realise the index is dirtier than intended. The fix is to reset the entire index:

git restore --staged .
# equivalent to: git reset

After this command, the index matches HEAD for every path. The working tree is untouched. The engineer can re-stage deliberately with git add <path> for each file that should be in the next commit.

git reset with no mode flag and no ref defaults to --mixed against HEAD, which is the same operation. The two forms are exact synonyms.

Case 3: discard working tree changes for a file

The third case is an edit in the working tree that should be thrown away. The fix is to replace the working tree file with the version from the index:

git restore terraform/main.tf
# equivalent to: git checkout -- terraform/main.tf

After this command, the working tree bytes for the named file are replaced with whatever the index had. The index is untouched. The previous working tree bytes are gone (no backup is created; the recovery path is the editor’s undo history or git reflog for files that were committed before the edit).

Case 4: undo the last commit but keep the changes

A commit was made that should not yet have been made — for example, a commit that was supposed to be on a feature branch but was made on main. The fix is to undo the commit while keeping the changes ready to re-commit:

git reset --soft HEAD~1

After this command, HEAD points at the commit’s parent. The index still holds all the changes from the now-undone commit (staged for re-commit). The working tree is unchanged. The cleaner fix when the engineer also needs to move to another branch is git reset --soft HEAD~1 && git stash followed by git switch <branch> && git stash pop, because switching branches updates the index to match the target branch’s tree.

Case 5: undo the last commit and unstage everything

The same scenario, but the engineer wants the changes back in the working tree as unstaged edits (so they can be re-staged deliberately, perhaps with git add -p):

git reset HEAD~1

The default mode is --mixed, which moves HEAD and resets the index but leaves the working tree alone. The commit’s changes are now in the working tree as unstaged edits.

Case 6: undo the last commit and discard everything

This is the destructive form. It is sometimes the right choice on a local branch that has not been pushed:

git reset --hard HEAD~1

After this command, HEAD, the index, and the working tree all match HEAD~1. The commit and its changes are gone from all three trees.

A decision tree

The six cases above cover almost every staging mistake. The decision tree:

flowchart TB
    A["what do you want to undo?"] --> B["staged but should not be"]
    A --> C["edited in WT but should not be"]
    A --> D["just committed but should not be"]
    A --> E["committed and changes should be discarded"]

    B --> B1["git restore --staged <path>"]
    C --> C1["git restore <path>\n(commit first!)"]
    D --> D1["git reset --soft HEAD~1\n(keep staged)"]
    D --> D2["git reset HEAD~1\n(unstage)"]
    E --> E1["git reset --hard HEAD~1\n(local-only)"]
    E --> E2["git revert HEAD\n(safe on shared)"]

The decision comes down to two questions:

  1. Are the changes a problem only in the index, or in the working tree as well? If only in the index, git restore --staged fixes it without touching the working tree.
  2. Is the branch shared? If yes, use git revert for commit-level undos. If no, git reset --hard is local and safe.

Production discipline

  1. Prefer git restore for new workflows. The new porcelain gives each operation a single unambiguous name. git restore --staged and git restore are the two operations engineers run dozens of times a day.
  2. Treat git reset --hard as a local-only operation. On a pushed branch, the safe undo is git revert. On a local feature branch, git reset --hard is fine.
  3. Commit before discarding. A throwaway WIP commit is the cheapest insurance against a destructive git restore. The commit can be undone with git reset --soft HEAD~1; the discarded working tree bytes cannot.

Cross-course references

  • Linux for Production Sysadmins - Part XXVII (BackupAndRecovery) covers the analogous decision tree for filesystem recovery: when to delete a file versus when to move it to a holding directory. The Git version is structural — the three trees give multiple recovery paths the filesystem does not.
  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) treats git revert as the production-safe alternative to git reset --hard for any commit that has been pushed.
  • Terraform for Production Sysadmins - Part IX (State) covers terraform apply as the equivalent of git commit and terraform destroy as the equivalent of git reset --hard. The same discipline applies: never destroy production state that has been “pushed” (applied) without a deliberate plan.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer has staged a file with `git add terraform/backend.tf` but now realises it does not belong in the next commit. The working tree file is correct and should be kept as-is. Which command is most appropriate?

  2. Q2. `git reset --hard HEAD~1` is not necessarily the safest way to undo the most recent commit on a branch that has already been pushed to the remote.

  3. Q3. Name the three modes of `git reset` and identify which one(s) leave the working tree unchanged.

  4. Q4. Undo a commit that has already been pushed to the shared remote, choosing the right command and explaining why the obvious one is wrong.

    An engineer committed a Terraform change directly to `main` (bypassing the pull-request workflow) and pushed it. The CI pipeline caught it and the team needs the commit undone. The engineer proposes `git reset --hard HEAD~1` followed by `git push --force`. The team lead objects. The team needs to choose the right command and document the alternative.

Passing score: 75%. Answers are checked in this browser.