A minimal imitation of Git, built from scratch to understand how Git actually works.
After building and deploying two full-stack web apps, I realized they didn't actually help me understand computer science. While it definitely made me a better programmer, I wanted to go deeper.
I chose Git because it's something I work with almost every day without really understanding what's going on behind the scenes. Turns out, Git is really just a content-addressable key-value store. Three object types (blobs, trees, commits) glued together by SHA-1 hashes. This project builds that same process bit by bit.
Phase 1 - the object store
mygit init— creates a.mygit/directory with an object store and amainbranch ref, just likegit init.mygit hash-object [-w] <file>— hashes a file's contents the same way Git does ("blob <size>\0<content>", SHA-1'd), and optionally writes it to the object store, compressed with zlib.mygit cat-file -t|-s|-p <sha>— reads an object back out of the store and prints its type, size, or raw content.
The blob hashing is verified against real Git's output (see
tests/test_phase1.py) — hashing "hello world\n" produces the exact
same SHA-1 that git hash-object would.
Phase 2 — trees (directory snapshots)
mygit write-tree [path]— recursively snapshots a directory: every file becomes a blob, every subdirectory becomes its own nested tree object, and the whole thing is tied together into one tree object forpath(default: current directory).mygit ls-tree <sha>— lists a tree object's contents (mode, type, hash, name), the same waygit ls-treedoes.
Note: real Git stores each entry's hash as raw 20 binary
bytes to save space. This implementation stores it as plain hex text
instead, so tree objects are directly human-readable via cat-file -p.
Phase 3 — commits and the HEAD/branch chain
mygit commit-tree <tree-sha> -m <message> [-p <parent-sha>]— creates a commit object pointing at a tree snapshot and (optionally) a parent commit. Mirrors real Git's plumbing command of the same name — it does not move any branch pointer on its own.mygit update-ref <commit-sha>— moves the current branch (whateverHEADpoints to) forward to a given commit.mygit rev-parse HEAD— resolvesHEADdown to the actual commit hash it currently points at.
Note: real Git tracks a separate "author" and "committer" line. This implementation tracks one identity, but the underlying concept is the same.
Phase 4 — staging area and everyday porcelain commands
mygit add <file>...— hashes each file as a blob, writes it to the object store, and records it in the index (.mygit/index) keyed by its path relative to the project root.mygit commit -m <message>— builds a tree straight from whatever's currently staged in the index, wraps it in a commit object (parented to the currentHEAD, if any), and moves the branch forward to it. This is whatcommit-tree+update-refdo by hand.mygit log— walks the commit chain fromHEADback through eachparent, printing the hash, author, date, and message for each one.mygit checkout <sha>— restores files to disk from a commit or tree object, recursing into subdirectories as needed.
Note: the index is a flat, plain-text file (sha path per line),
rewritten in full on every add, and there's no diffing against what's
already staged (restaging a path always overwrites its entry). checkout
also doesn't clear files that aren't part of the tree being checked out,
so it's closer to an overlay than a true git checkout.
- Phase 5 (stretch):
branch, basic merges,diff
- Open this folder in VS Code (
File > Open Folder...). - Open a terminal (
Ctrl+`/Cmd+`) and create a virtual environment:VS Code should prompt you to select this as the workspace interpreter — accept it (or set it manually via the Python extension's interpreter picker in the bottom-right corner).python -m venv venv source venv/bin/activate # on Windows: venv\Scripts\activate
- Install the package in editable mode, so
mygitbecomes a real command and code changes take effect immediately without reinstalling:pip install -e . - Install pytest to run the test suite:
pip install pytest
mkdir /tmp/demo && cd /tmp/demo
mygit init
echo "hello world" > greeting.txt
mygit hash-object -w greeting.txt
# -> 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
mygit cat-file -t 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # blob
mygit cat-file -s 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # 12
mygit cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad # hello worldPeek inside .mygit/objects/ afterward — you'll see the compressed object
sitting there under a folder named after the first two hash characters,
exactly like real Git.
Snapshotting a whole directory:
mkdir notes && echo "todo stuff" > notes/todo.txt
TREE_SHA=$(mygit write-tree)
mygit ls-tree $TREE_SHABuilding a two-commit history by hand:
TREE1=$(mygit write-tree)
COMMIT1=$(mygit commit-tree $TREE1 -m "first commit")
mygit update-ref $COMMIT1
echo "more work" > another.txt
TREE2=$(mygit write-tree)
COMMIT2=$(mygit commit-tree $TREE2 -m "second commit" -p $(mygit rev-parse HEAD))
mygit update-ref $COMMIT2pytest tests/ -vmygit-project/
├── mygit/
│ ├── __init__.py
│ ├── cli.py # argparse command wiring
│ ├── commit.py # building and reading commit objects
│ ├── objects.py # hashing, compression, object read/write
│ ├── repository.py # init + repo discovery + HEAD/branch refs
│ └── tree.py # building and reading tree objects
├── tests/
│ ├── test_phase1.py
│ ├── test_phase2.py
│ ├── test_phase3.py
| └── test_phase4.py
├── pyproject.toml
└── README.md