Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXVI · Restore and SwitchRestoreSwitch

Restore and the three trees — sources, destinations, and which combinations are valid

Intermediate⏱ ~18 mingit

What you'll learn

  • Map every git restore flag onto a source tree and a destination tree
  • Identify the valid source-destination combinations and the destructive ones
  • Predict the three-tree state after any git restore invocation
  • Distinguish git restore (file move) from git reset (HEAD move)
  • Use git restore to inspect the contents of any tree without writing to the working tree

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.

Lesson II-02 introduced the three-tree model: HEAD, the index, and the working tree. Lesson VI-06 used the model to describe git reset (which moves HEAD and cascades to the index and working tree depending on the mode). This lesson uses the same model to describe git restore, which moves file contents between any two of the three trees. The model makes every legal invocation predictable and every illegal invocation impossible.

The three trees as a coordinate system

git restore operates on files. It picks a source tree, reads the bytes, and writes them to a destination tree. Every combination of source tree, destination tree, and path produces a well-defined result. The three trees form a three-by-three matrix of possible moves - but only six of the nine moves are legal:

flowchart LR
    H["HEAD"]
    I["Index"]
    W["Working tree"]
    H -- "git restore --staged <path>" --> I
    H -- "git restore --staged --worktree <path>" --> W
    H -- "git restore --source=HEAD <path>" --> W
    I -- "git restore <path>" --> W
    I -- "git restore --source --staged <path>" --> H
    W -- "ILLEGAL" --> H
    W -- "ILLEGAL" --> I

The illegal moves are working-tree-to-anywhere. The working tree is not a content source for git restore; the command moves bytes from a content-addressed tree (HEAD or the index) into a tree, not the reverse. To capture working tree bytes, the engineer stages them with git add first, then restores from the index.

Default source and destination

The defaults encode the most common case:

  • Default source: the index. Bare git restore <path> reads the blob OID from the index and writes the bytes to the working tree. This is the “discard working tree edits” case.
  • Default destination: the working tree. Bare git restore --staged <path> writes the source’s version into the index. The working tree is left alone.

The defaults are the safe defaults. The engineer does not have to remember which tree is which - the bare command reads from the staging area (which represents “what the next commit will look like”) and writes to the working tree (which represents “what the user sees”).

The —source flag and its semantics

--source=<ref> (or -s <ref>) replaces the default source. The flag accepts any tree-ish: a commit OID, a tag, a branch name, a remote-tracking ref, HEAD, or HEAD~1.

# Source is the index (default)
git restore terraform/main.tf

# Source is HEAD
git restore --source=HEAD terraform/main.tf

# Source is a specific commit
git restore --source=8a3f9d2 terraform/main.tf

# Source is a tag
git restore --source=v3.2.7 terraform/main.tf

# Source is a sibling branch
git restore --source=feature/iam-rotation terraform/main.tf

The destination is still the working tree by default. Combining --source with --staged writes to the index; combining with --worktree is redundant with the default.

flowchart TD
    A["git restore invocation"] --> S["source\n(index by default, --source=<ref> overrides)"]
    A --> D["destination\n(worktree by default, --staged redirects to index)"]
    A --> P["path\n(required)"]
    S --> R["read blob OID from source tree"]
    D --> R
    R --> W["write bytes to destination tree"]
    P --> W

Reading the three trees with —source

The most powerful use of --source is to inspect any tree without changing the working tree:

# Compare a file from a specific commit to the working tree
git diff --no-index -- \
    <(git show 8a3f9d2:terraform/main.tf) \
    terraform/main.tf

# Alternative: pipe to less for human reading
git show 8a3f9d2:terraform/main.tf | less

These inspections are not git restore invocations - they use plumbing commands to read a blob and pipe it to a diff tool. But they share the underlying mechanism with git restore: the bytes are pulled from a specific tree object and made available to the user.

The point is operational: an engineer who knows the three-tree model can read any tree’s bytes at any time. An engineer who does not know the model will copy a file by hand from an old checkout, missing the obvious git restore --source=&lt;ref&gt; &lt;path&gt; shorthand.

Combining —staged and —worktree

The two flags are not mutually exclusive. The combination writes the source’s bytes to both the index and the working tree:

# Write HEAD's version of the file to both the index and the working tree
git restore --staged --worktree terraform/main.tf

The shorthand is useful in one case: the engineer wants both the staged version and the working tree version to match HEAD, in one command. The two-write behaviour is sometimes surprising - the command writes to the index first (which is fast and inside .git/) and then to the working tree (which clobbers the user-visible file).

Destructive combinations

Three of the legal moves are destructive in the sense that they overwrite bytes without backup:

SourceDestinationCommandDestructive?
IndexWorking treegit restore &lt;path&gt;Yes
HEADIndexgit restore --staged &lt;path&gt;No (reversible by git add)
HEADWorking treegit restore --staged --worktree &lt;path&gt;Yes
HEADWorking treegit restore --source=HEAD &lt;path&gt;Yes
AnyIndexgit restore --source=&lt;ref&gt; --staged &lt;path&gt;No (reversible by git add)
AnyWorking treegit restore --source=&lt;ref&gt; &lt;path&gt;Yes

The “destructive” column refers to working-tree writes specifically. Index writes are reversible: the index is just a staging area, and git add rewrites it freely. Working tree writes are destructive: there is no automatic backup.

flowchart TB
    A["git restore"] --> B{"writes to\nworking tree?"}
    B -- "no (--staged only)" --> C["safe\n(reversible by git add)"]
    B -- "yes" --> D{"source == index\nor HEAD?"}
    D -- "yes" --> E["discards edits\n(recovery: editor undo, reflog, stash)"]
    D -- "no (named ref)" --> F["replaces bytes\n(same recovery options)"]

Restore versus reset

The two commands operate on different things:

  • git restore moves file contents. It never moves HEAD. The branch tip stays where it was.
  • git reset moves HEAD (and, depending on mode, the index and working tree). The branch tip changes.

This is the sharpest distinction between the two. An engineer who wants to undo a commit uses git reset. An engineer who wants to unstage or discard a file uses git restore. The two commands overlap on the index-and-working- tree restore (git restore --staged --worktree &lt;path&gt; and git reset HEAD &lt;path&gt; produce similar three-tree states), but the HEAD movement is the differentiator.

flowchart LR
    A["intent: undo a commit"] --> R["git reset"]
    B["intent: undo a stage"] --> RST["git restore --staged"]
    C["intent: discard WT edit"] --> RST2["git restore"]
    R --> R1["HEAD moves"]
    RST --> R2["index only"]
    RST2 --> R3["working tree only"]

Production discipline

  1. Map every restore onto the source-destination table. Before running git restore, identify the source tree, the destination tree, and the path. The table from this lesson makes the result predictable.
  2. Avoid git restore --staged --worktree in CI scripts. The combined flag is convenient interactively but harder to audit; explicit two-step restore makes the intent clearer in a script.
  3. Use git show &lt;ref&gt;:&lt;path&gt; for inspection. Reading a file from any tree does not require git restore; git show (or its plumbing equivalent) reads the blob and prints the bytes without touching any tree.

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) describes the source-destination matrix in its onboarding doc; new engineers learn the table before they learn the flags.
  • GitOps with Argo CD - Part IV (AppSources) uses git restore --source in reconciliation logic to inspect historical manifests without writing them to the controller’s working tree.
  • Terraform for Production Sysadmins - Part XV (CIHooks) uses the matrix in this lesson as the reference for which restore command belongs in which hook: pre-commit uses --staged, post-merge uses --source=HEAD --worktree to discard CI-only files.

Quiz

Knowledge check · 4 questions

  1. Q1. Which combination of source and destination trees is NOT a legal `git restore` invocation?

  2. Q2. `git restore --staged &lt;path&gt;` is reversible: running `git add &lt;path&gt;` after it restores the original staged bytes from the working tree.

  3. Q3. Distinguish `git restore` from `git reset` in terms of which trees they move, and identify the operation that only one of them can do.

  4. Q4. Choose the right `git restore` invocation for a three-step staging cleanup, and explain why each step uses the source-destination pair it does.

    An engineer has made three mistakes in staging. (a) They ran `git add terraform/backend.tf` for a file that should not be in the next commit; the working tree version is correct. (b) They made a working tree edit to `terraform/main.tf` that should be thrown away; the staged version of that file is correct. (c) They need to pull a single fix from `feature/iam-rotation`'s version of `terraform/iam/policy.json` onto `main` without merging the entire branch.

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