Mini git from scratch (P1)

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:

  • how Git commits work internally
  • what branches really are
  • what HEAD actually points to

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:

  • an id
  • a commit message
  • a pointer to its parent commit

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:

  • shared commit history
  • independent branch movement
  • how HEAD switching actually works

What This Clarified for Me

  • Commits are nodes, not snapshots in a list
  • Branches are movable pointers
  • HEAD is just a reference

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