Four object types and a content-addressable store
Everything Git knows is stored as one of four object types in .git/objects/, addressed by SHA-1 hash of the content. Once you can identify a blob, a tree, a commit, and a tag in the repo, every higher-level Git command stops feeling magical — it is all manipulation of these four shapes.
Blob stores the raw bytes of a single file. No filename, no permissions — just contents. Two files with identical contents (a duplicate config, the same fixture in two test suites) hash to the same blob and are stored once. Tree stores a directory listing: name, mode (file permissions and type), and the hash of either a blob (for a file) or another tree (for a sub-directory). Trees give names and structure to blobs.
Commit ties it together: a small object holding the hash of the root tree (the project's snapshot), the hash(es) of parent commit(s), the author and committer with timestamps, and the message. Walking commit parents takes you backwards through history; reading a commit's tree takes you sideways into "what did everything look like at this point." Tag is the rarer fourth type: a named pointer with metadata, used for annotated tags. Lightweight tags are just refs, not tag objects.
The store itself is content-addressable. echo "hello" | git hash-object --stdin computes the hash; git cat-file -p <hash> reads any object back. The first two characters of the hash become the directory name under .git/objects/, and the remaining 38 are the filename — that is why early Git inspections show files like .git/objects/3a/4f5d6e.... As repos grow, Git compresses many objects into pack files for efficiency (next lesson). The mental model survives compression: hash → object → content.