Git Internals: The Object Model
7 pages · ~5 min✓ Reviewed
Builds on Git Hooks & Automation.
The Object Database Mental Model
1. Content-addressable store
- Key = SHA-1 of the content (40 hex chars)
- Value = the object, kept in
.git/objects/ab/cdef... - ID = SHA-1 of
type size\0+ content - Same content = same hash, so it is stored once
bash
echo 'test content' | git hash-object -w --stdin
-w writes the blob into .git/objects
output
d670460b4b4aece5915caf5c68d12f560a9fe3e4
2. Four object types
| Type | Holds |
|---|---|
| blob | file bytes, no name |
| tree | directory listing |
| commit | snapshot pointer + metadata |
| tag | annotated, named pointer |
3. How they link
- Committree + parent commits
- Treeblobs + sub-trees
- Blobfile bytes
Objects are immutable
Change one byte and the hash changes. 'Editing' always creates a new object; the old one stays.
Blobs, Trees, Commits and Tags
1. Blob and tree entry
- Blob = file contents only, no name
- Name and mode live in the parent tree entry
- Entry format:
mode type hash name - Rename = new tree entry, no new blob
| Mode | Type | Means |
|---|---|---|
100644 | blob | file |
100755 | blob | executable |
120000 | blob | symlink |
040000 | tree | directory |
2. Commit and tag
- Commit: tree hash, parent hash(es), author, committer, message
- Root commit: no parent
- Merge commit: two or more parents
- Lightweight tag: just a ref to a commit
- Annotated tag: real object with tagger, date, message
bash
git tag -a v1.0 -m 'msg'
annotated tag
3. Reuse by hash
- Unchanged subtree keeps its hash, so it is reused
- A new commit adds objects only for what changed
Remember
A commit points to one root tree plus its parents.
Refs, HEAD and the Index
1. Branches and HEAD
- Branch = file
.git/refs/heads/<name>with one commit hash - Creating a branch writes 41 bytes: 40 hex + newline
- HEAD is normally a symbolic ref:
ref: refs/heads/main - Detached HEAD = HEAD holds a raw commit hash
- Risk: commits made detached are lost once you switch away
- Recover:
git reflog→git branch <name> <sha>
2. The Index
- Index = staging area, binary file
.git/index - Lists path, mode, blob hash and stat info for the next commit
- git addhash the file
- Write blobobject store
- Update entryin the index
3. Status and Reflog
| Command | Compares | Shows |
|---|---|---|
git status | HEAD vs index | staged |
git status | index vs working tree | unstaged |
git reflog | HEAD and refs history | lost commits |
- Why can a detached commit be lost? Nothing points to it, so gc can prune it; reflog still finds it
- What does git add write? A blob in the object store, then the index entry
How a Commit Is Built and Packed
1. Building a commit
- hash-object + update-index
- write-tree
- commit-tree
- update-ref
git add= hash-object + update-indexgit commit= write-tree, commit-tree, update-ref
bash
sha=$(echo hi | git hash-object -w --stdin) git update-index --add --cacheinfo 100644,$sha,a.txt tree=$(git write-tree) c=$(git commit-tree $tree -m msg) git update-ref refs/heads/main $c
2. Hashes chain history
- Hash covers tree, parents, author, dates, message
- Change an ancestor: every descendant hash changes
- Amend: makes a new commit
- Children of the old commit still point to the old one
3. Packing
| Loose | Pack | |
|---|---|---|
| Layout | one zlib file each | .pack + .idx |
| Made by | each write | git gc / git repack |
- Similar objects stored as deltas vs a base: repo grows far slower than its snapshots
- Unreachable = no branch, tag, HEAD or reflog entry reaches it
git gcprunes unreachable objects after the 2-week expiry
Plumbing Commands and Common Mistakes
1. Inspect and Write Objects
| Command | Does |
|---|---|
git cat-file -t <sha> | prints the type |
git cat-file -s <sha> | prints the size |
git cat-file -p HEAD | pretty-prints content (the commit) |
git hash-object <file> | prints the hash only |
git ls-tree -r HEAD | root tree; -r recurses subtrees |
git ls-files -s | index entries |
git rev-parse HEAD | name to full hash |
git show-ref | lists all refs |
git verify-pack -v <idx> | inspects a pack |
hash-objectalone writes nothing; add -w to store the blob in the object database
2. Common Mistakes
| Myth | Truth |
|---|---|
| Branch contains commits | It points to one tip; commits live in the object database |
| Deleted branch = commits gone | They survive until gc prunes them after gc.pruneExpire (2 weeks) |
| SHA-1 is a signature | It identifies content; only signed commits and tags prove the author |
Losing commits for good
- 1reflog expire
--expire=now --all - 2gc --prune=nowdeletes unreachable
- 3Goneno recovery path
Never
Run gc --prune=now or delete files in .git/objects by hand: it destroys the reflog recovery path.
Git Internals Cheat Sheet
1. Objects and refs
| Type | Holds |
|---|---|
| blob | file bytes |
| tree | directory |
| commit | snapshot + parents |
| tag | annotated name |
- Stored at
.git/objects/xx/yyyy .git/refs/heads/*,.git/refs/tags/*- HEAD = symbolic ref or raw hash
- Reflog:
.git/logs
2. Inspect and write
| Inspect | Command |
|---|---|
| Type / size / content | cat-file -t / -s / -p <sha> |
| Tree of HEAD | ls-tree -r HEAD |
| Index | ls-files -s |
| Name to hash | rev-parse HEAD |
| All refs | show-ref |
- hash-object -wblob
- update-index --add --cacheinfoindex
- write-treetree
- commit-treecommit
- update-refbranch moves
3. Storage
- Loose: one zlib file per object
git gc/repack→.pack+.idx- Packs use delta compression
- Plain
git gcprunes unreachable objects only pastgc.pruneExpire(2 weeks);--prune=nowskips it - Amend adds one commit, old children stay; rebase rewrites every descendant hash
Branch = pointer
One commit hash, 41 bytes. Commits outlive branch deletion until gc.
Check yourself
History is A←B←C. You amend B. What happens to C?
- A new commit B' replaces B; C still points to the old B and keeps its hash
- The branch tip now reaches B', so C is orphaned
- C gets a new hash only if replayed, e.g. rebased onto B'
After git branch -D feat, are its commits gone?
- Usually not gone: reflog entries still reference them, so gc skips them
- Lost only after the reflog entries expire and gc prunes, or if objects are deleted by hand
Two repos each run echo hi | git hash-object --stdin. Same hash?
- Yes: the hash depends only on the header
blob sizeplus the content - Without
-w, nothing is written to .git/objects
In detached HEAD you commit, then run git switch main. Where is that commit?
- Still in the object store, but only the reflog reaches it
- Recover it: find the hash in
git reflog, thengit branch rescue <sha>