Overview
Git stops being arcane the moment you see the data model: a content-addressed object store (blobs, trees, commits — each named by its hash) plus a thin layer of movable pointers (refs). Every command is a manipulation of those two things. The payoff is practical fearlessness — rebases, resets, and "I think I just lost my work" all become mechanical once you know the objects are immutable and the reflog remembers where the pointers were.
Key points
-
Four object types: blob (file contents, no name), tree (directory: names → blobs/ trees), commit (a tree + parent commit(s) + author/message), tag. Content-addressing means identical content stored once, and history is tamper-evident: change anything and every downstream hash changes.
-
A commit is a snapshot, not a diff: each commit points to a complete tree; diffs are computed on demand (packfiles delta-compress under the hood, invisibly). This is why checkout is fast and why "which files changed" is a comparison, not a stored fact.
-
Branches are 41-byte pointers: a ref is a file containing a commit hash; HEAD points to a ref (or directly to a commit — "detached"). Creating a branch copies nothing; merging/rebasing/resetting just build objects and move pointers.
The whole model in one picture — a branch is nothing but the
REFbox, a file holding one commit hash:erDiagram COMMIT ||--|| TREE : "snapshots (complete, not a diff)" COMMIT }o--o{ COMMIT : "parent commit(s)" TREE ||--o{ BLOB : "names file contents" TREE ||--o{ TREE : "nests subdirectories" REF }o--|| COMMIT : "a branch is just a file with this hash" HEAD |o--o| REF : "points to" HEAD |o--o| COMMIT : "or directly (detached)" -
The index (staging area) is a real thing: a snapshot-in-progress sitting between working tree and repository —
git statusis two diffs (worktree↔index, index↔HEAD); partial staging (add -p) is editing that middle snapshot.flowchart LR WT["Working tree"] -->|"add -p edits the middle snapshot"| IX["Index (staging area)"] IX --> RP["Repository (HEAD)"] WT -.-|"git status diff 1: worktree vs index"| IX IX -.-|"git status diff 2: index vs HEAD"| RP -
The reflog is the safety net: every movement of HEAD and each ref is journaled locally (~90 days) — after a botched rebase or hard reset,
git reflog+git reset --hard <entry>recovers the "lost" state, because commits aren't deleted, only unreferenced. -
Rewriting history = writing new history: rebase/amend create new commits and move pointers; the old ones linger until GC. Hence the one social rule — never rewrite what's pushed and shared — and
--force-with-leaseas the seatbelt when you must. -
Merge mechanics: three-way merge from the common ancestor; a conflict is merely both sides touching the same lines — the markers are the three versions laid bare;
rerereremembers your resolutions. -
To explore:
cat-file/ls-treespelunking (recreate a commit by hand once — it cures all fear), packfiles & GC, worktrees, sparse checkout & partial clone at scale, interactive-rebase fluency as history authorship.
Practice
- Learn Git Branching (source) — visual, interactive practice moving pointers with branch/merge/rebase/reset; makes "refs are just pointers" obvious.
- git katas (source) — targeted drills for reset modes, reflog recovery, and interactive rebase — the fearlessness reps.
- Git objects by hand (source) — use
hash-object,cat-file,write-tree,commit-treeto build a commit from raw objects; the "cure all fear" exercise. - Write Yourself a Git (exercise) — implement blobs, trees, commits, and refs from scratch until the object model is muscle memory.
Related
- Check which process is listening on a given port — fellow CLI craft.
- CI/CD & delivery engineering — trunk-based workflows assume this fluency.
- Architecture documentation — ADRs lean on history as the audit trail.