Skip to content

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 → Blobs

The graph is more accurate than imagining a commit as a ZIP archive containing a separate physical copy of every project file.

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 filename

Two differently named files with identical bytes can refer to the same Blob:

app/User.php ─┐
├──> Blob B1
backup.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 T2

A 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 B5

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

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/ → Tree

A 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 Tree

This 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
└── message

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

The object graph becomes:

tag name → Annotated Tag Object → Commit → Root Tree → Trees → Blobs

Creating 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 → Commit

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

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 → Blob

The Commit gives the snapshot a place in history. The Trees supply names and structure. The Blobs supply file content.

  • 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.
Blob → file content bytes, without a filename
Tree → names, modes, and Object IDs for one directory snapshot
Commit → Root Tree + parent history + author/committer + message
Annotated Tag → metadata-bearing object that targets another object
Lightweight Tag → direct reference; not an independent Tag Object
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.