Commits: Are They Diffs, Snapshots, or Something Else?

Everyone who uses Git has a mental model of what a commit is, but those models often diverge sharply. A recent, highly unscientific poll on Mastodon asked developers how they primarily think about commits. The split was surprisingly close: 51% said a diff, 42% said a snapshot, while a small remainder saw a commit as a history of every previous version or something else entirely.

This near-even division suggests the answer isn't as clear-cut as it might seem. Defining the terms helps sharpen the question. A “diff” is what you see when you run git show COMMIT_ID — the patch of changes. A “snapshot” is the complete state of all files you get after running git checkout COMMIT_ID. Git internally refers to this file listing as the “tree.”

What Git Actually Stores

Internally, Git represents commits as snapshots. A commit object stores metadata alongside a pointer to a tree object, which lists every file and subdirectory in the repository at that point in time. Because the full state is saved, checking out any commit — yesterday's or the millionth ancestor — is equally fast. Git never needs to replay a chain of diffs to reconstruct a directory tree.

The Truth About Compression: Packfiles

That said, the intuition behind the “commits-as-diffs” crowd is not baseless. Git does compress snapshot data using packfiles. During garbage collection or a fresh git clone, Git reduces disk usage by storing objects as an “original file” reference plus a delta. These deltas are markedly different from human-readable diffs. They are essentially instructions that copy byte ranges from the original and splice in new text; there is no concept of a deletion, only copies and additions.

The original file chosen as a base isn't necessarily the previous commit. Git uses heuristic algorithms to pick an efficient base object, and the resulting layers of deltas are generally shallow. Packfiles are effectively a behind-the-scenes optimization — they make snapshots feasible without consuming excessive disk space.

Weirdness in the git show Pipeline

The process for displaying a commit reveals an interesting workflow:

  1. Git reconstructs the tree objects for the target commit and its parent by applying deltas from packfiles.
  2. Git then diffs the two trees. This is fast in most cases because unchanged files can be skipped by comparing their hashes.
  3. Finally, the calculated diff is rendered for you.

So a request starts with delta-like data, converts it into a full snapshot, and ends with a calculated diff. Despite appearing circuitous, it makes sense because packfile deltas and displayed diffs serve completely different purposes.

A Useful, “Wrong” Mental Model

Despite the internal reality, a common and surprisingly coherent mental model treats commits as follows:

  • Commits are stored as diffs from their parent, along with metadata.
  • To inspect an old commit, Git replays all prior commits from the beginning.

This model is factually incorrect, yet it is intuitive because presenting a commit as a diff matches how users interact with Git most frequently. It frames the working mental model of a repository around the change itself. Merge commits are the main wrinkle, but one can gloss over that by thinking of a merge as a diff from its first parent.

This philosophy extends to other “wrong” ideas that don't hinder daily use. You might think you can edit a commit message or move a commit to a new base. In reality, Git copies the commit, creates a new one, and leaves the original intact. For routine work, these misconceptions cause few problems — the trouble usually surfaces only during complex undo operations or error recovery.

When Diff-Thinking Makes Sense

Thinking of a commit as a diff is often the most practical approach. When you're altering a single line, your focus is on that one change, not the entire codebase. It's also how tools present commits: clicking on a commit on GitHub or running git show shows a patch. For developers who frequently use rebase, which revolves around replaying changes, the diff view is the natural way to reason about operations.

The Case for Thinking in Snapshots

Snapshot-centric thinking has its own wins. Git's handling of file moves can be misleading — when you move and edit a file, Git might flag it as a delete and an add because it only stores snapshots. The “renamed” status is just a guess based on content similarity. Thinking in snapshots also makes operations like git checkout COMMIT_ID seem less conceptually stressful; you're not replaying thousands of commits, just hopping to a new state. This model makes merge commits clearer, too: a merge isn't a calculated combination but a brand new state that can technically contain arbitrary change made during conflict resolution — which explains why careful review is critical.

Alternate Perspectives on Commits

Beyond the primary diff/snapshot dichotomy, developers attach other meanings to commits. Some treat them as bundles of out-of-band metadata, such as links to a pull request or context from a conversation with a coworker. Others frame a diff as a paired “before state plus after state.” For many, the definition shifts depending on the task at hand, and terminology can help disambiguate: “revision” leans transactional toward snapshots, while “patch” strongly implies a diff.

The takeaway is that there is no single right way to think about commits. Both principal models have significant utility, and their mix in practice is a matter of context. Recognizing the underlying internal structure of a commit as a snapshot, while appreciating the pragmatic value of the diff-based model, provides a robust toolkit for navigating Git's complexities.