Skip to content
m manifester.io
All posts

The four git object types: blobs, trees, commits, and tags

Everything in a git repository boils down to four object types stored in a content-addressable key-value store. Here's how blobs, trees, commits, and tags fit together.

At its core, git is a content-addressable filesystem with a version-control UI bolted on top. Strip away the porcelain commands and you’re left with a simple key-value store: you hand git some bytes, it hands you back a 40-character SHA-1 (or SHA-256) hash, and that hash is the only thing you ever need to get those bytes back.

Every hash points at an object, and there are exactly four kinds.

1. Blobs

A blob stores the raw contents of a single file — and nothing else. No filename, no permissions, no timestamps. Just bytes.

$ echo "hello mgit" | git hash-object --stdin
9f8c8a... # the blob's id

Because the id is derived from the content, two identical files anywhere in your history share one blob. Rename a file a hundred times and the blob never changes — only the things that point at it do.

2. Trees

A tree is git’s notion of a directory. It’s an ordered list of entries, where each entry records:

  • a mode (file, executable, symlink, or subdirectory),
  • a name (the filename or directory name), and
  • the id of the blob or tree it points to.
100644 blob 9f8c8a...    README.md
040000 tree a1b2c3...    src

So a tree references blobs (files) and other trees (subdirectories), forming a Merkle tree of your entire working directory. Change one byte in one file and every tree on the path up to the root gets a new id — that cascading is exactly what makes git tamper-evident.

3. Commits

A commit is a snapshot plus metadata. It points at one root tree (the state of the whole project at that moment) and carries:

  • the author and committer (name, email, timestamp),
  • the commit message, and
  • zero or more parent commit ids.
tree a1b2c3...
parent 7d8e9f...
author Ada <ada@example.com> 1718870400 +0000
committer Ada <ada@example.com> 1718870400 +0000

Add packfile reader

Zero parents means a root commit. One parent is a normal commit. Two or more parents is a merge. Follow the parent links and you’ve walked the entire history — no diffs required, because each commit already has a complete snapshot via its tree.

4. Tags (annotated)

An annotated tag is a named, signed pointer to another object — almost always a commit. Unlike a lightweight tag (which is just a ref), an annotated tag is a real object with its own id, a tagger, a message, and an optional GPG signature.

object 7d8e9f...
type commit
tag v1.0.0
tagger Ada <ada@example.com> 1718870400 +0000

mgit 1.0

How they’re stored

Each object is serialized as a short header — "<type> <size>\0" — followed by the raw content, then zlib-compressed and written to .git/objects/ under a path derived from its hash. These are loose objects: one file per object.

.git/objects/9f/8c8a...   # loose blob, zlib-compressed

That’s wonderfully simple, but it doesn’t scale: a repository with millions of objects becomes millions of tiny files. The fix is to roll many objects into a single compressed packfile — which is exactly what we’ll dig into next.

For a system like mgit that puts repositories on object storage, this model is a gift: every object is immutable and addressed by its own hash, so it maps cleanly onto a bucket where keys are content hashes and values never change.