What Git actually stores
What you will be able to do
- Read any file's contents directly out of the object databaseobject databaseThe store under .git/objects holding every blob, tree, commit and tag the repository knows about., without checking anything out
- Predict the object ID of a file before Git has ever seen it
- Explain why storing the same file twice costs nothing
- Follow a commitcommitAn immutable snapshot of the entire tracked tree, together with author, committer, message and parent pointers. to its treetreeThe object that stores a directory listing: names, modes, and the object ID each name points at. and the tree to its blobsblobThe object that stores a file’s contents. A blob holds bytes only — no filename, no path, no permissions.
The problem
Almost everyone learns Git as a tool that records changes. A commit, in this telling, is a bundle of edits — lines added here, lines removed there — and history is a chain of such bundles, each describing how to get from the previous state to the next.
That model is comfortable, widely taught, and wrong. It survives contact with
git commit and git log perfectly well, then falls apart at the first
interesting question. Why is checking out a two-year-old commit instant, if Git
must replay two years of changes to reconstruct it? Why does deleting a large
file fail to shrink the repository? Why does a rebase produce commits with new
hashes when the content is identical?
Those questions share one answer, and it is not a fact about commands. It is a fact about storage.
How it works
Git stores snapshots, not differences. Each commit records the complete state of every tracked file at that moment.
The obvious objection is that this must waste enormous space — two hundred commits of a thousand files means two hundred thousand copies. It does not, because of a second decision that does all the work:
Every object is named by the hash of its own contents.
That is the whole idea: content-addressed storage. Two files with identical contents produce the same hash, so they are the same object, so Git stores them once — whether they appear in one commit or in ten thousand.
There are four object types. This lesson covers the three you meet daily.
| Object | Holds | Does not hold |
|---|---|---|
| blob | A file's bytes | Its name, path or permissions |
| tree | A directory listing: names, modes, and what each points at | File contents |
| commit | One tree, zero or more parents, author, committer, message | Any file data |
Note what a blob does not contain. A blob has no filename. The name lives in the tree that points at it — which is why renaming a file creates no new blob, and why two identical files share one.
Git 2.28+git init -b main sets the initial branch name directly. On older releases, run
git init and then git branch -m main.
Doing it
Everything below runs in a throwaway repository. Nothing depends on your existing work, and you can delete the directory afterwards.
Create a repository and a file
~/practicegit init -b main object-model cd object-model Initialized empty Git repository in /home/you/practice/object-model/.git/Now create a file, but do not add it to Git yet.
~/practice/object-modelmainprintf 'Hello, Git.\n' > greeting.txt git hash-object greeting.txt 01f7bec5543e727e155f937a853c68da28072be3You should see that hash exactly — not something similar, but that string, character for character.
git hash-objectcomputed what Git would call this content. It stored nothing, and Git does not know the file exists:~/practice/object-modelmainfind .git/objects -type f | wc -l 0The hash is a pure function of the bytes. Your machine, your username and the time of day play no part in it.
Stage the file and find the object
~/practice/object-modelmaingit add greeting.txt find .git/objects -type f .git/objects/01/f7bec5543e727e155f937a853c68da28072be3There is the hash you predicted, split into a two-character directory and a thirty-eight-character filename. That split keeps any one directory from growing to millions of entries.
Note what happened:
git addwrote a permanent object. The staging area is not a waiting room — the content is in the database already.Read the object back
git cat-fileinspects objects. Three flags matter:-tfor type,-sfor size,-pto print.~/practice/object-modelmaingit cat-file -t 01f7bec5543e727e155f937a853c68da28072be3 git cat-file -s 01f7bec5543e727e155f937a853c68da28072be3 git cat-file -p 01f7bec5543e727e155f937a853c68da28072be3 blob 12 Hello, Git.A blob, twelve bytes, holding your text. No filename anywhere in it.
Commit, and follow the chain
~/practice/object-modelmaingit commit -m "Add a greeting" [main (root-commit) 3dd789b] Add a greeting 1 file changed, 1 insertion(+) create mode 100644 greeting.txtOne file and one commit produce three objects:
~/practice/object-modelmaingit cat-file --batch-check --batch-all-objects 01f7bec5543e727e155f937a853c68da28072be3 blob 12 3dd789b7a52e3ca99668e8c8874624ad0c41e4c7 commit 175 91d1f6f01f52ecd5a42c43dc1311c9695a4e23aa tree 40Your blob hash matches mine. Your commit hash will not: a commit includes your name, your email and a timestamp. Worth remembering — blobs and trees are reproducible, commits are not.
Print the commit:
~/practice/object-modelmaingit cat-file -p HEAD tree 91d1f6f01f52ecd5a42c43dc1311c9695a4e23aa author Ada Lovelace <[email protected]> 1784772121 +0900 committer Ada Lovelace <[email protected]> 1784772121 +0900 Add a greetingA commit is a short text file. It names exactly one tree, and that tree is the snapshot:
~/practice/object-modelmaingit cat-file -p HEAD^{tree} 100644 blob 01f7bec5543e727e155f937a853c68da28072be3 greeting.txtThere is the filename — in the tree, attached to a blob. The full chain:
A commit points at one tree; the tree names each file and points at the blob holding its contents. Text description of this diagram
c1on commit — root commit, HEAD points heret1on tree — parent of record: c1b1on blob — parent of record: t1
Store the same content twice
Copy the file and see what Git makes of it.
~/practice/object-modelmaincp greeting.txt copy.txt git hash-object copy.txt git add copy.txt git cat-file --batch-check --batch-all-objects | wc -l 01f7bec5543e727e155f937a853c68da28072be3 3The same hash, and still three objects. A second file with identical contents added nothing at all. This is why a repository carrying the same licence text in forty directories stores it once.
Change a file, and watch nothing get overwritten
~/practice/object-modelmainprintf 'Hello, Git.\nSecond line.\n' > greeting.txt git hash-object greeting.txt git add greeting.txt git commit -q -m "Add a copy and a second line" git cat-file --batch-check --batch-all-objects f3831a2b182c54b7fec70cb74b45cd6feef03b42 01f7bec5543e727e155f937a853c68da28072be3 blob 12 10631115fe42fac0fe21638b23b13c6a94b1e47c tree 76 3dd789b7a52e3ca99668e8c8874624ad0c41e4c7 commit 175 4e4ab6513d88bdb5b5a7843e87284540f89bae5e commit 237 91d1f6f01f52ecd5a42c43dc1311c9695a4e23aa tree 40 f3831a2b182c54b7fec70cb74b45cd6feef03b42 blob 25Six objects. Editing the file did not modify the old blob — it created a second one. The original is untouched and still readable:
~/practice/object-modelmaingit cat-file -p 01f7bec5543e727e155f937a853c68da28072be3 Hello, Git.Objects are immutable. Nothing in Git edits one; operations create new objects and move pointers. That single sentence explains most of what makes Git recoverable.
Checkpoint
Rungit cat-file --batch-check --batch-all-objects | wc -l
6 — two blobs, two trees, two commits.Where the hash comes from
The object ID is neither magic nor a database key. It is the SHA-1 of the object's type, its length, a null byte, and its contents. You can compute it without Git at all:
printf 'blob 12\0Hello, Git.\n' | sha1sum
01f7bec5543e727e155f937a853c68da28072be3 -The same hash Git gave you, produced by a general-purpose checksum tool. There is nothing proprietary inside the object database.
When it goes wrong
"I deleted a large file but the repository is the same size." Expected. The blob remains, referenced by every commit that contained it. Removing a file from the current snapshot does not remove it from history.
"Two people made the same change and got different commit hashes." Also expected. Identical content yields identical blobs and trees, but a commit also hashes author, committer and timestamps.
"find .git/objects shows almost nothing in my real repository." Git
periodically compresses loose objects into packfiles. Everything is still there;
git cat-file --batch-check --batch-all-objects reads loose and packed objects
alike, which is why this lesson uses it rather than find.
"git cat-file -p reports Not a valid object name." Usually a truncated or
mistyped hash. Any unambiguous prefix works — git cat-file -p 01f7bec is enough
in this repository.
Practice
#Read a deleted file without restoring it
Delete copy.txt from your working tree with rm copy.txt. Now print its
contents — without running git checkout, git restore or git show.
Hint 1
git cat-file -p HEAD^{tree}. What does each column mean?Hint 2
Show solution
Print the tree to find the blob, then print the blob. No checkout, no restore, no touching the working tree:
git cat-file -p HEAD^{tree}
git cat-file -p 01f7bec5543e727e155f937a853c68da28072be3
100644 blob 01f7bec5543e727e155f937a853c68da28072be3 copy.txt
100644 blob f3831a2b182c54b7fec70cb74b45cd6feef03b42 greeting.txt
Hello, Git.The content was never in the working tree during this exercise. It came out of the object database, which is where it had been all along.
#Predict the cost of a rename
In a fresh repository, commit a single file a.txt and count the objects. Now
rename it to b.txt and commit again.
Before running anything: how many objects will exist afterwards, and how many of them are blobs?
Hint 1
Hint 2
git cat-file --batch-check --batch-all-objects.Show solution
Three objects before, five after. The rename added a tree and a commit. It added no blob, because the contents did not change and a blob has never known its own name.
git mv a.txt b.txt
git commit -q -m "Rename a.txt to b.txt"
git cat-file --batch-check --batch-all-objects
1efaaf1199c5c63d3861e828d12006a7328d14a1 commit 170
20e50a07feffafe7699bf38ff4027a606f406eaa tree 33
4eadd486e1cafbea3b92ac5883a14b704f5fcee6 commit 230
5626abf0f72e58d7a153368ba57db4c673c0e171 blob 4
c7d8a0fdb10f11cf53e42b807f501251dd94366a tree 33It is also why Git stores no rename record: renames are detected by comparing trees, not written down as events.
Summary
Key takeaways
- Git stores snapshots, not differences. A commit names one tree, and that tree is the complete state of the project.
- Every object is named by the hash of its own contents, so identical content is stored exactly once however often it appears.
- A blob holds bytes and nothing else. Names and permissions live in trees, which is why renaming a file creates no new blob.
- Objects are immutable. Editing a file creates a new blob and leaves the old one intact — the reason so much in Git turns out to be recoverable.
- Blobs and trees are reproducible on any machine. Commits are not, because they hash your identity and the time.
You can delete the practice directory now; nothing else depends on it.
Next: the Foundations overview lists the remaining lessons in this module.