πŸ“š Learning Hub
Β· 3 min read

How Git Tracks Your Changes (It's Not What You Think)


Most developers think Git stores the differences between file versions. It doesn’t. Git stores complete snapshots of every file at every commit. Here’s how.

The three objects

Everything in Git is stored as one of three object types:

1. Blob (Binary Large Object)

A blob is the contents of a file. Not the filename β€” just the contents. If two files have identical contents, Git stores one blob and points to it twice.

# See the blob hash for a file
git hash-object README.md
# e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

2. Tree

A tree is a directory listing. It maps filenames to blobs (files) and other trees (subdirectories).

tree abc123
β”œβ”€β”€ README.md β†’ blob e69de2
β”œβ”€β”€ src/ β†’ tree def456
β”‚   β”œβ”€β”€ index.js β†’ blob 789abc
β”‚   └── utils.js β†’ blob 012def
└── package.json β†’ blob 345ghi

3. Commit

A commit points to a tree (the snapshot of your entire project at that moment), plus metadata: author, date, message, and parent commit(s).

commit 1a2b3c
β”œβ”€β”€ tree: abc123 (the root tree)
β”œβ”€β”€ parent: 9x8y7z (previous commit)
β”œβ”€β”€ author: Alice <alice@example.com>
β”œβ”€β”€ date: 2026-03-16 10:30:00
└── message: "Fix login bug"

What happens when you commit

Step 1: git add

When you run git add file.js, Git:

  1. Computes the SHA-1 hash of the file contents
  2. Compresses the contents and stores it as a blob in .git/objects/
  3. Updates the staging area (.git/index) to point to this blob

Step 2: git commit

When you run git commit, Git:

  1. Creates a tree object from the staging area (mapping filenames to blobs)
  2. Creates a commit object pointing to that tree, with your message and the parent commit
  3. Updates the branch pointer (e.g., refs/heads/main) to this new commit

That’s it. A commit is just a pointer to a snapshot.

How branches work

A branch is literally a file containing a commit hash. That’s all.

cat .git/refs/heads/main
# 1a2b3c4d5e6f7890abcdef1234567890abcdef12

When you create a branch, Git creates a new file with the same commit hash. When you commit on that branch, Git updates the file to the new commit hash.

HEAD is a pointer to the current branch:

cat .git/HEAD
# ref: refs/heads/main

How Git is efficient (if it stores full snapshots)

β€œWait, if every commit stores every file, doesn’t that use insane amounts of space?”

Three tricks:

1. Content-addressable storage

If a file hasn’t changed between commits, the blob hash is the same. Git reuses the existing blob. Only changed files get new blobs.

2. Compression

All objects are zlib-compressed. Text files compress extremely well.

3. Packfiles

Periodically (and during git gc), Git packs objects into .pack files using delta compression. It stores the full content of one version and deltas for similar objects. This is where Git gets its space efficiency β€” but it’s an optimization layer, not the core model.

The mental model

HEAD β†’ main β†’ commit C β†’ tree β†’ blobs (your files)
                ↓
              commit B β†’ tree β†’ blobs
                ↓
              commit A β†’ tree β†’ blobs

Every commit is a complete snapshot. Branches are movable pointers. Tags are fixed pointers. That’s Git.

Why this matters

Understanding this model explains everything:

  • Why git checkout is fast β€” it just swaps which tree your working directory points to
  • Why branches are cheap β€” they’re a 41-byte file
  • Why rebasing rewrites history β€” it creates new commit objects with new hashes
  • Why force-pushing is dangerous β€” you’re moving a pointer, and the old commits become unreachable
  • Why git reflog can save you β€” it tracks where HEAD pointed, even after β€œdeleted” commits

Related: Fix: fatal: not a git repository Related: Fix: Git merge conflict

πŸ“˜