01 — Git Object Model: Blobs, Trees, Commits & DAGs

At a glance

After completing this lesson you can:

Time: ~45 minutes · Prerequisites: 03 — Shell Pipelines & Diagnostics

Reading Companion

Why this matters in production

Git is not a record of line diffs; it is an immutable, content-addressable snapshot store. Developers who don't understand this get terrified by detached HEAD, accidentally force-push over colleagues' work, or believe deleted files are lost forever. Understanding the DAG turns Git into a transparent tool for history auditing, automated bisecting, and reliable CI deployments.

Deep dive

1. The Four Git Objects

Every object in .git/objects/ begins with an uncompressed header: f"{obj_type} {size_in_bytes}\x00", followed by the content, hashed with SHA-1 (or SHA-256) and compressed with zlib.

Commit Object (SHA: a1b2c3)
  ├── tree: 9f8e7d ────────► Tree Object ("src/")
  │                           ├── 100644 blob e5d4c3 "main.py"
  │                           └── 040000 tree 8b7a6c "utils/"
  └── parent: 010203 (Previous Commit)

Hands on

python3 phase0-foundations/02-git-and-workflow/01-git-object-model/examples/01_demo.py
python3 phase0-foundations/02-git-and-workflow/01-git-object-model/examples/02_trap.py

AI Trap & Code Review

# BAD AI-GENERATED CODE — WRONG GIT OBJECT HASHING
import hashlib

def git_hash_object(content: str) -> str:
    # Fails! Git prefixes header: f"blob {len(bytes)}\0" before hashing
    return hashlib.sha1(content.encode()).hexdigest()

Exercises

  1. Rung 1 (hash_git_object): Compute the 40-character hex SHA-1 of a Git object given obj_type: str and data: bytes. Format: sha1(f"{obj_type} {len(data)}\x00".encode() + data).
  2. Rung 2 (parse_commit_object — Fix the AI Bug): Parse raw Git commit text into a structured dictionary with keys tree, parents (list of parent commit SHAs), author, committer, and message. Handle multiple parent commits (merge commits) correctly.
  3. Rung 3 (build_tree_manifest): Given a list of entries (mode_str, name, sha_hex), produce the sorted deterministic tree representation text lines.
  4. Rung 4 (find_reachable_commits): Traverse a commit DAG represented as dict[str, list[str]] (commit -> parents) starting from a list of tip references (e.g. ["main", "feat"]) and return the set of all reachable commit IDs.
  5. Rung 5 (detect_dangling_commits): Given all existing commit IDs in the object database and a set of active ref pointers, return the set of dangling (unreachable) commits eligible for garbage collection (git prune).

Wrapping up

Can you explain?

  1. Why does renaming a file in Git not create a new blob object if the content is unchanged?
  2. What actually changes on disk when you run git branch feature-x?
  3. What is a "detached HEAD" state at the filesystem level in .git/HEAD?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 01 Git Object Model

  1. Why file rename doesn't duplicate blobs: Git indexes files by the SHA-1 of their contents, not their path. If you rename app.py to server.py, the blob SHA remains identical; only the containing tree object updates its entry name pointer.
  2. Creating a branch: A branch in Git is literally a 41-byte text file under .git/refs/heads/<branch_name> containing the 40-character commit hash of its tip, plus a newline.
  3. Detached HEAD: .git/HEAD normally contains ref: refs/heads/main. In a detached HEAD state, .git/HEAD contains a raw 40-char commit SHA instead of a branch reference pointer.

Self-check

Run python3 -m unittest phase0-foundations/02-git-and-workflow/01-git-object-model/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.