You type git commit
a dozen times a day—but what actually happens when you hit Enter?
What if I told you that you could domesticate version control forever—by building your own modern Git clone from scratch?
Meet GITTY—a simplified, beautiful Git clone we’re going to build together using Node.js. By the end of this journey, you won’t just use Git—you’ll finally understand how it works under the hood.
Oh, and bonus? You’ll have an incredible project for your portfolio.
Let’s dive in.
🛠️ Setting Up Our Project
First things first—let’s initialize our Node.js project:
mkdir gitty && cd gitty
npm init -y
The key detail here? Setting "type": "module"
in package.json
so we can use modern ES6 imports.
Installing Dependencies
We’ll need a few tools to make our CLI sleek and interactive:
- Commander – For a professional command structure.
- Inquirer – For beautiful interactive prompts.
- Chalk & Figlet – For some CLI flair.
npm install commander inquirer chalk figlet
Project Structure
We’ll keep things clean with:
-
gitty.js
(main executable) -
commands/
(for modular logic)
In package.json
, we add a "bin"
field to make gitty.js
executable:
"bin": {
"gy": "./gitty.js"
}
Now, let’s scaffold our CLI in gitty.js
:
import { program } from 'commander';
import figlet from 'figlet';
import chalk from 'chalk';
console.log(chalk.blue(figlet.textSync('GITTY')));
program
.version('1.0.0')
.description('A beautiful Git clone built with Node.js')
.command('init', 'Initialize a new GITTY repository')
.command('add', 'Stage files for commit')
.command('commit', 'Create a new commit');
program.parse();
With this skeleton in place, we’re ready to build our first command.
🔨 Building gitty init
– The Brain of Our System
The goal? Create a .gitty
directory—our version control database.
Inside commands/init.js
:
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
const init = () => {
if (fs.existsSync('.gitty')) {
console.log(chalk.red('GITTY repository already exists!'));
process.exit(1);
}
// Create essential directories
fs.mkdirSync('.gitty');
fs.mkdirSync('.gitty/objects'); // Stores blobs, trees, commits
fs.mkdirSync('.gitty/refs'); // Stores branch pointers
// Initialize HEAD (points to current branch)
fs.writeFileSync('.gitty/HEAD', 'ref: refs/heads/main');
console.log(chalk.green('GITTY repository initialized!'));
};
export default init;
What’s happening?
-
.gitty/objects
– Stores all file data (blobs) and commits. -
.gitty/refs
– Holds branch references (likemain
). -
.gitty/HEAD
– Tracks the currently checked-out branch.
Now, running gitty init
sets up our version control brain.
📦 Implementing gitty add
– Staging Files Like a Pro
Git doesn’t store duplicate files—it uses content-addressable storage.
When you add
a file:
- Its contents are hashed (SHA-1).
- The hash becomes the filename inside
.gitty/objects
. - The mapping (filename → hash) is stored in the index.
Here’s how we do it:
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import chalk from 'chalk';
const add = (files) => {
const gyDir = path.resolve('.gitty');
const indexFile = path.join(gyDir, 'index');
let index = {};
if (fs.existsSync(indexFile)) {
index = JSON.parse(fs.readFileSync(indexFile));
}
files.forEach((file) => {
const content = fs.readFileSync(file);
const hash = crypto.createHash('sha1').update(content).digest('hex');
// Save blob to objects
fs.writeFileSync(path.join(gittyDir, 'objects', hash), content);
// Update index
index[file] = hash;
});
fs.writeFileSync(indexFile, JSON.stringify(index));
console.log(chalk.green('Files staged!'));
};
export default add;
Now, gitty add file.txt
stores the file forever in .gitty/objects
.
💾 The Magic of gitty commit
– Snapshotting History
A commit is a permanent snapshot with:
✅ Author & timestamp
✅ Commit message
✅ Tree hash (directory state)
✅ Parent hash (previous commit)
Here’s how we build it:
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import chalk from 'chalk';
const commit = async () => {
const { message } = await inquirer.prompt({
type: 'input',
name: 'message',
message: 'Enter commit message:',
});
const gittyDir = path.resolve('.gitty');
const indexFile = path.join(gittyDir, 'index');
// 1. Build the tree object (represents directory structure)
const index = JSON.parse(fs.readFileSync(indexFile));
const tree = { files: index };
const treeHash = crypto.createHash('sha1').update(JSON.stringify(tree)).digest('hex');
fs.writeFileSync(path.join(gittyDir, 'objects', treeHash), JSON.stringify(tree));
// 2. Find parent commit (if exists)
let parentHash = null;
const headRef = fs.readFileSync(path.join(gittyDir, 'HEAD'), 'utf-8').trim();
const branchFile = path.join(gittyDir, headRef.split(': ')[1]);
if (fs.existsSync(branchFile)) {
parentHash = fs.readFileSync(branchFile, 'utf-8').trim();
}
// 3. Create commit object
const commit = {
tree: treeHash,
parent: parentHash,
author: 'Your Name <[email protected]>',
message,
timestamp: new Date().toISOString(),
};
const commitHash = crypto.createHash('sha1').update(JSON.stringify(commit)).digest('hex');
fs.writeFileSync(path.join(gittyDir, 'objects', commitHash), JSON.stringify(commit));
// 4. Update branch pointer
fs.writeFileSync(branchFile, commitHash);
console.log(chalk.green(`Commit ${commitHash.slice(0, 7)} created!`));
};
export default commit;
Now, gitty commit
captures history forever.
🎉 The Grand Finale
Let’s test it:
npm link # Make `gitty` globally available
gitty init # Initialize repo
gitty add file.txt # Stage a file
gitty commit # Create a commit
Boom! You’ve just built a functional Git clone.
🔍 Key Takeaways
✔ Blobs – Hashed file contents.
✔ Trees – Snapshots of directories.
✔ Commits – Immutable history points.
✔ Branches – Just pointers to commits.
Now, you understand Git deeply—not just use it.
🚀 What’s Next?
Want to extend Gitty? Try:
-
Branching (
gitty branch
) -
Checking out commits (
gitty checkout
) -
Remote repositories (
gitty remote
)
What feature should we add next? Drop a comment below!
If you enjoyed this deep dive, smash that like button, subscribe, and share with fellow devs.
Happy coding! 🚀
Top comments (2)
pretty cool seeing someone break it down like this - def makes stuff click way easier for me. you ever find building something from scratch teaches you stuff tutorials just never hit?
Yes for me building something from scratch and the way of thinking to create it with the new stacks and packages gets me so hyped and lots of learning happens in the process.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.