skipnothing/Git

Commits & the Object Model

You saved the folder three times: notes/, notes-backup/, notes-final/, and felt every copy waste space. Git quietly keeps every version too, yet .git/objects/ barely grows and never once records 'what changed' between them.

  • ▸Git never keeps your filename next to your file's bytes. Rename all you like, the stored content never budges.
  • ▸Two identical files cost one copy on disk; a thousand versions of unchanged code cost almost nothing.
  • ▸Each saved version records your whole project, not the lines that changed, yet .git/ barely grows.
01

The name comes from the content

Open .git/objects/ in any repo and you find short directories named 2e, 52, 1d, each holding a file with a 38-character name. Nothing in there says app.py or config.json, just 40 hex characters per entry, split into a 2-char folder and a 38-char filename.

Where does a name like 2eb1080d... come from? Not from you, and not from a counter git bumps each time. Run git hash-object app.py and 40 hex characters print. Change one byte of the file, run it again, and a completely different 40 print. Put the bytes back exactly, and the first name returns, every single time.

The name is computed straight from the bytes. Same bytes in, same name out, on your laptop, on a teammate's machine, on a server, forever. Git prepends a few bytes of header and runs SHA-1 over the whole thing. The formula is public and deterministic: no registry, no luck.

In the viz, change the bytes and try to keep the first 4 characters of the name. You won't.

CHANGE THE BYTES, KEEP THE NAME
02

A blob is content with no name

Pick one object and open it. git cat-file -p 2eb1080d prints exactly what was in app.py: print("notes"). Ask its type with git cat-file -t 2eb1080d and git answers blob.

A blob holds a file's bytes and nothing else. No filename. No path. No timestamp. No line recording that these bytes were ever called app.py. The header git adds before hashing is just the word blob, a space, the byte count, and a null byte: blob 15\0print("notes")\n. SHA-1 of that whole string is the name 2eb1080d....

Now copy app.py to copy.py, byte for byte, and hash both. Identical bytes, identical header, identical name: one blob, 2eb1080d..., for two files on disk. Git keeps the content once and never notices they had different names.

So the filename had to go somewhere else. app.py and copy.py really are different files, and that difference is real, just not inside the blob. The next object type is where the names live.

OPEN THE OBJECT, FIND NO NAME
03

Trees hold the names

The filename lives in a tree. Run git cat-file -p on a tree object and a table prints:

100644 blob 7c88e8a5...	README.md
100644 blob 2eb1080d...	app.py
100644 blob 1d89dd47...	config.json

Each row shows a mode, a type, a hash, and a name. Only three of those are stored: the raw row is 100644 app.py, a null byte, then the hash as 20 raw bytes. cat-file prints the type from the mode, blob for a file and tree for a folder. A tree is a directory listing: it maps names to the hashes of blobs (files) and, for subfolders, other trees. The name app.py sits here, in the tree, next to its blob's hash. The blob still holds only bytes.

Rename app.py to main.py and re-hash everything. The blob's name is unchanged, 2eb1080d..., because its bytes are unchanged. The tree's name changes, because one of its rows now reads main.py where it read app.py. Content kept its identity; the listing got a new one.

That split is the whole point. Bytes live in blobs, named by content. Names and folder structure live in trees, which point at blobs by hash. Put a folder inside a folder and it is simply a tree row pointing at another tree: one cat-file deeper prints the same kind of table.

RENAME A FILE, WATCH THE TREE NOT THE BLOB
04

A commit points at a whole tree

Two object types down, one to go. git cat-file -p on a commit prints a short block of text:

tree 385ae245f2625e87083913aa8e9514158037859f
author A U Thor <author@example.com> 1700000240 +0000
committer A U Thor <author@example.com> 1700000240 +0000

Add config.json

That is the entire commit. One line names a tree, the root of the project. Two lines record who and when. A blank line, then your message. Nothing else.

The tree line is the key. It points at one tree, which points at every blob and subtree in the project. Follow it and you can rebuild the whole folder from this single commit's hash, 92dfbbba.... A commit is not a list of changes. It is a labeled pointer to a complete snapshot.

Its hash covers every byte of that text: the tree, the author and committer with their times, and the message. Fix a typo in the message and re-commit, and the tree is byte-identical but the commit's hash changes, because its text changed. Re-commit a minute later with nothing changed at all and the hash still changes, because the committer line now carries a different time. A commit made after another also carries a parent line naming the one before it. Following those links is the next topic; here, one commit already holds a whole project.

UNFOLD A COMMIT INTO THE WHOLE PROJECT

Keep going, sign up to unlock the rest

1 more part in this topic, plus 18+ more topics in Git.

Sign up, it's freeSee the full Git
How Git Thinks0/3
#2 Refs & HEAD