Inside Git: How It Works and the Role of the .git Folder
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
.gitfolder?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:
Blob (Binary Large Object) → Stores file contents
Tree → Stores directory structure
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 file100755= Executable file040000= 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?
Detect Changes Instantly
Old hash: 3b18e51... New hash: 7f92a3d... → Content changed!Ensure Data Integrity
If content is corrupted, hash won't match → Git detects itEfficient 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:
Experiment with
git cat-file -pto explore objectsRead
.git/HEADand.git/refs/heads/mainto see pointersCreate branches and observe how
.git/refs/heads/changesLearn 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