Skip to main content
RunBook Academy

Git, CI/CD & GitOpsII · Git ArchitectureArchitecture

The three-trees model — HEAD, index, and working tree as a navigation system

Foundation⏱ ~18 mingit

What you'll learn

  • Name the three trees Git uses and the role of HEAD as the named commit pointer
  • Explain the difference between git diff with no args, --cached, and HEAD
  • Choose between git reset, git restore, and git checkout for a given state transition
  • Trace how a failed merge surfaces as a divergence between the index and HEAD
  • Recognise why HEAD is a ref, not a commit, and how detached HEAD changes the model

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.

The three-trees model is the navigation system for Git. Where lesson II-01 introduced the three areas as storage, this lesson treats them as trees of file content — three views of the same repository, each at a different point in the commit lifecycle — and shows how every diff and reset command is a movement between two of them. The mental shift is from “what file am I editing?” to “which two of the three trees am I comparing?”

The three trees

flowchart TB
    HEAD["HEAD tree\n(the last commit)"]
    IDX["Index tree\n(the next commit)"]
    WT["Working tree\n(the filesystem)"]

    HEAD -- "git diff HEAD" --> WT
    HEAD -- "git diff --cached" --> IDX
    IDX -- "git diff" --> WT
  • HEAD tree. The file contents of the commit that HEAD currently points at. HEAD is a ref (a file under .git/refs/ or an entry in .git/packed-refs) that names a commit. The tree is the set of files that commit records.
  • Index tree. The candidate snapshot. It is the file contents that will become the next commit if you run git commit now.
  • Working tree. The actual files on disk. This is the only tree a human or an editor touches.

The crucial observation: these three trees can diverge. They usually diverge during a working session — the user edits the working tree, runs git add, which updates the index, and only at git commit does the HEAD tree catch up. A merge in progress diverges all three: HEAD is the merge base, the index has two parents of unresolved conflict, and the working tree holds the conflict markers.

The three diff forms

git diff with no arguments compares the working tree to the index. The other two common forms compare the other two pairs:

git diff              # working tree vs index      (uncommitted edits)
git diff --cached     # index vs HEAD              (what will be committed)
git diff HEAD         # working tree vs HEAD       (everything unstaged or staged)
flowchart LR
    A["git diff\nworking tree vs index"] --> B["edits not yet staged"]
    C["git diff --cached\nindex vs HEAD"] --> D["staged changes"]
    E["git diff HEAD\nworking tree vs HEAD"] --> F["all uncommitted changes"]

A simple rule for choosing the right form:

  • “What did I change since my last git add?” → git diff
  • “What will my next commit contain?” → git diff --cached
  • “What is the total delta in my working tree vs the last commit?” → git diff HEAD

Reset, restore, and checkout — moves between the trees

The three commands restore / move state between the three trees:

git checkout $REF    # HEAD -> $REF, then working tree -> HEAD
git restore $FILE_PATH    # working tree -> index (default) or index -> HEAD
git reset $REF       # HEAD -> $REF (--soft), index -> HEAD (--mixed), or working tree -> HEAD (--hard)

The --soft, --mixed, and --hard flags of git reset are the classic three-trees movement diagram:

flowchart LR
    HEAD["HEAD ref"] --soft--> IDX["Index unchanged"]
    HEAD --mixed--> IDX2["Index -> HEAD"]
    HEAD --hard--> WT["Working tree -> HEAD"]
  • git reset --soft <ref>: move HEAD only. The index and working tree are untouched. The changes that were committed are now staged for re-commit.
  • git reset --mixed <ref> (the default): move HEAD and reset the index to match. The working tree is untouched. The changes that were committed are now unstaged edits.
  • git reset --hard <ref>: move HEAD, reset the index, and reset the working tree. The changes that were committed are gone from all three trees (recoverable only via git reflog for a short window).

Detached HEAD

When a ref like main is checked out, HEAD is a symbolic ref pointing at refs/heads/main. When a commit hash is checked out instead, HEAD is detached — it points directly at a commit object, not at a branch ref.

git checkout 8a3f9d2        # detached HEAD
git checkout main           # symbolic HEAD -> refs/heads/main

A detached HEAD is normal for bisects, for inspecting old commits, and for signing tags from a known commit. It is dangerous only when commits are made in the detached state and then git checkout moves to a different branch — the new commits become unreachable and are eligible for garbage collection within ~30 days. The recovery is git reflog to find the orphaned commits, then git checkout -b <branch> <oid> to give them a branch.

Failed merges and the three trees

A merge in progress is the most informative state of the three-trees model. After git merge <branch> reports conflicts:

flowchart LR
    HEAD["HEAD tree\n(merge base)"] --> IDX["Index tree\n(2 parent entries, stage 1 / 2 / 3)"]
    IDX --> WT["Working tree\n(conflict markers)"]

The index now has up to three entries per conflicted path: stage 1 holds the merge base, stage 2 holds the HEAD version, stage 3 holds the other branch’s version. The working tree contains the conflict markers (<<<<<<<, =======, >>>>>>>). Resolving the merge means editing the working tree to the resolved content, then git adding the path, which collapses the three index entries back to a single stage 0 entry. The commit that ends the merge has both branch tips as its parents.

git ls-files --stage --unmerged
# 100644 <base-oid> 1   terraform/main.tf
# 100644 <ours-oid> 2   terraform/main.tf
# 100644 <theirs-oid> 3 terraform/main.tf

Production discipline

  1. Inspect the diff of the area pair you care about. Before every commit, before every push, before every CI run, name the two trees you are comparing and run the matching diff form.
  2. Treat --hard as a last resort. A --hard reset is a local-only operation that overwrites the working tree. In a shared repository, the production-safe alternative is git revert, which produces a new commit and preserves the audit trail.
  3. Pin branches out of detached HEAD. If you need to make commits from a detached state (for example, to apply a fix from an old tag), create a branch first: git checkout -b fix/from-v3.2.7 &lt;oid&gt;. Never commit to a detached HEAD without a branch name to receive the commit.

Cross-course references

  • Kubernetes for Production Sysadmins - Part XXIV (GitOps) uses the three-trees model to reason about the convergence loop: the GitOps controller observes the cluster (working tree), the declared state (HEAD), and the buffer of pending changes (index).
  • Terraform for Production Sysadmins - Part IX (State) uses the three-trees model to describe a Terraform plan: the state file is HEAD, the configuration is the working tree, and the plan file is the index.
  • Ansible for Production Sysadmins - Part IX (Inventory) describes the analogous three-trees state for an Ansible run: the inventory (HEAD), the live hosts (working tree), and the pending changes (index).

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer has staged a change with `git add` and now runs `git diff` with no arguments. The output is empty. Which of the following is true?

  2. Q2. `git reset --hard HEAD~1` is not necessarily the safest way to undo the most recent commit on a branch that has already been pushed to the remote.

  3. Q3. Name the three trees and the diff form that compares each pair of them.

  4. Q4. Diagnose the state of an in-progress merge and the right resolution path.

    An engineer runs `git merge feature/add-cache` to bring a new feature branch into `main`. The merge reports conflicts in `terraform/main.tf` and `ansible/hosts.yml`. The engineer runs `git diff` and sees the conflict markers, runs `git diff --cached` and sees nothing, and runs `git status` which lists both files as 'Unmerged'. The engineer wants to know exactly what state the three trees are in and what commands will resolve the merge.

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