Git Object Types
Git’s repository database is built from a small set of object types connected by Object IDs. The four types in this lesson are Blob, Tree, Commit, and Annotated Tag. Their relationships turn raw content into named project snapshots, connect those snapshots into history, and optionally attach release metadata.
Annotated Tag → Commit → Root Tree → Nested Trees → BlobsThe graph is more accurate than imagining a commit as a ZIP archive containing a separate physical copy of every project file.
1. Blob: file content without a filename
Section titled “1. Blob: file content without a filename”A Blob represents file-content bytes. It does not normally contain:
- a filename or path;
- an operating-system modification date;
- an author or commit message;
- a file extension or programming-language label.
The useful mental model is:
Blob = an immutable box of content with no filenameTwo differently named files with identical bytes can refer to the same Blob:
app/User.php ─┐ ├──> Blob B1backup.txt ───┘Moving or renaming a file without changing its bytes does not require a new Blob. Another structure records the changed name or location.
2. Tree: directory entries in one snapshot
Section titled “2. Tree: directory entries in one snapshot”A Tree represents the entries of a directory at a particular snapshot. Each entry connects a name and mode to another Object ID:
Root Tree├── 100644 README.md → Blob B1├── 100755 artisan → Blob B2└── 040000 app → Tree T2A nested directory points to another Tree:
Tree T2: app/├── 040000 Models → Tree T3└── 040000 Services → Tree T4
Tree T3: app/Models/└── 100644 User.php → Blob B5The exact numeric modes are inspected later; for this mental model they help Git distinguish ordinary files, executable files, directories, and certain other entry kinds. Names and modes live in Trees, not Blobs.
Git does not normally record an empty directory by itself. A directory becomes
represented through tracked entries beneath it. Files such as .gitkeep are
ordinary conventional files used when a project wants a path to remain
present.
3. How many Trees exist?
Section titled “3. How many Trees exist?”Think of one Tree for each represented directory in a snapshot, including one Root Tree for the repository root:
Root Tree├── app/ → Tree│ ├── Http/ → Tree│ │ └── Controllers/ → Tree│ └── Models/ → Tree├── config/ → Tree├── database/ → Tree│ └── migrations/ → Tree├── routes/ → Tree└── tests/ → TreeA folder does not own one permanent Tree ID for all time. A Tree ID identifies one exact set of entries. If an entry’s name, mode, or referenced Object ID changes, the directory has new Tree data and therefore a new Tree ID.
Two directory snapshots with exactly matching entries can reuse one Tree ID, even when reached from different commits or branches.
4. A nested edit propagates to the Root Tree
Section titled “4. A nested edit propagates to the Root Tree”Suppose app/Models/User.php changes.
First, its content receives a new Blob ID. The Models Tree must now point to
that new ID, so the Tree changes. The app Tree points to the Models Tree,
and the Root Tree points to the app Tree. The changed reference therefore
propagates upward:
User.php Blob (new) ↓Models Tree (new) ↓app Tree (new) ↓Root Tree (new)Unrelated subtrees remain reusable:
Root Tree (new)├── app/ → new Tree├── config/ → reused old Tree├── routes/ → reused old Tree└── tests/ → reused old TreeThis structural sharing is why Git can describe complete snapshots without duplicating every unchanged file and directory for every commit.
5. Commit: snapshot identity plus history metadata
Section titled “5. Commit: snapshot identity plus history metadata”A Commit does not contain all project files directly. It points to the Root Tree and records history and attribution data:
Commit C2├── tree → Root Tree T2├── parent → Commit C1├── author├── committer└── messageThe Root Commit has no parent. A normal later commit has one parent. A merge commit can have more than one parent, but merge mechanics belong to Phase 7.
The distinction between a Tree and a Commit is fundamental:
Tree → What did the project snapshot contain?Commit → Which snapshot, where in history, recorded by whom, and why?Two commits can point to the same Root Tree while having different Commit IDs. For example, an intentionally empty commit can reuse its parent’s Tree, and a message-only amend can create a replacement commit that still points to the same Tree. Different parents, messages, timestamps, or identities change the Commit object data.
6. Annotated Tag: an object describing a named target
Section titled “6. Annotated Tag: an object describing a named target”An Annotated Tag is a stored object that normally targets a Commit and carries metadata about the tag:
Annotated Tag Object├── object → target Object ID, usually a Commit├── type → target type├── tag → v1.0.0├── tagger → identity and timestamp└── message → release descriptionThe object graph becomes:
tag name → Annotated Tag Object → Commit → Root Tree → Trees → BlobsCreating the tag does not copy the commit’s snapshot and does not change the Commit, Tree, or Blob objects below it.
7. Lightweight Tags are references, not Tag objects
Section titled “7. Lightweight Tags are references, not Tag objects”Git also supports Lightweight Tags. They are direct named references, usually to commits:
Lightweight Tag:tag name → Commit
Annotated Tag:tag name → Tag Object → CommitA Lightweight Tag has no independent Tag Object containing a tagger, message, and tag timestamp. It is “lightweight” because it is only the named reference. Tag commands and release trade-offs are covered in Phase 14; reference storage is covered in Checkpoint 5.4.
8. Full Laravel snapshot example
Section titled “8. Full Laravel snapshot example”Commit C2└── Root Tree ├── artisan → Blob ├── composer.json → Blob ├── composer.lock → Blob ├── app/ → Tree │ ├── Http/ → Tree │ │ └── Controllers/ → Tree │ │ └── UserController.php → Blob │ ├── Models/ → Tree │ │ └── User.php → Blob │ └── Services/ → Tree │ └── PaymentService.php → Blob ├── config/ → Tree │ └── app.php → Blob ├── routes/ → Tree │ ├── api.php → Blob │ └── web.php → Blob └── tests/ → Tree └── Feature/ → Tree └── UserTest.php → BlobThe Commit gives the snapshot a place in history. The Trees supply names and structure. The Blobs supply file content.
9. Common mistakes
Section titled “9. Common mistakes”- Saying a Blob stores its filename or path.
- Treating a folder as if it has one permanent Tree ID.
- Expecting only the Blob to change after editing a nested file.
- Saying a Commit physically contains every file instead of pointing to a Root Tree.
- Assuming every new commit must have a different Root Tree.
- Calling a Lightweight Tag a Tag Object.
- Assuming an Annotated Tag copies or modifies the target snapshot.
- Confusing a complete snapshot model with full duplication of unchanged data.
10. Summary
Section titled “10. Summary”Blob → file content bytes, without a filenameTree → names, modes, and Object IDs for one directory snapshotCommit → Root Tree + parent history + author/committer + messageAnnotated Tag → metadata-bearing object that targets another objectLightweight Tag → direct reference; not an independent Tag Object11. Knowledge check
Section titled “11. Knowledge check”Why does renaming a file change a Tree but not necessarily its Blob?
The filename is a Tree entry, while the Blob represents content. If the bytes are unchanged, the existing Blob can be reused under the new Tree entry.
Why do parent Trees change after a nested file edit?
Each parent Tree must point to the new child Object ID. That changed entry changes the parent Tree data and ID, propagating to the Root Tree.
What is the path from an Annotated Tag to file content?
Annotated Tag Object → Commit → Root Tree → any nested Trees → Blob.