Skip to main content

What Git actually stores

What you will be able to do

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.

ObjectHoldsDoes not hold
blobA file's bytesIts name, path or permissions
treeA directory listing: names, modes, and what each points atFile contents
commitOne tree, zero or more parents, author, committer, messageAny 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.

  1. Create a repository and a file

    ~/practice
    git 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-modelmain
    printf 'Hello, Git.\n' > greeting.txt
    git hash-object greeting.txt
    01f7bec5543e727e155f937a853c68da28072be3

    You should see that hash exactly — not something similar, but that string, character for character.

    git hash-object computed what Git would call this content. It stored nothing, and Git does not know the file exists:

    ~/practice/object-modelmain
    find .git/objects -type f | wc -l
    0

    The hash is a pure function of the bytes. Your machine, your username and the time of day play no part in it.

  2. Stage the file and find the object

    ~/practice/object-modelmain
    git add greeting.txt
    find .git/objects -type f
    .git/objects/01/f7bec5543e727e155f937a853c68da28072be3

    There 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 add wrote a permanent object. The staging area is not a waiting room — the content is in the database already.

  3. Read the object back

    git cat-file inspects objects. Three flags matter: -t for type, -s for size, -p to print.

    ~/practice/object-modelmain
    git 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.

  4. Commit, and follow the chain

    ~/practice/object-modelmain
    git commit -m "Add a greeting"
    [main (root-commit) 3dd789b] Add a greeting
    1 file changed, 1 insertion(+)
    create mode 100644 greeting.txt

    One file and one commit produce three objects:

    ~/practice/object-modelmain
    git cat-file --batch-check --batch-all-objects
    01f7bec5543e727e155f937a853c68da28072be3 blob 12
    3dd789b7a52e3ca99668e8c8874624ad0c41e4c7 commit 175
    91d1f6f01f52ecd5a42c43dc1311c9695a4e23aa tree 40

    Your 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-modelmain
    git cat-file -p HEAD
    tree 91d1f6f01f52ecd5a42c43dc1311c9695a4e23aa
    author Ada Lovelace <[email protected]> 1784772121 +0900
    committer Ada Lovelace <[email protected]> 1784772121 +0900
    
    Add a greeting

    A commit is a short text file. It names exactly one tree, and that tree is the snapshot:

    ~/practice/object-modelmain
    git cat-file -p HEAD^{tree}
    100644 blob 01f7bec5543e727e155f937a853c68da28072be3    greeting.txt

    There is the filename — in the tree, attached to a blob. The full chain:

    committreeblobHEAD3dd789b91d1f6f01f7bec
    A commit points at one tree; the tree names each file and points at the blob holding its contents.
    Text description of this diagram
    • c1 on commit — root commit, HEAD points here
    • t1 on tree — parent of record: c1
    • b1 on blob — parent of record: t1
  5. Store the same content twice

    Copy the file and see what Git makes of it.

    ~/practice/object-modelmain
    cp greeting.txt copy.txt
    git hash-object copy.txt
    git add copy.txt
    git cat-file --batch-check --batch-all-objects | wc -l
    01f7bec5543e727e155f937a853c68da28072be3
    3

    The 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.

  6. Change a file, and watch nothing get overwritten

    ~/practice/object-modelmain
    printf '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 25

    Six objects. Editing the file did not modify the old blob — it created a second one. The original is untouched and still readable:

    ~/practice/object-modelmain
    git 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

Expect
The number 6 — two blobs, two trees, two commits.
If not
A different count means an extra edit or commit crept in. The exercises do not depend on it, so carry on — or delete the directory and work through the steps again.

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:

~/practice/object-modelmain
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

FoundationalNo prior Git experience assumed beyond the previous lesson.10 min

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
Start from git cat-file -p HEAD^{tree}. What does each column mean?
Hint 2
The third column of a tree entry is the object ID of that file's blob.
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

FoundationalNo prior Git experience assumed beyond the previous lesson.10 min

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
Which object type holds a filename?
Hint 2
Count objects before and after with 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 33

It is also why Git stores no rename record: renames are detected by comparing trees, not written down as events.

A file is unchanged across two hundred commits. How many blobs hold its contents?
Which of these is stored inside a blob?

Summary

Key takeaways

  1. Git stores snapshots, not differences. A commit names one tree, and that tree is the complete state of the project.
  2. Every object is named by the hash of its own contents, so identical content is stored exactly once however often it appears.
  3. A blob holds bytes and nothing else. Names and permissions live in trees, which is why renaming a file creates no new blob.
  4. 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.
  5. 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.