How Git Actually Stores Your Code · Day 1 of 14

The Name Is the Content

Open a terminal inside any Git repository and run this:

echo 'test content' | git hash-object --stdin

Git prints forty characters: d670460b4b4aece5915caf5c68d12f560a9fe3e4. Run it again on another machine, in another repository, in another country, ten years from now. You get the same forty characters. Run it on a different piece of text and you get a completely different forty characters, with no visible relationship to the first.

That is the whole foundation. Everything else in this course is built on it.

Here is what Git actually did. It took your fourteen bytes of text, and stuck a small header on the front: the word blob, a space, the number of bytes as a decimal string, and a zero byte. So the thing it hashed was blob 13\0test content\n. Then it ran SHA-1 over the lot. SHA-1 is a hash function — it chews through any amount of data and spits out exactly 160 bits, which we write as 40 hexadecimal characters. Change one byte of the input and the output changes completely and unpredictably.

If you add -w to that command, Git writes the object to disk. It compresses the header-plus-content with zlib and saves it at .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4. The first two characters of the hash become a directory name and the remaining thirty-eight become the filename. That split is pure housekeeping — filesystems get slow when one directory holds a hundred thousand files, so Git spreads them across 256 buckets.

So Git’s core is a key-value store. You hand it some bytes, it hands you back a key. You hand it the key later, it hands you back the bytes. The git cat-file -p d670460 command does exactly that: it prints test content.

The unusual part is that you don’t get to choose the key. In a normal key-value store — a dictionary in Python, a table in a database — you pick the name and Git would pick the value. Here the key is computed from the value. This is called content-addressed storage: the address of a thing is derived from the thing itself. Linus Torvalds built Git this way in April 2005, over about two weeks, after the Linux kernel project lost access to the commercial tool it had been using. The design decision looks small. It has three consequences that shape everything you do with Git for the rest of your life.

The first is that objects are immutable. Not by policy, not because of a permission bit, but because there is no operation that could change one. Suppose you want to edit that blob to say test content v2. You can compute the hash of the new text, and it will be some entirely different forty characters. The old object is still sitting at d6/70460... with its old bytes. You haven’t modified anything, you’ve added a second object. Nothing in .git/objects is ever edited in place. This is why, when people talk about “rewriting Git history”, what they actually mean is something much narrower than it sounds — new objects get written, and some pointers elsewhere get moved to aim at them. We will come back to those pointers.

The second consequence is deduplication, and it comes free. If the same bytes appear twice, they hash to the same name, so they are the same object. Copy a licence file into forty subdirectories and commit it: one blob on disk. Delete a file in one commit and restore it identically ten commits later: no new blob, Git just points at the one already there. The empty file has a hash too — e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 — and every empty file in every Git repository on Earth is that same object. Git never has to check whether it already has a copy of something. Storing a thing it already has means writing the same filename with the same contents, which is a no-op.

The third is verification. Because the name is a function of the content, you can always check whether an object is what it claims to be: read it, hash it, compare. That is most of what git fsck does. If a disk flips a single bit inside one of those compressed files, the recomputed hash won’t match the filename it’s stored under, and Git will tell you. Compare that to a system where revision 4512 is whatever the server says revision 4512 is. There is no way to check it from the inside.

Now, the objection. Two different inputs could in principle hash to the same forty characters — there are only 2^160 possible outputs and infinitely many possible inputs. In practice that number is large enough that random collisions don’t happen. Deliberate ones are a different matter. In February 2017, researchers at Google and CWI Amsterdam published SHAttered: two different PDF files with the same SHA-1 hash, produced with roughly 6,500 CPU-years and 110 GPU-years of computing. Git’s answer came in two parts. Version 2.13, released later that year, switched to a hardened SHA-1 implementation that detects the specific patterns a collision attack leaves behind and refuses the input. And from version 2.29 in October 2020, Git can be built to use SHA-256 instead, giving 64-character object names. That mode exists and works, but almost nobody uses it, because a repository using it cannot talk to a repository using SHA-1, and that includes every repository your colleagues have.

Here is the thing worth carrying out of today. Two people who have never met, working offline, who happen to write a file with the same contents, will independently arrive at the same forty-character name for it. No server assigned it. No registry handed it out. No coordination of any kind took place. When one of them later says “do you have d670460?”, the question is meaningful to the other.

That is why Git can be distributed at all. A centralised system has to hand out revision numbers, because there is no other way for two machines to agree on what to call the same thing. Git doesn’t hand out anything. It just computes.

Continue reading

That was day one of 14. The rest of How Git Actually Stores Your Code arrives one morning at a time, at an hour you pick.

Subscribe to this course

1 person reading it. Unsubscribing is one click in any email.

The whole course