# How Git Actually Works: A Look Inside the .git Folder

Most developers use Git every day and understand it as a set of commands: `commit`, `push`, `pull`, `merge`. That's enough to get by — until it isn't, until you hit a detached HEAD, a confusing rebase conflict, or wonder why creating a new branch is instant even in a repo with 100,000 commits.

The truth is that Git isn't really a version control tool with some clever commands bolted on. Underneath, it's a surprisingly simple key-value data store, and once you see that model, most of Git's "weird" behavior stops being weird.

## Git Is a Content-Addressable Filesystem

Strip away the commands and Git is, at its core, a system for storing content and retrieving it by the hash of that content — not by filename, not by path. Every piece of data Git tracks gets hashed (SHA-1, historically, with SHA-256 support arriving in newer versions), and that hash becomes its permanent address inside the `.git/objects` folder.

This one idea — content-addressable storage — explains an enormous amount of what makes Git fast, safe, and space-efficient.

## The Three Object Types That Do Almost Everything

Git builds its entire history out of just a few object types.

**Blobs** A blob stores the raw content of a file — nothing else, no filename, no permissions, no path. Two files with identical content, even in totally different folders, produce the exact same blob and get stored only once.

**Trees** A tree is Git's version of a directory listing. It maps names to other objects — blobs for files, or other trees for subdirectories — along with file modes (permissions). A tree is essentially a snapshot of a folder structure at a point in time.

**Commits** A commit is a small object that points to exactly one tree (the root of the project at that moment), zero or more parent commits (none for the very first commit in a repo, more than one for a merge), and metadata: author, timestamp, and message. Notably, a commit doesn't store a diff — it stores a full snapshot reference. Git just happens to be very good at storing those snapshots efficiently through deduplication and compression.

## Seeing It For Yourself

You don't need to take this on faith — you can inspect it directly. In any Git repo:

```bash
echo "hello world" > file.txt
git add file.txt
git commit -m "Add file"

# Find the hash Git assigned to your file's content
git rev-parse HEAD:file.txt
```

That returns `3b18e512dba79e4c8300dd08aeb37f8e728b8dad` — a SHA-1 hash. That's not a filename; it's the address of the blob containing "hello world\\n". You can go look at it directly:

```bash
git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# hello world
```

You can inspect any object the same way — commits, trees, and blobs are all readable with `git cat-file -p <hash>`. Peek at your latest commit object and you'll see it's just a tree pointer, a parent pointer, and metadata — nothing more mysterious than that.

## Where `git add` Actually Puts Things

There's a fourth piece we skipped: the staging area, also called the index. It's a single file at `.git/index` that lists exactly what will go into your next commit.

When you run `git add file.txt`, Git doesn't touch the object database in any dramatic way — it just writes a blob for `file.txt`'s current content, and records that blob's hash in the index alongside the filename. `git commit` then builds a tree straight from whatever the index currently contains, not from your working directory.

This explains a few things that confuse newcomers. Stage a file, then edit it again without re-staging, and `git status --short` will show `MM` next to it — modified in the index *and* modified in the working directory, because the two now genuinely disagree. It's also why `git diff` and `git diff --staged` show different things: plain `git diff` compares your working directory against the index, while `--staged` compares the index against `HEAD`.

Which raises the obvious follow-up: if commits don't store diffs, how does `git diff` produce one? By computing it on the fly. Git walks the two trees (or blobs) being compared and generates the diff in that moment. A diff in Git is a view, computed on demand — never a fact stored on disk.

## Branches Are Just Pointers

Here's the part that changes how people think about Git: a branch is not a copy of your code. A branch is a plain text file containing a single commit hash.

```bash
cat .git/refs/heads/main
# 9fceb02d0ae598e95dc970b74767f19372d61af
```

That's it. That's the entire branch. When you run `git checkout -b feature`, Git creates a new, tiny text file pointing at the same commit you're currently on. No files get copied, no history gets duplicated. That's why creating a branch in Git is instantaneous regardless of repo size — you're writing a 40-character string to a file, not cloning a project.

`HEAD` works the same way, one level up: it's a pointer to whichever branch you currently have checked out (or, in a "detached HEAD" state, directly to a commit). That's the entire mystery behind detached HEAD — you've pointed `HEAD` straight at a commit instead of at a branch, so new commits won't move any branch pointer forward. They just float, unreferenced, until something points at them again, or Git eventually garbage-collects them.

## The Whole Picture

![Mermaid Diagram](https://mermaid.ink/img/pako:eNpdkE1PwkAQhv_KOCcNW0gbTz2Y2BqiBy9SvVAO03ZKN-5Hs7sKCPx3U4qYcJw37_PMZPZY24YxxVbZTd2RC1A8lQYAIHNk6m5ZouPWzzqmxs80SVPiCqLoAfJkWWJutZYBboNjhgn05NgEmIBm72nNdyWuRlueDNBhLBwgj69gAcae8WsIimRZOOZzWIxhNhgyZasUWql4Grbhwv1VkkvFMTWap7r5d8ejOx7dKFCz0yQbTPcYOtbDVxpu6UsFFGPyQU5SpdgPndaaMCct1Q5TjKjvFUd-5wNrAZmS5vOV6sVpnlsTBJS44LVleH8pUcCbrWywAp5ZfXOQNQl4dJKUAE_GR56dbFGclizkz3BLfN9v8XgUWK1zq6zDFG82nQyMx19Q-Y94?type=png align="center")

A branch points to a commit. A commit points to a tree and its parent commit. A tree points to blobs and other trees. Everything below the branch pointer is immutable and content-addressed; only the pointer itself ever moves.

## Merging, Rebasing, and Why History Changes

Once you see commits as immutable, content-addressed snapshots, a few common confusions resolve themselves:

*   **A merge commit** is just a normal commit object with two parents instead of one.
    
*   **Rebasing rewrites history** because a commit's hash is derived from its content, including its parent. Change the parent, and you get a brand-new commit with a brand-new hash, even if the code is identical. This is why force-pushing after a rebase is necessary, and why rebasing shared branches causes pain: you're not editing old commits, you're creating new ones and abandoning the old ones.
    
*   **Fast-forward merges** happen when your branch pointer can simply slide forward along an existing line of commits — no new commit needed at all.
    

## Why This Actually Matters

Understanding Git's object model turns a lot of "just memorize the commands" situations into things you can reason about:

*   **Recovering "lost" commits.** Because objects stick around until garbage collected, `git reflog` and `git fsck --lost-found` can often recover commits that no branch points to anymore.
    
*   **Understanding** `.git` **folder size.** Since identical content is stored once, Git repos are often far smaller than you'd expect, even with deep history.
    
*   **Debugging confusing merge/rebase states.** Once you know a commit is just a snapshot plus parent pointers, conflicts stop feeling arbitrary — you can trace exactly which snapshots are being compared.
    

## The Takeaway

Git's command-line interface makes it look like a tool for tracking changes and diffs. Underneath, it's closer to a simple, elegant database: content-addressed objects (blobs, trees, commits) and lightweight pointers (branches, HEAD, tags) that reference them. Every confusing Git moment — a detached HEAD, a surprising rebase, an "impossible" merge conflict — gets easier to debug once you stop thinking in commands and start thinking in objects and pointers.

Next time a coworker asks what a branch actually *is*, you'll know: it's a 40-character string in a text file, pointing at a snapshot of your code.

* * *

*Published via* [*ZyVOP*](https://zyvop.com/how-git-actually-works-a-look-inside-the-git-folder-pnm2p?utm_source=hashnode&utm_medium=crosspost&utm_campaign=syndication) *— Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium & Hashnode in 1 click.*
