BACK TO HOME INDEX
TECH & SYSTEMS
INTERMEDIATE6 MIN READ
INDEXED: AUG 2026

Git Under the Hood: How Git Actually Stores Your Data Inside `.git/objects`

K
Kaniska Ranjan Barman
kaniskaranjanbarman@gmail.com
TAGS:#Tech#Git#DevOps
I used to be the person who ran git rebase -i, panicked at the list of commits, and closed my laptop. What actually fixed this wasn't memorizing commands—it was spending an afternoon inside .git with cat-file to realize Git is just an elegant key-value object store.

1. De-mystifying Git Commands Through the Object Store

I used to be the person who ran git rebase -i, panicked at the list of commits that showed up, closed my laptop, and just... didn't push that day. Rebase felt like a spell I was casting without understanding the words. Same with cherry-pick. Same with reflog, which I only discovered existed after accidentally deleting three days of work and Googling "git undo delete branch please" at 1am.

What actually fixed this wasn't memorizing more commands. It was spending one slow afternoon poking around inside a repo's .git folder with cat-file and realizing Git isn't really a version control tool that happens to store data somewhere. It's a tiny, dumb, extremely elegant filesystem — and the version control part is just a UI bolted on top of it. Once that clicked, the commands stopped feeling like spells.

2. A Quick Word on Why It Exists at All

Git exists because of a falling-out. In April 2005, the Linux kernel team lost access to BitKeeper, the proprietary tool they'd been using to manage kernel source history, after a dispute with the company behind it. Linus Torvalds, rather than adopting an existing open-source alternative, spent about a week writing the core of a new one from scratch.

He wasn't trying to build "SVN but free." He explicitly didn't want a centralized, diff-based history tracker. What he built instead was a content-addressable object store — something closer to a key-value database — with version control history layered on top as a directed acyclic graph (DAG) of snapshots. That distinction matters more than it sounds like it should, because almost every Git command that confuses people is confusing specifically because it's operating on that underlying object store, not on "files and diffs" the way older tools did.

"Git is essentially a simple content-addressable filesystem with a VCS user interface written on top of it."

3. The Four Things Git Actually Stores

Everything in a Git repository — every file, every folder, every commit, every tag — gets reduced down to one of four object types, each stored as a zlib-compressed blob inside .git/objects/. Every object is named by a SHA hash calculated from its own content (SHA-1 by default in most repos today, with SHA-256 support rolling out). That naming scheme is the whole trick: if two files have identical content, Git stores them as the exact same object, once, regardless of what they're named or where they live in your project.

• Blob: Just the raw bytes of a file's content. Nothing else — no filename, no permissions, no timestamp. This surprised me the first time I really sat with it: Git doesn't actually know or care what a file is called. That information lives one level up.

• Tree: This is where filenames and permissions actually get recorded. A tree object is basically a directory listing — it maps names and file modes to either blob hashes (for files) or other tree hashes (for subdirectories).

• Commit: A small plain-text object. It points to exactly one tree hash (a snapshot of your entire project at that moment), one or more parent commit hashes, and metadata — author, committer, timestamp, message.

• Annotated tag: A named, persistent pointer to a specific commit, optionally signed with GPG. (There's also a lighter "lightweight tag," which is really just a ref — more on that below.)

That's it. That's the entire data model. Everything else — branches, HEAD, staging, merges — is bookkeeping built on top of these four object types.

4. Actually Looking at One

Reading about this only got me halfway there. What made it stick was doing it by hand, in an empty test repo, with nothing to lose:

BASHSOURCE CODE
# Write a string straight into Git's object store as a blob —
# no file, no commit, nothing. Just raw content.
echo "Hello InfoHub Readers" | git hash-object -w --stdin
# → 86b45e2c7a...

# Ask Git what kind of object that hash points to
git cat-file -t 86b45e2c7a
# → blob

# Ask Git to decompress and print the raw content
git cat-file -p 86b45e2c7a
# → Hello InfoHub Readers

5. The Thing That Made Branches Finally Make Sense

For a long time I assumed branching in Git was expensive-ish under the hood — some kind of copy operation, just a cheaper one than SVN's full directory copy. It isn't. A branch is a text file. That's the whole feature.

When you run git branch feature-login, Git writes a new file at .git/refs/heads/feature-login containing exactly one thing: the 40-character SHA of the commit you were standing on. That's a 41-byte file (40 hex characters plus a newline). Switching branches with git checkout or git switch just moves which ref HEAD points at and updates your working directory to match the tree that commit points to.

This is why creating a branch in Git is instant regardless of repo size — you could have a hundred thousand commits of history, and git branch still just writes 41 bytes. It's also why "branches" in Git feel so disposable and cheap to create compared to older tools, where reviewers actively discouraged branching because of the storage and merge overhead. In Git, the cost of a branch is a rounding error.

6. Building a Commit Without `git commit`

This is the exercise that actually cemented all of the above for me, and I'd genuinely recommend doing it once in a scratch repo rather than just reading it. You can construct a full, valid commit using nothing but the low-level "plumbing" commands — no add, no commit, no porcelain at all:

BASHSOURCE CODE
# 1. Store a blob for a file that doesn't even exist on disk
BLOB_HASH=$(echo "console.log('Hello');" | git hash-object -w --stdin)

# 2. Build a tree object that maps a filename to that blob
TREE_HASH=$(git mktree <<EOF
100644 blob $BLOB_HASH	index.js
EOF
)

# 3. Wrap that tree in a commit object
COMMIT_HASH=$(echo "Initial manual commit" | git commit-tree $TREE_HASH)

# 4. Point the main branch ref at the new commit
git update-ref refs/heads/main $COMMIT_HASH

7. Why This Is Worth an Afternoon of Your Time

Once you've internalized "everything is a content-addressed object, and refs are just pointers to commits," a lot of previously scary commands stop being scary:

git reset --hard isn't erasing your work in some mysterious way — it's moving a ref, and the old commits usually still exist as unreferenced objects for a while (which is exactly what git reflog finds for you).

git rebase isn't rewriting history in place — it's building brand new commit objects with different parent pointers and moving the branch ref to the new tip. The old commits are still sitting in the object store, orphaned, until garbage collection eventually cleans them up.

git cherry-pick is just "take the diff this commit represents, apply it on top of my current tree, and create one new commit object."

None of these are special cases anymore. They're all the same four object types and the same handful of ref-pointer operations, recombined. That reframing is, honestly, worth more than memorizing another twenty flags.

RELATED TECHNICAL EXPLAINERS

VIEW ALL ARTICLES →
LINUX & OS9 MIN READ

Why CS Students Should Learn Linux Early with Fedora

Every CS student should gain hands-on Linux experience early. Fedora is an excellent environment for learning modern Linux tools, experimenting with current developer software, and building the operational habits that later transfer to Ubuntu, cloud servers, and production systems.

ESSENTIALRead
TECH & SYSTEMS11 MIN READ

JDK 26 Is Out. Here's Why "Gamechanger" Is the Wrong Word — and What's Actually True Instead

I updated the Java version in a side project's Dockerfile the same week JDK 26 went GA, mostly out of habit. A realistic breakdown of JDK 26's 10 JEPs — final features like HTTP/3 and GC-neutral AOT caching, preview features like Structured Concurrency (6th preview) and primitive patterns, and why non-LTS releases matter for feature iteration rather than immediate production deployments.

INTERMEDIATERead
BUSINESS8 MIN READ

Why Indian IT Companies Are Profitable but Hiring Less: A Data-Backed Look

A relative of mine joined Infosys in 2007. Engineering degree, campus placement, predictable career script. For the first time in its history, that machine is profitable and shrinking at the same time. Here is a data-backed look at why.

INTERMEDIATERead