Inside .git: the Object Store

blobs, trees, commits in the object database - plus the refs and HEAD pointing in

.git - the archive under your project folder
.git - the archive under your project folder

The Where Your Work Lives chapter ended at the sealed record. This chapter opens the archive it drops into: the object database. Every OBJECT git stores lives in .git/objects as immutable objects of four types. Three of them assemble every snapshot - blob, tree, commit - and the fourth, the tag object, waits for the releases card. Objects are immutable - never edited in place; unreachable ones can eventually be swept.

$ git cat-file -p HEAD
tree 1509b96...
parent 159fa2b...
...
salt the water
$ git cat-file -p 1509b96
100644 blob acf854c8...  menu.md
100644 blob 345f58d...  pasta.md
$ git cat-file -p 345f58d
pasta:
  boil 9 min
  salt the water
the whole walk - real output

Open the newest record and walk it down to raw bytes. The commit is small text: one tree id, its parent(s) - zero for the first record, several for a merge - author, committer, your note. The tree is one folder's listing - a mode, a name, an object id per entry. The blob is the file's bytes and nothing else.

Notice what the blob does NOT hold: a filename. menu.md is a label on the tree's row, not on the print. Rename a file and only the tree changes; the blob's id stays. That is also why the same content under two names is stored exactly once.

$ git hash-object hello.txt  # hello + newline
ce013625030ba8dba906f75...
$ git hash-object hello.txt  # newline deleted
b6fc4c620b67d95f953a5c1...
one byte less - a different universe

Where do the ids come from? Git derives them: an id is the SHA fingerprint of the object's bytes. Ask any SHA-1-format git on earth for the id of hello plus a newline and it answers the same (SHA-256 repos answer differently - the format is a per-repo choice). Delete ONE newline and the answer changes completely - identity IS content.

Because trees name blobs by id, and commits name trees and parents by id, one commit id transitively seals the entire history behind it. Two machines that agree on a commit's full id agree on every byte and every ancestor. That is what makes git distributable without anyone in charge.

$ ls .git/refs/heads
dinner-ideas
garlic-bread
main
$ cat .git/refs/heads/main
65427efc7213033d5973258bf...
three stickers on one timeline

The object database is immutable, but nobody browses a warehouse by fingerprint. The human layer is refs: names holding one commit id each. A branch is exactly that - in this repo, cat .git/refs/heads/main prints one 41-byte line in this classic loose-ref repo (refs can also live packed - the file is an X-ray, not an API). Creating a branch writes one pointer; deleting one removes it. The commits themselves never move and never copy.

the pin - two states
the pin - two states

One more pointer closes the loop: HEAD, the you-are-here pin. Attached, it holds a branch NAME - ref: refs/heads/main - which is why committing moves your branch: the sticker slides, the pin rides. Detached, it holds a raw id: perfect for visiting an old moment, risky only if you file new records there and leave without naming them. The hatch is always one command: git switch -c rescue.

The chapter's payoff, honestly stated: MOST history commands become understandable as blobs, trees, commits - plus refs and the index. Most, not all: less magic, not zero magic. When a later card says a branch moved or a commit was replayed, you now know the machinery underneath: small immutable objects, tiny movable names, one pin.

tline