Skip to content

The Three Trees: Working Directory, Index, and Repository

Git organizes your work into three distinct areas, commonly called the "three trees." Understanding how changes flow between them is the single most important concept in Git. Every command you run either moves data between these areas, inspects the differences between them, or manipulates what they contain.


The Three Areas

Working Directory (Working Tree)

The working directory is the actual directory of files on your filesystem. When you open a file in your editor, you're editing the working copy. This is the only area you interact with directly - the other two are managed by Git.

Your working directory contains every tracked file at its current state, plus any new files you've created that Git doesn't know about yet.

Staging Area (Index)

The staging area - also called the index - is a holding zone between your working directory and the repository. When you run git add, you're copying the current state of a file into the index. The index represents what will go into your next commit.

The index is actually a binary file at .git/index. It doesn't store file contents directly - it stores references to blob objects and metadata (file paths, permissions, timestamps). You can think of it as a manifest of what the next commit will look like.

Why have a staging area at all? Because it gives you precise control over commits. If you've changed five files but only two changes are related, you can stage just those two and commit them with a focused message. The other three changes stay in your working directory, ready for a separate commit.

Repository (Committed History)

The repository is the .git directory at the root of your project. It contains the complete history of every committed snapshot, all branches and tags, configuration, and the object database. When you run git commit, Git takes everything in the staging area, creates a permanent snapshot, and adds it to the repository.

Once something is committed, it's safe. Git's content-addressable storage means committed data is checksummed and extremely difficult to lose (even if you try).


The File Lifecycle

Every file in a Git repository is in one of these states:

State Where it lives What it means
Untracked Working directory only Git sees the file but isn't tracking it
Tracked, unmodified Working directory = repository File matches the last commit, no changes
Modified Working directory differs from index You've changed the file but haven't staged the changes
Staged Index differs from repository Changes are queued for the next commit
Committed Repository Changes are permanently recorded

A file can be in multiple states simultaneously. If you modify a file, stage it, then modify it again, the file is both staged (the first set of changes) and modified (the second set). This is normal and useful - it means your next commit captures only the changes you explicitly staged.

flowchart LR
    A[Untracked] -->|git add| B[Staged]
    B -->|git commit| C[Unmodified]
    C -->|edit file| D[Modified]
    D -->|git add| B
    C -->|git rm| A
    D -->|git restore| C
    B -->|git restore --staged| D

Creating a Repository: git init

Every Git project starts with initialization. git init creates the .git directory with all the internal structures Git needs:

git init myproject
cd myproject

Or initialize in an existing directory:

cd existing-project
git init

After git init, your directory looks like this:

myproject/
└── .git/
    ├── HEAD            # Points to the current branch
    ├── config          # Repository-specific configuration
    ├── description     # Used by GitWeb (rarely relevant)
    ├── hooks/          # Sample hook scripts
    ├── info/           # Global exclude patterns
    ├── objects/        # All content (blobs, trees, commits, tags)
    └── refs/           # Branch and tag pointers
        ├── heads/      # Local branches
        └── tags/       # Tags

The important pieces: HEAD tells Git what branch you're on. objects/ stores all your data. refs/ stores branch and tag pointers. The Object Model and Refs, the Reflog, and the DAG guides cover these in depth.


Checking Status: git status

git status is the command you'll run most often. It shows you exactly where things stand across all three areas:

git status

The output groups files by state:

  • Changes to be committed - files in the staging area (ready for commit)
  • Changes not staged for commit - tracked files that have been modified but not staged
  • Untracked files - files Git doesn't know about

For a compact view:

git status -s

This shows two-column status codes. The left column is the staging area status, the right column is the working tree status:

Code Meaning
?? Untracked
A New file, staged
M Modified, staged
M Modified, not staged
MM Modified, staged, then modified again
D Deleted, staged
D Deleted, not staged

Staging Changes: git add

git add copies the current state of files from the working directory into the staging area:

# Stage a specific file
git add README.md

# Stage multiple files
git add file1.txt file2.txt

# Stage all changes in a directory
git add src/

# Stage all changes in the entire working tree
git add .

# Stage parts of a file interactively
git add -p README.md

git add -p (patch mode) is particularly powerful. It shows you each change in a file and lets you choose which ones to stage. This means you can make several unrelated edits to one file and commit them separately.

Stage with intention

Get in the habit of staging specific files rather than using git add . for everything. This forces you to review what you're about to commit and produces cleaner, more focused commits.


Unstaging and Restoring: git restore

Made a mistake? git restore (introduced in Git 2.23) provides clear commands for undoing changes:

# Discard changes in working directory (restore from index)
git restore README.md

# Unstage a file (restore the index from HEAD, keep working changes)
git restore --staged README.md

# Restore a file from a specific commit
git restore --source=HEAD~2 README.md

git restore discards changes

git restore README.md (without --staged) permanently discards your uncommitted working directory changes to that file. There is no undo. If you haven't committed or stashed those changes, they are gone. Use git stash first if you're unsure.

Before Git 2.23, the same operations used git checkout and git reset:

Modern command Legacy equivalent
git restore file git checkout -- file
git restore --staged file git reset HEAD file

The modern restore commands are clearer about what they do, but you'll see the legacy forms in older documentation and tutorials.


Removing Files: git rm

To remove a tracked file from both the working directory and the staging area:

git rm old-file.txt

This deletes the file from disk and stages the deletion. Your next commit will record the removal.

To stop tracking a file but keep it on disk (useful for files that should have been in .gitignore):

git rm --cached config.local

The file stays in your working directory but Git stops tracking it. Add it to .gitignore to prevent accidentally re-adding it.


The Init-to-Commit Workflow

Here's the complete workflow from creating a repository through your first commit, with each step showing the state of the three trees:


HEAD: Your Current Position

HEAD is a special reference that points to whatever you currently have checked out - usually a branch, which in turn points to a commit. Think of HEAD as "you are here" on the commit graph.

When HEAD points to a branch name (the normal case), it's a symbolic reference:

HEAD → main → e4f5g6h

When you make a new commit, the branch pointer moves forward and HEAD follows it.

Detached HEAD

If you check out a specific commit (by hash, tag, or remote branch) rather than a branch name, HEAD points directly to that commit. This is called detached HEAD state:

git checkout a1b2c3d    # Detached HEAD
git checkout v1.0       # Also detached HEAD (tag, not branch)

In detached HEAD, you can look around and even make commits, but those commits aren't on any branch. If you switch away without creating a branch, those commits become unreachable and will eventually be garbage collected.

To get out of detached HEAD and keep any commits you made:

git branch my-new-branch    # Create a branch at the current commit
git switch my-new-branch     # Switch to it (HEAD is now attached)

The .gitignore File

Not every file in your working directory should be tracked. Build artifacts, dependency directories, editor configs, OS files, and secrets should be excluded. The .gitignore file tells Git which files to ignore.

Create .gitignore in your repository root:

# Compiled output
*.o
*.pyc
__pycache__/

# Dependencies
node_modules/
vendor/

# Build directories
dist/
build/
*.egg-info/

# IDE and editor files
.idea/
.vscode/
*.swp
*~

# OS files
.DS_Store
Thumbs.db

# Environment and secrets
.env
.env.local
*.key
*.pem

Pattern Syntax

Pattern Matches Example
*.log All .log files in any directory error.log, src/debug.log
/build build directory in the repo root only build/, but not src/build/
build/ Any directory named build build/, src/build/
doc/*.txt .txt files in doc/ (not subdirs) doc/notes.txt, but not doc/sub/notes.txt
doc/**/*.txt .txt files anywhere under doc/ doc/notes.txt, doc/sub/notes.txt
!important.log Exception - track this file even if *.log matches Negation must come after the pattern it overrides
# Comment line Ignored by Git

Global gitignore

For OS-specific files (.DS_Store, Thumbs.db) and editor files (.idea/, *.swp) that apply to all your projects, use a global gitignore instead of adding them to every repository:

git config --global core.excludesFile ~/.gitignore_global

Then put your personal patterns in ~/.gitignore_global. This keeps each repository's .gitignore focused on project-specific patterns.

Checking What's Ignored

To verify your .gitignore patterns work:

# Check if a specific file would be ignored
git check-ignore -v debug.log

# List all ignored files
git status --ignored

Putting It All Together


Further Reading


Previous: Introduction: Why Git, and Why Version Control | Next: Commits and History | Back to Index

Comments