Git, CI/CD & GitOpsXV · ResetModes
Reset with file paths — `git reset <commit> -- <path>` and the reset-vs-restore distinction
What you'll learn
- Identify the three argument slots in a `git reset` command: commit, mode, paths
- Use `git reset <commit> -- <path>` to reset the index entry for a single path
- Recognise that path-scoped reset is implicitly `--mixed` and never moves HEAD
- Distinguish `git reset <commit> -- <path>` from `git restore --source=<commit> --staged <path>`
- Choose between reset-with-paths and restore for the unstaging and discard operations
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
git reset looks like one command but it has three argument
slots that together decide what it does: the commit slot, the
mode slot, and the path slot. Filling one slot is a different
command from filling two. The path-scoped form — git reset <commit> -- <path> — never moves HEAD, is implicitly
--mixed, and acts on the index entries for exactly the named
paths. Understanding which slot is filled is what turns git reset from an overloaded footgun into a precise tool.
The three argument slots
A git reset invocation has three slots:
git reset $MODE_FLAG $COMMIT -- $PATHSPEC
- Mode flag (optional): one of
--soft,--mixed,--hard,--merge,--keep. Default is--mixedwhen omitted. - Commit (required for
--soft,--hard,--merge,--keep; optional for--mixedwhere it defaults to HEAD): the target commit to rewind to. - Pathspec (optional): one or more paths. The double dash separates the commit argument from the pathspec; without it, Git parses the first path as a ref.
Filling the pathspec slot makes the command path-scoped:
HEAD is never moved, the mode is implicitly --mixed, and the
operation is “reset the index entries for the named paths to
match the commit’s tree”.
| Slot filled | HEAD | Index | Working tree |
|---|---|---|---|
| commit only | moves | resets | untouched |
| commit + mode | moves | resets (per mode) | per mode |
| commit + paths | does NOT move | resets named paths | untouched |
| commit + mode + paths | does NOT move | resets named paths (per mode) | per mode |
The fourth row is the surprise: when paths are given, the mode
flag is honoured for the index and working tree but HEAD is
never moved. git reset --hard HEAD -- terraform/main.tf
does not rewind the branch tip; it overwrites the working tree
bytes for terraform/main.tf with the version at HEAD and
updates the index accordingly. That is the “discard working
tree edits for one file” operation, and it is the reason path
--hardis occasionally useful.
COMMIT=abc1234
git reset $COMMIT -- terraform/main.tf
# HEAD unchanged, index entry for terraform/main.tf matches $COMMIT
git reset HEAD -- terraform/main.tf
# equivalent: default commit is HEAD, default mode is --mixed when paths given
Use case: unstage a single file
The most common path-scoped reset is “unstage one file”. The form is:
git reset HEAD -- terraform/backend.tf
HEAD is implicit, the mode is --mixed (the default when paths
are given), and the pathspec is terraform/backend.tf. After
the command, the file’s index entry matches HEAD (i.e. it is
no longer staged). The working tree bytes are unchanged. The
branch tip is unchanged.
The git restore equivalent is git restore --staged terraform/backend.tf, which is the same operation with a name
that does not overlap with HEAD-movement. The two commands
are exact synonyms for this case.
flowchart LR
A["HEAD"] -->|"git reset HEAD -- <path>"| B["index entry matches HEAD"]
B -->|"git restore --staged <path>"| A
style A fill:#cfc
style B fill:#cfc
The green highlight is the invariant: the working tree and the branch tip are untouched in both directions. The arrow is bidirectional because the two commands are interchangeable.
Use case: reset a file from an older commit
The second use case is “this file should be the version from three commits ago, not the version I have on disk right now”:
git reset HEAD~3 -- terraform/main.tf
git checkout -- terraform/main.tf
# or, with git restore:
git restore --source=HEAD~3 terraform/main.tf
The first command resets the index entry for
terraform/main.tf to match HEAD3. The second command
overwrites the working tree file with the version from the
index (which is now HEAD3’s version). The result: working
tree matches HEAD~3 for that file, while everything else in
the repository is at HEAD.
Note that git reset HEAD~3 -- terraform/main.tf alone is not
enough: it resets the index but the working tree still has the
current bytes. The two-step is required because path-scoped
reset is implicitly --mixed, which does not touch the
working tree.
reset-with-paths versus git restore
The two commands overlap in three operations:
| Operation | git reset form | git restore form |
|---|---|---|
| Unstage a file | git reset HEAD -- <path> | git restore --staged <path> |
| Reset a file to older commit’s version (WT) | git reset <commit> -- <path> + git checkout -- <path> | git restore --source=<commit> <path> |
| Discard working tree edits for a file | git reset --hard HEAD -- <path> | git restore <path> |
The git restore column is one command per operation. The
git reset column is one or two commands and requires
understanding the implicit---mixed-for-paths rule. The
production guidance is the same as in Part VI: prefer
git restore for new workflows and the index/working-tree
operations; use git reset for the HEAD-movement operations
where there is no git restore analogue.
Why HEAD never moves with paths
The reason HEAD is excluded from the path-scoped form is that
moving HEAD and changing the index for a subset of paths would
create an inconsistent repository state. A commit’s tree is a
complete snapshot of every path; you cannot have a branch tip
at commit X with an index entry for terraform/main.tf taken
from commit Y. Either the index matches HEAD (path-scoped
reset does this) or the branch tip is moved (commit-only
reset does this), but not both for a subset of paths.
This is why path-scoped reset never moves HEAD and full reset never accepts paths. The two operations are intentionally disjoint in their argument slots.
Production discipline
- Prefer
git restorefor the index/working-tree operations. The names are clearer, the operation is one command, and the mode is not implicit. - Use
git reset HEAD -- <path>for the unstage operation when the engineer prefers the older porcelain. The command is exact and safe because the path slot is filled. - Reach for
git reset --hard <commit>only when both the branch tip and the working tree need to move together. If only the working tree needs to change,git restore --source=<commit> <path>is the right tool.
Cross-course references
- Git, CI/CD & GitOps — Part VI (Resetting and restore) —
the introduction to
git restoreand the unstage-and-discard decision tree. - Git, CI/CD & GitOps — Part II (Three trees model) — the HEAD/index/working-tree model that determines which slots can be filled in which combinations.
- Git, CI/CD & GitOps — Part XIV (Revert) — for a commit
that has been pushed,
git revertis the additive alternative that does not require any reset. - Linux for Production Sysadmins — Part XII (Filesystem
hierarchy) — the analogue is
cpversusrsync --delete: pick the granularity that loses the least.
Quiz
Knowledge check · 4 questions
Q1. An engineer runs `git reset HEAD~3 -- terraform/main.tf`. Which of the following statements is correct?
Q2. When a pathspec is given to `git reset`, the branch tip (HEAD) is never moved regardless of the mode flag.
Q3. What is the purpose of the double dash in `git reset <commit> -- <path>`, and what happens without it?
Q4. Use the path-scoped reset form to unstage a single file that was added by mistake, without disturbing the branch tip or any other staged file.
An engineer staged five Terraform files for the next commit but realised one of them, `terraform/scratch.tf`, does not belong in the commit. The engineer wants to unstage only `terraform/scratch.tf` while keeping the other four staged, and without moving the branch tip or rewriting any working tree bytes.
Passing score: 75%. Answers are checked in this browser.