# How Git Works Internally: A Simple Guide

Most developers use Git every day `git add`, `git commit`, `git push` but have you ever wondered what actually happens behind the scenes? Understanding Git's internals transforms it from a mysterious tool into a logical system you can reason about. Let's peek under the hood.

## Creating a Git Repository: The `.git` Folder

When you run `git init` in a directory, Git creates a hidden folder called `.git`. This folder is Git's brain—it contains everything Git needs to track your project's history.

Let's see this in action:

```bash
$ mkdir my-project
$ cd my-project
$ git init
Initialized empty Git repository in /path/to/my-project/.git/
```

That's it! Your directory now has version control. But what did Git actually create?

## Inside the `.git` Folder

Let's explore what's inside:

```bash
$ ls -a
.  ..  .git

$ tree .git
.git/
├── HEAD
├── config
├── description
├── hooks/
├── info/
├── objects/
│   ├── info/
│   └── pack/
└── refs/
    ├── heads/
    └── tags/
```

Let's understand the important parts:

### Key Files and Folders

`HEAD` - A pointer to your current branch. Think of it as "You are here" marker.

```bash
$ cat .git/HEAD
ref: refs/heads/main
```

`config` - Repository-specific settings (your name, email, remote URLs, etc.)

`objects/` - This is where Git stores all your data (files, folders, commits). Everything!

`refs/` - References to commits (branches and tags are stored here)

* `refs/heads/` - Your branches live here
    
* `refs/tags/` - Your tags live here
    

`index` - The staging area (appears after your first `git add`)

## Git Objects: The Building Blocks

Git stores everything as objects. There are only four types, but we'll focus on three main ones:

1. **Blob** - Stores file content
    
2. **Tree** - Stores directory structure
    
3. **Commit** - Stores commit information
    

Let's understand each with examples.

### 1\. Blob Objects (File Content)

A blob stores the content of a file. Just the content—no filename, no permissions, nothing else.

Let's create our first blob:

```bash
$ echo "Hello, Git!" > README.md
$ git add README.md
```

Git just created a blob! Let's find it:

```bash
$ git ls-files --stage
100644 8d0e4... 0	README.md
```

That `8d0e4...` is the SHA-1 hash of the blob. Let's look inside the objects folder:

```plaintext
.git/objects/
├── 8d/
│   └── 0e41234abcd... (this is our blob!)
├── info/
└── pack/
```

Git splits the hash: first 2 characters become the folder name (`8d`), the rest becomes the filename.

We can view the blob content:

```bash
$ git cat-file -p 8d0e4
Hello, Git!
```

**Key Point**: The blob only contains "Hello, Git!"—it doesn't know it's called [README.md](http://README.md).

### 2\. Tree Objects (Directory Structure)

Trees store directory information. They link blobs to filenames and can point to other trees (subdirectories).

A tree object looks like this internally:

```plaintext
100644 blob 8d0e4...  README.md
100644 blob a1b2c...  index.html
040000 tree 3d4e5...  src
```

Each line says: "Here's a file/folder, its permissions, its type, its hash, and its name."

Let's create a more complex structure:

```bash
$ mkdir src
$ echo "console.log('Hello');" > src/app.js
$ git add .
```

Now if we could peek at the tree (we'll see how after we commit), it would look like:

```plaintext
100644 blob 8d0e4...  README.md
040000 tree 9f8e7...  src
```

And the `src` tree would contain:

```plaintext
100644 blob 6a7b8...  app.js
```

**Key Point**: Trees connect blobs to filenames and organize them into a directory structure.

### 3\. Commit Objects (Snapshots in Time)

A commit ties everything together. It points to a tree (the root directory at that moment) and stores metadata.

```bash
$ git commit -m "Initial commit"
[main (root-commit) a3f5b2c] Initial commit
```

A commit object contains:

```plaintext
tree 7d8e9f1...
author John Doe <john@example.com> 1704067200 +0000
committer John Doe <john@example.com> 1704067200 +0000

Initial commit
```

Let's visualize how these objects connect:

```plaintext
[Commit: a3f5b2c]
│
├─ tree: 7d8e9f1
├─ author: John Doe
├─ date: 2024-01-01
└─ message: "Initial commit"
     │
     └─> [Tree: 7d8e9f1]
          │
          ├─ blob: 8d0e4... → README.md
          └─ tree: 9f8e7... → src/
               │
               └─> [Tree: 9f8e7]
                    │
                    └─ blob: 6a7b8... → app.js
```

**Key Point**: A commit is a snapshot of your entire project at a specific moment. It points to the root tree, which points to all files and subdirectories.

## What Happens When You Run Git Commands?

Now let's see what actually happens inside `.git` when you use common Git commands.

### Step 1: Creating and Modifying a File

```bash
$ echo "# My Project" > README.md
```

**What happened in** `.git`? Nothing yet! Git doesn't track changes until you tell it to.

```plaintext
Working Directory: [README.md] ✓
Staging Area:      [ empty ]
Repository:        [ empty ]
```

### Step 2: Running `git add`

```bash
$ git add README.md
```

**What happened internally?**

1. Git reads the file content: "# My Project"
    
2. Git compresses it and calculates its SHA-1 hash: `3b18e512...`
    
3. Git creates a blob object in `.git/objects/3b/18e512...`
    
4. Git updates the staging area (`.git/index`) to record: "[README.md](http://README.md) → blob 3b18e512..."
    

```plaintext
.git/objects/
├── 3b/
│   └── 18e512... (contains "# My Project")
└── ...

.git/index
├── README.md → blob 3b18e512...
```

The file is now staged but not committed.

```plaintext
Working Directory: [README.md] ✓
Staging Area:      [README.md → blob 3b18e512] ✓
Repository:        [ empty ]
```

### Step 3: Running `git commit`

```bash
$ git commit -m "Add README"
[main (root-commit) a3f5b2c] Add README
```

**What happened internally?**

1. Git reads the staging area (`.git/index`)
    
2. Git creates a tree object representing the current directory structure
    
3. Git creates a commit object pointing to that tree
    
4. Git updates the branch reference (`refs/heads/main`) to point to the new commit
    
5. Git updates `HEAD` to track this branch
    

```plaintext
.git/objects/
├── 3b/
│   └── 18e512... (blob: "# My Project")
├── 7d/
│   └── 8e9f1c... (tree: root directory)
└── a3/
    └── f5b2c4... (commit: "Add README")

.git/refs/heads/main
└── a3f5b2c4... (points to commit)

.git/HEAD
└── ref: refs/heads/main
```

Now everything is committed:

```plaintext
Working Directory: [README.md] ✓
Staging Area:      [README.md → blob 3b18e512] ✓
Repository:        [Commit a3f5b2c] ✓
```

### Step 4: Making Another Commit

Let's add another file:

```bash
$ echo "print('Hello')" > main.py
$ git add main.py
$ git commit -m "Add main.py"
```

**What happened?**

1. New blob created for [`main.py`](http://main.py)
    
2. New tree created (contains both [README.md](http://README.md) and [main.py](http://main.py))
    
3. New commit created, with **parent** pointing to previous commit `a3f5b2c`
    

```plaintext
[Commit: b4c5d6e] "Add main.py"
│
├─ tree: 9e8f7a...
├─ parent: a3f5b2c  ← Links to previous commit!
└─ message: "Add main.py"
     │
     └─> [Tree: 9e8f7a]
          │
          ├─ blob: 3b18e5... → README.md
          └─ blob: 6f7a8b... → main.py
```

The complete history chain:

```plaintext
[b4c5d6e] Add main.py
    │
    └─ parent
        │
        ↓
[a3f5b2c] Add README
```

### Step 5: Creating a Branch

```bash
$ git branch feature
```

**What happened?**

Git simply created a new file:

```plaintext
.git/refs/heads/feature
└── b4c5d6e... (points to same commit as main)
```

That's it! A branch is just a pointer to a commit. This is why branches in Git are so lightweight—they're just 41 bytes (40-character hash + newline).

```plaintext
        HEAD → main
         ↓      ↓
     [b4c5d6e] Add main.py
         ↑
      feature
```

### Step 6: Switching Branches

```bash
$ git checkout feature
```

**What happened?**

Git just updated HEAD:

```plaintext
.git/HEAD
└── ref: refs/heads/feature
```

```plaintext
              main
               ↓
          [b4c5d6e] Add main.py
               ↑
        HEAD → feature
```

## How Git Uses Hashes for Integrity

Every object in Git (blob, tree, commit) has a SHA-1 hash based on its content. This creates an amazing property: **Git's entire history is tamper-proof**.

If you change a file:

* The blob hash changes
    
* The tree containing that blob hash changes
    
* The commit pointing to that tree hash changes
    
* Every subsequent commit hash changes (because parent hash changed)
    

This is why Git can detect corruption instantly—if any hash doesn't match, something is wrong.

## Understanding Git's Mental Model

Here's the key mental model to carry with you:

**Git is a content-addressable filesystem with a version control system built on top.**

* Every file content is stored as a blob
    
* Every directory structure is stored as a tree
    
* Every commit is a snapshot pointing to a tree
    
* Branches are just movable pointers to commits
    
* The entire history is a directed acyclic graph (DAG) of commits
    

When you understand that:

* `git add` creates objects and updates the staging area
    
* `git commit` creates a commit object and moves a branch pointer
    
* `git branch` creates a new pointer
    
* `git checkout` moves the HEAD pointer
    

...Git stops being mysterious and becomes logical.

## Exploring Git Objects Yourself

Want to see this for yourself? Try these commands:

```bash
# See what's in the staging area
git ls-files --stage

# View any object (replace hash with actual hash)
git cat-file -p <hash>

# See the type of an object
git cat-file -t <hash>

# Find the hash of a file
git hash-object README.md

# See commit history as a graph
git log --oneline --graph --all
```

## Conclusion

Git isn't magic it's just blobs, trees, and commits linked together with hashes. Branches are pointers. History is a graph. That's it.

Now when someone says "just rebase it" or you hit a merge conflict, you won't blindly copy Stack Overflow commands. You'll understand *why* things break and *how* to fix them. You'll think in objects and pointers, not mystery and fear.

So crack open that `.git` folder. Run `git cat-file` on everything. Break things in a test repo and watch what happens. Git's not mysterious anymore it's just data structures, and now you speak its language.
