sharmahritik2002@gmail.com

Mini git from scratch (P1) - 17 Dec 2025

Building a Toy Git in JavaScript

Git is one of those tools we use every day, yet rarely stop to question what’s happening under the hood.

Commands like:

git commit
git checkout
git branch

feel intuitive — but internally, Git is doing something surprisingly simple.

To truly understand it, I decided to build a tiny, toy version of Git in JavaScript.
Not production-ready. Not complete. Just accurate enough to strip away the magic.

This post walks through:


The Core Mental Model of Git

Before code, it’s important to simplify Git aggressively.

1. Commits Are Linked Nodes

A commit is nothing more than:

This naturally forms a linked list:

C3 → C2 → C1 → null

There’s no timeline, no array — just pointers.


2. Branches Are Just Pointers

A common misconception is that a branch contains commits.

It doesn’t.

A branch is simply:

{
  name: "main",
  commit: <latest_commit>
}

That’s it — a named pointer to the latest commit.


3. HEAD Is a Reference to a Branch

HEAD does not point to a commit directly.

It points to the current branch, which then points to a commit:

HEAD → main → C3

Once you internalize this, many Git concepts suddenly click.


Building a Toy Git in JavaScript

Let’s encode this mental model into code.

class Git {
  constructor(name) {
    this.name = name;
    this.lastCommitId = -1;
    this.log = [];

    this.branch = { name: "main", commit: null };
    this.HEAD = this.branch;
    this.branches = [this.branch];
  }

  getRepoInfo() {
    return { name: this.name };
  }

  commit(message) {
    const commit = {
      id: ++this.lastCommitId,
      message,
      parent: this.HEAD.commit
    };

    this.log = [commit, ...this.log];
    this.HEAD.commit = commit;
    return commit;
  }

  getLog() {
    const log = [];
    let currentCommit = this.HEAD.commit;

    while (currentCommit) {
      log.push(currentCommit);
      currentCommit = currentCommit.parent;
    }

    return log;
  }

  getCurrentBranch() {
    return this.branch;
  }

  checkout(branchName) {
    for (let i = 0; i < this.branches.length; i++) {
      if (this.branches[i].name === branchName) {
        this.branch = this.branches[i];
        this.HEAD = this.branch;
        return this;
      }
    }

    this.branch = {
      name: branchName,
      commit: this.HEAD.commit
    };

    this.HEAD = this.branch;
    this.branches.push(this.branch);
    console.log(`Switched to new branch ${branchName}`);
    return this;
  }
}

This single class captures the essence of Git’s data model.


How Commit History Works

Instead of storing a full history, we walk backward using parent pointers:

getLog() {
  const log = [];
  let currentCommit = this.HEAD.commit;

  while (currentCommit) {
    log.push(currentCommit);
    currentCommit = currentCommit.parent;
  }

  return log;
}

This mirrors how Git internally traverses commit history.


Branching and Checkout Explained

When you run:

git checkout test

One of two things happens:

  1. If the branch exists → HEAD moves to it
  2. If it doesn’t → Git creates a new branch pointing to the current commit

No commits are copied.
Branches share history until they diverge.


Running the Toy Git

const repo = new Git("test");

repo.commit("Change 0");
repo.commit("Change 1");
console.log(historyToIdMapper(repo.getLog()));

repo.checkout("test");
repo.commit("Change 2");
console.log(historyToIdMapper(repo.getLog()));

repo.checkout("main");
repo.commit("Change 3");
console.log(historyToIdMapper(repo.getLog()));

Output

1-0
2-1-0
3-1-0

This demonstrates:


What This Clarified for Me


Final Thoughts

I’ve found that the fastest way to understand systems is to build toy versions of them.

Small. Incomplete. Honest.

References used writing this blog: Ref1

Get in touch

Email me at sharmahritik2002@gmail.com sharmahritik2002@gmail.com link or follow me via my social links.