How Git Works Internally
Reviewed & published by Brayan K
Under its commands, Git is a tiny content-addressable database of four object types — blobs, trees, commits, and tags — every one keyed by a hash. Once you see the object model, refs, HEAD, and the commit DAG, everything else (amend, rebase, why commits are immutable) suddenly makes sense. This lesson opens the hood.
Part of the free Git course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- The four objects: blobs, trees, commits, tags
- Content-addressable storage keyed by a hash
- Explore .git/objects with cat-file and hash-object
- Refs and HEAD as pointers to commits
- The commit DAG and parent links
- Why commits are immutable snapshots
1️⃣ The Object Model
Everything in Git is one of four objects, each keyed by a hash. A blob is the bytes of one file; a tree is a directory mapping names to blobs and sub-trees; a commit is a snapshot pointing at a root tree plus parent(s), author, and message; a tag is an annotated pointer to (usually) a commit. A commit points to a tree, which points to blobs.
# Git's whole database is just four kinds of object, each keyed by a hash.
#
# BLOB -> the raw bytes of ONE file (no name, no metadata)
# TREE -> a directory: names + modes pointing at blobs and sub-trees
# COMMIT -> a snapshot: one root tree + parent(s) + author + message
# TAG -> a named, annotated pointer to another object (usually a commit)
#
# A commit points to a tree; a tree points to blobs and more trees:
#
# commit --> tree --> blob (README.md)
# \--> tree --> blob (src/app.js)
#
# Everything is content-addressable: the id IS the hash of the content.2️⃣ Content-Addressable Storage
Git stores objects under a hash of their content (SHA-1, or SHA-256 in newer repos), in .git/objects. git hash-object shows the id Git would give content, and git cat-file -p pretty-prints any object. Identical content always hashes the same — that's how Git deduplicates.
# Prove that Git ids are hashes of content. Hash a string yourself:
echo "hello git" | git hash-object --stdin
# -> 8d0e41234f24b6da002d962a26c2495ea16a425f
# Identical content always hashes to the SAME id (that's deduplication).
# Now look inside the real object store of a repo:
ls .git/objects # subdirs named by the first 2 hash chars
# Pretty-print ANY object by its id with cat-file:
git cat-file -p HEAD # shows the commit object3️⃣ Refs, HEAD & the Commit DAG
Branches and HEAD are just refs — small files holding a commit id. A branch ref under .git/refs/heads points to a commit; HEAD usually points to the current branch. Because each commit records its parent, commits form a Directed Acyclic Graph (DAG): the shape of your history.
# Branches and HEAD are just POINTERS (refs) — plain files holding a hash.
# A branch ref is literally a file containing one commit id:
cat .git/refs/heads/main
# 9b1d4e0a7c3f2e1d8b6a5c4f3e2d1c0b9a8f7e6d
# HEAD usually points at the CURRENT branch, not a commit directly:
cat .git/HEAD
# ref: refs/heads/main
# So: HEAD -> branch ref -> commit -> tree -> blobs. Each commit also
# records its PARENT, forming a Directed Acyclic Graph (DAG) of history.4️⃣ Why Commits Are Immutable
A commit's id is a hash of its content — tree, parent(s), author, message. Change one byte of a file and the blob hash changes, so the tree hash changes, so the commit hash changes. The old commit still exists, untouched, with its original id. Nothing is edited in place — which is precisely why amend and rebase create new commits.
# Commits are IMMUTABLE because the id is derived from the content.
# Change one byte of a file and EVERYTHING downstream gets a new id:
# 1) Inspect the tree a commit points to
git cat-file -p HEAD^{tree}
# 2) Edit a file, commit again — the blob hash changes, so the tree hash
# changes, so the commit hash changes. The old commit still exists,
# untouched, with its original id. Nothing was edited in place:
git log --oneline -2
# That's why "amend" and "rebase" create NEW commits rather than editing.Your turn. Walk from HEAD down toward a file's bytes and hash content yourself. Fill in the two blanks.
# 🎯 YOUR TURN — replace each ___ then run it in your terminal.
# Goal: walk the object graph by hand, from HEAD down to a file's bytes.
# 1) Pretty-print the current commit to see its tree + parent + message
git ___ -p HEAD # 👉 the command that prints any object
# 2) Hash a piece of content yourself to see Git's content-addressing
echo "demo" | git ___ --stdin # 👉 the command that computes an object id
# 3) Show the branch ref is just a file holding a commit id
cat .git/refs/heads/main
# ✅ Expected: step 1 shows a commit with a tree line and a parent line;
# step 2 prints a 40-char hash; step 3 prints that same kind of hash.Common Misconceptions (and the truth)
- "A commit stores diffs": no — a commit references a tree that is a full snapshot. Diffs are computed between commits on demand.
- "A blob knows its filename": it doesn't. The name lives in the tree entry that points to the blob.
- "Amend edits a commit in place": it can't — a new id means a new object. The old commit still exists until garbage-collected.
- "A branch is a copy of files": a branch is just a ref — one file holding a commit id, nothing more.
Pro Tips
- 💡 cat-file -t tells the type: use git cat-file -t <hash> to see if an id is a blob, tree, or commit.
- 💡 Trees are snapshots: remembering that explains why history is cheap and branching is instant.
- 💡 Hashes mean integrity: any corruption changes the hash, so Git can detect it.
- 💡 Reflog saves you: even "lost" commits exist by id until garbage collection runs.
📋 Quick Reference
| Command | Purpose |
|---|---|
| git cat-file -p <hash> | Pretty-print any object's content |
| git cat-file -t <hash> | Show an object's type |
| git hash-object <file> | Compute the id Git would store content under |
| cat .git/HEAD | See which branch HEAD points to |
| git rev-parse HEAD | Resolve a ref to its commit id |
Mini-Challenge: Trace the Graph
No commands given this time — just the plan. Walk from a commit all the way down to raw file bytes.
🎉 Lesson Complete!
- ✅ Git's database is four objects: blobs, trees, commits, tags
- ✅ Storage is content-addressable — the id is a hash (SHA-1 or SHA-256) of the content
- ✅ git cat-file -p and git hash-object let you explore .git/objects
- ✅ Branches and HEAD are refs; commits link to parents, forming a DAG
- ✅ Commits are immutable snapshots — change anything and you get a new id
- ✅ That's the Git course complete — you now understand Git from commands to internals
Practice quiz
Which Git object stores the raw contents of a single file?
- A blob
- A tree
- A commit
- A tag
Answer: A blob. A blob holds file contents only — no name and no metadata; the filename is recorded by the tree that points to it.
Which object represents a directory, mapping names to blobs and other trees?
- A commit
- A blob
- A tree
- A ref
Answer: A tree. A tree lists entries (name + mode + object id) pointing at blobs (files) and sub-trees (subdirectories).
What does a commit object contain?
- Only the file contents
- A top tree, parent commit(s), author/committer, and a message
- Just a branch name
- A list of remotes
Answer: A top tree, parent commit(s), author/committer, and a message. A commit references the root tree (a full snapshot), its parent(s), author and committer info, and the message.
Every Git object is identified by...
- Its filename
- A random UUID
- A timestamp
- A hash of its content (SHA-1, or SHA-256 in newer repos)
Answer: A hash of its content (SHA-1, or SHA-256 in newer repos). Git is content-addressable: an object's id is the hash of its contents, so identical content yields the same id.
In which directory does Git keep its objects?
- .git/objects
- .git/refs
- .git/hooks
- .git/index
Answer: .git/objects. Loose and packed objects live under .git/objects, addressed by their hash.
Which command pretty-prints the content of an object by its hash?
- git show-object
- git cat-file -p <hash>
- git read <hash>
- git print
Answer: git cat-file -p <hash>. git cat-file -p <hash> pretty-prints any object — blob, tree, commit, or tag.
Which command computes the object id Git would give a piece of content?
- git rev-parse
- git id
- git checksum
- git hash-object
Answer: git hash-object. git hash-object reads content and prints the hash Git would store it under (with -w it also writes the object).
What is a branch, at the object level?
- A copy of all the files
- A separate object database
- A ref: a movable pointer to a commit
- A type of tag that can't move
Answer: A ref: a movable pointer to a commit. A branch is just a ref under .git/refs/heads holding the id of the commit it currently points to.
What does HEAD usually point to?
- The root tree
- The current branch (which in turn points to a commit)
- The remote
- The staging area
Answer: The current branch (which in turn points to a commit). HEAD is normally a symbolic ref naming your current branch; following it leads to the checked-out commit.
Why are commits effectively immutable?
- Because a commit's id is a hash of its content, so changing anything produces a different id and a new commit
- Because the OS forbids edits
- They are stored on read-only disks
- Git locks the files
Answer: Because a commit's id is a hash of its content, so changing anything produces a different id and a new commit. Since the hash is derived from the content (including parent and tree), any change yields a new object with a new id rather than mutating the original.
Continue this course
- Previous: Git LFS & Large Files
- Quick reference: Git cheat sheet
Frequently asked questions
What are the four object types in Git?
Git's database is built from four object types, each addressed by a hash. A blob stores the raw contents of a single file (no name, no metadata). A tree represents a directory, mapping names to blobs (files) and other trees (subdirectories). A commit captures a full snapshot — it points to one root tree, references its parent commit(s), and records author/committer info and a message. A tag is an annotated, named pointer to another object, usually a commit. Together these form the entire history.
What does it mean that Git is content-addressable?
It means an object's identity is the hash of its content, not a name you assign. Git runs the object's bytes through SHA-1 (or SHA-256 in newer repositories) and stores it under that hash in .git/objects. A powerful consequence is deduplication: two identical files anywhere in history produce the same blob id and are stored once. It also means you can't change an object without changing its id — content and identity are inseparable.
How do I look inside Git objects myself?
Two commands open the hood. git cat-file -p <hash> pretty-prints any object, so you can read a commit's tree/parent/message, list a tree's entries, or dump a blob's bytes. git hash-object computes the id Git would assign to a piece of content (and with -w writes it into the store). Combined with browsing .git/objects, these let you trace a commit down to a tree and then to the raw file content of a blob.
What are refs, branches, and HEAD really?
A ref is just a pointer: a small file under .git/refs holding a commit id. A branch is a ref under refs/heads that moves forward as you commit. HEAD is normally a symbolic ref naming your current branch, so following HEAD leads to a branch, which leads to a commit, whose tree leads to blobs. Because each commit also records its parent(s), the commits form a Directed Acyclic Graph (DAG) — the shape of your project's history.
Why are commits immutable?
Because a commit's id is a hash of its content — its tree, its parent(s), the author and committer details, and the message. Change any of those, even one byte of one file, and the blob hash changes, which changes the tree hash, which changes the commit hash. You don't get an edited commit; you get a brand-new object with a new id, while the original still exists untouched. This is exactly why operations like amend and rebase create new commits rather than modifying old ones in place.