Sitemap

Git Series Part 2: Designing a Version Control System the Git Way

Instead of jumping straight to commands, we build a mini version control tracker step-by-step, mirroring Git’s design choices to understand its architecture in a practical way.

Press enter or click to view image in full size

In Part 1, we explored the chaotic world of software development before Git — the era of copy files, lost changes, and developers accidentally overwriting each other's work.

We ended with Linus Torvalds facing a crisis. The Linux project had lost access to BitKeeper, and he needed a better solution fast.
So he built Git.

But instead of jumping straight to Git & Git commands, let’s do something different. Let’s put ourselves in Linus’s shoes and think:

If I had to build a version control system from scratch, how would I do it?

By building our own simple version tracker, we’ll understand exactly what Git does under the hood and why its design works so well.

🤔 The Core Problem: What Does a Version Tracker Actually Need?

let’s think like Linus might have done (this is our assumption based on the problems we saw in Part 1).

Version control systems before Git solved many problems, and Git improved upon them with advanced features. But to understand the core, let’s focus on the fundamental issues:

The Five Essential Problems

Problem 1: Saving Versions of Our Code
As we work, code keeps changing. We need to save versions at different moments like taking photos over time, so we can go back if needed.

Problem 2: Knowing What Changed
It’s not enough to save versions. We need to see what changed between them which files were added, edited, or removed.

Problem 3: Understanding the Context
Every saved version should include:
> When it was made
> Why it was made (a short message)

Problem 4: Keeping a Clear History
All saved versions must be stored cleanly and organized, allowing us to view the full project history and travel back to any point in time.

Problem 5: Working Freely Without Breaking History
We need clear separation between:
> Files we’re currently editing (our workspace)
> Versions that are already saved (our history)

This lets us experiment and develop freely without breaking anything that’s already saved.

🔨 Building Our Own Content Version Tracker

Let’s try to design a simple content version tracker. We’ll call it Bit.
You can choose any name — I’m keeping it similar to Git so it’s easier to relate things later.

(And yes, if you’re curious why Git is called Git, Linus has explained it himself — worth looking up 😄)

Press enter or click to view image in full size

When Linus started designing Git, he was probably thinking something like this:

🚨 Problem 1: Where Should We Store Version History?

Linus thought: 🤔
First things first… I need a way to save multiple versions of code.
When a developer saves a version, I should store it somewhere and be able to restore it later.

So the question is: Where should these versions be stored?”

Option 1: Store Versions in a Database?

Initial thought: Databases are good at storing data. Tables, records, queries…etc maybe I can store code versions in a database?

But wait… 🤨 Problems with databases:

  • Setup Complexity: Every developer needs to install database software (MySQL, PostgreSQL, etc.)
  • Configuration headaches: Different operating systems, different installations
  • Server dependency: Databases need servers running
  • Knowledge barrier: Not every developer knows SQL or other query languages
  • No offline work: What if developers work without internet?

NOTE: Linus already experienced this frustration before with BitKeeper — he wanted developers to work anywhere, even offline in the middle of nowhere.

Linus’s conclusion: NO, databases won’t work. I need something that:
- Works immediately, without setup
- Doesn’t need a server
- Works completely offline
- Works on every machine
- Is simple for any developer to use

There has to be a better way…

Option 2: Store Versions as Files on the Computer?

Linus thought: What if I just use the file system itself?

Every computer already has one.
- No installation
- No servers, no configuration
- Works offline by default
- Just files and folders

What if I save versions in the same machine, in the same project folder? Like this:

Press enter or click to view image in full size
Initial structure

Linus’s realization: This also solves the BitKeeper problem! BitKeeper required licensing, network access and a central server. But with local file storage:
- Developers work completely offline.
- They have their own complete copy of history.
- They can create versions, view history , everything without any network.
- Later, when online, they can share with others.

This is a distributed version control system not centralized!. That means the code is not stored in just one central place. Instead, every developer has their own full copy of the project, including its history. Each person can work on their copy, and later share or combine their changes with others.

But Wait… There’s a Problem

Looking at our folder structure again, the version-history/ folder is visible right there in the project.

Problems with a visible history folder:

❌ Risk of Accidental Deletion
If someone accidentally deletes or modifies this folder, all saved versions are lost. One mistake, and everything is gone.

❌ Visual Clutter
Developers want to see their actual project files, not an extra folder getting in the way. New team members wonder:
- Is this a backup folder?
- Should I put my files here?
- Is this the real code or the history?

❌ Confusion
Which files are the “real” ones I should edit? The ones in the root? Or in version-history? That’s too risky and confusing!

💡 The Solution: Hidden Folders

Linus’s next thought:
I need to hide this folder and keep it separate from working files, but still accessible when needed.

The realization: On Unix-like systems (Linux, macOS), files and folders starting with a dot (.) are treated as hidden by convention.

Examples: — .ssh, .config, .bashrc

These files still exist normally, but most tools hide them from everyday view to reduce visual clutter.”

Press enter or click to view image in full size
Behavior Comparison: Before [.] folder convention & After [.] folder convention

Perfect! The history folder can be hidden using just a naming convention:

  • ✅ History is stored locally (works offline)
  • ✅ It’s hidden from normal view (no clutter)
  • ✅ Users focus on their actual code
  • ✅ History is still accessible when needed

Choosing a Name for the Hidden Folder

For Git, Linus chose .git - short and directly related to the tool name.

For our version control system Bit, we’ll call our hidden folder .bit

Press enter or click to view image in full size
Our new folder structure with name .bit

✅ Problem 1 Solved!

We’ve solved how to save versions:
✨ Store everything in a hidden .bit folder.

📌 BONUS: Understanding Hidden Folders

If .bit is hidden, how do I access it? And why is it still visible in VS Code?

How Hidden Folders Work
On Unix-like systems (macOS, Linux), any file/folder starting with . is hidden by convention.

What this means:
—
❌ .bit won't appear in Finder (macOS) or File Explorer (Windows) by default
— ✅ .bit is still visible in VS Code by default

Why? — VS Code has a built-in list of folders to hide (like .git, .DS_Store), but .bit isn't on that list yet since we just created it!

Hiding .bit in VS Code

Step 1: View Default Hidden Files

  • Press Cmd + Shift + P (Mac) or Ctrl + Shift + P (Windows/Linux)
  • Search: “Preferences: Open Default Settings (JSON)”
  • Search inside for: "files.exclude"

You’ll see something like:

"files.exclude": {
"**/.git": true,
"**/.DS_Store": true
...
...
}

These are VS Code’s default hidden files. The file is read-only, so you shouldn’t try to edit it.

If you’ve ever wondered why some folders with names like these don’t appear in VS Code, this is the reason they are hidden by default.

Step 2: Add Your Own Rule

  1. Press Cmd + Shift + P
  2. Search: “Preferences: Open User Settings (JSON)”
  3. Add this and save:
"files.exclude": {
"**/.bit": true
}

🎉 The .bit folder now disappears from VS Code's Explorer (but still exists on disk).

Accessing Hidden Folders When Needed

So how do you access .bit when it's hidden? Let me show you.
First, check whether the hidden folder exists. Open the VS Code terminal and type:

>> ls -a

Surprised by the result? Yes — this command shows all files and folders, including hidden ones (those that start with a dot, like .bit).

View hidden folders:

  • In Finder (macOS): Press Cmd + Shift + . to toggle visibility
  • In VS Code: Remove "**/.bit": true from User Settings (JSON)

🚨 Problem 2: Knowing What Changed

Now that we have a place to store history (.bit folder), let's figure out what we actually store inside it.

Linus thought: 🤔
Okay, I can save versions. But HOW exactly?

  • Do I copy entire files every time?
  • Do I only save what changed?
  • How do I make this efficient?”

Let’s explore the options…

Option 1: Store the Entire Project Every Time?

Simple approach — What if I copy the entire project folder for each version?

.bit/
version-1/
index.html (full file)
style.css (full file)
version-2/
index.html (full file)
style.css (full file)
app.js (full file - NEW!)

But… this is exactly the chaos from Part 1 — the “Multiple Version Copies” problem!
💥 Massive storage waste.
💥 Same files copied over and over.
💥 Gets exponentially worse with each version.

Option 2: Store Only What Changed (Diffs)?

Linus thought:
What if we only save the changes/difference between versions — just the lines that are different, instead of saving the whole file each time

Example scenario: — Let’s say we have index.html with this content:

Press enter or click to view image in full size

Instead of storing the entire file again, just store the difference!

This is way more efficient! For large projects where only a few lines change, this could save massive amounts of space. But wait let me think about the consequences.

New problems emerge: 🤨

❌ Slow to Rebuild Old Versions
To see what index.html looked like at Version 50:
- Start with Version 1 (original)
- Apply diff from Version 2
- Apply diff from Version 3
- Keep going through Versions 4, 5, 6… all the way to 50!

That’s 49 operations just to see one file!
For 1000 versions? Painfully slow! 😫

❌ One Corrupted Diff Breaks Everything
Imagine:
- Versions 1–10: Perfect ✓
- Version 11: Difference gets corrupted 💥
- Versions 12–100: All stored as diffs…

Now when reconstructing Version 50:
- Versions 1–10 apply successfully ✓
- Version 11 fails ❌ CORRUPTED!
- Everything after Version 11 becomes unusable! 😱

One corrupted difference breaks the entire history chain!

Option 3: The Better Solution — Think About Content, Not Versions

Linus’s breakthrough moment:
Until now, I’ve been thinking in terms of Version 1, Version 2, Version 3. But maybe that’s the wrong question. Instead of storing versions, what if the real goal is to store the content efficiently?

Look at what actually happens in real projects:

Press enter or click to view image in full size

Do you see the pattern? 🤔

  • Most files don’t change between versions!
  • In Version 2: Only index.html changed, but we'd be wasting space storing style.css again even though it's identical!
  • In Version 3: Only app.js is new! But copying everything wastes space storing identical files!
  • In Version 4: NOTHING changed! But we’d still waste space if we copied everything!

Linus’s Realization: Store Unique Content Once
I don’t really need to think in terms of versions .What really matters is the content itself. What if I store every unique piece of content only once — and reuse it whenever it appears again?

The Magic Idea: Give Each Unique Content a Special ID

  • Take any file content
  • Generate a unique ID for that specific content
  • Store it once using that ID
  • Whenever I need that same content again, just refer to the same ID!

It’s like a fingerprint! 🖐️

Think about it: — Your fingerprint is unique to you. If police find your fingerprint at 10 different places, they don’t need 10 copies in their database — they just match it to the ONE fingerprint stored for you!

Get Rajesh Kumar’s stories in your inbox

Join Medium for free to get updates from this writer.

Same concept here!!

How This Works — A Complete Example

Step 1: Create Your First Version
Suppose, Initially your project has two files:

index.html → "Hello World"
style.css → "body { color: blue; }"

You want to save this as Version 1. But now, we’re changing how versions are stored. Instead of saving the actual file text inside the version folder, we will first give each file’s content a unique ID.

Step 2: Generate an ID for each file’s content
For each file, we look at its content and create a fingerprint-like ID.
Let’s say:

index.html: content → generates ID: abc123
style.css: content → generates ID: abc124

Step 3: Create a new folder called content-storage
Up to now, everything lived inside version-* folders. But now we introduce a new place:

.bit/content-storage/

This folder is our warehouse where we store the real file contents, but only once. We save our two contents like this:

.bit/content-storage/
abc123 → contains "Hello World"
abc124 → contains "body { color: blue; }"

You can think of content-storage/ like a warehouse where:
- Every unique file content is stored exactly once
- No duplicates
- No repeated copies
- Content is identified by its unique ID

Step 4: Update the version folder to store only IDs
Now we return to our version record. But instead of saving the content inside Version 1, we only save which ID belongs to which file.
So version-1 becomes:

.bit/versions/
version-1/
index.html → abc123
style.css → abc124

Meaning:
- index.html uses the content stored at ID abc123
- style.css uses the content stored at ID abc124

The real contents lives in content-storage/. The version just points to it.

Step 5: Let’s Save a Second Version
Now suppose only index.html changes:

index.html → "Hello Friends" (Changed)
style.css → "body { color: blue; }" (unchanged)

We generate IDs again:
- New index.html content → new ID, say abc125
-style.css content is identical → same previous ID abc124

So we store only the new updated content:

.bit/content-storage/
abc123 → contains "Hello World"
abc124 → contains "body { color: blue; }"
abc125 → "Hello Friends" ← NEW UPDATED!

We don’t duplicate style.css because same content already exists.

Step 6: Version 2 Points to Both Old and New Content

.bit/versions/
version-1/
index.html → abc123
style.css → abc124
version-2/
index.html → abc125 (changed → new ID)
style.css → abc124 (same → reused ID!)

We reused the existing content(id →abc124) forstyle.css instead of storing it again!

Step 7: Add a New File
Now we just add a new file but don’t change anything else:

index.html → "Hello Friends"  (unchanged)
style.css → "body { color: blue; }" (unchanged)
app.js → "console.log('test');" (NEW!)

Generate IDs:

index.html  → abc125 (same → reused ID!)
style.css → abc124 (same → reused ID!)
app.js → abc126 (new ID new file/content)

Store only the new content:

.bit/content-storage/
abc123 → contains "Hello World"
abc124 → contains "body { color: blue; }"
abc125 → "Hello Friends"
abc126 → "console.log('test');" ← NEW! File content Added

version 3 :

.bit/versions/
version-1/
index.html → abc123
style.css → abc124
version-2/
index.html → abc125
style.css → abc124
version-3/
index.html → abc125 (same → reused ID)
style.css → abc124 (same → reused ID)
app.js. → abc126 (new file → new ID)

The Beautiful Result 🎨

Now when we track versions, we don’t store files — we store IDs!

Press enter or click to view image in full size

Only 4 unique pieces of content stored!
Even though we have 4 versions × 3 files = 12 files total, we only stored 4 unique contents! 🎉

How Do We Know What Changed Between Versions?

It’s beautifully simple: To check if a file changed between commits:

  • Look at Version 1: index.html → abc123
  • Look at Version 2: index.html → abc125
  • Different IDs? → Content changed! ✓

To check if a file stayed the same:

  • Look at Commit 2: style.css → abc124
  • Look at Commit 3: style.css → abc124
  • Same ID? → Content unchanged! ✓

No need to compare actual file contents! Just compare IDs!

✅ Problem 2 SOLVED!

We can now:
✨ Store content efficiently (no duplicates!).
✨ Track what changed between snapshots (compare IDs!).
✨ Retrieve any snapshot quickly (no need to rebuild from diffs!).

🚨 Problem 3: Adding Context to Each Version

Linus thought: 🤔
Okay… now I can save versions.Each version points to content IDs.
I can even tell what changed by comparing IDs.

But then another problem hits. Imagine you have 50 versions. You want to find the one where you fixed the login bug.

.bit/versions/
version-1/
version-2/
version-3/
...
version-50/

How do you find it? By looking at each version one by one? That’s not practical!
Saving versions alone is not enough.!!

What’s Missing?

A version is not just — Files or Content IDs.
A version is a decision made by a developer.

Every version needs answers to basic questions:
- When was it saved?
- Why was it saved?
- What was included at that moment?

Without this information, version history is just numbered folders.
So Linus decides: — What if every version just has a small file that explains what it is?

Press enter or click to view image in full size
Enhancing the Version Folder

What goes in version-info.txt?

Version: version-1
Created On: 2024-01-15 14:30:00

Message:
Initial project setup

Files in this version:
index.html → abc123
style.css → abc124

Now this version:
- Explains when it was created
- Explains why it exists
- Lists what files it represents

✅ Problem 3 Solved!

Now versions are:
✨ Understandable
✨ Searchable
✨ Meaningful

But another question appears…

🚨 Problem 4: Keeping a Clear History

Linus thought: 🤔
I can store commits. Each commit has meaning.
But how do I know the order in which commits were created?

Right now, versions are just folders:

version-1
version-2
version-3

But this ordering is fake. What if:
- Someone deletes version-2?
- Someone renames folders?
- Versions are copied manually?

The numbering alone cannot be trusted.

What’s Missing?

Versions need a connection.Each version must know, Which version came before it?
This creates a history chain, not just a list.

Linus’s Idea: Every Version Points to Its Parent

Linus thought: — A version is created from another version. So every commit should record where it came from.
So Linus adds one more line to version-info.txt.

Version: version-3
Parent Version: version-2 <--- new line
Created On: 2024-01-15 16:10:00

Message:
Added login validation

Files in this version:
index.html → abc125
style.css → abc124
app.js → abc126

Now the relationship is clear.

version-1
↓
version-2
↓
version-3
↓
version-4

Each version knows exactly where it came from.

✅ Problem 4 Solved!

We now have:
✨ A reliable history chain.
✨ No dependency on folder names.
✨ A clear timeline of evolution.

But one last major problem remains…

🚨 Problem 5: Separating Working Files from Saved History

Linus thought: 🤔
Developers edit files constantly.But saved commits should never change.
How do I clearly separate these two worlds?

Right now we have:
- Project files (workspace)
- .bit/ (version history)

But we still need one clear rule: which version should be treated as the current saved state? In other words, when we open the project, which version should the system say is the latest one

Why This Matters

Imagine this situation:
- You saved version-5
- Then you edited index.html
- But you haven’t saved a new version yet

This means your current files(workspace) are different from the last saved version in history.
That’s why we need a way to answer this question: — Which version is the latest one that was actually saved?

Linus’s Simple Solution: Track the Current Version

Linus thought: — I don’t need a complicated system. I just need one small file that tells me which version is the current one.

When I save a new version, this file will simply point to that version.This way, the system always knows which version is the latest saved state.

So Linus adds a simple file: current-version.txt

Updated .bit folder

Contents of current-version.txt: — it’s just store the current recent save version details which is “version-2”

That’s it. Simple. Clear. Unambiguous.
This file always points to: — The last saved version.

🧠 Final Mental Model

Now everything is clearly separated:

  1. Workspace
    — These are your real project files
    — This is what you edit and work on
  2. Current Version
    — Saved in a file called current-version.txt
    — It tells us which version is the latest saved state
  3. History
    — All old versions are stored safely inside
    .bit/versions/ and .bit/content-storage/
    — You don’t change these files directly

✅ Problem 5 Solved!

We now have:
✨ A clear separation between your working files and the saved history.
✨ A reliable pointer that tells us which version is the current one.
✨ A safe, unchangeable (immutable) history of all versions

🧱 The Final .bit Structure (So Far)

.bit/
current-version.txt ← points to the current saved version

content-storage/ ← stores unique file contents (content blobs)
abc123 ← raw file content (no extension)
abc124 ← same content reused across versions if identical
abc125
abc126

versions/ ← all saved versions
version-1/
version-info.txt ← metadata: message, timestamp, etc.
index.html → abc123 ← file points to stored content
style.css → abc124

version-2/
version-info.txt ← describes this version only
index.html → abc125
style.css → abc124
...
..
version-4/
version-info.txt
index.html → abc125
style.css → abc124
app.js → abc126

🎯 What We’ve Accomplished

From chaos to clarity in five elegant solutions:

✅ Problem 1: Storage
- Hidden .bit/ folder - local, offline, no setup required

✅ Problem 2: Efficiency
- Content-addressed storage — each unique content stored once

✅ Problem 3: Context
- Metadata files — when, why, what for every commit

✅ Problem 4: History
- Parent pointers — reliable chain of commits

✅ Problem 5: Separation
- current-version pointer — clear boundary between work and history

💭 But Something’s Missing…

Our Bit system is solid. It works. It’s elegant.
But, this system has one big limitation — It assumes history always moves in a straight line

Version 1 → Version 2→ Version 3 → Next Versions…

And real life doesn’t work like that. Developers don’t always work in a simple sequence. Sometimes we start one task then need to pause it, then fix another bug, then switch back again.

Real work is messy. Real life doesn’t wait. And that’s when our perfect, linear history hits a wall.

⚡ The Moment Everything Changes

Imagine this real-world situation:

You’re halfway through a new feature. Files are changed. Code is incomplete. Nothing is ready to save.
Suddenly…
🚨 A critical production bug appears.
🚨 You must fix it immediately.
🚨 But your workspace is full of unfinished work.

You can’t:
❌ Throw away your half-done changes.
❌ Save a broken version into history.
❌ Copy the whole project like the old days.

For the first time, our system doesn’t know what to do.
We need a way to:
- Continue unfinished work safely.
- Save an emergency fix separately.
- Keep history clean — without mixing changes.

🪄 The Birth of a New Idea

Linus realizes something important:

History should not be a single straight line.Developers should be able to create a new path, work on it, and then merge it back when the work is ready.

This idea will change everything. It introduces:
- Parallel work
- Independent development paths
- Safe experimentation
- Collaboration without chaos

🚀 Coming Next

In the next part of this series, we’ll explore:

  • Why a single, linear history fails in real-world development
  • What branches are, and how they were created to solve this exact problem
  • Why the five problems we solved so far are only enough for a basic tracker and why real projects need more advanced features
  • How our manual ideas will gradually turn into commands and automation, so the .bit folder becomes a real working tool instead of a manual process

We’re now moving from a simple learning model…to the foundations of a real version control system.

Stay tuned — the story is just getting exciting 😄

Here is the next article link: https://rky211.medium.com/git-series-part-3-designing-parallel-histories-in-version-control-systems-why-one-timeline-6bce6f0a3c05

🙏 Found this helpful? Tap the 👏 — it really helps! 💬 Got questions, ideas, or something you’re building? Drop a comment — I’d love to hear from you!

Written by Rajesh Kumar

Senior Frontend Developer | AI | GenAI