Git internals — what a commit hash actually identifies
Every earlier lesson in this domain treated a commit hash as an opaque ID. This is what it actually is — a content hash of a real, inspectable object, which is why so many of Git's behaviors (immutable history, cheap branches, why rebasing changes hashes) fall out of one underlying mechanism.
4 min read
Everything in Git is one of four object types
blob - the raw content of one file, nothing else (no filename, no permissions)
tree - a directory listing: names, permissions, and which blob/tree each one points to
commit - a snapshot: which tree it points to, its parent commit(s), author, message
tag - (annotated tags) a pointer to a commit, plus a message
Every one of these earlier lessons' concepts — a file's content, a commit's snapshot, a commit's history — is stored as one of exactly four object types, all living in the same object database inside .git. A blob stores a file's raw content. A tree stores a directory's structure — a list of names, each pointing at either a blob (a file) or another tree (a subdirectory). A commit points at one tree (the root of the project at that snapshot) plus its parent commit(s), author, and message. This is the concrete, literal version of the "commits are snapshots" claim from the very first lesson in this domain.
A commit hash is a hash of the commit's actual content
commit content includes:
- the tree hash (which snapshot)
- the parent commit's hash (or hashes, for a merge commit)
- author, committer, timestamp
- the commit message
SHA-1 (or SHA-256, in newer Git) hash of all of that -> the commit's own hash
A commit's hash isn't an arbitrary ID assigned by Git — it's a cryptographic hash computed from the commit's actual content, including its parent's hash. This one fact explains several behaviors from earlier lessons directly: changing a commit's message, its content, or its parent produces a completely different hash, because any of those changes the input to the hash function — which is exactly why rebase (from its own lesson) replaying a commit onto a new parent necessarily produces a new hash, even when the actual code changes are identical.
Why this makes history genuinely tamper-evident
Because each commit's hash incorporates its parent's hash, the commits form a chain where altering anything anywhere breaks every hash after that point — this is what makes Git history tamper-evident: two people comparing the same commit hash can be confident they're looking at bit-for-bit identical history, all the way back, without needing to manually diff every file. Changing history isn't literally forbidden, but doing so is exactly what rebase, reset, and commit-amending all do explicitly — and it visibly changes hashes, rather than silently rewriting content while keeping the same identifier.
Why a branch being "just a pointer" is a real, checkable fact
$ cat .git/refs/heads/main
a3f8e21f9c2d1049c8e7...
The branching lesson's claim that a branch is "a lightweight, movable pointer" isn't a simplification — it's literally true: a branch is a small text file inside .git/refs/heads/, containing nothing but one commit hash. Moving a branch (committing, merging, resetting) means overwriting that one line with a different hash; nothing about a branch involves copying any actual file content, which is the concrete reason branch creation and switching are as fast as they are.
Why git gc and the reflog's expiry make sense, given this model
An object with no reference pointing to it (no branch, no tag, no reflog
entry) is genuinely unreachable — nothing in the repository can find it
by walking from any starting point
The reflog lesson described commits eventually being cleaned up by git gc after no longer being referenced — this is now the concrete mechanism: gc walks every reachable object starting from every branch, tag, and reflog entry, and anything not reachable that way is safe to delete, since nothing in the repository could ever find it again anyway. This is also exactly why the reflog is what makes recently-orphaned commits recoverable: as long as some reflog entry still points at a commit, gc considers it reachable and leaves it alone.
The payoff: every earlier lesson's behavior follows from this model
- "Commits are snapshots, not diffs": literally, each commit points at a full tree.
- "Branches are cheap": literally, a branch is one line in one small file.
- "Rebase changes hashes": literally, hash depends on parent, parent changes.
- "History is tamper-evident": literally, hashes chain, so tampering is visible.
- "The reflog recovers 'lost' commits": literally, unreferenced objects aren't deleted immediately.
None of the behavior covered across this entire domain is arbitrary or memorized trivia — every one of these observations is a direct, mechanical consequence of "everything is a hashed object, and refs are just pointers to hashes." Understanding this underlying model is what turns Git's behavior from a list of commands to memorize into something predictable, reasoned out from one consistent mechanism.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Which Git object type is the literal mechanism behind 'a commit is a snapshot, not a diff'?
2. Is a commit's hash an arbitrary identifier Git assigns, or is it derived from something?
3. Why does a commit including its parent's hash make Git history 'tamper-evident'?
4. What is a Git branch, literally, on disk?