Git Refresher: Beyond the Basics
Git Refresher: Beyond the Basics
Every developer has a Git workflow. Most developers have a Git *habit*. The difference matters more than you'd think. If you've ever found yourself in the middle of a merge conflict wondering why Git is showing you something called "ours" versus "theirs," or if you've typed `git reset --hard` and then sat in stunned silence as your uncommitted work vanished, then you've already learned the hard way that Git rewards understanding over memorization.
The question isn't whether you know the commands. You do. The question is whether you understand what happens when you run them—and that understanding starts with a single, foundational concept: Git is a content-addressable filesystem with a version control interface bolted on. Once that clicks, everything else follows.
This chapter peels back the surface-level commands you use daily and reveals the elegant machinery underneath. We'll trace exactly what happens when you commit, explore how Git actually stores your work, and build the mental model that will make every branch operation, merge strategy, and recovery scenario feel intuitive rather than magical. By the end, you'll have a repository you built yourself, with a complete understanding of every file Git created and why.
Let's start where all Git journeys begin: with three trees you probably never consciously thought about.
The Three-Tree Architecture
When you work with Git, you're constantly moving changes between three distinct locations. Most tutorials introduce these abstractly—"working directory," "staging area," "repository"—but understanding *why* Git has three trees, and what each one actually represents, is what separates developers who use Git from developers who *comprehend* Git.
What Each Tree Actually Is
The working directory is your actual project folder—the files you see and edit in your code editor. This is the most familiar tree because it's the only one you interact with directly. When you create a new file or modify an existing one, those changes appear here first. Think of it as your sandbox, your workbench, your "files on disk."
The staging area (sometimes called the index or cache) is a file—typically `.git/index`—that acts as a snapshot of what the *next* commit will look like. When you run `git add`, you're taking the files you've modified in your working directory and placing them into this staging area. The staging area doesn't store your file contents; it stores references to where those contents will be stored in the repository. More on that in a moment.
The repository is where Git permanently stores your project's history. Everything in the repository is stored in the `.git/objects` directory as compressed, content-addressed objects. When you run `git commit`, you're taking everything in the staging area and turning it into a permanent snapshot, complete with metadata, author information, and a pointer to the previous commit.
The Flow of Changes
Here's what actually happens during a typical workflow:
```mermaid flowchart LR subgraph WD["Working Directory"] A["file.txt"] end
subgraph SA["Staging Area<br/>(Index)"] B["index entry<br/>for file.txt"] end
subgraph REPO["Repository<br/>(.git/objects)"] C["blob object"] D["commit object"] end
WD -->|"git add"| SA SA -->|"git commit"| REPO REPO -->|"git checkout/reset"| SA SA -->|"git checkout/reset"| WD
style WD fill:#e1f5fe style SA fill:#fff3e0 style REPO fill:#e8f5e9 ```
When you modify `file.txt` in your working directory and run `git add file.txt`, Git computes a SHA-1 hash of the file's contents, compresses the file using zlib, and stores it in `.git/objects` as a blob object. The staging area then records: "At this moment, I want `file.txt` to point to this blob." When you run `git commit`, Git creates a commit object that points to a tree object, which in turn points to all the blob objects representing your project at that moment.
The power of this architecture becomes clear when you understand that *nothing is duplicated*. If you change a single line in a 10,000-line file, Git doesn't store a new copy of the entire file. It stores a new blob for that file (because the file's content hash changed), but any unchanged files continue pointing to their existing blobs. This is why Git repositories are typically so storage-efficient, and it's why understanding blobs is crucial for understanding how branches, diffs, and history work.
Why Three Trees? The Practical Reason
You might wonder why Git doesn't just have two states: modified files and committed files. The staging area exists to solve a real problem: the gap between "I want to save my work" and "I want to save *exactly* this work."
Imagine you're building a feature that requires changes to three files. You've finished the first two and they're solid. The third file is in progress—you like some of the changes but want to experiment further. Without staging, you'd either have to commit an incomplete third file or leave everything uncommitted and vulnerable. With staging, you can add the two finished files, commit them with a meaningful message, and continue working on the third without losing anything.
More subtly, the staging area lets you craft precise commits. You can add specific hunks from a file (using `git add -p`), staging only the changes that belong together logically. A commit becomes not just a snapshot but a coherent unit of work—a bug fix, a refactoring, a feature increment.
The three-tree architecture is Git's answer to the question every version control system must answer: how do you give developers granular control over what becomes part of history, without making that control feel like bureaucracy? The answer is staging: a deliberate, explicit pause between "I've done work" and "This work is permanent."
Inside the .git Directory
Now that you understand the conceptual trees, let's look at where the real action happens. The `.git` directory is the entire universe of your Git repository. Everything Git knows about your project lives here. Understanding its structure demystifies Git's behavior and gives you the confidence to troubleshoot when things go wrong.
The Directory Layout
When you run `git init` in an empty folder, Git creates a `.git` directory with this basic structure:
``` .git/ ├── HEAD ├── config ├── description ├── hooks/ ├── info/ ├── objects/ └── refs/ ```
Each component has a purpose. The `HEAD` file points to your current branch (more on this shortly). The `config` file stores repository-specific settings that override your global Git configuration. The `description` file is used by GitWeb (not something most developers touch directly). The `hooks/` directory contains scripts that run at specific points in Git's workflow—we'll explore these in later chapters when we discuss team workflows.
For now, focus on two directories: `objects/` and `refs/`. These are where Git stores everything that matters.
The Objects Directory: Git's Content Store
The `objects/` directory is where Git stores your actual data. All objects are content-addressed, meaning the object's *name* (a 40-character SHA-1 hash) is derived from its *content*. This is a crucial insight: if two files have identical content, they will have the same hash, and Git will store them as a single object. This is how Git achieves its legendary storage efficiency.
Git has four types of objects:
Blobs store file contents. A blob contains raw file data—nothing else. No filename, no path, no metadata. The filename is stored separately in tree objects.
Trees store directory listings. A tree contains pointers to blobs (for files) and other trees (for subdirectories), along with the filenames and file mode information. A tree is essentially a snapshot of what a directory looked like at a specific point in time.
Commits store