How Git Actually Works Under the Hood
Ever stared at a "detached HEAD" state and considered just deleting your repository and cloning it again? We've all been there. Most of us learn Git by memorizing a handful of commands, treating the tool like a moody magic box that occasionally bites. But once you look past the command-line interface, Git isn't a complex version control system—it's essentially a simple file system mapping keys to values. Let's crack open that hidden directory and see what's actually happening when you type git commit.
The Magic Box is Just a Folder
When you run git init in a fresh project, Git creates a hidden .git directory. If you've never looked inside it, you're missing out on the entire mechanism of your version control. This folder holds everything Git needs to reconstruct your project's history.
If you list the contents of a newly initialized .git directory, you'll see a few specific files and folders. The most important ones are the objects directory, the refs directory, and a file simply named HEAD.
Everything you do in Git—branching, committing, stashing—is just a manipulation of the files sitting in these specific directories.
Click to expand
It's Just a Key-Value Store
At its core, Git is a content-addressable file system. This is a fancy way of saying it's a key-value data store. You hand Git some data, and it hands you back a key that you can use to retrieve that data later.
The "key" in this case is a 40-character SHA-1 hash. When you feed a file into Git, it hashes the contents of that file. The resulting hash becomes the file name, and the file's actual content is compressed and stored inside the .git/objects directory.
Because the hash is generated from the content itself, two files with the exact same text will produce the exact same hash. If you change a single comma, the hash changes completely.
Click to expand
The Three Main Suspects: Blobs, Trees, and Commits
Git only really cares about three primary types of objects. Once you understand how these three pieces connect, the entire system stops feeling like magic.
1. Blobs (The Data)
When you tell Git to track a file, it creates a blob (Binary Large Object). A blob stores the file's raw content, but strangely, it doesn't store the file's name. To Git, the content is what matters. You could have two files named index.js and app.js with identical code, and Git will only store one blob to represent both.
2. Trees (The Directories)
If blobs are the files, trees are the directories. A tree object solves the problem of file names and grouping. It contains a simple list of pointers—some pointing to blobs, others pointing to nested trees.
100644 blob a906cb2a4a904a152... index.js
040000 tree 99f1a6d12cb4b6f19... componentsThis is how Git reconstructs your folder structure. It reads a tree, sees the file names, and fetches the corresponding blobs to fill those files with text.
3. Commits (The Snapshots)
A commit is simply a metadata wrapper around a single top-level tree. When you make a commit, Git creates a small text file containing a pointer to the root tree of your project at that exact moment.
The commit object also includes the author's name, the timestamp, the commit message, and crucially, a pointer to the parent commit. This parent-child relationship is what creates your project's history. Git doesn't store a timeline of diffs or changes; it stores a chain of complete snapshots.
Click to expand
Branches Are Just Cheap Pointers
One of the biggest misconceptions about Git is that creating a branch copies your files into a new container. In older systems like SVN, branching was heavy and slow. In Git, a branch is literally just a text file containing a 40-character hash.
If you look inside .git/refs/heads/, you'll see a file for each of your local branches. Open one of them, and you'll find a single SHA-1 string pointing to the latest commit on that branch.
When you make a new commit, Git creates the new commit object, and then simply updates the text in that branch file to hold the new hash. That's why branching in Git is instantaneous. You aren't moving data; you're just writing 40 characters to a text file.
Click to expand
What is a Detached HEAD?
The HEAD file is Git's way of knowing where you currently are. Usually, it contains a reference to a branch, like ref: refs/heads/main. It's a pointer to a pointer.
When you check out a specific commit hash instead of a branch name, Git updates HEAD to contain that raw hash directly. Congratulations, your HEAD is now "detached" from a branch. It sounds terrifying, but you've just swapped a variable reference for a hardcoded value.
The Staging Area (The Index)
You might wonder why you have to run git add before git commit. Why not just commit directly? This introduces the index, commonly known as the staging area.
The index is a binary file located at .git/index. Its job is to keep track of what your next tree object is going to look like. When you modify a file and run git add, Git immediately hashes the new content, creates a blob in the objects folder, and updates the index with the new hash and filename.
When you finally run git commit, Git just looks at the index, builds a tree object from it, and wraps it in a commit. The staging area is basically your drafting board for the next snapshot.
Click to expand
Putting It All Together
Let's trace a standard workflow. You create a file and type git add file.txt. Git compresses the text, hashes it, stores the blob, and writes the filename and hash to the index.
You type git commit -m "initial commit". Git reads the index and writes a tree object representing your directory. It then creates a commit object pointing to that tree, adding your author info and message. Finally, it takes the hash of that new commit and writes it into your current branch file in .git/refs/heads/.
Quick Recap
Instead of a messy timeline of file diffs, Git is an elegant collection of snapshots connected by parent-child relationships. Blobs hold your raw data, trees hold your folder structure, commits hold your metadata, and branches just point to the latest commits. Once you internalize that your repository is just a graph of text files sitting in a hidden folder, those bizarre merge conflicts and rebase errors start making a lot more sense.