Understanding Git Internals: How Git Stores Data

Git is a content-addressable database with a version control interface on top. Every file, directory and commit is stored as an object named by the hash of its own contents, inside .git/objects. Branches and tags are just small pointers to those objects. Once you see that, most “scary” Git situations (detached HEAD, lost commits, rebases) become easy to reason about.

This guide walks through Git’s internals with commands you can run yourself. Every output below is real, from Git 2.55. Part of our complete Git and GitHub Actions guide.


The .git Directory

Run git init and Git creates a .git folder. That folder is the repository; your working files are just one checked-out copy of it.

Path What it holds
objects/ Every blob, tree, commit and tag, loose or in packfiles
refs/ Branches (refs/heads/), tags (refs/tags/) and remote-tracking branches
HEAD Which branch (or commit) you currently have checked out
index The staging area: what the next commit will contain
config Repository settings and remotes

Objects Are Named by Their Contents

$ echo "Hello Git" > hello.txt
$ git hash-object hello.txt
9f4d96d5b00d98959ea9960f069585ce42b1349a

That hash is not random. Git computes it from a short header plus the file contents, and you can reproduce it yourself:

$ printf 'blob 10\0Hello Git\n' | shasum -a 1
9f4d96d5b00d98959ea9960f069585ce42b1349a  -

Same content, same hash, on every machine in the world. That is why Git can deduplicate identical files for free, why two clones can compare history quickly, and why changing any byte of an old commit changes its hash and every hash after it.

When you stage the file, Git writes the object into .git/objects, using the first two hex characters as a folder name:

$ git add hello.txt
$ find .git/objects -type f
.git/objects/9f/4d96d5b00d98959ea9960f069585ce42b1349a

The Four Object Types

Blob: file contents

A blob stores the contents of one file and nothing else: no filename, no permissions. Two files with identical contents share a single blob.

$ git cat-file -t 9f4d96d
blob
$ git cat-file -p 9f4d96d
Hello Git

Tree: a directory listing

A tree maps names to blobs (files) and other trees (subdirectories), along with file modes.

$ git cat-file -p HEAD^{tree}
100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a    hello.txt

Commit: a snapshot plus metadata

A commit points to exactly one tree (the full project snapshot), to its parent commit(s), and records the author, committer and message.

$ git commit -m "Add hello.txt"
$ git cat-file -p HEAD
tree 2f092e9cadfc1eb4a6d2febfddb941f4c1fe6fd6
author Ada Dev <ada@example.com> 1790330400 +0000
committer Ada Dev <ada@example.com> 1790330400 +0000

Add hello.txt

This first commit has no parent line. Every later commit has one (or two, for a merge), which is what links commits into history.

Tag: a named, annotated pointer

An annotated tag is an object that points to a commit and stores a tagger, date, message and optional signature. Lightweight tags are not objects at all, just refs. More in our Git tags and releases guide.


Snapshots, Not Diffs (Mostly)

Each commit references a complete tree of the project, not a list of changes. Unchanged files simply reuse the same blob hashes, so a snapshot costs almost nothing extra.

To save space, git gc (which also runs automatically) later packs loose objects into a packfile and stores similar objects as deltas of each other:

$ git gc
$ ls .git/objects/pack
pack-02fda204....idx
pack-02fda204....pack
pack-02fda204....rev

$ git count-objects -v
count: 0
in-pack: 6
packs: 1

So Git’s model is snapshots, and deltas are only a storage optimization inside packfiles. That distinction explains why operations like checking out an old commit or comparing two branches are fast.


Refs, Branches and HEAD

A branch is a file containing a commit hash. That is all it is:

$ cat .git/HEAD
ref: refs/heads/main
$ cat .git/refs/heads/main
94b060a209bdf9dfdd9a418b3d6b2a443884e492
  • Committing writes new objects, then moves the current branch’s ref to the new commit.
  • HEAD points to a branch. When it points directly to a commit instead, you are in “detached HEAD” state.
  • Deleting a branch deletes only the pointer. The commits stay in the object database until garbage collection, which is why git reflog can usually bring them back.

Use git rev-parse main and git symbolic-ref HEAD rather than reading these files directly in scripts. Once a repository uses the newer reftable format (below), those files no longer exist.


The Index (Staging Area)

.git/index is a binary file listing exactly what the next commit will contain: path, mode and blob hash for every tracked file.

$ git ls-files --stage
100644 9f4d96d5b00d98959ea9960f069585ce42b1349a 0    hello.txt

git add writes a blob and updates the index; git commit turns the index into tree objects and a commit. The number after the hash is the merge stage, which is non-zero only during a merge conflict. Our guide to rebase, stash and cherry-pick builds on this.


What Changes in Git 3.0

Git 3.0 (not yet released; Git 2.55 is current) plans two changes to the internals covered here:

  • SHA-256 becomes the default hash for new repositories. Object names become 64 hex characters instead of 40. Existing SHA-1 repositories keep working.
  • reftable becomes the default ref storage. Refs move from individual files under .git/refs into a compact binary table in .git/reftable, which is faster with many refs and avoids case-sensitivity problems on macOS and Windows.

You can try both today:

$ git init --object-format=sha256 demo-sha256
$ echo "Hello Git" > h.txt && git hash-object h.txt
81b504e19fffe541beccd5eddb8a8213f67001271bebafb66a7c137f266a07b0

$ git init --ref-format=reftable demo-reftable
$ ls demo-reftable/.git/reftable
tables.list  0x000000000001-0x000000000001-eb4478d1.ref

Details and everything else that changes: Git 3.0: What Actually Breaks.


Commands for Exploring Any Repository

Command What it shows
git cat-file -t <hash> The type of an object
git cat-file -p <hash> The contents of an object
git rev-parse HEAD^{tree} The tree hash of the current commit
git ls-tree -r HEAD Every file and blob in a commit
git count-objects -v Loose vs packed object counts and sizes
git fsck Integrity check of all objects
git reflog Where HEAD and branches have pointed recently (your undo history)

Frequently Asked Questions

How does Git store data internally?

As a content-addressable key-value store. Every file, directory tree, and commit is stored under the hash of its own contents, inside the .git/objects directory.

What are the four Git object types?

Blob (file contents), tree (a directory listing), commit (a snapshot pointing to one tree plus parent commits and metadata), and tag (an annotated pointer to another object).

What is the difference between a branch and HEAD in Git?

A branch is a movable pointer to a commit, stored under .git/refs/heads. HEAD is a pointer to the branch you currently have checked out.

Does Git store diffs or full snapshots?

Full snapshots. Each commit references a complete tree of the project. Git only compresses similar objects into deltas later, inside packfiles, as a storage optimization.

You Might Also Like

Related guides

Uncategorized