The Git Mindset: Philosophy and Foundational Mechanics
To master Git, you must first unlearn the myth that it is merely a tool for tracking file changes. It is, more profoundly, a *philosophy*—a radical re-imagining of what version control means, built upon a foundation of immutable data, cryptographic integrity, and a non-linear model of history. This chapter deconstructs that philosophy, translating its abstract principles into the concrete mental models required for expert-level workflows. We will move beyond `git commit` and `git push` to understand *why* Git behaves as it does, equipping you to predict its behavior in complex scenarios, design robust collaboration strategies, and recover from situations where the UI alone fails you. Our journey begins not with commands, but with the single most important data structure in all of version control: the Directed Acyclic Graph (DAG).
The Foundational Metaphor: Snapshots, Not Deltas
Traditional version control systems (VCS) like Subversion (SVN) are built on a linear, delta-based model. They store the *difference* (delta) between successive versions of a file. To reconstruct any version, the system must start from a base version and sequentially apply every stored delta. This model is intuitive—it mirrors how we think of editing a document: version 1, then version 2 with changes, then version 3 with more changes. History is a straight line.
Git rejects this paradigm entirely. Git stores complete snapshots of the entire project at each commit. When you commit, Git doesn't record *what changed*; it records a full picture of *what every file looked like* at that precise moment, but it does so with breathtaking efficiency through a mechanism called content-addressable storage. The implication is seismic: every commit is a standalone, self-contained artifact. You do not need any other commit to reconstruct it. This is the first pillar of the Git mindset: history is not a chain of edits, but a collection of immutable, interconnected states.
This snapshot model is what makes Git's branching and merging so powerful and lightweight. Creating a branch does not copy files; it creates a new, movable pointer to a snapshot. Switching branches does not transform your working directory; it replaces it entirely with another snapshot. Operations are not *transformations* but *substitutions* of one complete, valid project state for another.
The Directed Acyclic Graph (DAG): Git's True Data Structure
If commits are snapshots, how are they related? Through parent pointers. Every commit object (with the exception of the initial, root commit) contains a reference to one or more *parent* commits. This creates a Directed Acyclic Graph (DAG).
- **Directed**: The relationship has a direction—from child to parent. You can trace history *backwards* from a commit to its ancestors, but not forwards from an ancestor to its descendants without external references (branches, tags).
- **Acyclic**: There are no loops. A commit cannot be its own ancestor. This is enforced by the immutable nature of commit objects; once created, their parent pointer cannot be altered.
```mermaid graph TD A[Commit C0<br/>Root Commit] --> B[Commit C1<br/>Parent: C0] B --> C[Commit C2<br/>Parent: C1] C --> D[Commit C3<br/>Parent: C2<br/>Feature Branch] C --> E[Commit C4<br/>Parent: C2<br/>Main Branch] D --> F[Commit C5<br/>Merge Commit<br/>Parents: C3, C4] F --> G[Commit C6<br/>Parent: C5] ```
*Figure 1.1: A simple Git DAG. Arrows point from child to parent. Note the merge commit (C5) with two parents, creating a non-linear history.*
This DAG is the single source of truth for your project's history. Branches (`main`, `feature-x`) and tags (`v1.0`) are merely human-readable pointers (refs) to specific nodes (commits) in this graph. Their power and fragility stem from this: a branch is just a name for a commit. Moving a branch (`git branch -f main <new-commit>`) is simply re-pointing that name to a different node. Deleting a branch removes the name; the commits themselves persist as long as they are reachable from some other ref (another branch, a tag, or the `HEAD`).
Contrast with Linear Models: In a linear VCS, history is a single, unbreakable thread. Branching often involves physical copying of files. In Git's DAG, branching is a constant-time pointer operation. Merging is the process of finding a common ancestor (the merge base) and synthesizing a new commit that has *two parents*, thereby weaving two threads of history together without destroying either. This non-linear, graph-based model is the engine of Git's collaborative flexibility.
The Four Fundamental Object Types: Atoms of the Git Universe
Everything in Git is an *object*. These objects are immutable, content-addressed, and form the complete repository. They are stored in `.git/objects/`, compressed and named by their SHA-1 hash. Understanding these four types is understanding Git's DNA.
1. Blob: Raw Data
A blob (Binary Large OBject) is the simplest object: it stores the raw contents of a file. It has no filename, no permissions, no timestamp—just the exact byte stream of a file's content. Two files with identical content, anywhere in the repository, at any time, will point to the *same blob object*. This is Git's first deduplication mechanism. A blob's identity is its content: change a single byte, and you get a completely new blob with a new SHA-1 hash.
```bash
Create a file and see its blob
echo "Hello, World!" > greeting.txt git add greeting.txt
The blob is now in the object database. Find it:
git hash-object -w greeting.txt
Output: a 40-character SHA-1 like '3b18e510c5a32d5d8a0b5a2d...'
``` The blob hash is calculated over the string `"blob <content-length>\0<content>"`. The null byte (`\0`) is a critical part of the Git object format, preventing ambiguity.
2. Tree: Directory Structure
A tree object represents a snapshot of a directory. It maps filenames to *mode*, *type*, and *SHA-1 hash*. The type is almost always `blob` for files or `tree` for subdirectories. A tree is essentially a list of entries: ``` 100644 blob 3b18e5... greeting.txt 040000 tree 4a8258... src/ ``` A tree captures the *state* of a single directory. A complete project snapshot (like a commit) is represented by a root tree object, which recursively contains other tree objects for subdirectories and blob objects for files. This hierarchical structure is how Git efficiently stores full project snapshots without duplicating unchanged file contents across commits.
3. Commit: The Snapshot Point in Time
A commit object is the anchor of history. It contains:
- A pointer to a **root tree object** (the complete snapshot of the project at that moment).
- One or more **parent commit SHA-1s** (forming the DAG edges).
- **Author** and **committer** information (name, email, timestamp).
- A **commit message**.
The commit does not store *deltas*; it stores a pointer to a full tree snapshot. When you run `git show <commit>`, Git reconstructs that snapshot by recursively traversing the tree objects starting from the commit's root tree.
4. Tag: A Named, Immutable Pointer
A tag object is a named, immutable reference to *any* other object (usually a commit). It stores:
- The **SHA-1 of the object** it tags.
- A **tag name** (e.g., `v1.0.0`).
- **Tagger** information and a **message**.
- An optional **PGP signature** for cryptographic verification.
Tags are not branches. Branches move; tags are fixed. They are the primary mechanism for marking release points in a project's history.
Interrelationships Visualized: ```plantuml @startuml class Commit { + tree: SHA1 (root tree) + parent: [SHA1] + author: string + message: string } class Tree { + entries: {filename -> (mode, type, SHA1)} } class Blob { + data: bytes } class Tag { + object: SHA1 + name: string + message: string }
Commit "1" *-- "1" Tree : root tree > Tr