Skip to main content

Command Palette

Search for a command to run...

How Git Works Internally: A Simple Guide

Updated
8 min readView as Markdown
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:

$ 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:

$ 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.

$ 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:

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

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

$ 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:

.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:

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

Key Point: The blob only contains "Hello, Git!"—it doesn't know it's called 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:

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:

$ 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:

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

And the src tree would contain:

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.

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

A commit object contains:

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:

[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

$ echo "# My Project" > README.md

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

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

Step 2: Running git add

$ 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 → blob 3b18e512..."

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

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

The file is now staged but not committed.

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

Step 3: Running git commit

$ 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

.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:

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

Step 4: Making Another Commit

Let's add another file:

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

What happened?

  1. New blob created for main.py

  2. New tree created (contains both README.md and main.py)

  3. New commit created, with parent pointing to previous commit a3f5b2c

[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:

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

Step 5: Creating a Branch

$ git branch feature

What happened?

Git simply created a new file:

.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).

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

Step 6: Switching Branches

$ git checkout feature

What happened?

Git just updated HEAD:

.git/HEAD
└── ref: refs/heads/feature
              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:

# 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.

More from this blog