How Git works internally
Git stores everything as content-addressed objects: blobs, trees, commits, and tags. Understanding the object model, DAG structure, and ref mechanics explains why rebase, merge, and reset work the way they do.
The Problem Statement
Interviewer: "You accidentally ran
git reset --hardon the wrong branch and lost three days of commits. Walk me through what Git actually did under the hood. Are those commits truly gone? How would you recover them?"
This question tests three things: whether you understand Git's internal object model (content-addressed storage, the commit DAG), whether you understand what operations like reset, merge, and rebase actually do to the data structures, and whether you can reason about data recovery from first principles rather than just memorizing commands.
I find this question incredibly revealing. Candidates who know Git as a series of commands ("commit, push, pull, merge") will panic at the scenario. Candidates who understand that Git is a content-addressed object store with pointers will calmly explain that git reset --hard only moves a pointer, the commit objects still exist, and git reflog tracks where the pointer used to be.
What you will walk away with after reading this:
- The four object types (blob, tree, commit, tag) and how they form a Merkle DAG.
- Why content-addressable storage means identical files are stored once regardless of how many commits reference them.
- What branches, tags, and HEAD actually are (just text files containing SHA hashes).
- What merge and rebase actually do to the DAG and why rebase rewrites history.
- How the index (staging area) mediates between your working directory and the repository.
- How packfiles and delta compression keep Git fast despite storing every version of every file.
- Why reflog is your safety net and how garbage collection can remove unreachable objects.
Git is one of those tools that everyone uses and almost nobody truly understands. Once you see the object model, everything else (merge conflicts, detached HEAD, interactive rebase, cherry-pick) becomes logical consequences of a simple data structure. This article gives you that mental model.
The analogy I like: Git is a journal written in pen, not pencil. You can add new pages (commits), but you cannot erase old pages (immutability). You can put sticky notes on pages (branches, tags), and you can move those sticky notes around (checkout, reset). The pages themselves never change.
Clarifying the Scenario
You: "Before I dive into the recovery, let me build up from Git's internals so the answer makes sense."
You: "When you say I lost three days of commits, should I assume these were local commits that were never pushed to a remote?"
Interviewer: "Yes, purely local. Never pushed."
You: "Got it. And should I focus on the object model and data structures, or do you want me to go into the network protocol (push, fetch, pack negotiation) as well?"
Interviewer: "Focus on the local internals. Cover how Git stores data and why operations like reset, merge, and rebase work the way they do."
You: "Perfect. I will structure this in four parts: the object model (blobs, trees, commits), the DAG and refs, how merge and rebase transform the DAG, and then the recovery answer using reflog."
My Approach
Git is not a diff-based system. It is a content-addressed object store with version control layered on top. Every single piece of data (file contents, directory structure, commit metadata) is stored as an immutable object identified by the SHA-1 hash of its contents. Once you internalize this, every Git operation becomes a logical manipulation of objects and pointers.
I break this into four parts:
- The object model: The four object types (blob, tree, commit, tag) and how content-addressing works.
- The DAG and refs: How commits form a directed acyclic graph and how branches, tags, and HEAD are just mutable pointers into that graph.
- Merge vs rebase: What these operations actually do to the commit graph, and why rebase creates new commit objects with different hashes.
- Storage optimization: How the index, packfiles, and garbage collection keep Git fast and compact.
Here is a quick reference for the core concepts:
| Concept | What it is | Where it lives |
|---|---|---|
| Blob | Raw file contents (no filename) | .git/objects/ |
| Tree | Directory listing: filenames + blob/tree references | .git/objects/ |
| Commit | Snapshot pointer (tree) + parent(s) + metadata | .git/objects/ |
| Tag | Named, optionally signed pointer to any object | .git/objects/ or .git/refs/tags/ |
| Branch | Mutable pointer: a text file containing a commit SHA | .git/refs/heads/ |
| HEAD | Pointer to the current branch (or directly to a commit) | .git/HEAD |
| Index | Staging area: the snapshot that becomes the next commit | .git/index |
| Reflog | History of where each ref has pointed | .git/logs/ |
| Packfile | Compressed bundle of objects with delta encoding | .git/objects/pack/ |
The core insight is that Git never modifies objects. Every operation creates new objects or moves pointers. When you "amend" a commit, Git creates a new commit object with a different hash. The old commit still exists in the object store (until garbage collection removes it). This immutability is what makes Git robust and recoverable.
The Architecture
Here is how the four object types relate to each other. A commit points to a tree, which points to blobs and sub-trees, forming a complete snapshot of the repository at that point in time.
Walk through what is happening here:
Blobs store raw file contents with no filename attached. The SHA-1 hash is computed from the content bytes prefixed with the object type and size (e.g., blob 42\0...content...). If two files have identical content (even in different directories or different commits), they share the same blob object. This is automatic deduplication. Renaming a file does not create a new blob because the content has not changed; only the parent tree changes.
Trees store a directory listing. Each entry has a file mode (100644 for regular files, 100755 for executables, 040000 for subdirectories), an object type (blob or tree), a SHA-1 hash, and a filename. Trees can reference other trees (subdirectories). Notice how blob1 (README.md) is referenced by all three trees: T1, T2, and T3. Git does not re-store the file. This is why git mv is cheap: it creates a new tree, not a new blob.
Commits tie everything together. Each commit points to exactly one tree (the root of the project at that moment), one or more parent commits (except the initial commit, which has no parent), and metadata (author, committer, timestamp, message). The commit's SHA-1 hash is derived from all of these fields combined (including the parent hash), which means changing any ancestor commit changes every descendant's hash. This creates a Merkle DAG: a tamper-evident, cryptographically verifiable history chain.
Refs are just text files. refs/heads/main contains the 40-character hex string c4a2b8.... Moving a branch is literally writing a new SHA into that file. HEAD is a special ref that usually contains ref: refs/heads/main (attached to a branch) or a raw commit SHA (detached HEAD).
Git's content-addressing means that git status is extremely fast. To check if a file changed, Git computes the SHA-1 of the working copy and compares it to the hash stored in the index. If they match, the file is unchanged. No need to diff the actual content. This is why Git can check the status of a repository with 100,000 files in milliseconds.
For your interview: say "Git is a content-addressed object store. Blobs store file contents, trees store directory structures, and commits point to a root tree plus their parent commits, forming a Merkle DAG." That single sentence shows you understand the data model, not just the commands.
The Object Model and Content-Addressable Storage
Content-addressable storage is the core insight that makes Git work. Every object is stored at a path derived from its SHA-1 hash: the first two hex characters become a directory name, the remaining 38 become the filename. For example, a blob with hash a3b4c5d6... lives at .git/objects/a3/b4c5d6....
You can verify this yourself. Run echo "hello" | git hash-object --stdin and it returns ce013625030ba8dba906f756967f9e9ca394464a. Run it again, same hash. Run it on any machine in the world, same hash. The hash is purely a function of the content. This is the foundation that everything else builds on.
This has three powerful consequences:
Automatic deduplication. If the same file appears in 1,000 commits, it is stored once. The 1,000 tree objects all point to the same blob hash. When you copy a file to a new directory, Git does not copy the blob.
Integrity verification. Since the hash is derived from the content, Git can verify any object by recomputing its hash. If the hash does not match (disk corruption, tampering), Git detects it immediately. This is the same principle as Merkle trees in blockchain and certificate transparency logs.
Immutability. You cannot change an object without changing its hash. Changing a blob changes the tree that references it, which changes the commit that references that tree, which changes every descendant commit. This cascading hash change is why "rewriting history" in Git means creating entirely new commit objects.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.