How Git Stores Data
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:
echo "Hello from Kochi" > hello.txt
git hash-object hello.txt4e6f17313d16b0da741fd32c5dcbc7c839aa316c
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:
printf 'blob 17\0Hello from Kochi\n' | sha1sum4e6f17313d16b0da741fd32c5dcbc7c839aa316c *-
The four kinds of objects
Everything in Git's history is stored as one of four object types in .git/objects:
| Object | Stores | Points to |
|---|---|---|
| Blob | The content of one file (no name, no permissions) | Nothing |
| Tree | A folder listing: names, modes and the blob or tree for each entry | Blobs and other trees |
| Commit | Author, committer, dates, message | One tree (the project snapshot) and its parent commits |
| Tag | An annotated tag's name, tagger, message | Usually 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:
git add hello.txt
find .git/objects -type f.git/objects/4e/6f17313d16b0da741fd32c5dcbc7c839aa316c
Committing adds two more objects: a tree for the project folder and the commit itself:
git commit -m "Add greeting"
find .git/objects -type f[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:
git cat-file -p HEADtree 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:
git cat-file -p HEAD^{tree}
git cat-file -p 4e6f173100644 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
mainhash 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:
cat .git/HEAD
cat .git/refs/heads/mainref: 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:
git count-objects -v
git gc
find .git/objects -type fcount: 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:
git init --object-format=sha256 tiny256
cd tiny256
echo "Hello from Kochi" > hello.txt
git hash-object hello.txt
git rev-parse --show-object-formatInitialized 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/objectsby 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 -tand-pshow 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
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 --stageThe 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?
Names live in trees; blobs hold only content.
2. Why does amending a commit give it a new hash?
Any change to a commit's data produces a different hash.
3. Two files in different folders have exactly the same content. How many blobs does Git store?
Content addressing means identical content gets the same hash and is stored once.
4. What do SHA-256 repositories change?
SHA-256 replaces SHA-1 as the hash used to name objects.
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
