Git Fundamentals: Understanding Working Directory, Staging Area, Repository
AI generated
git
HEAD
Git · Version Control · Fundamentals · Developer Basics
Git Fundamentals: Understanding Working Directory, Staging Area, Repository
Why git add is a step of its own

Anyone who treats Git as a black box full of mysterious commands ends up typing by feel and getting confused by error messages. This article explains the three-tree model of Working Directory, Staging Area and Repository, shows what git add, commit, checkout and restore actually do under the hood, and gives you the mental model that makes later Git concepts like branches, merges and reset finally click.

12 min read Working Directory · Staging Area · Repository Git 2.x · CLI · Everyday Development

1. Why the three-tree architecture is Git's foundation

Most beginner confusion around Git traces back to a single cause: treating Git like a simple backup tool with one state per file. In reality, Git manages every file across three distinct states at once: the Working Directory, the Staging Area, and the Repository. This three-tree architecture is not an accident of design, it is the central idea that sets Git apart from older version control systems like SVN.

Once this model clicks, it immediately explains why git add is a separate step, why git status reports three distinct categories of change, and why git reset exists in several variants. Without this mental model, Git commands feel like memorized incantations. With it, they become logical operations that move data deliberately between three clearly defined areas. This article builds that exact model, step by step.

2. Working Directory: the visible state of your files

The Working Directory is the folder on disk where you actually work with files. It is the only one of the three trees that a text editor or IDE sees directly. Every change made by typing, saving, or running a code generator lands first, and exclusively, here. Git observes this directory but does not alter it on its own unless explicit commands like checkout or restore are invoked.

Important detail: the Working Directory has no concept of Git history. A file there is either identical to the most recently committed version, differs from it (modified), or is entirely new and unknown to Git (untracked). These three states, unchanged, modified, and untracked, are the foundation for everything git status later reports. A common beginner mistake is assuming a saved file is automatically "in Git" - in reality Git has no knowledge of a new file until it is explicitly captured with git add.

3. Staging Area (Index): the staging ground for the next commit

The Staging Area, also called the Index, is what actually sets Git apart from most other version control systems. It is a binary file at .git/index that holds a snapshot of what will actually be saved by the next git commit. The crucial point: the Staging Area is independent of the Working Directory. A file may already have been edited further on disk while the Index still holds an older intermediate version that was staged earlier.

This decoupling enables deliberate, selective committing. Instead of dumping every change from a work session into one messy commit, git add -p lets you pick exactly the lines that belong together and produce a clean, focused commit. Technically the Index is a flat list of file path, blob hash, and metadata, comparable to a directory tree, but existing entirely separate from the real files in the Working Directory. That is precisely what makes it a real staging ground instead of a mere marker.


# ~/.gitconfig: aliases that make the Staging Area explicit in daily work
[alias]
	# Show only what is currently staged (Index vs HEAD)
	staged = diff --staged

	# Unstage a file: copy HEAD back into the Index, keep Working Directory
	unstage = restore --staged

	# Interactive, hunk-by-hunk staging
	addp = add --patch

	# Status in compact form: two columns show Index vs HEAD and WD vs Index
	s = status --short --branch

4. Repository and HEAD: the history that never lies

The Repository is the database under .git/objects where every commit is stored as an immutable snapshot. Unlike the Working Directory and Staging Area, which are both mutable and volatile, a commit once created is preserved in its original form forever, unless garbage collection removes orphaned objects. Every commit references its predecessor by hash, forming a directed, acyclic history.

HEAD is not a tree of its own but a pointer, normally pointing at the current branch, which in turn points at the latest commit on that branch. When Git documentation talks about "HEAD," it usually means "the most recently committed state of the current branch" - the reference version against which the Working Directory and Staging Area are compared during git status. A commit object itself consists of a reference to a tree, which represents the complete state of every file at that point in time, not as a diff but as a complete, addressable snapshot.


# The three trees in action: create, stage, commit
$ echo "console.log('hello');" > app.js
$ git status
Untracked files:
  app.js

$ git add app.js
$ git status
Changes to be committed:
  new file:   app.js

$ git commit -m "Add app.js"
[main a1b2c3d] Add app.js
 1 file changed, 1 insertion(+)

# Now modify the working directory again, index stays untouched
$ echo "console.log('world');" >> app.js
$ git status
Changes not staged for commit:
  modified:   app.js

# Working Directory != Staging Area != Repository, all three differ now

5. git add in detail: what really happens when you stage

Technically, git add does two things: it creates a new blob object in the repository that stores the current file content as a compressed, content-addressed snapshot (identified by SHA-1 or SHA-256), and it updates the Index entry for that path to point at the hash of this new blob. Important: the blob already lands in the object store under .git/objects, even though no commit exists yet. That explains why staged but uncommitted changes are often still recoverable with git fsck --lost-found after an accidental reset --hard.

The decisive reason git add is a separate step: it decouples the question "what did I change" from the question "what should form one coherent commit". Without a Staging Area, every commit invocation would necessarily capture all changes in the Working Directory - exactly the behavior many SVN users expect out of habit, and exactly what Git deliberately avoids. With git add -p app.js you can even decide, line by line within a single file, which change should go into the next commit and which should remain for later in the Working Directory.

Tracing this mechanism with git ls-files --stage makes it directly visible that the Index is nothing more than a flat list of file path, mode, and blob hash: 100644 3b18e512d...b8dad 0 app.js. With git cat-file -p <hash> you can even read out the content of this blob, despite no commit ever having taken place. That makes it clear staging is not merely a bookkeeping note, it already creates real objects in the repository.

6. git commit in detail: snapshots, not diffs

A widespread misconception is that Git stores commits as diffs, similar to older version control systems. In reality, git commit creates a complete tree snapshot of the current Index state. For unchanged files, no new blobs are created at all - Git recognizes from the already known hash that the content is identical and simply references the existing object. That makes commits storage-efficient even though they are conceptually complete snapshots, not incremental deltas.

A commit object consists of four parts: the hash of the root tree, the hash of the parent commit (or several, in a merge), author and committer metadata with timestamps, and the commit message. Once the commit succeeds, something decisive happens: the branch pointer that HEAD points to is updated to the hash of the new commit. After a commit, the Working Directory and Staging Area are once again identical to the Repository state - the cycle starts over with the next change in the working directory.


# .pre-commit-config.yaml: hooks that operate only on staged content
repos:
  - repo: https://github.com/pre-commit/mirrors-phpcs
    rev: v3.9.0
    hooks:
      - id: phpcs
        # Runs only against files currently in the Staging Area,
        # never against unstaged Working Directory changes
        files: \.php$
        args: ["--standard=PSR12"]

  - repo: local
    hooks:
      - id: no-debug-statements
        name: Block var_dump/dd() in staged PHP files
        entry: sh -c '! git diff --staged --name-only | grep "\.php$" | xargs grep -l "var_dump\|dd("'
        language: system
        stages: [commit]

7. git checkout, restore and reset: moving files between the trees

Once the three-tree model is understood, checkout, restore, and reset can be understood as deliberate copy operations between the trees, rather than as unrelated commands. git checkout -- file.txt (or, more modern, git restore file.txt) copies the version from the Index back into the Working Directory, irrecoverably overwriting local changes. git restore --staged file.txt does the reverse: it copies the version from the last commit (HEAD) back into the Index without touching the Working Directory - the file becomes "unstaged" but remains modified on disk.

git reset operates on HEAD and the branch pointer itself, with three modes of increasing reach: --soft moves only HEAD, leaving the Index and Working Directory untouched - the changes from the undone commit end up as already-staged changes in the Index. --mixed (default) additionally moves the Index to the target state, leaving the Working Directory untouched. --hard additionally overwrites the Working Directory too, making it the only one of the three modes that can irrecoverably delete local changes.


# git restore: copy Index -> Working Directory (discards local edits)
$ git restore app.js
$ git diff app.js
# no output: Working Directory now matches the Index again

# git restore --staged: copy HEAD -> Index (unstage, keep local edits)
$ git restore --staged app.js
$ git status
Changes not staged for commit:
  modified:   app.js

# git reset moves HEAD and the branch pointer, with three levels of reach
$ git reset --soft HEAD~1   # only HEAD moves, Index and WD untouched
$ git reset --mixed HEAD~1  # HEAD + Index move, WD untouched (default)
$ git reset --hard HEAD~1   # HEAD + Index + WD all overwritten, irreversible

8. git status and git diff: making the three trees visible

At its core, git status is nothing more than a two-part comparison: Working Directory against Index, and Index against HEAD. The output "Changes to be committed" lists differences between the Index and HEAD - what would be saved by the next commit. "Changes not staged for commit" lists differences between the Working Directory and the Index - changes that exist on disk but are not yet earmarked for the next commit. "Untracked files" are paths that appear in neither of the other two trees.

The same logic applies to git diff, a command that regularly confuses beginners because it compares different trees depending on the flag. git diff without arguments compares Working Directory against Index - exactly the changes a git add would capture next. git diff --staged (synonym: --cached) compares Index against HEAD - exactly the changes a git commit would save next. Mixing up these two commands makes it look like "there are no changes", even though staged changes actually exist, they were simply queried with the wrong diff mode.


# git diff compares Working Directory vs Index
$ git diff
diff --git a/app.js b/app.js
-console.log('hello');
+console.log('hello world');

# git diff --staged compares Index vs HEAD (the last commit)
$ git add app.js
$ git diff --staged
diff --git a/app.js b/app.js
-console.log('hello');
+console.log('hello world');

# After the change above is also staged, plain "git diff" shows nothing
$ git diff
$ echo "No output: Working Directory now equals Index"

9. The three-tree mental model, compared directly

The Git documentation itself occasionally calls the Working Directory, Staging Area, and HEAD the "three trees," because each area is internally represented by a tree-like state, even though only the Repository side is actually stored as a tree object. For daily use, a simpler picture is enough: the Working Directory is the draft, the Staging Area is what is about to go into the envelope, and the Repository is the letter that has already been sent and can no longer be altered. Every Git command moves content deliberately between exactly two of these three stations.

The table below resolves the most common beginner misconceptions by contrasting the intuitive but wrong reflex with Git's actual behavior.

Situation Common misconception Correct mental model Why it matters
git add file.txt The file is now saved/committed The blob only sits in the Index, Repository untouched Prevents false confidence before committing
git commit without prior add All changes get committed automatically Only what is in the Index gets committed Explains "nothing to commit, working tree clean"
git checkout -- file.txt A harmless, purely cosmetic action Overwrites the Working Directory irrecoverably Prevents loss of local changes
git reset HEAD~1 Deletes the last commit entirely Only moves the branch pointer, objects remain intact Explains why reset is usually reversible
git diff without arguments Shows all pending changes Shows only Working Directory vs. Index Explains the role of --staged/--cached

Anyone who internalizes these five rows has already resolved most of the typical Git confusion. Every other command, from git stash to git cherry-pick, ultimately boils down to the same question: which of the three trees is being read here, and which one is being written?

Mironsoft

Git workflows, code reviews and CI/CD pipelines for PHP and Magento teams

Want clean Git workflows across your team?

We help development teams introduce branching strategies, commit conventions and review processes built on a solid understanding of Working Directory, Staging Area and Repository.

Git training

Hands-on workshops on staging, branching and merge strategies for teams

Workflow audit

Analyzing existing Git processes and tightening them toward clean commit history

CI/CD integration

Setting up pre-commit hooks, linting and automated pipelines around Git

10. Summary

Git's three-tree model, Working Directory, Staging Area and Repository, is not an advanced detail, it is the foundation every other Git command builds on. The Working Directory is the visible, mutable state on disk. The Staging Area is a deliberately decoupled staging ground that enables selective, focused committing. The Repository stores commits as immutable, complete snapshots, not diffs, and HEAD always points to the most recently committed state of the current branch.

git add copies from the Working Directory into the Index, git commit copies from the Index into the Repository, and checkout/restore copy back in the opposite direction. git status and git diff are ultimately just comparison operations between exactly two of these three trees. Anyone who internalizes this model can classify any Git command by asking a single question: which tree is being read, and which one is being written?

Git Fundamentals: The Three Trees at a Glance

Working Directory

The visible folder on disk. Changes land here first, independent of Git.

Staging Area (Index)

.git/index stores what the next commit will contain. git add populates it deliberately.

Repository & HEAD

Immutable snapshots under .git/objects. HEAD points at the latest commit on the current branch.

Movement between trees

add (WD to Index), commit (Index to Repo), restore/checkout (back again).

11. FAQ: Working Directory, Staging Area, Repository

1What is the difference between Working Directory, Staging Area and Repository?
Working Directory is the real files on disk. Staging Area (Index) determines what the next commit contains. Repository stores all commits as immutable snapshots.
2Why is git add a separate step?
It decouples "what changed" from "what should form a commit". git add -p lets you pick individual lines for focused commits.
3What does git commit technically do with the Index?
Creates a full tree snapshot of the Index, references the parent commit, and moves the branch pointer. Unchanged files produce no new blobs.
4How do I see what's in the Staging Area?
git status shows Changes to be committed. For line-level detail: git diff --staged compares the Index against HEAD.
5git diff vs. git diff --staged?
git diff compares Working Directory against Index. git diff --staged compares Index against HEAD, the last commit.
6Is git checkout -- file.txt dangerous?
Yes. It copies the Index version back into the Working Directory, irrecoverably overwriting unstaged local changes.
7git restore vs. git restore --staged?
restore copies Index to Working Directory (discards local changes). restore --staged copies HEAD into the Index (unstage, WD stays untouched).
8What do soft, mixed and hard do in reset?
soft moves only HEAD. mixed additionally moves the Index. hard additionally overwrites the Working Directory and can delete data irrecoverably.
9Is Staging Area the same as Index?
Yes, both terms refer to the same file, .git/index, and the Git documentation uses them interchangeably.
10What does detached HEAD mean?
HEAD points directly at a commit instead of at a branch. New commits are possible but hard to find again once you switch away without a branch pointer.