The Three Trees
Git tracks three versions of your project at once: the working tree (the files on disk you edit), the index or staging area (what the next commit will contain), and HEAD (the last commit). Almost all the confusion around add, reset, and restore dissolves the moment you can name which of these three an operation moves content between.
The staging area is the part that surprises people coming from systems that commit the whole working copy at once. It is not overhead — it is the feature that lets you turn a messy working tree into a clean, deliberate series of commits.
The Working Tree
The working tree is the ordinary directory of files you open in your editor. It is the only one of the three your compiler, your tests, and your tools actually see. Changes here are just changes on disk; Git does nothing with them until you stage them. This is also the only tree whose uncommitted contents Git cannot recover for you — lose unstaged edits and the reflog cannot bring them back.
The Index (Staging Area)
The index is a binary file at .git/index holding the proposed contents of the next commit. git add copies the current content of a file from the working tree into the index. The crucial subtlety: it captures a snapshot at the moment you run it. Edit the file again afterward and those new edits are not in the index — the staged version is frozen as of the add, and a plain commit will record the frozen version, not your latest disk content.
Because the index is a distinct, persistent layer, you can build up exactly the change you want to record — even part of a single file, via git add -p — while leaving other edits unstaged. That is the lever that makes focused, reviewable commits possible.
HEAD
HEAD is a pointer to the commit that represents your last committed state, usually reached through the current branch. It is the baseline the other two trees are compared against: git diff --staged compares the index to HEAD, and git diff compares the working tree to the index. Knowing which comparison you are looking at is what stops a staged change from looking like it "disappeared."
How Commands Move Content Between Trees
Read the everyday commands as movements between the three trees. git add moves content from the working tree into the index. git commit writes the index into a new commit and advances HEAD. git restore <file> copies from the index (or HEAD) back over the working tree. git reset moves HEAD and, depending on its flag, the index and working tree with it — --soft moves HEAD only, --mixed also resets the index, --hard also overwrites the working tree.
That last one is the dangerous member of the family: --hard overwrites all three trees, and any uncommitted working-tree changes it discards are gone for good, because they were never committed for the reflog to track.
The Staging Area as a Feature
Most version control systems commit the whole modified working copy. Git's separate index lets you choose precisely what goes into each commit, so one chaotic afternoon of edits can become three clean commits: the bug fix, the refactor, and the unrelated typo, each reviewable on its own. The discipline of staging deliberately is what makes git log and code review worth reading later.
Staging area (Git) — a persistent index lets you select exactly which changes, down to individual hunks with git add -p, go into the next commit. Choose Git's model when commit quality and reviewability matter.
Direct commit (SVN, default Mercurial) — the whole modified working copy is committed unless you name specific paths; there is no persistent "staged but not committed" layer. Simpler, but no built-in way to craft a partial commit.
- Editing a file after
git addand assuming the new edits are staged — the index holds the content as of theadd, so the commit records the older version unless you re-add. - Running
git reset --hardto "undo staging" and silently destroying uncommitted working-tree changes, which the reflog cannot recover. - Confusing
git restore <file>(discards working-tree changes) withgit restore --staged <file>(unstages but keeps changes) — the wrong one either keeps or loses your edits. - Committing without running
git statusfirst, then discovering only half the intended files were staged. - Reading plain
git diffafter staging everything, seeing nothing, and concluding the change vanished — it is staged, sogit diff --stagedis the right view.
- Run
git statusbefore every commit to confirm exactly which paths are staged. - Use
git add -pto stage hunks selectively so each commit is one coherent change. - Re-run
git addon a file after editing it, since staging captures a point-in-time snapshot. - Use
git restore --staged <file>to unstage andgit restore <file>to discard, reservinggit reset --hardfor when losing working-tree changes is the intent. - Use
git difffor working-vs-index andgit diff --stagedfor index-vs-HEAD to see each boundary distinctly.
Knowledge Check
You run git add file.txt, then edit file.txt again, then git commit. What gets committed?
- The content as of the
add; the later edits are unstaged and not in the commit - The latest content on disk, because commit always reads straight from the working tree
- Both versions of the file, recorded in the one commit as two separate sequential changes
- Nothing at all — Git refuses to commit any file that was edited again after staging
Which command unstages a file while keeping your changes on disk?
git restore --staged <file>git restore <file>git reset --hard <file>git rm --cached <file>
A staged change shows in git diff --staged but not in plain git diff. Why?
- Plain
git diffcompares working tree to index; once content is staged it matches the index, so the difference is only visible against HEAD - The change was lost during the staging step and only a partial cached copy of it now remains tucked away inside the index, with the rest gone
git diffonly ever shows brand-new untracked files that Git has never recorded, and it never shows ordinary edits to already-tracked ones- Staging physically deletes the working-tree copy of the file off disk, so plain diff is left with nothing on either side to compare
What does git reset --hard touch that git reset --soft does not?
- The index and the working tree —
--softmoves only HEAD, while--hardoverwrites all three trees - Only the commit message of HEAD, rewriting its text in place while leaving every one of the three trees fully untouched
- The remote tracking branch on the server as well as the local one, quietly syncing both of them in a single step
- Nothing different at all between them; the two flags are simply interchangeable aliases for the exact same operation
You got correct