DEV Community

Ethan Arrowood
Ethan Arrowood

Posted on


Building Git with Node.js and TypeScript - Part 0


I recently found this amazing eBook, Building Git, by James Coglan.

Building Git is a deep dive into the internals of the Git version control system. By rebuilding it in a high-level programming language, we explore the computer science behind this widely used tool. In the process, we gain a deeper understanding of Git itself as well as covering a wide array of broadly applicable programming topics

I'm not a Ruby developer, so I'll be implementing the Git clone, jit, using Node.js and Typescript. This blog series will track that process, and I'll do my best to not spoil the book either. It is full of some incredible details not just about Git, but also about file systems, operating systems, and more!

In this first post I'm going to share my local development environment and my code from chapter 3 section 1. There won't be a remote repository for my implementation at first, but later on, I'll be sure to upload and share that too.

Also, none of this series is sponsored and the links I provide to the book are not referrals. I'm writing this series because I like to learn in public, and sharing links to the book because I want to support the author.

Development Environment

For my local environment I'm using Visual Studio Code editor on macOS Catalina. I'll be using the latest versions of Node.js and TypeScript. At the time of writing this post, I'm using Node.js v14.2 and TypeScript v3.8; I'll be keeping these up to date in the future as well.

I started my project with:

mkdir jit-ts
cd jit-ts
npm init -y
npm i -D typescript @types/node
Enter fullscreen mode Exit fullscreen mode

And then added the following sections to my package.json

    "main": "./lib/jit.js",
    "types": "./lib/jit.d.ts",
    "bin": {
        "jit": "./lib/jit.js"
    "scripts": {
        "build": "tsc -w -p ."
Enter fullscreen mode Exit fullscreen mode

Additionally, I prefer to use the built in VSCode debugger to step through and test my code so I have these two debug files in the .vscode directory:


    "version": "0.2.0",
    "configurations": [
            "type": "node",
            "request": "launch",
            "name": "jit-ts init",
            "program": "${workspaceFolder}/src/jit.ts",
            "preLaunchTask": "tsc: build - jit-ts/tsconfig.json",
            "postDebugTask": "jit cleanup",
            "outFiles": ["${workspaceFolder}/lib/**/*.js"],
            "args": ["init"]
Enter fullscreen mode Exit fullscreen mode


    "version": "2.0.0",
    "tasks": [
            "type": "typescript",
            "tsconfig": "tsconfig.json",
            "problemMatcher": [
            "group": "build",
            "label": "tsc: build - jit-ts/tsconfig.json"
            "label": "jit cleanup",
            "type": "shell",
            "command": "rm -rf ${workspaceFolder}/.git"
Enter fullscreen mode Exit fullscreen mode

These debug files will expand and change as the implementation grows, but these work good enough for the first section.

Lastly, I created a typescript configuration and the source file:

mkdir src
touch src/jit.ts
npx typescript --init
Enter fullscreen mode Exit fullscreen mode


    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "declaration": true,
        "sourceMap": true,
        "outDir": "lib",
        "rootDir": "src",
        "strict": true,
        "moduleResolution": "node",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true
Enter fullscreen mode Exit fullscreen mode

Init Command

#!/usr/bin/env node

import fs from 'fs'
import { resolve, join } from 'path'

function jit() {
    const command = process.argv[2]

    switch (command) {
        case 'init': {
            const path = process.argv[3] || process.cwd()
            const rootPath = resolve(path)
            const gitPath = join(rootPath, ".git")

            ;["objects", "refs"].forEach(dir => {
                    join(gitPath, dir),
                    { "recursive": true }

            console.log(`initialized empty jit repo in ${gitPath}`)
        default: {
            console.log(`jit: '${command}' is not a jit command`)

Enter fullscreen mode Exit fullscreen mode

Chapter 3 section 1 is all about the init command. The code is simple to start and lacks input validation. Utilizing process.argv to access the command line arguments, the function starts by setting the command. Inside the 'init' block, the code retrieves the input path from the command line arguments; this value also defaults to the user's current working directory. Next, it resolves the input path from a relative path to an absolute path and prepends .git to create the path for the git root directory.

Wondering what the difference between relative and absolute paths are? The book does a fantastic job explaining the difference and why it is necessary to use an absolute path, so I encourage you to purchase a copy of Building Git for yourself and read the explanation on page 28 😉.

After path resolution, the function iterates over a shortlist of strings and generates directories using the fs.mkdirSync command. It uses the recursive property which was introduced in Node.js v10.

Thats all the function does for now, and thus concluding the first bit of implementing Git using Node.js and TypeScript.


I'm very excited about this series. Learning the intricacies of Git and building a clone in my favorite language will be a great learning experience. If you have enjoyed this first post and are interested in joining me on this learning journey, make sure to follow me on Twitter (@ArrowoodTech) and here on Dev. And don't forget to check out the book, Building Git.

Until next time, happy coding!

Top comments (1)

sagartyagi121 profile image
Gajender Tyagi

This is really awesome!!
I'll be following this thread as you write it.