Git has a reputation for a confusing command line sitting on top of a beautifully simple data model. The command line you can look up. The data model is worth genuinely understanding, because every command becomes predictable once you have it.
Git is a content-addressed key-value store
Underneath everything, Git stores objects in a database where the key is a hash of the value. Put content in, get a hash back; hand back the hash, get the content.
That single design choice produces most of Git's properties:
- Identical content is stored once. Two files with the same bytes hash the same, so there is one object.
- History is tamper-evident. A commit's hash covers its content and its parents, so altering anything old changes every hash after it.
- Integrity is free. Corruption changes the content, which no longer matches its own name.
The four object types
Blob — file contents
A blob is the bytes of a file. Just the bytes: no filename, no path, no permissions. Those live one level up.
Tree — a directory listing
A tree maps names to blobs and to other trees, with file modes. This is where filenames actually live:
100644 blob a45bd8… README.md 100644 blob 9f2c31… index.js 040000 tree c7e9a2… src
Because a tree references other trees, one top-level tree describes the entire project.
Commit — a snapshot plus context
A commit records:
- exactly one tree — the complete state of the project at that moment;
- zero or more parents — none for the first commit, one normally, two or more for a merge;
- author and committer, each with a timestamp;
- the commit message.
A commit stores a snapshot, not a diff. Git shows you diffs, but it computes them on demand by comparing two snapshots. Once this lands, a lot of confusion evaporates: rebasing is not "replaying patches onto a branch" in the storage sense, it is creating new commits; and checking out an ancient commit is as fast as checking out the newest one.
Tag — a named, annotated pointer
An annotated tag is an object with a target, a name, a tagger, a message and an optional GPG signature. A lightweight tag is not an object at all — just a ref file. Use annotated tags for releases; you want the metadata.
Refs: human names for hashes
Nobody wants to type a3f9c2e8…. Refs are files under .git/refs/ containing a hash:
.git/refs/heads/main— the branchmain.git/refs/tags/v1.0.0— the tagv1.0.0.git/refs/remotes/origin/main— whereorigin/mainwas when you last talked to the remote
So a branch is a 41-byte file. That is the whole implementation. It explains why creating a branch is instant on a repository of any size, and why deleting one destroys nothing.
HEAD
.git/HEAD normally contains not a hash but a reference to a branch:
ref: refs/heads/main
This indirection is the mechanism behind committing. When you commit, Git writes the new commit object, then updates whatever branch HEAD names to point at it. The branch advances because HEAD pointed at the branch, not at a commit.
If HEAD contains a hash directly, you are in detached HEAD — you can commit, but nothing advances, and moving away leaves those commits unreferenced.
Seeing it yourself
These are plumbing commands, not for daily use, but running them once makes the model concrete:
# What type of object is this?
git cat-file -t HEAD # commit
# Show the commit object
git cat-file -p HEAD
# tree 8f2a1c...
# parent 4b7d90...
# author You <you@example.com> 1737200000 +0530
# committer You <you@example.com> 1737200000 +0530
#
# Add login validation
# Show the tree it points at
git cat-file -p HEAD^{tree}
# Hash some content without storing it
echo "hello" | git hash-object --stdin
# ce013625030ba8dba906f756967f9e9ca394464a
That last hash is deterministic. Run it on any machine, any Git version, any operating system, and you get the same value — because it is a hash of the content and nothing else.
Git historically used SHA-1, and you will see 40-character hashes almost everywhere. SHA-256 repositories exist and produce 64-character hashes, but interoperability is still limited, so SHA-1 remains the default. The collision attacks published against SHA-1 do not translate into a practical attack on Git, which additionally uses collision detection — but it is why the migration path exists.