# Git Internals: Your Repo Is a Content-Addressed Database

> Git feels like a pile of special commands until you see the data model underneath: four object types in a key-value store keyed by hash. Learn the model and every confusing command suddenly makes sense.

URL: https://harshith.in/blog/git-internals/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: git, tools, systems

Git's command line is famously confusing. The data model underneath is not. It's
a small content-addressed key-value store with exactly four object types. Once you
can see the objects, commands like `reset`, `checkout`, and `rebase` stop being
incantations and become obvious manipulations of a graph.

## Content Addressing: The Hash Is the Key

Git stores everything by the hash of its content. Hash the bytes, and that hash
*is* the address:

```bash
echo 'hello' | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a
```

Same content always produces the same hash, so identical files are stored once.
Change one byte and you get a completely different address — which is how Git
detects any change and why history is tamper-evident.

## The Four Objects

Everything in `.git/objects` is one of four types:

```text
   blob    → file contents (no name, no metadata, just bytes)
   tree    → a directory: names + modes → blob/tree hashes
   commit  → a snapshot: one tree + parent(s) + author + message
   tag     → an annotated pointer to an object
```

They compose into the snapshot of your project:

```text
   commit ──▶ tree ──▶ blob   (README.md)
                  └──▶ tree ──▶ blob   (src/main.c)
              parent
                │
                ▼
            commit ──▶ tree ──▶ ...
```

A commit doesn't store a diff — it points at a **tree**, a full snapshot of every
file at that moment. Unchanged files reuse the same blob hashes across commits, so
snapshots are cheap despite being complete.

## Commits Are a Linked List

Each commit records its parent(s), forming a directed graph back through history:

```text
   A ◀── B ◀── C ◀── D   (main)
               ▲
               └── E ◀── F   (feature, branched at C)
```

A **merge** is just a commit with two parents. There's no special "branch
history" structure — branches are derived by walking parent pointers.

## Refs: Branches Are Just Pointers

A branch isn't a copy of anything. It's a 40-character file containing the hash of
one commit:

```bash
cat .git/refs/heads/main
# d4f7a... (the tip commit of main)
```

Creating a branch writes one tiny file. That's why branching is instant. `HEAD`
points at the current branch; committing just advances the branch's pointer to the
new commit. Now the commands fall out:

- `git reset` moves a branch pointer to a different commit.
- `git checkout`/`switch` moves `HEAD` and updates your working tree to that
  tree's blobs.
- `git rebase` replays commits onto a new parent, creating new commit objects.

## Takeaways

- Git is a content-addressed store: an object's address is the hash of its
  content, so identical content is deduplicated and history is tamper-evident.
- Four object types — blob, tree, commit, tag — compose into full snapshots, not
  diffs.
- Commits link to parents to form the history graph; a merge is a two-parent
  commit.
- Branches and `HEAD` are just pointers to commits, which is why branching and
  reset are instant.
