git Distributed version control system, created by Linus Torvalds

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:

PathPurpose
.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/HEADA pointer to the currently checked-out ref (usually refs/heads/<branch>)
.git/indexThe staging area — a binary file listing what will go into the next commit
.git/configRepository-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:

AreaWhat it isCommand to move in
Working directoryThe actual files on disk you edit
Staging area / indexA snapshot of what will go into the next commitgit add
Repository (.git/objects)Permanent, committed historygit 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

  1. Reads the current staging area (the index).
  2. Writes a blob for every changed file’s contents (skipped if that exact content already exists as a blob).
  3. Writes tree objects representing the full directory structure of what’s staged.
  4. Writes a commit object pointing at the root tree and at the current commit as its parent.
  5. 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 foo writes a ref file, git checkout foo moves HEAD, git merge creates a new commit with two parents, and git reset just moves a ref (and optionally the index and working directory) to point somewhere else.