Understanding the Git Working Tree
The working tree is the set of files and directories you can actually see and edit — the checkout of one commit, expanded onto your filesystem, plus whatever you have changed since.
It is the part of Git that feels least like Git. It is just files. But precisely because it is just
files, it has no memory, and understanding what Git does and does not know about it explains most of
what git status tells you.
The short answer
Section titled “The short answer”The working tree is your project directory minus .git. Git populates it when you check out a commit,
then watches it for changes.
Formally: the working tree is a single checkout of one version of the project, held in ordinary files so you can edit them with ordinary tools.
Two consequences follow immediately, and both matter:
- It holds exactly one version of each file. There is no history in the working tree. History lives in the repository.
- Git does not automatically preserve it. An edit you have not staged or committed exists in exactly one place — on disk. Overwrite it and Git cannot help you.
Where it sits
Section titled “Where it sits”A project directory containing README.md, src/greet.py and a hidden .git directory. A bracket labels the visible files, excluding .git, as the working tree. Inside .git sit the index, holding one entry per tracked path, and the object database holding committed snapshots. An arrow labelled git add points from the working tree to the index; an arrow labelled git commit points from the index into the object database; an arrow labelled checkout points from the object database back out to the working tree.
The five states a file can be in
Section titled “The five states a file can be in”Every path Git reports is in one of these states. Learning the vocabulary makes git status readable.
| State | Meaning | Appears in git status? |
|---|---|---|
| Tracked, unmodified | In the index; working tree content matches | No — nothing to report |
| Tracked, modified | In the index; working tree content differs | Yes, under Changes not staged for commit |
| Tracked, staged | Index content differs from HEAD | Yes, under Changes to be committed |
| Untracked | On disk, not in the index, not ignored | Yes, under Untracked files |
| Ignored | Matched by a .gitignore rule and untracked | No — that is the point |
A file can be in more than one of these at once. A staged file that you then edit again is both staged and modified, because the index and the working tree hold different content.
Tracked
Section titled “Tracked”A file is tracked if it has an entry in the index. That normally happens the first time you git add
it, and persists through subsequent commits.
Tracked files are the ones Git compares. It knows what they looked like at the last commit and what they look like now, so it can report differences.
Untracked
Section titled “Untracked”A file in your working tree with no index entry. Git sees it — it lists it — but manages nothing about it. Untracked files are not in any commit and would be lost if you deleted them.
Modified
Section titled “Modified”A tracked file whose working tree content differs from its index entry. This is the most common state during active work.
Git detects modification cheaply: the index stores each file’s size and timestamp alongside its content
hash. If those match, Git assumes the file is unchanged without reading it. If they differ, Git reads
the file and compares hashes. This is why git status stays fast in large repositories.
Deleted
Section titled “Deleted”Delete a tracked file with your file manager or rm, and Git notices — the index has an entry for a
path that no longer exists on disk:
Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) deleted: notes.mdThe deletion is itself a change to be staged and committed. git rm notes.md deletes the file and
stages the deletion in one step; deleting it yourself and then running git add notes.md achieves the
same result.
Ignored
Section titled “Ignored”A file matched by a pattern in .gitignore (or .git/info/exclude) and not already tracked. Git
excludes it from git status entirely.
Ignoring is what keeps git status useful. Without it, a project with a node_modules directory or a
build output folder would report thousands of untracked paths and you would stop reading the output.
Reading git status
Section titled “Reading git status”git status is the working tree’s primary interface. Its long form is written for humans:
git statusOn branch mainChanges to be committed: (use "git restore --staged <file>..." to unstage) modified: README.md
Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) deleted: notes.md
Untracked files: (use "git add <file>..." to include in what will be committed) scratch.txtThree sections, three comparisons:
| Section | Compares |
|---|---|
| Changes to be committed | Index ↔ HEAD |
| Changes not staged for commit | Working tree ↔ index |
| Untracked files | On disk, absent from the index |
Once you see it as three comparisons rather than three lists of files, the output stops being noise.
The short form
Section titled “The short form”git status --shortWhat it doesPrints one line per changed path using a two-character status code.
Why we run itIt is far quicker to scan than the long form once you know the codes, and it fits in a terminal alongside other work.
Expected resultTwo columns: the left character is the index status, the right is the working tree status.
M README.md D notes.md?? scratch.txtThe two-column layout is the whole trick — left column is the index, right column is the working tree:
| Code | Meaning |
|---|---|
M | Modified and staged (index differs from HEAD) |
M | Modified but not staged (working tree differs from index) |
MM | Staged, then modified again — two different versions |
A | Newly added to the index |
D | Deleted in the working tree, not yet staged |
D | Deletion staged |
?? | Untracked |
!! | Ignored (only shown with --ignored) |
MM is worth recognising on sight. It means the content you staged is not the content on disk, and
committing now records the older version.
Seeing ignored files
Section titled “Seeing ignored files”git status --short --ignored?? scratch.txt!! .env!! build/Useful when you suspect Git is ignoring something you expected it to track.
Clean versus dirty
Section titled “Clean versus dirty”A working tree is clean when it matches the index and the index matches HEAD — nothing to commit, nothing outstanding:
On branch mainnothing to commit, working tree cleanIt is dirty when anything differs. The distinction is not cosmetic; several Git operations refuse to run on a dirty tree because they would overwrite work Git has no copy of:
error: Your local changes to the following files would be overwritten by checkout: app.pyPlease commit your changes or stash them before you switch branches.That refusal is Git protecting you. The three ways forward:
- Commit the changes, if they are a coherent unit of work.
- Stash them —
git stashrecords them aside and cleans the working tree, andgit stash popbrings them back. - Discard them with
git restore, if they are genuinely unwanted.
Inspecting changes with git diff
Section titled “Inspecting changes with git diff”git status tells you which files changed. git diff tells you what changed inside them.
git diffWhat it doesShows the difference between the working tree and the index — unstaged changes only.
Why we run itIt answers “what have I changed but not yet staged?”, which is the question you have most often mid-task.
Expected resultA unified diff, or no output at all if everything is staged.
The three variants map onto the three areas:
| Command | Compares | Answers |
|---|---|---|
git diff | Working tree ↔ index | What have I not staged yet? |
git diff --staged | Index ↔ HEAD | What will the next commit contain? |
git diff HEAD | Working tree ↔ HEAD | Everything since the last commit |
Useful modifiers:
git diff --stat— a summary of files and line counts instead of full text.git diff -- path/to/file— restrict to one path.git diff --word-diff— highlight changed words rather than whole lines, which reads much better for prose and documentation.
Restoring files
Section titled “Restoring files”git restore is the modern command for putting working tree content back. It replaced git checkout’s
file-restoration role, which was easy to confuse with branch switching.
| Command | Effect |
|---|---|
git restore <file> | Overwrite the working tree copy from the index |
git restore --staged <file> | Reset the index entry from HEAD; working tree untouched |
git restore --staged --worktree <file> | Reset both to HEAD |
git restore --source=HEAD~1 <file> | Take the version from a specific commit |
Reading these as “which areas am I overwriting, and from where?” makes them predictable rather than memorised.
The working tree and HEAD
Section titled “The working tree and HEAD”HEAD identifies the commit your working tree started from. When you check out a commit, Git reads its tree, updates the index to match, and writes those files into the working tree.
From then on, the working tree drifts as you edit. git status is continuously reporting the size of
that drift.
Switching branches replays the process against a different commit. Files that differ between the two commits are rewritten; files that are identical are left alone; files that exist in one and not the other are created or removed. Untracked files are not part of any commit, so they are untouched.
Lesson 10 covers HEAD in detail.
Common mistakes
Section titled “Common mistakes”Assuming Git tracks everything in the folder. Git tracks what you have added. New files stay untracked until you stage them, and a commit will not include them.
Expecting .gitignore to untrack a file. It only affects untracked files. Use git rm --cached
to stop tracking something already committed.
Editing after staging and expecting the newer content to commit. The index holds what you staged.
MM in git status --short is the warning sign.
Treating the working tree as a backup. It holds one version of each file, with no history. Uncommitted work is unprotected.
Deleting files and expecting Git not to notice. Deleting a tracked file is a change like any other and must be staged and committed.
Running git add . without looking. It stages every untracked, non-ignored file under the current
directory. Run git status first.
Mental Model
Section titled “Mental Model”The working tree is your desk. It holds one version of each document, and whatever is on it right now is all there is.
The index is the outbox — the set of documents you have decided belong in the next delivery.
The repository is the filing cabinet, holding every delivery ever made.
git status compares the desk to the outbox, and the outbox to the last delivery. That is all it does,
and the three sections of its output are those two comparisons plus “things on the desk that were never
filed at all.”
What You Learned
Section titled “What You Learned”- The working tree is your project directory minus
.git: one checked-out version of the project. - Files are tracked or untracked; tracked files may be unmodified, modified, staged or deleted; matched files may be ignored.
git statusreports three comparisons: index↔HEAD, working tree↔index, and untracked paths.git status --shortuses two columns — left for the index, right for the working tree — andMMmeans staged content differs from what is on disk.- A clean working tree matches the index and HEAD; a dirty one blocks operations that would overwrite changes.
git diff,git diff --stagedandgit diff HEADcompare different pairs of areas.git restoreputs content back, and discarding working tree changes is permanent.
Try It Yourself
Section titled “Try It Yourself”In a disposable repository, predict each answer before running the command.
- Create
a.txtandb.txt. Rungit status --short. What codes appear? - Stage only
a.txt. Rungit status --shortagain. - Now edit
a.txtwithout staging. Predict the two-character code, then check. - Delete
b.txtfrom disk. What doesgit statussay, and in which section? - Create a
.gitignorecontaining*.log, then createdebug.log. Confirm it does not appear ingit status, but does appear ingit status --short --ignored. - Run
git diff, thengit diff --staged. Explain to yourself why they differ.
Step 3 should produce AM — staged as a new file, then modified again. If you predicted that, you have
the model.
Next Lesson
Section titled “Next Lesson”You have seen the index from the outside, as the thing git add writes to. The next lesson opens it
up: what it actually stores, why Git has one at all, and how it makes precise commits possible.