Mini Git: Version Control from Scratch
Rebuild the core of Git: content-addressed blobs hashed exactly like Git hashes them, commits chained by parent, line diffs, and branches that are nothing more than named pointers.
How it works in the real world
Git looks complicated from the outside and is beautifully simple inside:
- Blobs: every file's content is stored under the SHA-1 hash of that content, so identical files are stored once and any change produces a new id.
- Commits: a commit records a snapshot (which blob each file points to), a message and its parent commit, and is itself identified by a hash.
- History:
git logjust follows parent pointers backwards. - Branches: a branch is a name pointing at one commit. Committing moves the pointer.
Your step 1 hashes match real Git byte for byte: run git hash-object on the same content and compare.
Content-addressed blobs
Git hashes a file as sha1(b"blob " + size + b"\0" + content). Write hash_blob(text) and print the first 10 hex characters for each file, then whether two files with identical content share a blob.
hashlib.sha1(header + data).hexdigest(). The size is the number of bytes, not characters.
import hashlib
files = {
"README.md": "# Mini Git\n",
"hello.py": "print('hello')\n",
"copy.py": "print('hello')\n",
}
def hash_blob(text):
data = text.encode()
header = b"blob " + str(len(data)).encode() + b"\0"
return hashlib.sha1(header + data).hexdigest()
for name, text in files.items():
print(f"{hash_blob(text)[:10]} {name}")
print("same blob:", hash_blob(files["hello.py"]) == hash_blob(files["copy.py"]))
Run your code to check this step…
Commits and history
Write commit(files, message, parent): store each blob in OBJECTS, build a tree of name -> blob id, hash tree + parent + message into a commit id and store the commit. Make three commits, then print a log (newest first) by following parents.
tree = {name: sha("blob:" + text) for ...}; the commit id is sha(json.dumps({"tree": tree, "parent": parent, "message": message}, sort_keys=True)). Walk with while cid: ... cid = OBJECTS[cid]["parent"].
import hashlib
import json
OBJECTS = {}
def sha(data):
return hashlib.sha1(data.encode()).hexdigest()
def commit(files, message, parent=None):
tree = {}
for name, text in sorted(files.items()):
blob_id = sha("blob:" + text)
OBJECTS[blob_id] = text
tree[name] = blob_id
body = {"tree": tree, "parent": parent, "message": message}
cid = sha(json.dumps(body, sort_keys=True))
OBJECTS[cid] = body
return cid
c1 = commit({"app.py": "print(1)\n"}, "first commit")
c2 = commit({"app.py": "print(2)\n"}, "change output", c1)
c3 = commit({"app.py": "print(2)\n", "README.md": "docs\n"}, "add readme", c2)
cid = c3
while cid:
print(f"{cid[:7]} {OBJECTS[cid]['message']}")
cid = OBJECTS[cid]["parent"]
print(f"{len(OBJECTS)} objects stored")
Run your code to check this step…
Line diffs
Show what changed between two versions of a file with difflib.unified_diff, labelled a/app.py and b/app.py (pass lineterm=""), then print a summary in Git's format: 1 file changed, N insertions(+), N deletions(-).
Diff old.splitlines() against new.splitlines(). Count lines that start with + or - but not with +++ or ---.
import difflib
old = "def greet(name):\n print('Hi', name)\n\ngreet('Ada')\n"
new = "def greet(name, punct='!'):\n print('Hi', name + punct)\n\ngreet('Ada')\ngreet('Linus', '?')\n"
diff = list(difflib.unified_diff(old.splitlines(), new.splitlines(),
"a/app.py", "b/app.py", lineterm=""))
for line in diff:
print(line)
added = sum(1 for l in diff if l.startswith("+") and not l.startswith("+++"))
removed = sum(1 for l in diff if l.startswith("-") and not l.startswith("---"))
print(f"1 file changed, {added} insertions(+), {removed} deletion{'s' if removed != 1 else ''}(-)")
Run your code to check this step…
Branches are just pointers
Add BRANCHES and HEAD. commit_on(branch, files, message) commits with the branch tip as parent and moves the branch. Create main with two commits, branch feature from it, commit on both, then print each branch's history as message <- message <- ... and the commit they share.
commit_on: cid = commit(files, message, BRANCHES.get(branch)); BRANCHES[branch] = cid. The merge base is the first commit in feature's history that also appears in main's history.
import hashlib
import json
OBJECTS, BRANCHES = {}, {}
def sha(data):
return hashlib.sha1(data.encode()).hexdigest()
def commit(files, message, parent=None):
body = {"files": files, "parent": parent, "message": message}
cid = sha(json.dumps(body, sort_keys=True))
OBJECTS[cid] = body
return cid
def commit_on(branch, files, message):
cid = commit(files, message, BRANCHES.get(branch))
BRANCHES[branch] = cid
return cid
def history(cid):
out = []
while cid:
out.append(cid)
cid = OBJECTS[cid]["parent"]
return out
commit_on("main", {"app.py": "v1"}, "init")
commit_on("main", {"app.py": "v2"}, "add login")
BRANCHES["feature"] = BRANCHES["main"]
commit_on("feature", {"app.py": "v2", "dark.css": "x"}, "dark mode")
commit_on("main", {"app.py": "v3"}, "fix typo")
for name in ["main", "feature"]:
print(f"{name}: " + " <- ".join(OBJECTS[c]["message"] for c in history(BRANCHES[name])))
main_ids = set(history(BRANCHES["main"]))
base = next(c for c in history(BRANCHES["feature"]) if c in main_ids)
print(f"merge base: {OBJECTS[base]['message']}")
Run your code to check this step…
Project shipped!
You just built a complete, working mini git: version control from scratch: the same architecture running in production software. Pick your next build →
Take it further
Make it a real command-line tool that stores objects on disk, just like the .git folder. Nothing below is graded: it runs on your own machine, not in this tab.
Stretch goals
- Write each object to
.minigit/objects/ab/cdef...compressed withzlib, exactly as Git does, then read one back withgit cat-file -pto prove it matches. - Add a three-way merge: compare both branch tips against the merge base and flag lines changed on both sides as conflicts.
- Build
status: hash the working directory and compare it with the last commit's tree. - Use
argparseto exposeinit,commit -m,loganddiffsubcommands.
Check your hashes against real Git
# In any terminal with git installed:
printf 'print(\x27hello\x27)\n' | git hash-object --stdin
# In Python, the same id:
import hashlib
data = b"print('hello')\n"
print(hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest())
Ask an AI to review your build
I rebuilt the core of Git in Python: content-addressed blobs hashed like git hash-object, commits chained by parent, unified diffs and branches as pointers. Here is my code: [paste it]. Explain what real Git does differently in its tree objects and commit format, and outline how I would implement a three-way merge with conflict markers.