Skip to content

Understanding the Git Working Tree

Lesson 8 of 12Beginner9 min readGit Fundamentals · Getting StartedVerified: Git 2.43.0 on Ubuntu 24.04

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 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.
The working tree relative to the index and the repository

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.

my-project/README.mdsrc/greet.pynotes.mdworking tree.git/indexone entry per tracked pathobjects/committed snapshotsgit addworking tree → indexgit commitindex → objectscheckout / switchobjects → working tree

Every path Git reports is in one of these states. Learning the vocabulary makes git status readable.

StateMeaningAppears in git status?
Tracked, unmodifiedIn the index; working tree content matchesNo — nothing to report
Tracked, modifiedIn the index; working tree content differsYes, under Changes not staged for commit
Tracked, stagedIndex content differs from HEADYes, under Changes to be committed
UntrackedOn disk, not in the index, not ignoredYes, under Untracked files
IgnoredMatched by a .gitignore rule and untrackedNo — 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.

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.

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.

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.

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.md

The 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.

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.

git status is the working tree’s primary interface. Its long form is written for humans:

Terminal window
git status
On branch main
Changes 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.txt

Three sections, three comparisons:

SectionCompares
Changes to be committedIndex ↔ HEAD
Changes not staged for commitWorking tree ↔ index
Untracked filesOn disk, absent from the index

Once you see it as three comparisons rather than three lists of files, the output stops being noise.

Terminal window
git status --short

What 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.txt

The two-column layout is the whole trick — left column is the index, right column is the working tree:

CodeMeaning
M Modified and staged (index differs from HEAD)
MModified but not staged (working tree differs from index)
MMStaged, then modified again — two different versions
A Newly added to the index
DDeleted 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.

Terminal window
git status --short --ignored
?? scratch.txt
!! .env
!! build/

Useful when you suspect Git is ignoring something you expected it to track.

A working tree is clean when it matches the index and the index matches HEAD — nothing to commit, nothing outstanding:

On branch main
nothing to commit, working tree clean

It 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.py
Please commit your changes or stash them before you switch branches.

That refusal is Git protecting you. The three ways forward:

  1. Commit the changes, if they are a coherent unit of work.
  2. Stash them — git stash records them aside and cleans the working tree, and git stash pop brings them back.
  3. Discard them with git restore, if they are genuinely unwanted.

git status tells you which files changed. git diff tells you what changed inside them.

Terminal window
git diff

What 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:

CommandComparesAnswers
git diffWorking tree ↔ indexWhat have I not staged yet?
git diff --stagedIndex ↔ HEADWhat will the next commit contain?
git diff HEADWorking tree ↔ HEADEverything 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.

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.

CommandEffect
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.

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.

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.

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.”

  • 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 status reports three comparisons: index↔HEAD, working tree↔index, and untracked paths.
  • git status --short uses two columns — left for the index, right for the working tree — and MM means 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 --staged and git diff HEAD compare different pairs of areas.
  • git restore puts content back, and discarding working tree changes is permanent.

In a disposable repository, predict each answer before running the command.

  1. Create a.txt and b.txt. Run git status --short. What codes appear?
  2. Stage only a.txt. Run git status --short again.
  3. Now edit a.txt without staging. Predict the two-character code, then check.
  4. Delete b.txt from disk. What does git status say, and in which section?
  5. Create a .gitignore containing *.log, then create debug.log. Confirm it does not appear in git status, but does appear in git status --short --ignored.
  6. Run git diff, then git 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.

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.