You run git commit every day, maybe dozens of times. But have you ever wondered what actually happens when you do? Where does your code go? What even is a commit? Most of us treat Git like a magic black box β we memorize commands, and when things break, we panic.
Why It Matters
Understanding Git internals isn't just trivia for computer science nerds. When you know how Git actually stores your data, you can recover from catastrophic mistakes, finally understand why merge conflicts happen the way they do, and debug issues that would otherwise leave you copy-pasting from Stack Overflow. Git stops being magic and starts being a simple, elegant system you actually control.
π§ The .git Directory
Every Git repository has a hidden .git folder at its root. This is the database. Everything Git knows about your project lives here. Let's peek inside:
# Create a fresh repo and explore
git init my-project && cd my-project
ls -la .git/
The key contents:
-
objects/β The object database. Every file, directory snapshot, and commit lives here. -
refs/β Pointers to commits (branches and tags). -
HEADβ A file that tells Git which branch you're currently on. -
configβ Repository-level configuration. -
indexβ The staging area (what's queued for the next commit).
That's it. Delete this folder, and your entire history vanishes. Keep it, and you can reconstruct everything β even without working files.
β‘ Git Objects: The Building Blocks
Git stores everything as objects in that objects/ directory. There are exactly four types:
| Type | Stores | Example |
|---|---|---|
| Blob | Raw file content (no filename!) | The text of index.js
|
| Tree | Directory structure (maps filenames β blobs) | "index.js" points to blob a3f2...
|
| Commit | A tree + metadata (author, message, parent) | Your snapshot in time |
| Tag | Annotated pointer to a commit |
v1.0.0 release marker |
Here's the key insight: Git is a content-addressable filesystem. That sounds fancy, but it's simple β Git takes the content of whatever you're storing, runs it through a hash function called SHA-1, and uses the resulting 40-character hex string as the filename. Same content always produces the same hash, which means identical files are stored only once.
Think of it like a Polaroid camera. Every commit takes a full snapshot of your project, not a diff. But if a file hasn't changed, Git doesn't take a new photo β it just points to the existing identical blob. Efficient and simple.
Let's prove it:
# Hash some content and see what Git produces
echo "hello world" | git hash-object --stdin
# Output: 95d09f2b10159347eece71399a7e2e907ea3df4f
# Store it in the database
echo "hello world" | git hash-object -w --stdin
# Read it back using the hash
git cat-file -p 95d09f2b10159347eece71399a7e2e907ea3df4f
# Output: hello world
Every blob, tree, commit, and tag is just an object retrievable by its SHA-1 hash.
π οΈ How a Commit Actually Works
Let's trace what happens step by step when you stage and commit a file:
Step 1: git add β Creating Blobs
- Git reads the file content
- Computes the SHA-1 hash
- Stores the content as a blob object in
objects/ - Updates the index (staging area) to reference this blob
# Create a file and stage it
echo "console.log('hello');" > app.js
git add app.js
# The blob now exists in .git/objects
find .git/objects -type f
# Output: .git/objects/4e/1243... (the blob)
# The index now tracks it
git ls-files --stage
# Output: 100644 4e1243... 0 app.js
Step 2: git commit β Assembling the Snapshot
- Git takes the staging area and writes it as a tree object (mapping filenames to blob hashes)
- Git creates a commit object pointing to that tree, plus metadata (author, timestamp, message, parent commit)
- Git updates the current branch ref to point to this new commit
git commit -m "Initial commit"
# Inspect the commit object
git cat-file -p HEAD
# Output:
# tree 8a3f...
# author Arnav <arnav@example.com> 1723...
# committer Arnav <arnav@example.com> 1723...
#
# Initial commit
# Inspect the tree it points to
git cat-file -p 8a3f...
# Output: 100644 blob 4e1243... app.js
The chain is: branch ref β commit β tree β blobs. That's the entire data model.
π Refs, Branches & HEAD
Here's the thing that blew my mind when I first learned it: a branch is just a file. Literally a 41-byte text file containing a commit hash.
# Prove it β read the main branch directly
cat .git/refs/heads/main
# Output: d3b07384d113edec49eaa6238ad5ff00 (a commit SHA)
That's all a branch is. When you "create a branch," Git creates a new file in refs/heads/ with the current commit hash. When you make a new commit, Git overwrites that file with the new commit's hash. No copying, no branching tree structures β just a pointer that moves forward.
HEAD is one level of indirection above that:
cat .git/HEAD
# Output: ref: refs/heads/main
HEAD points to the current branch, which in turn points to a commit. If you git checkout a specific commit hash instead of a branch name, HEAD points directly to the commit β that's what detached HEAD means. You're not on any branch.
- Creating a branch = writing a 41-byte file
- Switching branches = changing what HEAD points to
- Deleting a branch = deleting that file (the commits still exist)
π― Pack Files & Garbage Collection
You might wonder: if Git stores full snapshots, doesn't the .git folder grow enormous? In practice, no β because of pack files.
When your repository accumulates many loose objects, Git compresses them using delta compression into .pack files inside .git/objects/pack/. Instead of storing complete copies, pack files store one full version of an object and then deltas (differences) for similar objects.
# Trigger packing manually
git gc
# See the pack files
ls .git/objects/pack/
# Output: pack-abc123.idx pack-abc123.pack
This happens automatically during git push, git fetch, or when loose object count gets high. It's why a repository with thousands of commits doesn't consume thousands of times more disk space.
π Key Takeaways
- Git's entire database lives in the
.gitdirectory βobjects/for data,refs/for pointers,HEADfor your current position. - Everything is stored as one of four object types: blobs (content), trees (directories), commits (snapshots), and tags (named markers).
- Git is content-addressable: the SHA-1 hash of content becomes its identifier. Same content = same hash = stored once.
- A branch is just a file containing a commit SHA. Creating, switching, and deleting branches are trivially cheap operations.
- Pack files and delta compression keep repositories efficient despite storing full snapshots.
Keep Exploring
If you enjoyed peeking behind the curtain of tools you use daily, you might also like:
Top comments (0)