Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXV · ResetModes

Reset with file paths — `git reset <commit> -- <path>` and the reset-vs-restore distinction

Advanced⏱ ~18 mingit

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

Not yet marked complete on this device.

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 --mixed when omitted.
  • Commit (required for --soft, --hard, --merge, --keep; optional for --mixed where 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 filledHEADIndexWorking tree
commit onlymovesresetsuntouched
commit + modemovesresets (per mode)per mode
commit + pathsdoes NOT moveresets named pathsuntouched
commit + mode + pathsdoes NOT moveresets 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

  • --hard is 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:

Operationgit reset formgit restore form
Unstage a filegit 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 filegit 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

  1. Prefer git restore for the index/working-tree operations. The names are clearer, the operation is one command, and the mode is not implicit.
  2. 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.
  3. 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 restore and 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 revert is the additive alternative that does not require any reset.
  • Linux for Production Sysadmins — Part XII (Filesystem hierarchy) — the analogue is cp versus rsync --delete: pick the granularity that loses the least.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer runs `git reset HEAD~3 -- terraform/main.tf`. Which of the following statements is correct?

  2. Q2. When a pathspec is given to `git reset`, the branch tip (HEAD) is never moved regardless of the mode flag.

  3. Q3. What is the purpose of the double dash in `git reset <commit> -- <path>`, and what happens without it?

  4. 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.