| English | 中文 | Interactive demo | Runnable lab |
When an AI agent says, “I committed two files,” Git did not actually save a commit containing two file copies. It saved a set of objects connected by object IDs: file content goes into blobs, directory structure goes into trees, and the commit points to the project’s root tree.
Once you understand the object graph, you can answer three questions directly: which object stores the content, which object records the filename, and which project snapshot a commit references.
commit C
└── tree T0 project root
├── blob B1 app.txt file content
└── tree T1 docs subdirectory
└── blob B2 note.txt file content
git ls-tree also resolves and displays the object type.HEAD point to commits belongs to the later chapter on refs. This chapter looks inside the commit first.The statement from Chapter 1 that “a commit records a project snapshot” is implemented as a tree-and-blob object graph with a commit as its entry point.
| Object | Primary content | What it does not own |
|---|---|---|
| blob | A sequence of file-content bytes | Filename, path, commit time |
| tree | Names, modes, and object IDs for one directory level | Commit message, parent relationship |
| commit | Root tree, parent commits, author, committer, message | File content directly |
If app.txt and copy.txt have identical content, two entries in the same tree can use different names while pointing to one blob. Names belong to tree entries; content belongs to blobs.
For a blob, the logical input Git hashes can be represented as:
blob <content-byte-count>\0<file-content>
Changing one content byte changes that logical input and therefore its object ID. A tree records child object IDs, so changing a child produces a new tree. A commit records the root tree ID, so a new snapshot produces a new commit object.
Repositories can use different object formats, and their object ID lengths can differ. Scripts and tools should read complete IDs instead of treating an abbreviated example length as a fixed rule.
Open the Object Graph interactive. Step through writing blobs, staging paths, generating trees, and creating a commit to see each object appear and connect.
Start a static server from the repository root:
python3 -m http.server 8000
Then visit:
http://localhost:8000/interactive/git-mental-model/object-graph.html
When finished, press Ctrl-C in the terminal running the server.
The following experiment creates a new temporary repository. It does not modify the current project.
lab_dir=$(mktemp -d "${TMPDIR:-/tmp}/my-git-object-graph.XXXXXX")
git -c init.defaultBranch=main init -q "$lab_dir"
cd "$lab_dir"
git config user.name "My Git Lab"
git config user.email "lab@example.com"
mkdir docs
printf 'hello object graph\n' > app.txt
printf 'trees name objects\n' > docs/note.txt
git add app.txt docs/note.txt
git commit -q -m "build object graph"
The working tree, index, and HEAD now agree. The next steps leave the file-oriented view and inspect the objects directly.
commit_oid=$(git rev-parse HEAD)
root_tree_oid=$(git rev-parse 'HEAD^{tree}')
docs_tree_oid=$(git rev-parse 'HEAD:docs')
app_blob_oid=$(git rev-parse 'HEAD:app.txt')
note_blob_oid=$(git rev-parse 'HEAD:docs/note.txt')
git cat-file -t "$commit_oid"
git cat-file -t "$root_tree_oid"
git cat-file -t "$docs_tree_oid"
git cat-file -t "$app_blob_oid"
git cat-file -t "$note_blob_oid"
Expected output:
commit
tree
tree
blob
blob
git rev-parse HEAD:path resolves a path through the object graph rooted at HEAD and returns the object ID at the end of that path.
git cat-file -p "$commit_oid"
The output begins with fields equivalent to:
tree <root-tree-id>
author My Git Lab <lab@example.com> <timestamp> +0000
committer My Git Lab <lab@example.com> <timestamp> +0000
build object graph
The first object ID matches $root_tree_oid. The initial commit has no parent line; later commits record one or more parent objects.
git ls-tree "$root_tree_oid"
git ls-tree "$docs_tree_oid"
The normalized structure is:
100644 blob <app-blob-id> app.txt
040000 tree <docs-tree-id> docs
100644 blob <note-blob-id> note.txt
The root tree records the names app.txt and docs. The docs entry points to another tree, which records note.txt.
git cat-file -p "$app_blob_oid"
git cat-file -p "$note_blob_oid"
Expected output, in order:
hello object graph
trees name objects
Neither app.txt nor docs/note.txt appears in that output. The path comes from the trees above each blob.
same_content_oid=$(printf 'hello object graph\n' | git hash-object --stdin)
printf '%s\n' "$app_blob_oid"
printf '%s\n' "$same_content_oid"
test "$app_blob_oid" = "$same_content_oid" && echo "same content -> same blob"
Expected final line:
same content -> same blob
git hash-object calculates an object ID by default. Add -w to write the object into the object database.
When you inspect docs/note.txt in HEAD, Git can resolve it in this order:
HEAD
-> commit
-> root tree
-> tree entry "docs"
-> nested tree
-> tree entry "note.txt"
-> blob
-> file content
Useful inspection commands:
git cat-file -t HEAD
git cat-file -p HEAD
git ls-tree HEAD
git ls-tree HEAD:docs
git show HEAD:docs/note.txt
These commands only read objects. They do not move a branch, change the index, or overwrite the working tree.
Suppose committed app.txt is overwritten with incorrect content:
printf 'broken content\n' > app.txt
git status --short
git diff -- app.txt
First verify the object type and content saved by HEAD:
app_blob_oid=$(git rev-parse 'HEAD:app.txt')
git cat-file -t "$app_blob_oid"
git cat-file -p "$app_blob_oid"
You should see blob and the original content, hello object graph. After confirming that the incorrect working-tree content can be discarded, restore it:
git restore --source=HEAD --worktree -- app.txt
git status --short
The final command should produce no output. Recovery works because the commit can still reach the original blob through its trees. Content that never entered Git’s object database or another backup cannot be recovered through this graph.
A blob is file content. Multiple filenames, trees, and commits can reuse one blob.
A commit records the root tree’s object ID. File content lives in blobs reachable from that tree.
The filename lives in a tree entry. Renaming a path without changing its content can preserve the blob ID while changing the tree ID.
An object ID comes from the object’s type, length, and content. The same logical object gets the same ID; changed content gets another ID.
git hash-object always stores an object”It only calculates the ID by default. Use -w, or workflows such as git add and git commit, to write the relevant objects into the object database.
| Fact to verify | Inspection |
|---|---|
| Whether the claimed path entered the commit | git ls-tree -r --name-only HEAD -- <path> |
| Which snapshot the commit references | git rev-parse 'HEAD^{tree}' |
| Which object a path resolves to | git rev-parse 'HEAD:<path>', then git cat-file -t |
| Whether committed content matches the claim | git show HEAD:<path> |
| Whether two paths reuse the same content object | Compare the results of two git rev-parse 'HEAD:<path>' calls |
| Whether modes such as the executable bit are correct | git ls-tree HEAD -- <path> |
Practical rules:
cat-file and ls-tree for read-only evidence before choosing recovery or history-changing commands.app.txt?app.txt is renamed to main.txt without a content change, which objects change?