Visual walkthrough — Git internals — objects (blob, tree, commit, tag), DAG structure
Step 0 — The one word we must define first: hash
Before any Git object exists, we need one tool. A hash function eats any sequence of bytes and spits out a fixed-length fingerprint (Git uses SHA-1 → 40 hex characters).
Why this tool and not, say, a counter (1, 2, 3...)? A counter would give the same file different IDs on two computers — so they could never agree it's "the same file." A hash gives an ID derived from the content itself, so two machines that never talked reach the identical ID for identical bytes. That single property is what makes Git distributed. That's why hashing, not numbering.

Step 1 — Give the file's bytes an identity: the blob
WHAT. Take one file, hello.txt, whose entire contents are the 3 bytes h, i, newline — written hi\n. We wrap those bytes and hash them.
WHY. We want the content to have a name that does not depend on where the file lives or what it's called. So the blob deliberately throws away the filename — it is pure bytes.
PICTURE (annotated term-by-term):

Step 2 — Give the name a home: the tree
WHAT. The blob has no filename. So we make a tree — a directory listing — with one line saying "the file called hello.txt is blob B."
WHY. By separating name (tree) from bytes (blob), renaming a file only changes the tree; the blob is reused untouched. This is where Git's rename detection comes from.
PICTURE (each field explained where it sits):

Step 3 — Snapshot the whole project: the commit
WHAT. Wrap the root tree with authorship + a message → a commit C. Since this is the first commit, it has no parent.
WHY. From the commit you can reach every byte of the project: commit → tree → blobs. It's a photo of the entire room, not a diff.
PICTURE (line by line):

Step 4 — Deduplication: add an identical file
WHAT. Create notes.txt whose contents are also hi\n, then commit again.
WHY (the punchline of content-addressing): identical bytes → identical store string → identical hash B. Git stores no new blob. Only the tree changes, because the listing now has two names.
PICTURE:

Step 5 — The history edge: parent makes the DAG
WHAT. The second commit C2 records tree = T2 and parent = C.
WHY. That parent line is the arrow C2 → C. Follow parents backwards and you walk through time. This is exactly a directed acyclic graph.
Why acyclic, guaranteed? To create C2 you must already know C's hash — C existed first. You can never make a commit that points to a not-yet-created descendant, because you'd have to hash content mentioning a hash that doesn't exist yet. So arrows only ever point backwards in time → no cycle is possible.

Step 6 — Edge cases: 0, 1, and 2+ parents
WHAT. Every commit fits one of exactly three shapes.
| Commit kind | # parents | What the DAG does |
|---|---|---|
| Root | 0 | history starts |
| Normal | 1 | history chains |
| Merge | 2 (or more) | history fans in |
WHY it matters. A branch is just a movable pointer to one commit; a git merge of two branch tips makes a commit M with parent = [C2, D] — two arrows leave M. That's the only place history joins back together.

Recall Degenerate case: the empty file
An empty file is 0 bytes → store → hash e69de29bb2d1d6434b8b29ae775ad8c2e48c5391. Even nothing has a stable identity. This exact hash appears in nearly every repo.
The one-picture summary
Blobs (bytes) ← named by trees (listings) ← snapshotted by commits (photos) ← chained by parents (time). Same bytes always share one blob; commits point backward, never forward, so the whole thing is a DAG. Because every ID is a hash of header + content + child-links, editing any old object would change every hash above it — that's tamper-evidence, and it's why this structure is a Merkle tree.

Recall Feynman retelling — the whole walkthrough in plain words
A magic box prints a sticker from a toy's exact shape (Step 0: same shape → same sticker, that's a hash). Put in the file's bytes and it becomes a blob with sticker B (Step 1) — no name, just stuff. A tree is a labelled tray: "sticker B is called hello.txt" (Step 2). A commit is a photo of the whole room plus the tray label (Step 3). Copy the file's contents into a second file and the box prints the same sticker B — no new toy stored, just a new label in the tray (Step 4). Take a second photo and staple a note: "the photo before me is that one" — that staple is the parent, and it's the edge in the graph (Step 5). Every photo either starts a roll (0 staples), continues it (1), or joins two rolls (2 — a merge) (Step 6). Staples always point at older photos, so you can walk backwards forever but never in a circle. And since each photo's sticker is made from everything it points at, you can't quietly edit an old photo — every later sticker would change.
Recall Quick self-check
Why does copying hi\n into a new file create no new blob? ::: Same bytes → same store string "blob 3\0hi\n" → same SHA-1 → same key. Deduplicated.
Where does the filename hello.txt live? ::: In the tree entry, never in the blob.
Why is Git history acyclic? ::: A parent's hash is fixed before the child exists, so arrows point only backward in time — no cycle can form.
How many parents can a commit have? ::: 0 (root), 1 (normal), 2+ (merge).