Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXVI · Restore and SwitchRestoreSwitch

git restore in detail — restoring files from the index, from HEAD, and from any ref

Intermediate⏱ ~20 mingit

What you'll learn

  • Use git restore <path> to restore the working tree from the index
  • Use git restore --staged <path> to unstage without touching the working tree
  • Use git restore --source=<ref> <path> to pull a file from a named tree
  • Use git restore --staged --worktree <path> to restore both trees to HEAD
  • Recognise the default source and target of each flag combination
  • Distinguish destructive file restore from reversible commit-level undo

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 restore is the second of the two commands introduced in Git 2.23 to replace the overloaded git checkout. Its job is the complement of git switch: operations that move file contents, not HEAD. Its flag set encodes the source (where the bytes come from), the destination (which tree the bytes are written to), and the paths (which files are touched). With three concepts - source, destination, path - the entire flag set is described.

The three axes: source, destination, path

Every git restore invocation has three implicit choices:

  1. Source: where do the bytes come from? Default is the index. --source=<ref> (or -s <ref>) overrides this.
  2. Destination: which tree is written? Default is the working tree. --staged redirects the write to the index. --worktree redirects the write to the working tree. The two can be combined.
  3. Path: which files are touched? A path argument is required (or . for the whole tree, or --staged alone without a path resets the entire index).
flowchart LR
    A["source\n(default: index)"] --> B["git restore"]
    C["destination\n(default: worktree)"] --> B
    P["path\n(required)"] --> B
    A2["--source=<ref>"] -.-> A
    C1["--staged"] -.-> C
    C2["--worktree"] -.-> C
    B --> D["bytes written to destination"]

The defaults are the safe defaults: bytes come from the index (not from any other tree), and bytes are written to the working tree (not to the index). The defaults make “discard working tree edits” the bare command - the most common case of git restore.

Restoring the working tree from the index

The bare form, with no flags, restores the working tree file at each named path with the version that the index has:

# Discard working tree edits for one file
git restore terraform/main.tf

# Discard working tree edits for every file
git restore .

The operation reads the blob OID for each named path from the index, reads the corresponding blob object from the object store, and writes the bytes to the working tree. The index is not touched. HEAD is not touched. The previous working tree bytes are overwritten with no backup.

flowchart LR
    A["working tree path\n(edited bytes)"] --> B["git restore <path>"]
    C["index\n(blob OID for path)"] --> B
    B --> D["working tree\n(index bytes)"]

Restoring the index from HEAD

git restore --staged <path> moves the named paths in the index back to match HEAD. The working tree is untouched.

# Unstage a single file
git restore --staged terraform/backend.tf

# Unstage every file (the entire index matches HEAD)
git restore --staged .

This is the operation that undoes a git add without losing the working tree edits. The flow is the canonical fix for the “added too much” mistake: the file goes back to “modified, not staged” status, and the engineer can re-stage deliberately or discard separately.

flowchart LR
    A["index\n(staged blob OID)"] --> B["git restore --staged"]
    C["HEAD\n(tree OID)"] --> B
    B --> D["index\n(HEAD tree bytes)"]
    E["working tree\n(untouched)"] --> D

The default source for --staged is HEAD. There is no ambiguity: --staged without --source means “restore the index entries from HEAD”. Engineers who want to restore the index from a non-HEAD tree-ish must use --source.

Restoring both trees from HEAD

git restore --staged --worktree <path> writes the path into both trees from the HEAD tree. The operation is equivalent to:

git restore --staged $FILE_PATH
git restore $FILE_PATH

in one command. The shorthand is useful in two scenarios: the engineer wants to discard both the staged version and the working tree version in a single, atomic-feeling step.

# Discard both staged and working tree edits, restoring HEAD
git restore --staged --worktree terraform/main.tf

After this command, both the index and the working tree match HEAD for the named path. The previous bytes - both the staged version and the working tree version - are overwritten.

Restoring from a named source: —source

git restore --source=<ref> <path> (or -s <ref>) changes the source from “index” (default) or “HEAD” (default for —staged) to whatever tree-ish the flag names.

# Pull a single file from a specific commit into the working tree
git restore --source=8a3f9d2 terraform/main.tf

# Pull a file from a tag into the working tree
git restore --source=v3.2.7 terraform/main.tf

# Pull a file from another branch into the working tree
git restore --source=feature/iam-rotation terraform/main.tf

The destination is still the working tree by default. Combining --source with --staged writes the source’s version into the index; combining with --worktree writes into the working tree (redundant with the default, but explicit).

# Pull a file from feature/iam-rotation into the index
git restore --source=feature/iam-rotation --staged terraform/main.tf

After this command, the index has the version of terraform/main.tf that exists on feature/iam-rotation, but the working tree still has whatever was there before. The next git commit will record that version.

flowchart LR
    A["--source=<ref>"] --> B["source tree"]
    B --> C["git restore"]
    D["destination flag\n(--staged or default)"] --> C
    P["path"] --> C
    C --> E["destination tree\n(bytes from source)"]

Defaults and combinations summary

The flag combinations, their effect, and their legacy spelling:

CommandEffectLegacy equivalent
git restore <path>Working tree ← index for <path>git checkout -- <path>
git restore --staged <path>Index ← HEAD for <path>; working tree untouchedgit reset HEAD <path>
git restore --staged --worktree <path>Index ← HEAD and working tree ← HEAD for <path>git checkout HEAD -- <path>
git restore --source=<ref> <path>Working tree ← <ref> tree for <path>git checkout <ref> -- <path>
git restore --source=<ref> --staged <path>Index ← <ref> tree for <path>; working tree untouchedgit checkout <ref> -- <path> (when index desired)

Every row produces an identical three-tree state after it runs. The new commands do not add capability.

Production discipline

  1. Use git restore --staged to unstage, git restore to discard. The two commands map to two distinct intents; reading the command line tells the reviewer which intent is in play.
  2. Commit before discarding. A throwaway WIP commit is the cheapest insurance against an unwanted restore. The commit can be undone with git reset --soft HEAD~1; the discarded working tree bytes cannot.
  3. Prefer --source for cherry-picking one file. A git restore --source=<ref> --staged <path> followed by git commit is a single-file cherry-pick without the merge machinery of git cherry-pick.

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) uses git restore --staged in pre-commit hooks to unstage any playbook file that fails a lint check without losing the working tree edits for the engineer to fix and re-stage.
  • GitOps with Argo CD - Part IV (AppSources) uses git restore --source in reconciliation logic to pull a specific file from a known-good commit when the rendered manifest is suspect but the rest of the application is healthy.
  • Terraform for Production Sysadmins - Part XV (CIHooks) uses git restore terraform/.terraform.lock.hcl in the pre-commit hook to discard the lock file that terraform init regenerates on every run; the lock file should never enter the index.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer has staged `terraform/backend.tf` with `git add` and now wants to unstage it without losing the working tree edits. Which command is correct?

  2. Q2. `git restore <path>` creates a backup of the previous working tree bytes in `.git/`, allowing recovery with `git restore --source` for at least 30 days.

  3. Q3. What three concepts does every `git restore` invocation encode, and what are the defaults for each?

  4. Q4. Recover a single Terraform file from a sibling feature branch onto the current branch without merging the entire branch.

    An engineer is on `main` and needs the contents of `terraform/iam/policy.json` from the `feature/iam-rotation` branch - a single file out of dozens on the feature branch. A full merge would drag in unrelated changes; a cherry-pick is overkill for one file. The engineer needs the bytes on `main`'s next commit without disturbing the rest of the working tree.

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