Git, CI/CD & GitOpsXVI · Restore and SwitchRestoreSwitch
Restore and the three trees — sources, destinations, and which combinations are valid
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
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=<ref> <path> 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:
| Source | Destination | Command | Destructive? |
|---|---|---|---|
| Index | Working tree | git restore <path> | Yes |
| HEAD | Index | git restore --staged <path> | No (reversible by git add) |
| HEAD | Working tree | git restore --staged --worktree <path> | Yes |
| HEAD | Working tree | git restore --source=HEAD <path> | Yes |
| Any | Index | git restore --source=<ref> --staged <path> | No (reversible by git add) |
| Any | Working tree | git restore --source=<ref> <path> | 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 restoremoves file contents. It never moves HEAD. The branch tip stays where it was.git resetmoves 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 <path> and
git reset HEAD <path> 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
- 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. - Avoid
git restore --staged --worktreein CI scripts. The combined flag is convenient interactively but harder to audit; explicit two-step restore makes the intent clearer in a script. - Use
git show <ref>:<path>for inspection. Reading a file from any tree does not requiregit 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 --sourcein 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 --worktreeto discard CI-only files.
Quiz
Knowledge check · 4 questions
Q1. Which combination of source and destination trees is NOT a legal `git restore` invocation?
Q2. `git restore --staged <path>` is reversible: running `git add <path>` after it restores the original staged bytes from the working tree.
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.
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.