Skip to main content

Command Palette

Search for a command to run...

Git Internals

How it works and the role of the .git folder

Updated
•6 min read•View as Markdown
Git Internals

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/FilePurpose
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
HEADPoints to your current branch (or commit in detached state)
indexThe staging area—tracks which changes will go into your next commit
configRepository-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?

  1. Git reads the file's contents

  2. Computes the SHA-1 hash of that content

  3. Creates a blob object and stores it in .git/objects/

  4. 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?

  1. Create tree objects: Git scans the index to see what's staged. It builds tree objects representing our project's directory structure.

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

  3. Compute the commit hash: Git hashes all the commit metadata, producing the commit hash.

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

  5. 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/feature

  • That file contains a single line: the commit hash it points to

When we switch to that branch:

  • Git updates .git/HEAD to point to .git/refs/heads/feature

  • Git 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/feature with 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:

  1. Reads HEAD to find the current branch

  2. Reads that branch file to find the parent commit

  3. Creates a new commit with that as the parent

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

  1. git add computed: SHA-1("hello") = e965047...

    • Stored blob at .git/objects/e9/65047...

    • Updated .git/index to mark this blob as staged

  2. git commit created:

    • 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/main to contain a1b2c...

    • Updated .git/HEAD to point to refs/heads/main

Now our history is preserved forever. Every detail is traceable through the hashes.