Git, CI/CD & GitOpsVI · Index / Staging AreaIndex
Resetting and restore on the index — git reset, git restore, and choosing the right undo
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
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 viagit reflogfor 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:
- Are the changes a problem only in the index, or in the
working tree as well? If only in the index,
git restore --stagedfixes it without touching the working tree. - Is the branch shared? If yes, use
git revertfor commit-level undos. If no,git reset --hardis local and safe.
Production discipline
- Prefer
git restorefor new workflows. The new porcelain gives each operation a single unambiguous name.git restore --stagedandgit restoreare the two operations engineers run dozens of times a day. - Treat
git reset --hardas a local-only operation. On a pushed branch, the safe undo isgit revert. On a local feature branch,git reset --hardis fine. - Commit before discarding. A throwaway WIP commit is
the cheapest insurance against a destructive
git restore. The commit can be undone withgit 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 revertas the production-safe alternative togit reset --hardfor any commit that has been pushed. - Terraform for Production Sysadmins - Part IX (State)
covers
terraform applyas the equivalent ofgit commitandterraform destroyas the equivalent ofgit reset --hard. The same discipline applies: never destroy production state that has been “pushed” (applied) without a deliberate plan.
Quiz
Knowledge check · 4 questions
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?
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.
Q3. Name the three modes of `git reset` and identify which one(s) leave the working tree unchanged.
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.