Skip to content
hash-object: How Git Identifies Content
step 1/5

Reading — step 1 of 5

Read

~3 min readObject Storage

hash-object: How Git Identifies Content

Strip away branches, remotes, staging, and merge conflicts, and Git is one idea executed relentlessly: name every piece of content by the hash of the content itself. Not by filename, not by version number, not by timestamp — by what the bytes are. This lesson builds that naming function, and everything else in the course is plumbing around it.

Content addressing

A hash function maps any bytes to a fixed-size fingerprint; Git uses SHA-1 (160 bits, written as 40 hex chars). Same input → same hash, always, on every machine. Change one byte → a completely different hash. So the hash can be the name:

"hello world" → 95d09f2b10159347eece71399a7e2e907ea3df4f

Three enormous properties fall out of using content as identity:

  • Deduplication is automatic. A thousand files with identical content? One object, stored once — they all hash to the same name.
  • Integrity is built-in. If a disk error flips a bit, the content no longer matches its name — Git detects corruption every time it reads an object.
  • Distribution gets trivial. Two repos can compare what they have by comparing hashes — no timestamps, no "which copy is newer," no central authority. (This is why clones and fetches work at all; the remote-protocol lesson cashes this in.)

The blob format: header first

Git doesn't hash your file's bytes raw. It first wraps them in a header:

blob <size-in-bytes>\0<content>

For the 11-byte string hello world, the hashed bytes are literally:

"blob 11\0hello world"
 ─┬── ─┬ ┬ ─────┬────
 type len NUL   payload

— where \0 is a single zero byte. SHA-1 over that gives the object's name. Why the header? The type (blob for file content; tree and commit are coming) and length make every object self-describing: given any object's bytes, you know what it is and where it ends without any external bookkeeping. It also means a blob and, say, a commit containing identical text can never collide — different headers, different hashes.

Try it against real Git — this is a real command, and your implementation must agree with it byte for byte:

$ echo -n "hello world" | git hash-object --stdin
95d09f2b10159347eece71399a7e2e907ea3df4f

One famous constant to recognize on sight: the empty blob is e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 — you'll see it in every repo that ever committed an empty file.

What a blob is not

A blob is content and only content: no filename, no permissions, no timestamps, no author. README.md and LICENSE with identical text are one blob. Names and modes live one level up — in tree objects, two lessons from now — and separating the two is what makes renames cheap and dedup total. Git is a key-value store where the keys are cryptographic truth; everything with a human name is metadata pointing into it.

Your exercise: Compute Blob SHA-1

Given content, produce its Git blob hash: build blob <len>\0<content>, SHA-1 it, print 40 lowercase hex chars. The precision points where implementations go wrong: <len> is the byte length of the content only (not the header), the separator is one NUL byte (not the two characters \ and 0), and there's no trailing newline in the hashed bytes unless the content actually ends with one. Verify against git hash-object on a couple of strings — when your function agrees with Git on all of them, you have implemented Git's identity system. Everything from here on refers to objects by the names your function can now compute.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…