How Git Stores Data

Git · Lesson 26 of 28 8 min read Updated

You can use Git for years without knowing how it stores data. But a little knowledge of the inside makes everything else click: why commit IDs change when you amend, why branches are cheap, why Git almost never loses committed work, and what the coming switch from SHA-1 to SHA-256 means.

In this lesson you will build a tiny repository and inspect it with Git's low-level "plumbing" commands, which are the same building blocks the everyday commands use.

Git is a content-addressed store

At its heart, Git is a key-value database. You give it some content and it gives back a key: a hash calculated from the content itself. Store the same content twice and you get the same key, so Git keeps only one copy. This is called content addressing.

Priya creates a file and asks Git what key it would get, without storing anything:

Terminal
echo "Hello from Kochi" > hello.txt
git hash-object hello.txt
Output
4e6f17313d16b0da741fd32c5dcbc7c839aa316c

That 40-character hexadecimal string is a SHA-1 hash. It depends only on the content, so if you create the same file with the same text, you will get exactly the same hash on your computer. There is no randomness, no date and no username in it. Git computes it from a short header (the word blob, the size in bytes and a zero byte) followed by the content. You can reproduce it with the standard sha1sum tool:

Terminal
printf 'blob 17\0Hello from Kochi\n' | sha1sum
Output
4e6f17313d16b0da741fd32c5dcbc7c839aa316c *-

The four kinds of objects

Everything in Git's history is stored as one of four object types in .git/objects:

ObjectStoresPoints to
BlobThe content of one file (no name, no permissions)Nothing
TreeA folder listing: names, modes and the blob or tree for each entryBlobs and other trees
CommitAuthor, committer, dates, messageOne tree (the project snapshot) and its parent commits
TagAn annotated tag's name, tagger, messageUsually a commit

Watching objects appear

When Priya stages the file, Git writes the blob into the object database right away. The first two hex characters become a folder name and the rest the file name:

Terminal
git add hello.txt
find .git/objects -type f
Output
.git/objects/4e/6f17313d16b0da741fd32c5dcbc7c839aa316c

Committing adds two more objects: a tree for the project folder and the commit itself:

Terminal
git commit -m "Add greeting"
find .git/objects -type f
Output
[main (root-commit) d87c0d8] Add greeting
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt
.git/objects/4e/6f17313d16b0da741fd32c5dcbc7c839aa316c
.git/objects/d8/7c0d846de3215552e692140947adc63cd83d0c
.git/objects/fc/032f4c81551338147aac700850dc1dcb10bcfa

Reading objects with cat-file

The files are compressed, so you cannot open them in an editor. git cat-file -t prints an object's type and -p pretty-prints its content. Start from the commit:

Terminal
git cat-file -p HEAD
Output
tree fc032f4c81551338147aac700850dc1dcb10bcfa
author Priya Sharma <priya@example.com> 1793352670 +0530
committer Priya Sharma <priya@example.com> 1793352670 +0530

Add greeting

A commit is just a small text record: one tree, the author and committer with a timestamp (seconds since 1970) and time zone, and the message. A later commit would also have a parent line with the previous commit's hash. Follow the tree:

Terminal
git cat-file -p HEAD^{tree}
git cat-file -p 4e6f173
Output
100644 blob 4e6f17313d16b0da741fd32c5dcbc7c839aa316c	hello.txt
Hello from Kochi

The tree maps the name hello.txt to the blob, and the blob holds the text. That is the whole snapshot.

Why this design matters

Because every object is named by the hash of its content, and commits contain the hash of their tree and parents, changing anything changes every hash above it. Edit one letter in an old file and its blob hash changes, so its tree's hash changes, so that commit's hash changes, and so does every later commit, since each contains its parent's hash. That is why:

  • Amending or rebasing creates new commits with new IDs, as you saw in earlier lessons.
  • History cannot be secretly altered: if your main hash matches a teammate's, your whole history is identical, down to the last byte.
  • Unchanged files cost nothing: a new commit's tree simply points at the same old blob.

Branches, HEAD and refs

Branches and tags are refs: names that point to a commit hash. In the classic "files" format, each branch is a tiny text file, and HEAD names the current branch:

Terminal
cat .git/HEAD
cat .git/refs/heads/main
Output
ref: refs/heads/main
d87c0d846de3215552e692140947adc63cd83d0c

That is the entire cost of a branch: one file with 41 bytes. Large repositories with many thousands of refs hit limits with one-file-per-ref, so Git 2.45 added a new storage format called reftable, a compact binary table. You can try it with git init --ref-format=reftable; git rev-parse --show-ref-format tells you which format a repository uses.

Packfiles and garbage collection

Storing every version of every file as a separate compressed object would waste space. From time to time, Git packs objects into a single packfile, storing similar objects as small differences (deltas) from each other. This happens automatically, and you can trigger it with git gc:

Terminal
git count-objects -v
git gc
find .git/objects -type f
Output
count: 3
size: 0
in-pack: 0
packs: 0
size-pack: 0
prune-packable: 0
garbage: 0
size-garbage: 0
.git/objects/info/commit-graph
.git/objects/info/packs
.git/objects/pack/pack-76968871db3ea116653d93502d63a9a2cfe2386c.idx
.git/objects/pack/pack-76968871db3ea116653d93502d63a9a2cfe2386c.pack
.git/objects/pack/pack-76968871db3ea116653d93502d63a9a2cfe2386c.rev

The three loose objects moved into one .pack file with an .idx index. Garbage collection also deletes old unreachable objects once their reflog entries have expired, which is why lost commits can be recovered only for a limited time.

From SHA-1 to SHA-256

SHA-1 was chosen in 2005. In 2017, researchers demonstrated a practical SHA-1 collision (two different files with the same hash), known as SHAttered. Git responded by using a hardened SHA-1 implementation that detects the known attack technique, so Git repositories were not exposed to it. But the long-term fix is a stronger hash function: SHA-256, with 64-character IDs.

Git has supported SHA-256 repositories since version 2.29 (2020). Create one with --object-format:

Terminal
git init --object-format=sha256 tiny256
cd tiny256
echo "Hello from Kochi" > hello.txt
git hash-object hello.txt
git rev-parse --show-object-format
Output
Initialized empty Git repository in C:/Users/priya/projects/tiny256/.git/
c893b115e992f3a6cee3fd9d5e3a239263ea3dadc721c28e649c1fff0304cb52
sha256

The same content gets a longer, different ID. The catch today is compatibility: a SHA-256 repository cannot simply exchange objects with a SHA-1 repository, and not every hosting service and tool supports SHA-256 yet, so check before using it for a real project. The Git project plans to make SHA-256 the default for new repositories in Git 3.0, as explained in Old vs New Git & Git 3.0. Existing SHA-1 repositories will keep working.

Common mistakes

  • Editing files inside .git/objects by hand. This corrupts the repository. Use Git commands.
  • Thinking a commit stores only differences. Each commit points at a full snapshot (tree); deltas exist only inside packfiles, as a storage trick.
  • Assuming short hashes are always unique. Seven characters are usually enough, but in huge repositories Git may print more to avoid ambiguity.

Key points

  • Git is a content-addressed store: each object is named by a hash of its content.
  • There are four object types: blob (file content), tree (folder), commit (snapshot plus metadata) and tag.
  • git cat-file -t and -p show any object; a commit points to one tree and its parents.
  • Changing any content changes every hash above it, which makes history tamper-evident.
  • Branches are small refs; packfiles compress objects; SHA-256 repositories exist now and are planned as the default in Git 3.0.

Exercise

Create a new repository, add a file city.txt containing Lucknow and commit it. Using only plumbing commands, find the tree of your commit, then the blob for city.txt, and print its content. Predict whether a second file with the same text would add a new blob, then test it.

Show solution
Terminal
git cat-file -p HEAD
git cat-file -p HEAD^{tree}
git cat-file -p <blob-hash-from-the-tree>
cp city.txt copy.txt
git add copy.txt
git ls-files --stage

The commit shows its tree, and the tree lists city.txt with its blob hash. git ls-files --stage shows city.txt and copy.txt with the same blob hash: identical content is stored once, so no new blob is added.

Quick Quiz

1. What is a Git blob?

2. Why does amending a commit give it a new hash?

3. Two files in different folders have exactly the same content. How many blobs does Git store?

4. What do SHA-256 repositories change?

Learn Git with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call