Git, CI/CD & GitOpsIII · Git ObjectsGit Objects
Tree objects — directory entries, mode bits, and recursive sub-trees
What you'll learn
- Define a tree object as an ordered list of (mode, OID, name) entries
- Read a tree with git ls-tree and reconstruct it with git read-tree
- Identify the common mode bits and what each one means on disk
- Write a tree from the index with git write-tree and explain when it is useful
- Construct index entries for blobs that are not in the working tree with git update-index --cacheinfo
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A tree object is the directory level of Git’s content model. It is a flat, ordered list of entries; each entry carries a mode bit, an OID, and a name. Trees nest by referencing sub-trees, so a root tree represents the repository root and a sub-tree represents a subdirectory. The tree is what makes a commit point at a directory layout, not just a pile of bytes.
Anatomy of a tree entry
A tree is a sorted list of entries, one per file or subdirectory that exists directly inside the directory the tree represents. Each entry has three fields:
- Mode bits — Unix-style permission and type. The common
values are
100644for a regular file,100755for an executable file, and120000for a symbolic link. Sub-trees use040000(a directory). - OID — the SHA of the referenced object. For a file entry, this is the blob OID; for a sub-tree entry, this is the sub-tree’s OID.
- Name — the filename or subdirectory name, relative to the directory the tree represents.
git cat-file -p "$TREE_OID"
# 100644 blob 2aae6c35... README.md
# 100755 blob 8b3f9d01... scripts/deploy.sh
# 120000 blob 4d2c8e01... legacy/run.sh
# 040000 tree 7b3f9a01... src
Tree entries are stored in a canonical order (sort by name, with
sub-trees sorted as if their names had a trailing /). This
makes the on-disk representation canonical: two trees with the
same entries in any order hash to the same OID, because Git
sorts them before serialising.
Sub-trees and recursive directory structure
A tree does not contain a path hierarchy in its own bytes. It contains an entry whose OID points at another tree, and that sub-tree contains its own entries. The directory structure is reconstructed by walking the tree from the root downward:
flowchart TD
R["root tree\n040000 not stored; the tree IS root"] --> R1["README.md\nblob"]
R --> S1["src/\nsub-tree"]
R --> S2["scripts/\nsub-tree"]
S1 --> S1A["main.go\nblob"]
S1 --> S1B["util/\nsub-tree"]
S1B --> S1B1["helper.go\nblob"]
S2 --> S2A["deploy.sh\nblob 100755"]
There is no 040000 root entry. The root tree is the tree a
commit points at; “root” is a position in the graph, not a
property of the tree object. The first byte of a commit object’s
payload after the header is the OID of the root tree.
# Read the root tree of a commit
git cat-file -p "$COMMIT_OID" | head -1
# tree <root-tree-oid>
# Read the root tree directly using the commit dereference syntax
git cat-file -p "$COMMIT_OID^{tree}"
The ^{tree} suffix is the standard way to ask Git to dereference
a commit to its root tree, regardless of whether the OID points
directly at a tree or at a tag wrapping a commit.
Building a tree from the index
The index (.git/index) is the canonical staging area from
which trees are written. git write-tree reads the index,
serialises the entries in canonical order, and writes a tree
object for the index contents:
# Write the index as a tree and print the OID
git write-tree
# 4d2c8e01a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7
git write-tree is the plumbing command that git commit calls
internally to convert the index into a tree before constructing
the commit. You can use it directly when you are building a
commit by hand:
# 1. Stage a blob into the index under a chosen mode and name
git update-index --add --cacheinfo 100644,2aae6c35...,README.md
# 2. Write the index as a tree
git write-tree
# 3. Wrap the tree in a commit object (covered in lesson III-05)
git commit-tree "$TREE_OID" -m "manual commit"
The --cacheinfo <mode>,<oid>,<path> form of git update-index
is what lets you stage a blob that exists in the object store
but not on the working tree. This is the workflow used by
low-level tooling, by tests that build repositories from
scratch, and by Git’s own plumbing examples.
Reading a tree back
git ls-tree prints a tree’s entries in a human-readable form.
The -r flag recurses into sub-trees, so a single command can
list every file in the repository at a given tree:
# Just the entries of one tree
git ls-tree "$TREE_OID"
# Recursive, showing the full path of every blob
git ls-tree -r "$TREE_OID"
# 100644 blob 2aae6c35... README.md
# 100644 blob 8b3f9d01... src/main.go
# 100644 blob 4d2c8e01... src/util/helper.go
# 100755 blob 7b3f9a01... scripts/deploy.sh
git read-tree is the inverse operation: it reads a tree OID
and writes its entries into the index. You can then use
git checkout-index to materialise the working tree from the
index, completing the round-trip from tree to working tree:
# Read a tree into the index
git read-tree "$TREE_OID"
# Materialise the index into the working tree
git checkout-index -a -f
Production discipline
- Use
git write-treefor low-level workflows. When you are building commits by hand (for custom CI, for fixture generation, for forensic reconstruction),git write-treeis the entry point from the index to the object database. - Stage blobs with
git update-index --cacheinfo. When you have a blob OID but not a working tree file, use--cacheinfoto attach it to a tree entry with the right mode and path. - Trust the mode, not the bits on disk. When reading a tree, the mode in the entry is the canonical mode; the working tree’s on-disk permission bits are a consequence of the checkout, not a source of truth.
Cross-course references
- Docker for Production Sysadmins - Part XI (Content addressing) draws the same analogy: image manifests are trees of layer references, with each entry naming a layer (blob) and its role in the image.
- Terraform for Production Sysadmins - Part IX (State) shows how Terraform modules form a tree-of-references structure: a root module references child modules, each of which references resources. The graph is recursive.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers Ansible role and playbook directory layout, which is a tree structure the same way a Git tree object is.
Quiz
Knowledge check · 4 questions
Q1. Which of the following is true about a Git tree object?
Q2. Two tree objects constructed from the same final set of entries — even if the entries were added to the index in different orders — hash to the same tree OID.
Q3. Which plumbing command reads the index and writes a tree object, and which flag on git update-index lets you stage a blob by OID rather than from the working tree?
Q4. Reconstruct a tree for a low-level build pipeline and verify that the resulting tree OID matches the expected snapshot.
A custom CI pipeline needs to assemble a tree object from a curated set of blob OIDs (one per Terraform file) and verify that the resulting tree matches the tree OID recorded in an upstream signed tag. The pipeline does not have access to the working tree; it must build the tree in memory from blob OIDs alone.
Passing score: 75%. Answers are checked in this browser.