Git is arguably the most widely used tool in software development, and yet almost nobody needs to know how it works internally to use it. But understanding its data model explains why it behaves the way it does: why it is almost impossible to lose work, why commits are immutable and why branches are so cheap.
A versioned file system, not a change log
Most older version control systems stored differences (deltas) between versions: version 2 was “version 1 plus these changes”. Git does the opposite: each state of the project is stored as a complete snapshot of the content at that moment. The key is that those snapshots are compressed and deduplicated with hashes, so the space they occupy does not explode.
The objects: blob, tree, commit and tag
Everything in Git boils down to four object types stored in the hidden .git/objects folder. Each one is identified by the SHA-1 hash of its content.
Blob (Binary Large Object). It stores the content of a file, with no name or metadata. It is exactly the same binary in every snapshot in which the file has not changed, so Git only stores it once: much of the efficiency comes from here.
Tree. It is a directory: a list of named entries with the file mode (permissions) and the hash of the blob or sub-tree they contain. This is what makes it possible to rebuild the folder structure from the loose objects.
Commit. It is the object that gives meaning to everything: it points to a tree (the project root), to the hash of one or more parent commits (its history), to an author and to a message. Because it includes its parent’s hash, the chain is linked: modifying anything in the past changes all subsequent hashes. That is why the history is immutable.
Annotated tag. It is a named reference to a specific commit, used to mark releases (v1.0, v2.1…).
The hash: the backbone
The hash is computed with the cryptographic algorithm SHA-1 (160 bits, 40 hexadecimal characters). Although SHA-1 is cryptographically broken in other contexts, Git uses it mainly as a content identifier and not as a security mechanism: integrity is protected by the fact that objects are immutable. Any corruption or tampering changes the hash and breaks the chain immediately and detectably.
That same hash is what enables deduplication: if two commits contain the same blob, only one physical copy exists. And it is what makes comparing or merging branches a graph problem, not a text problem.
Areas: working tree, index and HEAD
Git maintains three distinct zones that confuse many users:
- Working tree: the files as you see them on your disk, ready to edit.
- Index (staging area): the preparation area; the changes you have marked with
git addand that will form part of the next commit. - HEAD: a pointer to the commit you are currently on (normally the latest one of your current branch).
When you run git commit, Git creates a tree with the state of the index, then a commit pointing to that tree and its parent, and finally moves the branch (and HEAD) to that new commit.
Branches: just movable pointers
A branch in Git is nothing more than a named pointer to a commit. Creating one is instantaneous because it copies nothing: it only creates a 40-character reference. That is why Git “encourages” creating branches for every task, unlike other systems where a branch meant copying the whole codebase.
The current branch is identified by the .git/HEAD file, which contains the reference (for example ref: refs/heads/main). Moving HEAD between branches is literally changing the contents of that tiny file.
Merge, rebase and the commit graph
Git’s history is a directed acyclic graph (DAG): each commit points to its parents and cycles can never occur. Merging branches with git merge creates a commit with two parents, joining two lines of history at a single point. With git rebase, on the other hand, the commits of a branch are “repositioned” on top of another, rewriting their hashes to obtain a linear history. Changing the hashes is what makes rewriting public history dangerous: any clone that already had the old commits would become desynchronized.
Garbage collection and orphaned objects
When you rewrite history or delete branches, the old commits do not disappear instantly: they remain as orphaned (unreachable) objects. Periodically, git gc removes them and compacts objects into compressed “packs” (packfiles) with delta-compression. That is why there is the famous advice that “nothing is lost”: until gc runs, a deleted commit can be recovered with git reflog, which records every movement of HEAD and of the branches.
Why all this matters
Understanding Git as a graph of immutable objects identified by hash changes the way you work: it stops being a magic box and becomes a predictable model. It explains the power of branches, the safety of history, the efficiency of storage, and why tools like GitHub or GitLab are not “Git” but interfaces on top of it. And, above all, it turns mistakes into something recoverable: as long as the graph exists, there is almost always a way back.





