
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 reflogcan 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/refsinto 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
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.
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).
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.
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
- 👉 Day 1: Git Fundamentals
- 👉 Day 2: Basic Git Workflow
- 👉 Day 3: Branching and Merging
- 👉 Day 4: Remote Repositories
- 👉 Day 5: Advanced Git Operations
- 👉 Day 6: Git Workflows and Best Practices
- 👉 Day 7: Troubleshooting and Expert Techniques
- 👉 Advanced Git Commands You Need to Master (With Examples)
- 👉 Git Best Practices for Branching and Approvals
- 👉 GitHub CODEOWNERS & Permissions: Best Practices with Teams
- 👉 Git Hooks Explained: Automate Your Workflow with Examples
- 👉 Git Tags and Releases Best Practices: A Complete Guide