
Introduction
Git is a content-addresable-filesystem which smartly stores the snapshot of our project. It saves complete pictures of our entire project at each moment. The magic is in how it uses cryptographic hashing to ensure nothing ever gets lost or corrupted, and how it reuses storage when files are identical.
Our entire repository's history lives in a hidden folder called .git, which contains all the objects (files, directories, commits) and pointers to them (branches, tags).
The .git folder
When we run the git init command. Git creates a hidden .git folder in our project folder. This is technically the entire repository.
| Directory/File | Purpose |
| objects/ | Stores all Git objects (blobs, trees, commits, tags)—the actual data |
| refs/heads/ | Contains branch files; each file stores the commit hash it points to |
| refs/tags/ | Contains tag references |
| HEAD | Points to your current branch (or commit in detached state) |
| index | The staging area—tracks which changes will go into your next commit |
| config | Repository-specific Git settings |
| logs/ | Records all changes made to branches and tags |
| hooks/ | Scripts that run automatically during Git operations |
The most important folder among all is objects/. This is where the git stores the actual history of our project.
Understanding objects/
Git has three main object types that form the backbone of version control:
Blobs: File Contents
A blob (Binary Large Object) is simply the contents of a file. When we git add a file, Git computes a SHA-1 hash of the file's contents and stores the compressed file as a blob.
Key point: If two files have identical content, they get the same hash and share the same blob. This saves enormous amounts of space.
Example:
File: hello.txt containing "Hello World"
↓
SHA-1 hash: 01d3f0d2851f7cc7e9a937e89c96e2476e8e1234
↓
Stored in: .git/objects/01/d3f0d2851f7cc7e9a937e89c96e2476e8e1234
The first two characters (01) become a folder name, and the rest becomes the file name.
Trees: Directory Structure
A tree represents a directory. It contains a list of entries—each entry is either a file (blob) or a subdirectory (another tree). Each entry stores:
Filename
File permissions (755, 644, etc.)
Hash of the blob or subtree it points to
When we commit, Git creates a tree for each directory in our project. The root tree essentially says: "Here's what the project looks like at this moment."
Root tree (hash: abc123...)
├── file1.txt (blob: 01d3f0...)
├── file2.txt (blob: 5529b1...)
└── src/ (tree: 7fe891...)
└── main.py (blob: 3bb0e8...)
Each tree is also stored as a Git object with its own SHA-1 hash.
Commits: Snapshots + Metadata
A commit object ties everything together. It contains:
Reference to the root tree (the project snapshot)
Hash of the parent commit (the previous state)
Author name and email
Committer name and email
Timestamp
Commit message
The commit hash is created by hashing all this metadata. If we change even a single character in the message, the entire commit hash changes. This is how Git ensures integrity—we can't tamper with history without it being obvious.
Example:
commit abc456def...
tree 7fe891xyz...
parent abc123...
author Alice <alice@example.com>
committer Alice <alice@example.com>
date 2026-01-29
"Fix login bug"
Wait, But What is SHA-1 Hashing?
Git uses SHA-1 hashing—a cryptographic algorithm that produces a unique 40-character hexadecimal string from any input.
Why this matters:
Content-addressable: The hash is the address. Find the content, find the hash.
Integrity verification: If even one byte changes, the hash changes completely.
Deduplication: Identical files get the same hash, so Git never stores duplicates.
How Git tracks changes?
When we work with Git, our project exists in three places simultaneously:
┌─────────────────┐
│ Working Dir │ ← Your actual files (what you edit)
└────────┬────────┘
│ git add
↓
┌─────────────────┐
│ Staging Area │ ← The index file (what will go in next commit)
│ (index file) │
└────────┬────────┘
│ git commit
↓
┌─────────────────┐
│ Local Repo │ ← .git folder (permanent history)
│ (.git/) │
└─────────────────┘
What happens when we run git add command?
Git reads the file's contents
Computes the SHA-1 hash of that content
Creates a blob object and stores it in
.git/objects/Updates the index file to say "remember this blob for the next commit"
Its like packing items into a box before shipping.
What happens when we run git commit command?
Create tree objects: Git scans the index to see what's staged. It builds tree objects representing our project's directory structure.
Create the commit object: Git creates a commit object containing:
Reference to the root tree
Reference to the parent commit (linking the chain)
Author/committer metadata
Our commit message
Compute the commit hash: Git hashes all the commit metadata, producing the commit hash.
Update the branch reference: Git updates the file at
.git/refs/heads/main(or whatever branch we're on) to point to this new commit hash.Clear the index: The staging area is cleared, ready for the next round.
Before commit:
└─ Working Dir has modified files
└─ Index says "add these files"
After commit:
├─ Commit created with snapshot of those files
├─ Chain linked: new commit → parent commit
├─ Branch pointer updated to new commit
└─ History preserved forever
How branching works in Git?
Branches are just files in .git folder.
When we create a branch called feature:
Git creates a file:
.git/refs/heads/featureThat file contains a single line: the commit hash it points to
When we switch to that branch:
Git updates
.git/HEADto point to.git/refs/heads/featureGit restores our working directory to match that commit's snapshot
When we make a new commit on feature:
Git creates the commit with parent = current commit
Git updates
.git/refs/heads/featurewith the new commit hash
Branches are just pointers in git and that is the reason why they’re so efficient.
What about the HEAD directory?
HEAD is a special file that tells Git: "You are here." It usually contains:
textref: refs/heads/main
This means "I'm on the main branch," and Git will look inside .git/refs/heads/main to find the actual commit hash.
When we make a commit, Git:
Reads HEAD to find the current branch
Reads that branch file to find the parent commit
Creates a new commit with that as the parent
Updates the branch file with the new commit hash
Putting is all together- A complete example
git init # Creates .git/
echo "hello" > file.txt
git add file.txt # Creates blob for "hello", updates index
git commit -m "first" # Creates tree, creates commit, updates refs/heads/main
What happened inside .git/:
git addcomputed:SHA-1("hello") = e965047...Stored blob at
.git/objects/e9/65047...Updated
.git/indexto mark this blob as staged
git commitcreated:Tree object for root:
SHA-1(tree contents) = 3b18e...Stored tree at
.git/objects/3b/18e...Commit object:
SHA-1(tree hash + parent + author + message) = a1b2c...Stored commit at
.git/objects/a1/b2c...Updated
.git/refs/heads/mainto containa1b2c...Updated
.git/HEADto point torefs/heads/main
Now our history is preserved forever. Every detail is traceable through the hashes.




