The build-your-own-x list, one of the most-starred repositories on GitHub, has a Build your own Git section, and I finally gave it an afternoon. The test I set: write the smallest git that the real git accepts as valid, meaning git log walks its history and git fsck finds nothing wrong. It came out at 88 lines of Python:
init,hash-object,cat-filewrite-tree,commit,log- No index, no branches beyond
main, no network, no packfiles
The whole storage model is one function
Git’s object store is simpler than its reputation. Every object, blob, tree, or commit, is the same envelope: type, size, null byte, body, SHA-1 hashed, zlib compressed, written to .git/objects/aa/bbcc...:
def store(kind, body: bytes) -> str:
raw = kind.encode() + b" " + str(len(body)).encode() + b"\0" + body
sha = hashlib.sha1(raw).hexdigest()
p = obj_path(sha)
os.makedirs(os.path.dirname(p), exist_ok=True)
if not os.path.exists(p):
with open(p, "wb") as f: f.write(zlib.compress(raw))
return sha
That is the entire database. Blobs are file bytes in that envelope. Trees are a binary list of mode name\0sha entries, one per directory item, recursing for subdirectories. A commit is plain text: a tree line, optional parent lines, author and committer with timestamps, blank line, message.
lines = [b"tree " + tree.encode()]
if parent: lines.append(b"parent " + parent.encode())
lines += [b"author " + who + b" " + ts,
b"committer " + who + b" " + ts,
b"", msg.encode(), b""]
sha = store("commit", b"\n".join(lines))
Content addressing does the rest. Two identical files are one blob. An unchanged subdirectory is the same tree hash, so history is cheap by construction.
The real git accepts it
After two commits from minigit, the crosscheck:
$ python3 ../minigit.py log
4c39978 extend hello.txt
12a23c6 first commit from minigit
$ git log --oneline
4c39978 extend hello.txt
12a23c6 first commit from minigit
$ git fsck
$ echo $?
0
Same hashes, because it is the same format. git cat-file -p HEAD prints my commit as if git had written it.
What the omissions teach
The gaps are as instructive as the code. I skipped the index (the staging area), so minigit commits the working directory directly, and the real git status in a minigit repo reports every file as deleted-and-untracked: git compares HEAD against an index that does not exist. The index turns out to be most of what day-to-day git commands actually manipulate; the object store underneath barely changes.
I also skipped packfiles, which is where the real storage engineering lives. Loose objects at one-file-per-object would fall over on any real repository; git’s delta compression is the difference between a teaching toy and a tool.
The repo’s README carries the Feynman line, “What I cannot create, I do not understand”, and it holds up here. I have used git daily for over a decade, and building it taught me why detached HEAD is a file with a hash in it instead of a ref, and why commits are cheap. An agent drafted these 88 lines faster than I could read the object-format docs. The understanding was still the point, and that is the part that did not delegate.