Skip to main content

Command Palette

Search for a command to run...

Inside Git: How It Works and the Role of the .git Folder

Updated
13 min readView as Markdown

Introduction: Beyond Git Commands

You've learned Git commands like git add, git commit, and git push. They work like magic! But have you ever wondered:

  • Where does Git actually store your file history?

  • How does Git know what changed?

  • What's inside that mysterious .git folder?

  • Why does Git use those weird hash strings like a1b2c3d4e5f6?

Today, we're going inside Git to understand how it really works. This knowledge will transform you from someone who memorizes commands to someone who truly understands Git.

No magic, just clever engineering!


What is the .git Folder?

When you run git init, Git creates a hidden folder called .git in your project directory:

MyProject/
├── index.html
├── style.css
└── .git/          ← All Git magic happens here!

The .git folder is Git's database. Everything Git knows about your project lives here:

  • Every commit you've ever made

  • Every file version throughout history

  • Branch information

  • Configuration settings

┌─────────────────────────────────────────────────────┐
│  Think of .git as a time machine's storage vault    │
│                                                     │
│  Your files → Present                               │
│  .git folder → Past, present, and all timelines     │
└─────────────────────────────────────────────────────┘

IMPORTANT: Never manually edit or delete files in .git unless you know exactly what you're doing!


Structure of the .git Directory

Let's explore what's inside .git:

.git/
├── HEAD                    ← Points to current branch
├── config                  ← Repository configuration
├── description             ← Repository description
├── index                   ← Staging area (binary file)
├── hooks/                  ← Scripts that run on Git events
├── objects/                ← All your data (commits, files, etc.)
│   ├── 12/
│   │   └── 34ab56...       ← Git objects (hashed)
│   ├── 78/
│   │   └── 9cde01...
│   ├── info/
│   └── pack/               ← Compressed objects
├── refs/                   ← Pointers to commits
│   ├── heads/              ← Branch pointers
│   │   └── main
│   └── tags/               ← Tag pointers
└── logs/                   ← History of ref changes

Key Components Explained

┌──────────────────┬────────────────────────────────────────┐
│ File/Folder      │ Purpose                                │
├──────────────────┼────────────────────────────────────────┤
│ HEAD             │ "You are here" marker                  │
│ index            │ Staging area contents                  │
│ objects/         │ DATABASE: All commits & file versions  │
│ refs/heads/      │ Branch pointers                        │
│ config           │ Repository settings                    │
│ hooks/           │ Custom automation scripts              │
└──────────────────┴────────────────────────────────────────┘

Let's examine the most important ones:

1. HEAD File

cat .git/HEAD

Output:

ref: refs/heads/main

What it means:
HEAD points to refs/heads/main, which tells Git you're currently on the main branch.

Visual:

HEAD → refs/heads/main → (commit hash)
                         ↓
                     Latest commit on main branch

2. refs/heads/ Directory

Contains files for each branch, where each file contains a commit hash:

cat .git/refs/heads/main

Output:

a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0

This hash is the latest commit on the main branch.

3. objects/ Directory

This is Git's database! Every file version, commit, and tree is stored here as an object.

Objects are stored using their hash as a filename:

Hash: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
                ↓
Stored as: .git/objects/a1/b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
           (first 2 chars as folder, rest as filename)

How Git Works Internally: The Big Picture

Git is built on a simple but powerful concept:

Everything in Git is stored as objects, identified by unique hashes.

┌─────────────────────────────────────────────────────┐
│           GIT'S INTERNAL MODEL                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Your Files                                         │
│      ↓                                              │
│  Git creates OBJECTS                                │
│      ↓                                              │
│  Objects are stored in .git/objects/                │
│      ↓                                              │
│  Objects are identified by HASH                     │
│      ↓                                              │
│  Branches point to COMMITS (which point to objects) │
│                                                     │
└─────────────────────────────────────────────────────┘

Git Objects: The Three Types

Git uses three types of objects to store everything:

  1. Blob (Binary Large Object) → Stores file contents

  2. Tree → Stores directory structure

  3. Commit → Stores snapshot metadata

┌─────────────────────────────────────────────────────┐
│              GIT OBJECT TYPES                       │
└─────────────────────────────────────────────────────┘

Commit Object
   ├─► Points to a Tree (directory snapshot)
   ├─► Contains author, message, timestamp
   └─► Points to parent commit(s)

Tree Object
   ├─► Represents a directory
   ├─► Lists files (points to Blobs)
   └─► Lists subdirectories (points to other Trees)

Blob Object
   └─► Stores raw file content

Let's explore each in detail!


1. Blob Object: Storing File Content

A blob stores the actual contents of a file, nothing more.

Example:

File: index.html
Content: <h1>Hello World</h1>
            ↓
Git creates Blob: "blob 20\0<h1>Hello World</h1>"
            ↓
Hashes it: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
            ↓
Stores at: .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad

Key insight:
Blobs don't store filenames, permissions, or directory structure – only content!

Visual:

┌─────────────────────────┐
│  Blob Object            │
├─────────────────────────┤
│ Type: blob              │
│ Size: 20 bytes          │
│ Content:                │
│ <h1>Hello World</h1>    │
└─────────────────────────┘

Important: If two files have identical content, Git stores only ONE blob!

index.html:  <h1>Hello</h1>
about.html:  <h1>Hello</h1>
                ↓
           SAME BLOB!

This is how Git saves space!


2. Tree Object: Storing Directory Structure

A tree represents a directory. It stores:

  • Filenames

  • File permissions

  • Pointers to blobs (files) or other trees (subdirectories)

Example:

MyProject/
├── index.html      (blob: 3b18e51...)
└── css/
    └── style.css   (blob: 7f92a3d...)

Tree for MyProject/ directory:

┌─────────────────────────────────────────┐
│  Tree Object (MyProject/)               │
├─────────────────────────────────────────┤
│ 100644 blob 3b18e51  index.html         │
│ 040000 tree 9c7f2a8  css                │
└─────────────────────────────────────────┘

Tree for css/ subdirectory:

┌─────────────────────────────────────────┐
│  Tree Object (css/)                     │
├─────────────────────────────────────────┤
│ 100644 blob 7f92a3d  style.css          │
└─────────────────────────────────────────┘

Visual Relationship:

Tree (root)
├─► Blob (index.html)
└─► Tree (css/)
    └─► Blob (style.css)

Permission codes:

  • 100644 = Regular file

  • 100755 = Executable file

  • 040000 = Directory (tree)

  • 120000 = Symbolic link


3. Commit Object: Storing Snapshots

A commit is a snapshot of your entire project at a specific point in time.

Commit object contains:

┌─────────────────────────────────────────┐
│  Commit Object                          │
├─────────────────────────────────────────┤
│ tree:    abc123...    (root directory)  │
│ parent:  def456...    (previous commit) │
│ author:  John <john@example.com>        │
│ date:    Fri Jan 31 10:30:00 2026       │
│ message: "Add homepage"                 │
└─────────────────────────────────────────┘

Visual:

Commit a1b2c3d
    │
    ├─► tree: xyz789    (points to root directory snapshot)
    ├─► parent: none    (first commit has no parent)
    ├─► author: John
    └─► message: "Initial commit"

Commit d4e5f6g
    │
    ├─► tree: abc123
    ├─► parent: a1b2c3d  (points to previous commit)
    ├─► author: John
    └─► message: "Add styles"

Complete Object Relationship Diagram

Here's how everything connects:

┌───────────────────────────────────────────────────────────┐
│        COMPLETE GIT OBJECT STRUCTURE                      │
└───────────────────────────────────────────────────────────┘

Commit: a1b2c3d "Initial commit"
   │
   └─► Tree (root directory)
        ├─► Blob: 3b18e51  (index.html content)
        ├─► Blob: 7f92a3d  (README.md content)
        └─► Tree (css/ subdirectory)
             └─► Blob: 9c7f2a8  (style.css content)

        ↓ (make changes and commit again)

Commit: d4e5f6g "Update homepage"
   │
   ├─► parent: a1b2c3d  (points back to previous commit)
   │
   └─► Tree (root directory)
        ├─► Blob: 8h9i0j1  (index.html - CHANGED)
        ├─► Blob: 7f92a3d  (README.md - UNCHANGED, reuses old blob!)
        └─► Tree (css/ subdirectory)
             └─► Blob: 9c7f2a8  (style.css - UNCHANGED, reuses old blob!)

Key insight:
Git only stores what changed! Unchanged files reuse the same blob.


How Git Tracks Changes: Content-Addressable Storage

Git uses hashing to identify objects. This is called content-addressable storage.

What is a Hash?

A hash is a unique fingerprint for data, generated by a hash function (Git uses SHA-1, moving to SHA-256).

Input:  "Hello World"
   ↓ SHA-1 hash function
Output: 0a4d55a8d778e5022fab701977c5d840bbc486d0

Properties:

  • Same input always produces same hash

  • Different input almost never produces same hash

  • Hash is always 40 characters (SHA-1)

  • Can't reverse a hash to get original content

Git's use:

File content → Hash → Object filename

Example:

index.html contains: <h1>Hello World</h1>
         ↓
Git hashes the content
         ↓
Hash: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
         ↓
Stored as: .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad

Why Use Hashes?

  1. Detect Changes Instantly

     Old hash: 3b18e51...
     New hash: 7f92a3d...  → Content changed!
    
  2. Ensure Data Integrity

     If content is corrupted, hash won't match → Git detects it
    
  3. Efficient Storage

     Same content = Same hash = Store once
    

What Happens Internally During git add

Let's trace what happens step-by-step when you run git add index.html:

┌─────────────────────────────────────────────────────┐
│  BEFORE git add                                     │
├─────────────────────────────────────────────────────┤
│  Working Directory                                  │
│  ├── index.html (modified)                          │
│                                                     │
│  Staging Area (empty)                               │
│                                                     │
│  .git/objects/ (unchanged)                          │
└─────────────────────────────────────────────────────┘

Step 1: Git Reads the File

git add index.html

Git reads index.html content.

Step 2: Git Computes Hash

Content: <h1>Hello World</h1>
    ↓
Hash: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad

Step 3: Git Creates Blob Object

Git stores the file content as a blob in .git/objects/:

.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad

Step 4: Git Updates Index (Staging Area)

Git updates .git/index (a binary file) to record:

index.html → blob 3b18e51... (staged)

Complete Flow Diagram

┌─────────────────────────────────────────────────────┐
│         git add index.html                          │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 1. Read file content                                │
│    index.html: <h1>Hello World</h1>                 │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 2. Hash the content                                 │
│    SHA-1: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 3. Create blob object                               │
│    Store at: .git/objects/3b/18e512...              │
│    Content: compressed file data                    │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 4. Update staging area (.git/index)                 │
│    Record: index.html → blob 3b18e51...             │
└─────────────────────────────────────────────────────┘
         │
         ↓
    File staged!

After git add:

┌─────────────────────────────────────────────────────┐
│  AFTER git add                                      │
├─────────────────────────────────────────────────────┤
│  Working Directory                                  │
│  ├── index.html                                     │
│                                                     │
│  Staging Area                                       │
│  ├── index.html → blob 3b18e51...                   │
│                                                     │
│  .git/objects/                                      │
│  └── 3b/18e512... (blob created!)                   │
└─────────────────────────────────────────────────────┘

What Happens Internally During git commit

Now let's see what happens when you run git commit -m "Add homepage":

┌─────────────────────────────────────────────────────┐
│  BEFORE git commit                                  │
├─────────────────────────────────────────────────────┤
│  Staging Area                                       │
│  ├── index.html → blob 3b18e51...                   │
│  └── style.css → blob 7f92a3d...                    │
│                                                     │
│  .git/objects/                                      │
│  ├── 3b/18e512... (blob for index.html)             │
│  └── 7f/92a3d... (blob for style.css)               │
└─────────────────────────────────────────────────────┘

Step 1: Git Creates Tree Objects

Git creates tree objects representing directory structure:

Root Tree:
├── index.html → blob 3b18e51...
└── style.css → blob 7f92a3d...

Git hashes this tree structure:

Tree hash: abc123def456...

Stores it:

.git/objects/ab/c123def456...

Step 2: Git Creates Commit Object

Git creates a commit object containing:

tree abc123def456...
parent d4e5f6g... (previous commit, if any)
author John Doe <john@example.com> 1738324200 +0600
committer John Doe <john@example.com> 1738324200 +0600

Add homepage

Git hashes this commit:

Commit hash: h7i8j9k0l1m2...

Stores it:

.git/objects/h7/i8j9k0l1m2...

Step 3: Git Updates Branch Pointer

Git updates .git/refs/heads/main to point to the new commit:

h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z5a6

Complete Flow Diagram

┌─────────────────────────────────────────────────────┐
│      git commit -m "Add homepage"                   │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 1. Read staging area (.git/index)                   │
│    Files staged:                                    │
│    - index.html → blob 3b18e51...                   │
│    - style.css → blob 7f92a3d...                    │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 2. Create tree object (directory snapshot)          │
│    Tree content:                                    │
│    100644 blob 3b18e51 index.html                   │
│    100644 blob 7f92a3d style.css                    │
│    ↓                                                │
│    Hash: abc123def456...                            │
│    Store at: .git/objects/ab/c123def456...          │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 3. Create commit object                             │
│    Commit content:                                  │
│    tree abc123...                                   │
│    parent d4e5f6g... (previous commit)              │
│    author John Doe                                  │
│    message "Add homepage"                           │
│    ↓                                                │
│    Hash: h7i8j9k0l1m2...                            │
│    Store at: .git/objects/h7/i8j9k0l1m2...          │
└─────────────────────────────────────────────────────┘
         │
         ↓
┌─────────────────────────────────────────────────────┐
│ 4. Update branch pointer                            │
│    .git/refs/heads/main → h7i8j9k0l1m2...           │
└─────────────────────────────────────────────────────┘
         │
         ↓
   Commit complete!

After git commit:

┌─────────────────────────────────────────────────────┐
│  AFTER git commit                                   │
├─────────────────────────────────────────────────────┤
│  .git/objects/                                      │
│  ├── 3b/18e512... (blob: index.html)                │
│  ├── 7f/92a3d... (blob: style.css)                  │
│  ├── ab/c123def... (tree: root directory)           │
│  └── h7/i8j9k... (commit object)                    │
│                                                     │
│  .git/refs/heads/main                               │
│  └── h7i8j9k0l1m2... (points to new commit)         │
│                                                     │
│  HEAD → refs/heads/main → h7i8j9k... (you are here) │
└─────────────────────────────────────────────────────┘

Complete Internal Flow: From File to Commit

┌────────────────────────────────────────────────────────────┐
│  COMPLETE INTERNAL FLOW                                    │
└────────────────────────────────────────────────────────────┘

1. Create/Edit File
   ┌──────────────────┐
   │ index.html       │
   │ <h1>Hello</h1>   │
   └──────────────────┘

2. git add index.html
         ↓
   ┌────────────────────────────────────┐
   │ Compute hash of content            │
   │ Hash: 3b18e51...                   │
   └────────────────────────────────────┘
         ↓
   ┌────────────────────────────────────┐
   │ Create blob object                 │
   │ .git/objects/3b/18e51...           │
   └────────────────────────────────────┘
         ↓
   ┌────────────────────────────────────┐
   │ Update staging area                │
   │ .git/index                         │
   │ index.html → 3b18e51...            │
   └────────────────────────────────────┘

3. git commit -m "Add homepage"
         ↓
   ┌────────────────────────────────────┐
   │ Create tree object                 │
   │ (directory structure)              │
   │ Hash: abc123...                    │
   │ .git/objects/ab/c123...            │
   └────────────────────────────────────┘
         ↓
   ┌────────────────────────────────────┐
   │ Create commit object               │
   │ - tree: abc123...                  │
   │ - parent: previous commit          │
   │ - author, message, timestamp       │
   │ Hash: h7i8j9k...                   │
   │ .git/objects/h7/i8j9k...           │
   └────────────────────────────────────┘
         ↓
   ┌────────────────────────────────────┐
   │ Update branch pointer              │
   │ .git/refs/heads/main → h7i8j9k...  │
   └────────────────────────────────────┘
         ↓
   ┌────────────────────────────────────┐
   │ Commit complete!                   │
   │ Project snapshot saved             │
   └────────────────────────────────────┘

How Git Detects Changes

Git uses hashes to detect changes efficiently:

Scenario 1: File Unchanged

Old commit:
  index.html → blob 3b18e51...

You run: git add index.html
Git computes hash: 3b18e51...
                   ↓
Hash matches! File unchanged, reuse existing blob

Scenario 2: File Changed

Old commit:
  index.html → blob 3b18e51...

You modify index.html

You run: git add index.html
Git computes new hash: 7f92a3d...
                       ↓
Hash different! File changed, create new blob

Visual:

┌────────────────────────────────────────────────────┐
│  HOW GIT DETECTS CHANGES                           │
└────────────────────────────────────────────────────┘

Old Content:        New Content:
<h1>Hello</h1>      <h1>Hello World</h1>
      ↓                    ↓
Hash: 3b18e51...    Hash: 7f92a3d...
      ↓                    ↓
   DIFFERENT HASHES = CONTENT CHANGED

This is why Git is so fast – it just compares hashes!


Practical Example: Exploring Git Objects

Let's create a real repository and examine its objects.

Step 1: Create Repository

mkdir test-repo
cd test-repo
git init

Step 2: Create and Commit a File

echo "Hello Git" > README.md
git add README.md
git commit -m "Initial commit"

Step 3: Find the Commit Hash

git log --oneline

Output:

a1b2c3d (HEAD -> main) Initial commit

Step 4: Examine the Commit Object

git cat-file -p a1b2c3d

Output:

tree 4d5e6f7...
author John Doe <john@example.com> 1738324200 +0600
committer John Doe <john@example.com> 1738324200 +0600

Initial commit

Step 5: Examine the Tree Object

git cat-file -p 4d5e6f7

Output:

100644 blob 8h9i0j1  README.md

Step 6: Examine the Blob Object

git cat-file -p 8h9i0j1

Output:

Hello Git

Visual of what we found:

Commit: a1b2c3d
   ├─► tree: 4d5e6f7
   │      └─► blob: 8h9i0j1 (README.md)
   │             └─► Content: "Hello Git"
   ├─► author: John Doe
   └─► message: "Initial commit"

How Branches Work Internally

Branches are just pointers to commits. That's it!

┌────────────────────────────────────────────────────┐
│  BRANCH = POINTER TO A COMMIT                      │
└────────────────────────────────────────────────────┘

.git/refs/heads/main contains:
  a1b2c3d4e5f6...  (commit hash)

.git/refs/heads/feature contains:
  d4e5f6g7h8i9...  (commit hash)

When you create a branch:

git branch feature

Git creates a new file:

.git/refs/heads/feature

Containing the current commit hash.

When you switch branches:

git switch feature

Git updates HEAD:

.git/HEAD: ref: refs/heads/feature

Visual:

Commits:    A --- B --- C --- D
                        ↑     ↑
                      main  feature
                              ↑
                            HEAD

Creating a branch is instant because it's just creating a 40-byte file!


Building a Mental Model: The Complete Picture

┌────────────────────────────────────────────────────────────┐
│          GIT'S COMPLETE INTERNAL MODEL                     │
└────────────────────────────────────────────────────────────┘

Your Working Directory
├── index.html (current state)
├── style.css
└── README.md
         │
         │ git add
         ↓
.git/index (Staging Area)
├── index.html → blob hash
├── style.css → blob hash
└── README.md → blob hash
         │
         │ git commit
         ↓
.git/objects/ (Object Database)
├── Blob objects (file contents)
├── Tree objects (directory structure)
└── Commit objects (snapshots with metadata)
         ↑
         │
.git/refs/heads/ (Branch Pointers)
├── main → commit hash
└── feature → commit hash
         ↑
         │
.git/HEAD (Current Position)
└── ref: refs/heads/main

Summary: Key Takeaways

1. Everything is Stored as Objects

Blob   = File content
Tree   = Directory structure
Commit = Snapshot + metadata

2. Objects are Identified by Hash

Content → SHA-1 hash → Object ID

3. Git Stores Snapshots, Not Differences

Each commit = Complete project snapshot
Unchanged files = Reuse same blob

4. Branches are Just Pointers

Branch = File containing commit hash

5. The Three Areas

Working Directory → git add → Staging Area → git commit → Repository

Conclusion

You now understand what happens behind the scenes when you use Git:

✅ The .git folder is Git's database
✅ Git uses three object types: blobs, trees, commits
✅ Objects are stored and identified by content hashes
git add creates blobs and updates the staging area
git commit creates tree and commit objects
✅ Branches are lightweight pointers to commits
✅ Git is fast because it only compares hashes

This knowledge transforms you from a Git user to a Git understander!

Now when something goes wrong, you can reason about what Git is doing internally rather than blindly running commands.

Happy Git-ing! 🚀


Next Steps:

  1. Experiment with git cat-file -p to explore objects

  2. Read .git/HEAD and .git/refs/heads/main to see pointers

  3. Create branches and observe how .git/refs/heads/ changes

  4. Learn about Git's garbage collection and packfiles for optimization

Useful Commands for Exploring:

git cat-file -p <hash>     # View object content
git cat-file -t <hash>     # View object type
git ls-tree <tree-hash>    # List tree contents
git rev-parse HEAD         # Get current commit hash

More from this blog

ArmanRuhit Blogs

14 posts