What Is Git’s Object Model?

What Is Git's Object Model?

An advanced, expert-level deep dive into the four object types — blobs, trees, commits, and tags — that quietly power every single operation Git and GitHub perform, from a single commit to an entire repository's history.

Every familiar Git command — commit, checkout, merge, log — is, underneath, a thin layer of convenience wrapped around a remarkably small, elegant data structure: Git’s object model. It isn’t a GitHub feature; it’s the foundational storage design of Git itself, and it’s exactly the same whether your repository lives on GitHub, GitLab, or entirely offline on a USB drive. This guide goes deep into that foundation: the four object types Git ever creates, how they’re addressed, stored, and linked together, and why understanding this model transforms Git from a memorized sequence of commands into a system you can actually reason about.

1Core Concept: What Is Git’s Object Model?

Git’s object model is the underlying data structure that stores literally everything in a repository — every version of every file, every folder structure, every commit, every tag — as one of exactly four object types, each identified not by a filename or a sequential number, but by a cryptographic hash of its own content. This is the single most important fact to internalize: Git doesn’t really store “files that changed.” It stores immutable, content-addressed objects, and builds every higher-level concept — commits, branches, history — as relationships between those objects.

Everyday Analogy

Imagine a library where books aren’t organized by title or author, but by a unique fingerprint generated from their exact contents — meaning two copies of the exact same book, word for word, would always get the identical fingerprint and only need to be stored once, no matter how many people “have” that book on their shelf. Git’s object database works exactly this way: identical content, wherever it appears in your project’s history, is stored exactly once and referenced everywhere it’s needed.

Consider a concrete example: a developer named Raj works on a 50,000-line codebase. He changes one line in one file and commits. Git doesn’t rewrite the other 49,999 lines somewhere — it creates exactly one new object for the changed file’s new content, one new object for the folder structure containing it (since that folder’s contents technically changed), and one new commit object tying it all together — while every unchanged file and folder simply reuses objects that already existed from the previous commit. This is the architectural secret behind why Git commits are fast and storage-efficient even in enormous repositories.

i
Why Design It This Way

By making every object’s identity a direct, deterministic function of its own content, Git gets two enormous benefits essentially for free: automatic deduplication (identical content is always the same object) and tamper-evidence (changing content even slightly produces a completely different hash, making silent corruption or manipulation immediately detectable).

2The Four Object Types

Git’s entire object model rests on exactly four object types — nothing else exists at this layer. Every single thing Git tracks is built from some combination of these four.

Blob

Raw File Content

Stores the exact contents of a single file, with no filename, permissions, or metadata attached — purely the bytes.

Tree

A Folder Snapshot

Represents a directory, listing filenames, permissions, and pointers to the blobs (or nested trees) it contains.

Commit

A Point in History

Points to exactly one tree (the project’s root at that moment), plus metadata: author, message, timestamp, and parent commit(s).

Tag

A Named, Optionally Signed Marker

An annotated tag object points to a commit and carries its own message, tagger info, and optional cryptographic signature.

Notice the clean hierarchy this creates: a commit points to a tree, a tree points to blobs and other trees, and a tag (when annotated) points to a commit. Nothing here is circular, and every relationship flows in one direction — from the abstract (a commit representing “the state of the project”) down to the concrete (a blob holding actual file bytes).

3Content-Addressable Storage

The mechanism that ties Git’s object model together is content-addressable storage: every object’s identifier — its SHA hash — is computed directly from that object’s own content, rather than being assigned arbitrarily. This single design decision cascades into nearly every property Git is known for.

1

Object Content Is Assembled

Git prepares the object’s raw content, prefixed internally with a small header indicating its type and size.

2

A Cryptographic Hash Is Computed

That entire content-plus-header is run through a hash function, producing a fixed-length identifier unique to that exact content.

3

The Hash Becomes the Object’s Address

Git stores the object using that hash as its identifier — meaning the exact same content will always compute to the exact same identifier, wherever or whenever it’s created.

4

Retrieval Is Verification

Whenever Git reads an object back, it can (and does, when checking integrity) recompute the hash from the retrieved content and confirm it still matches — instantly revealing any corruption.

!
A Detail Worth Internalizing Deeply

Because identity is derived purely from content, two completely unrelated developers, on opposite sides of the planet, who happen to create a file with byte-for-byte identical content, will generate the exact same object hash. This isn’t a coincidence to be worried about — it’s the entire point, and it’s what allows Git to deduplicate identical content automatically across the whole history of a repository.

4Architecture & Components

Beyond the four object types themselves, a few supporting structures work together to make the object model usable day to day.

ComponentRole
Object databaseThe actual storage (inside .git/objects) holding every blob, tree, commit, and tag object ever created
Refs (branches, tags)Simple, mutable pointers that hold a commit’s hash, giving humans a memorable name instead of a 40-character string
HEADA special reference pointing to whichever branch (or commit) is currently checked out
The index (staging area)A working list of blob references representing what will become the next commit’s tree
PackfilesA compressed, space-efficient storage format Git uses to consolidate many loose objects over time
?
What an Interviewer May Ask

“If branches are just pointers to a commit hash, what actually happens when you ‘switch branches’?” — The expected answer: Git reads the tree object the target branch’s commit points to, then updates your working directory’s files and the index to match that tree’s contents exactly — the branch itself never moves any data around; it’s the checkout operation that reconstructs the working directory from the object graph.

5Internal Working

Understanding how objects are actually created and connected during a normal commit operation reveals the object model in action.

1

Staged Files Become Blobs

For every file staged for commit, Git computes a blob object from its exact current content — reusing an existing blob if that exact content already exists somewhere in the object database.

2

Directory Structure Becomes Trees

Starting from the deepest folders and working upward, Git builds a tree object for every directory, listing entries that point either to blobs (files) or to other tree objects (subfolders).

3

A Commit Object Is Created

Git creates a new commit object pointing to the root tree just built, along with the author, committer, timestamp, message, and a pointer to the previous commit (its parent).

4

The Branch Reference Is Updated

The current branch’s ref is updated to point at this brand-new commit’s hash — this single, tiny pointer update is what “advancing the branch” actually means underneath.

Notice that steps one through three only ever create new objects for content that’s actually different — any file, folder, or subtree identical to what already existed simply reuses the existing object by its hash, rather than duplicating storage.

6Data Flow & Lifecycle

flowchart TD
    A["Working directory\nfile edited"] --> B["git add:\ncontent hashed into a blob"]
    B --> C["git commit:\ntree objects built bottom-up"]
    C --> D["Root tree object\nrepresents project state"]
    D --> E["New commit object created\npointing to root tree + parent"]
    E --> F["Branch ref updated\nto new commit hash"]
    F --> G["HEAD follows the branch\nto the new commit"]
    

Fig 1 — From an edited file to a fully linked object graph, ending with a moved branch pointer

This diagram is worth studying closely, because it’s the entire mental model condensed into one picture: content flows upward from raw bytes (blobs) through structure (trees) into meaning (a commit), and the only thing that actually “moves” at the end is a small, lightweight pointer — the branch ref — not any of the underlying data itself.

7The Blob Object in Depth

Advanced Concepts

A blob is the simplest of the four object types — deliberately so. It stores exactly one thing: the raw byte content of a single file at a specific point in time.

  • No filename is stored inside a blob. The name “config.json” doesn’t exist anywhere in the blob object itself — that association lives entirely in the tree object that references it.
  • No file permissions are stored inside a blob either. Whether a file is executable is, again, metadata carried by the tree entry pointing to the blob, not by the blob.
  • Identical content always produces the same blob. If two completely different files in your project happen to contain the exact same bytes, they are — at the object level — the exact same blob, referenced from two different tree entries.
  • A blob has no concept of “history.” There’s no chain connecting an old version of a file’s blob to its new version — that relationship only exists implicitly, through the commit history that references different blobs for the same filename over time.

8The Tree Object in Depth

A tree object represents a single directory’s structure — a list of entries, each pairing a name and a set of permissions with a pointer to either a blob (a file) or another tree (a subdirectory).

Trees Are Recursive by Nature

A project with nested folders is represented as nested tree objects — the root tree points to subfolder trees, which point to their own subfolder trees, all the way down until a tree’s entries finally resolve to blobs.

Renaming a File Creates a New Tree, Not a New Blob

Since a filename lives in the tree entry rather than the blob, renaming a file (with unchanged content) produces a new tree object reflecting the new name, while reusing the exact same, unchanged blob underneath.

Only Changed Subtrees Get Rebuilt

When you commit a change deep inside a nested folder, Git only needs to create new tree objects for that folder and every ancestor folder above it — sibling folders and their subtrees remain completely untouched and reused as-is.

9The Commit Object in Depth

A commit object is where the object model gains meaning as project history. It’s deliberately minimal, but every field it carries is load-bearing.

FieldWhat It Represents
Tree pointerThe exact root tree object representing the entire project’s state at this commit
Parent commit(s)The previous commit (or, for a merge, two or more previous commits) this one builds on
AuthorWho originally wrote the change, with a timestamp
CommitterWho actually created this commit object (can differ from the author, e.g. after a rebase)
Commit messageThe human-readable description of what changed and why

The parent pointer is what turns a scattered collection of independent commit objects into a coherent, traceable history — each commit knows exactly which commit came before it, and following that chain backward from any commit reconstructs the project’s entire lineage up to that point. A merge commit is structurally identical to any other commit object, except it simply lists more than one parent.

10The Tag Object in Depth

Unlike the other three object types, a tag object is optional — it only exists when you create an annotated tag. A lightweight tag, by contrast, is simply a ref pointing directly at a commit, with no separate tag object involved at all.

Everyday Analogy

A lightweight tag is like sticking a plain adhesive label directly onto a specific page of a book — just a name, nothing more. An annotated tag is like attaching a small signed certificate to that same page, complete with a date, a note explaining its significance, and a wax seal proving who attached it. Both point to the same page, but one carries real, verifiable metadata of its own.

An annotated tag object stores its own tagger name, timestamp, and message, entirely separate from the commit it points to — and it can optionally carry a GPG signature, allowing anyone to cryptographically verify that the tag genuinely came from whoever claims to have created it, and that neither the tag’s message nor its target commit have been tampered with since.

11Loose Objects vs Packfiles

Newly created objects start their life as individual, compressed files inside the object database — known as loose objects. But storing millions of tiny individual files becomes inefficient at scale, both in raw disk usage and in filesystem performance, so Git periodically consolidates them.

Loose Objects

  • Created immediately as new content is committed
  • Stored as one compressed file per object
  • Simple and fast to create, but inefficient in bulk
  • Take up more total disk space than packed equivalents

Packfiles

  • Consolidate many objects into a single, highly compressed file
  • Use delta compression, storing many objects as differences from similar ones
  • Created automatically during routine repository maintenance
  • Dramatically reduce both disk usage and network transfer size

This distinction is invisible during everyday Git use — commands work identically regardless of whether an object happens to be loose or packed — but it explains why a freshly cloned repository is often far smaller than the sum of every individual change ever made to it: packfiles exploit the enormous similarity between successive versions of the same files.

12How Everyday Commands Map to the Object Model

Once the object model clicks, nearly every familiar Git command reveals itself as a fairly thin operation on this underlying graph. Seeing this mapping explicitly is often the single biggest “aha” moment in understanding Git deeply.

Familiar OperationWhat’s Actually Happening to Objects
CommittingNew blob, tree, and commit objects are created; a branch ref is updated to the new commit
Switching branchesThe working directory is rebuilt to match the target commit’s tree; no objects are created or destroyed
Viewing historyGit walks backward through parent pointers, starting from a commit, following the chain
MergingA new commit object is created with two (or more) parent pointers, combining the referenced trees
Comparing two commits (diff)Git compares the two commits’ tree structures (and the blobs they reference) to compute differences
CloningThe entire object database — every blob, tree, commit, and tag ever created — is transferred and reconstructed locally

13Advantages, Disadvantages & Trade-offs

Advantages

  • Automatic deduplication of identical content, both within a single commit and across a project’s entire history
  • Tamper-evidence built directly into the storage model — any content change produces a different hash
  • Extremely efficient branching and history operations, since these are cheap pointer manipulations, not data copies
  • A remarkably small, learnable core (just four object types) underlying enormous practical flexibility

Disadvantages & Trade-offs

  • The abstraction is genuinely unintuitive at first — most beginners think in terms of “file changes,” not content-addressed object graphs
  • Rewriting history (rebasing, amending) necessarily creates entirely new commit objects, since even a tiny change produces a different hash for that commit and every commit after it
  • Historical reliance on SHA-1 introduced long-term cryptographic concerns, discussed further in the next chapter
  • Very large binary files interact poorly with the model’s diffing and delta-compression assumptions, since binary content rarely compresses well as small deltas

14Security Implications

Because the object model’s integrity guarantees rest entirely on cryptographic hashing, understanding its security properties — and their limits — matters for anyone relying on Git for anything security-sensitive.

  • SHA-1’s known weaknesses are a real, if narrow, concern. Git historically used SHA-1, which researchers have demonstrated practical collision attacks against — though exploiting this against a real Git repository requires substantial, deliberate effort, not something that happens accidentally.
  • Git has since hardened against known collision techniques. Modern Git includes collision detection specifically designed to catch the known attack patterns used in practical SHA-1 collisions, and the project has an ongoing transition path toward SHA-256 for long-term resilience.
  • Signed tags and commits provide genuine authenticity guarantees. A GPG-signed tag or commit lets anyone verify not just that content matches its hash, but that a specific, trusted key actually vouched for it — a meaningfully stronger guarantee than hashing alone.
  • The object model detects corruption reliably, even without malicious intent. Bit rot, disk errors, or transfer corruption reliably produce a hash mismatch, which Git’s integrity checks will catch, unlike systems that trust stored data blindly.
!
A Nuance Worth Getting Right

Content-addressing guarantees that identical content always has identical identity — it is not, on its own, proof of who authored that content or that it came from a trusted source. That stronger guarantee specifically requires cryptographic signing on top of the base object model, not the hashing scheme alone.

15Diagnostics & Integrity Verification

Because the entire object model is built on verifiable hashes, Git provides direct tools for inspecting and validating the object database itself — invaluable for anyone debugging a corrupted repository or simply trying to see the model in action.

Inspecting a Specific Object

Git’s low-level plumbing commands can print any object’s raw content, type, and size directly by its hash — a direct window into the object database that higher-level commands normally abstract away entirely.

Full Repository Integrity Checks

Git includes a dedicated consistency-checking command that walks the entire object graph, verifying every hash matches its stored content and flagging any dangling or corrupted objects it finds.

Reflog as an Object-Level Safety Net

Because deleted or “lost” commits are still valid objects sitting in the database until actively cleaned up, the reflog — a record of where refs have recently pointed — often provides a path back to recovering work that looked gone after a risky operation.

16Design Patterns & Anti-patterns

PATTERN-01 Recommended
Pattern

Reason about Git operations in terms of the object graph rather than memorizing command behavior in isolation.

Why It Works

Once you understand that a branch is just a movable pointer and a commit is just an immutable object with a parent link, an enormous number of “surprising” Git behaviors become straightforward, predictable consequences of the model rather than arbitrary rules to memorize.

PATTERN-02 Recommended
Pattern

Sign important tags and commits when authenticity genuinely matters, rather than relying on hashing alone.

Why It Works

It closes the gap between “this content hasn’t changed” (which hashing alone guarantees) and “this content genuinely came from who it claims to” (which requires cryptographic signing).

ANTI-PATTERN-01 Avoid
Anti-pattern

Storing large binary assets (media files, compiled artifacts) directly in a Git repository without any specialized handling.

Why It’s Risky

Binary content doesn’t compress or delta well against the object model’s assumptions, and since every version is stored as a distinct blob, repository size can balloon rapidly — dedicated large-file extensions exist specifically to address this mismatch.

ANTI-PATTERN-02 Avoid
Anti-pattern

Assuming a “deleted” commit is instantly and permanently gone the moment a branch is reset or force-pushed away from it.

Why It’s Risky

Underneath, the commit object itself is very likely still sitting untouched in the object database — treating it as truly, immediately gone can lead to unnecessary panic, or conversely, false confidence that sensitive data was actually removed when it may still be recoverable.

17Best Practices & Common Mistakes

Think in Graphs
commits, trees, blobs —
not just “file changes”
Sign It
for anything needing
real authenticity
LFS
for large binaries,
not raw commits
  • Build a mental model of the object graph, not just command syntax. Understanding blobs, trees, commits, and refs as a connected structure makes advanced operations (rebasing, cherry-picking, bisecting) far more intuitive.
  • Use Git LFS or similar tooling for large binary assets. Keeping the object model doing what it’s good at — text and moderately sized files — avoids the performance and storage pitfalls of forcing large binaries through it directly.
  • Treat “history rewriting” operations with appropriate caution on shared branches. Since rewriting history necessarily creates new commit objects with different hashes, anyone else’s local copy referencing the old objects will diverge — understand this before rebasing shared work.
  • Periodically run repository maintenance. Letting Git consolidate loose objects into packfiles keeps a long-lived repository efficient, both in disk usage and clone/fetch performance.
  • Don’t assume force-deleted history is unrecoverable without checking. Before treating accidentally lost work as permanently gone, check the reflog and the underlying object database — recovery is frequently possible.

A related common mistake worth flagging explicitly: assuming that because a file “looks” different between two commits, an entirely new object had to be created for every level of the containing directory tree, all the way up. In reality, only the directories actually on the path to the changed file get new tree objects — sibling folders elsewhere in the project remain completely untouched and reused, which is precisely why Git’s performance doesn’t noticeably degrade as a repository’s overall size grows, so long as individual changes remain small and localized.

18Real-World Usage Patterns

Massive Monorepos

Deduplication at Scale

Organizations running enormous shared repositories rely heavily on the object model’s automatic deduplication — identical configuration files, shared boilerplate, and common assets across thousands of commits are stored only once.

Signed Release Verification

Supply-Chain Trust

Security-conscious open-source projects sign release tags specifically so downstream users can cryptographically verify a release’s authenticity, building directly on the object model’s tag object and signature support.

Forensic History Analysis

Auditing Exactly What Changed, When

Because every commit is an immutable object with a verifiable hash and parent chain, security and compliance teams can reconstruct precise, tamper-evident timelines of exactly how a codebase evolved.

Git-Based Tooling Ecosystems

Built Directly on the Object Model

Tools ranging from GitHub’s own web interface to third-party visualization and blame tools all operate by reading the same underlying blob, tree, and commit objects — the model is genuinely the common substrate the entire Git ecosystem is built on.

“Git doesn’t track changes — it tracks snapshots, connected by history. The object model is where that entire idea lives.”

19Frequently Asked Questions

Q1Is Git’s object model specific to GitHub, or does it exist in Git itself?
It’s entirely a Git feature, predating GitHub and working identically regardless of hosting platform. GitHub, like every other Git host, simply stores and serves the same underlying object database Git itself defines.
Q2Why doesn’t a blob object store the file’s name?
Because content-addressing means a blob’s identity comes purely from its content — storing the filename inside it would mean the same content under two different names would need to be stored as two entirely separate objects, breaking deduplication. The filename instead lives in the tree entry that references the blob.
Q3What’s the practical difference between a lightweight and an annotated tag at the object level?
A lightweight tag is just a ref pointing directly at a commit hash — no separate object exists. An annotated tag creates an actual tag object, with its own message, tagger metadata, and optional signature, which then points to the commit.
Q4Can two different files ever have the same blob hash?
Yes, if — and only if — their content is byte-for-byte identical. Different content will (barring an extraordinarily unlikely and, for SHA-1, increasingly mitigated cryptographic collision) always produce different hashes.
Q5Does understanding the object model actually help with everyday Git usage, or is it purely academic?
It’s genuinely practical. Concepts that often feel like arbitrary rules — why rebasing changes commit hashes, why branches are so lightweight to create, why a “deleted” commit can sometimes be recovered — all become intuitive, predictable consequences once you understand the underlying object graph rather than memorizing command behavior in isolation.
Q6Why is Git moving away from SHA-1?
Researchers have demonstrated practical, deliberate collision attacks against SHA-1, meaning it’s no longer considered a fully robust cryptographic hash function by modern standards, even though exploiting this against a real repository requires significant, targeted effort. Git has added collision detection and an ongoing transition path toward SHA-256 to address this long-term.

20Summary and Key Takeaways

Key Takeaways

  • Git’s object model rests on exactly four object types — blobs (file content), trees (directory structure), commits (points in history), and tags (named, optionally signed markers) — with no other fundamental storage unit.
  • Every object is content-addressable: its identifier is a cryptographic hash computed directly from its own content, enabling automatic deduplication and built-in tamper-evidence.
  • A commit is best understood as a pointer to a root tree, plus a pointer to its parent commit(s) — this simple chain is what makes the entire concept of “history” possible.
  • Branches and tags are lightweight, movable (or fixed, for tags) references into this object graph — they don’t duplicate data, they simply point to it.
  • Loose objects and packfiles are two storage representations of the same underlying data, with packfiles providing major efficiency gains through delta compression at scale.
  • Security guarantees from hashing (tamper-evidence) and from signing (authenticity) are meaningfully different — the object model provides the former natively, while the latter requires deliberately signing commits and tags.
  • Nearly every familiar Git command is, underneath, a fairly thin operation on this object graph — internalizing the model is often what finally makes Git’s more advanced behavior feel predictable rather than mysterious.