Objects & the .git Directory
Most Git confusion disappears once you understand what’s actually stored on disk. Git is, underneath the porcelain commands, a simple content-addressed key-value store built on four object types.
The .git directory
git init creates a .git/ directory that is the repository — delete
it and the version history is gone, even though your files remain. Its most
important pieces:
| Path | Purpose |
|---|---|
.git/objects/ | Every blob, tree, commit, and tag, stored compressed by content hash |
.git/refs/heads/ | One file per local branch, containing the commit SHA it points to |
.git/refs/tags/ | One file per tag |
.git/refs/remotes/ | Remote-tracking branches, e.g. origin/main |
.git/HEAD | A pointer to the currently checked-out ref (usually refs/heads/<branch>) |
.git/index | The staging area — a binary file listing what will go into the next commit |
.git/config | Repository-local configuration |
.git/logs/ | The reflog: a history of where HEAD and branches have pointed |
The four object types
Everything Git tracks is one of these, each identified by the SHA of its own contents:
- Blob — the raw contents of a single file. No filename, no metadata, just bytes. Two files with identical content share one blob.
- Tree — a directory listing: a set of (mode, name, SHA) entries pointing at blobs and other trees. This is where filenames live.
- Commit — a snapshot pointer: one tree SHA (the project root at that moment), zero or more parent commit SHAs, author, committer, timestamp, and message.
- Tag — (for annotated tags) a named, signable pointer at a specific commit, with its own message and metadata.
You can inspect any of these directly:
git cat-file -t <sha> # print the object type
git cat-file -p <sha> # print the object contents
git log --oneline -1 # find a commit SHA to try this on
Refs and HEAD
A ref is just a named pointer to a commit SHA, stored as a small file
under .git/refs/. A branch is nothing more than a ref that moves
forward automatically every time you commit while it’s checked out — which
is why creating a branch is instantaneous and cheap: Git writes one 40 (or
64) character line to a file, it doesn’t copy any content.
HEAD is a pointer to the ref you currently have checked out. Most of the
time it points at a branch (ref: refs/heads/main), which in turn points
at a commit. When you check out a specific commit or tag instead of a
branch, HEAD points directly at that commit — a state called
detached HEAD.
The three trees
Day-to-day Git commands move changes between three areas:
| Area | What it is | Command to move in |
|---|---|---|
| Working directory | The actual files on disk you edit | — |
| Staging area / index | A snapshot of what will go into the next commit | git add |
Repository (.git/objects) | Permanent, committed history | git commit |
This intermediate staging step is what lets you build a commit out of only
part of your working-directory changes — run git add -p to stage
individual hunks instead of whole files.
What git commit actually does
- Reads the current staging area (the index).
- Writes a blob for every changed file’s contents (skipped if that exact content already exists as a blob).
- Writes tree objects representing the full directory structure of what’s staged.
- Writes a commit object pointing at the root tree and at the current commit as its parent.
- Moves the current branch ref to point at the new commit.
Because every commit stores the state of the entire project tree (not a
diff), Git can check out any commit directly without replaying history —
diffs you see in git log -p or git diff are computed on the fly by
comparing two trees.
Once you have this mental model, the "everyday" commands stop feeling like memorized incantations:
git branch foowrites a ref file,git checkout foomovesHEAD,git mergecreates a new commit with two parents, andgit resetjust moves a ref (and optionally the index and working directory) to point somewhere else.