# Under construction

The main reference for this tutorial is the Pro Git book section on GIT internals.

This tutorial uses three libraries:

- CodeMirror, released under the MIT license
- sha1.js, released under the MIT license
- pako 2.0.3, released under the MIT and Zlib licenses, see the project page for details.

# Introduction

GIT is based on a simple model, with a lot of shorthands for common use cases. This model is sometimes hard to guess just from the everyday commands. To illustrate how GIT works, we'll implement a stripped down clone of GIT in a few lines of JavaScript. * empty lines and single closing braces excluded, a few more in total.

# The Operating System's filesystem

# Model of the filesystem

We will simulate the Operating System's filesystem with a very simple key-value store. In this very simple filesystem, directories are entries mapped to `null` and files are entries mapped to strings. The path to the current directory is stored in a separate variable.

var filesystem = {}; var current_directory = '';

# Filesystem access functions (`read`, `write`, `mkdir`, `exists`, `cd`)

The filesystem exposes functions to read an entire file, create or replace an entire file, create a directory, test the existence of a filesystem entry, and change the current directory.

function read(filename) { return filesystem[filename]; } function write(filename, data) { return filesystem[filename] = ""+data; } function exists(filename) { return typeof(filesystem[filename]) !== 'undefined'; } function mkdir(dirname) { return filesystem[dirname] = null; } function cd(d) { current_directory = d; }

# Filesystem access functions (`listdir`)

It will be handy for some operations to list the contents of a directory.

function listdir(dirname) { var depth = dirname.split('/').length + 1; var descendents = filesystem .filter(function (filename) { return filename.startsWith(dirname + '/'); }); var children = descendents .map(function (filename) { return filename.split('/')[depth]; }); // remove duplicates: return Array.from(new Set(children)); }

# Example working directory

Our imaginary user will create a `proj` directory, and start filling in some files.

cd('proj'); mkdir('proj'); write('proj/README', 'This is my Scheme project.\n'); mkdir('proj/src'); write('proj/src/main.scm', '(map (lambda (x) (+ x 1)) (list 1 2 3))\n');

# `git init` (creating `.git`)

The first thing to do is to initialize the GIT directory. For now, only the `.git` folder is needed, The rest of the function implementing `git init` will be implemented later.

function join_paths(a, b) { return (a == "") ? b : (a + "/" + b); } function git_init_mkdir() { mkdir(join_paths(current_directory, '.git')); } git_init_mkdir();

# `git hash-object`.git)

The most basic element of a GIT repository is an object. It is a copy of a file that is stored in GIT's database. That copy is stored under a unique name. The unique name is obtained by hashing the contents of the file.

function hash_object(must_write, type, is_data, path_or_data) { var data = is_data ? path_or_data : read(current_directory + "/" + path_or_data); object_contents = type + ' ' + data.length + '\0' + data; var hash = sha1(object_contents) if (must_write) { mkdir(join_paths(current_directory, '.git/objects')); mkdir(join_paths(current_directory, '.git/objects/' + hash.slice(0,2))); var object_path = join_paths(current_directory, '.git/objects/' + hash.slice(0,2) + '/' + hash.slice(2)); write(object_path, deflate(object_contents)); } return hash; }

# Adding a file to the GIT database

So far, our GIT database does not know about any of the user's files. In order to add the contents of the `README` file in the database, we use `git hash-object -w -t blob README`, where `-w` tells GIT to write the object in its database, and `-t blob` indicates that we want to create a blob object, i.e. the contents of a file.

// git hash-object -w -t blob README hash_object(true, 'blob', false, 'README');

The objects stored in the GIT database are compressed with zlib (using the "deflate" compression method). The filesystem view shows the deflated: followed by the uncompressed data. Click on the file contents to toggle between this pretty-printed view and the raw compressed data.

You will notice that the database does not contain the name of the file, only its contents, stored under a unique identifier which is derived by hashing its contents. Let's add the second user file to the database.

// git hash-object -w -t blob src/main.scm hash_object(true, 'blob', false, 'src/main.scm');

# `zlib` compression

The real implementation of GIT compresses objects with zlib. To view a zlib-compressed object in your terminal, simply write this declaration in your shell, and then call e.g. `unzlib .git/objects/95/d318ae78cee607a77c453ead4db344fc1221b7`

    unzlib() {
      python -c \
        "import sys,zlib; \
         sys.stdout.buffer.write(zlib.decompress(open(sys.argv[1], 'rb').read()));" \
        "$1"
    }

# Storing trees (list of hashed files and subtrees)

Now GIT knows about the contents of both of the user's files, but it would be nice to also store the filenames. This is done by creating a tree object

A tree object can contain files (by associating the file's blob to its name), or directories (by associating the hash of other subtrees to their name). The mode (`100644` for the file and `40000`) incidates the permissions, and is given in octal using the values used by *nix

// base_directory is a string // filenames is a list of strings // subtrees is a list of {name, hash} objects. function store_tree(base_directory, filenames, subtrees) { function get_file_hash(filename) { return from_hex(hash_object(true, 'blob', false, join_paths(base_directory, filename))); } var blobs = filenames.map(function (filename) { return "100644 " + filename + "\0" + get_file_hash(filename) }); var trees = subtrees.map(function (subtree) { return "40000 " + subtree.name + "\0" + from_hex(subtree.hash); }); tree_contents = blobs.join('') + trees.join(''); // cat tree_contents | git hash-object -w -t tree --stdin return hash_object(true, 'tree', true, tree_contents); }

This function needs a small utility to convert hashes encoded in hexadecimal to a binary form.

function from_hex(hex) { var hex = String(hex); var str = "" for (var i = 0; i < hex.length; i+=2) { str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); } return str; }

# Example use of store_tree

//hash_src_tree = store_tree("src/", ["main.scm"], []); //hash_root_tree = store_tree("", ["README"], [{name:"src", hash:hash_src_tree}]);

# Storing a tree from a list of paths

Making trees out of the subfolders one by one is cumbersome. Here's a utility function which takes a list of paths, and builds a tree from those.

function paths_to_tree(paths) { var hierarchy = { subfolders: {}, files: [] }; for (var i = 0; i < paths.length; i++) { var path_components = paths[i].split('/'); var h = hierarchy; for (var j = 0; j < path_components.length - 1; j++) { if (! h.subfolders.hasOwnProperty(path_components[j])) { h.subfolders[path_components[j]] = { subfolders: {}, files: [] }; } h = h.subfolders[path_components[j]]; } h.files.push(path_components[i]); } var to_tree = function(base_directory, hierarchy) { var subtrees = []; for (var i in hierarchy.subfolders) { if (hierarchy.subfolders.hasOwnProperty(i)) { subtrees.push({ name: i, hash: to_tree(join_paths(base_directory, i), hierarchy.subfolders[i]) }); } } return store_tree(base_directory, hierarchy.files, subtrees); } return to_tree("", hierarchy); } // git add README src/main.scm paths_to_tree(["README", "src/main.scm"]);

# Storing a commit in the GIT database

Now that the GIT database contains the entire tree for the current version, a commit can be created. A commit contains

- a pointer to the tree
- a pointer to the previous ("parent") commit (or to multiple parent commits merging them, or no parents for the initial commit)
- information about the author (the person who initially wrote the code)
- information about the committer (the person who adds the code to the GIT database, often the same person as the author, but it can be a different person e.g. when someone else makes changes to the history or applies a patch recieved by e-mail)
- a description

The author and committer information contain

- the person's name
- the person's email
- the *nix timestamp at which the version was authored or committed
- the timezone for that timestamp

function store_commit(tree, parents, author, committer, message) { var commit_contents = ''; commit_contents += 'tree ' + tree + '\n'; for (var i = 0; i < parents.length; i++) { commit_contents += 'parent ' + parents[i] + '\n'; } commit_contents += 'author ' + author.name + ' <' + author.email + '> ' + format_date(author.date) + ' ' + format_timezone(author.timezoneMinutes) + '\n'; commit_contents += 'committer ' + committer.name + ' <' + committer.email + '> ' + format_date(committer.date) + ' ' + format_timezone(committer.timezoneMinutes) + '\n'; commit_contents += '\n'; commit_contents += '' + message + (message[message.length-1] == '\n' ? '' : '\n'); // cat commit_contents | git hash-object -w -t commit --stdin return hash_object(true, 'commit', true, commit_contents); } function format_date(d) { return Math.floor((+d) / 1000); } function left_pad(s, char, len) { while ((''+s).length < len) { s = '' + char + s; } return s; } function format_timezone(tm) { var h = Math.floor(Math.abs(+tm)/60); var m = Math.abs(+tm)%60; return (tm >= 0 ? '+' : '-') + left_pad(h, '0', 2) + left_pad(m, '0', 2); }

# Storing an example commit

It is now possible to store a commit in the database. This saves a copy of the tree along with some metadata about this version. The first commit has no parent, which is represented by passing the empty list.

initial_commit = store_commit( paths_to_tree(["README", "src/main.scm"]), [], {name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60}, {name:'Ada Lovelace', email:'ada@analyti.cal', date:new Date(1617120803000), timezoneMinutes: +60}, 'Initial commit');

# `git branch`

A branch is a pointer to a commit, stored in a file in `.git/refs/heads/name_of_the_branch`. The branch can be overwritten with `git branch -f`. Also, as will be explained later, `git commit` can update the pointer of a branch.

function git_branch(branch_name, commit_ref, force) { var commit_hash = git_rev_parse(commit_ref); mkdir(join_paths(current_directory, '.git/refs')); mkdir(join_paths(current_directory, '.git/refs/heads')); if (!force && exists(join_paths(current_directory, '.git/refs/heads/' + branch_name))) { alert("branch already exists"); return false; } else { write(join_paths(current_directory, '.git/refs/heads/' + branch_name), commit_hash + '\n'); return true; } } // git branch main 0123456789012345678901234567890123456789 git_branch('main', initial_commit, false); // git branch -f main 0123456789012345678901234567890123456789 git_branch('main', initial_commit, true);

# `HEAD`

The HEAD indicates the "current" commit. It is set at first as part of the `git init` routine.

function git_init_head() { write(join_paths(current_directory, '.git/HEAD'), 'ref: refs/heads/main\n'); } git_init_head();

# `git commit`

If the `HEAD` points to a commit hash, then `git commit` updates the `HEAD` to point to the new commit. Otherwise, when the `HEAD` points to a branch, then the target branch (represented by a file named `.git/refs/heads/the_branch_name`) is updated.

var gitconfig = { user: { name: 'Ada Lovelace', email: 'ada@analyti.cal', } }; var $EDITOR = function() { return window.prompt('Commit message:'); } function git_commit(file_paths, message) { var now = new Date(); var timestamp = (+now)/1000; var timezoneMinutes = -(now.getTimezoneOffset()); var parent = git_rev_parse('HEAD'); var parents = parent ? [parent] : [] var new_commit_hash = store_commit( paths_to_tree(file_paths), parents, {name:gitconfig.user.name, email:gitconfig.user.email, date:now, timezoneMinutes:timezoneMinutes }, {name:gitconfig.user.name, email:gitconfig.user.email, date:now, timezoneMinutes:timezoneMinutes }, message || $EDITOR()); advance_head(new_commit_hash); return new_commit_hash; } function advance_head(new_commit_hash) { var referenced_branch = git_symbolic_ref('HEAD'); if (referenced_branch) { // Update the target of the ref: write(join_paths(current_directory, '.git/' + referenced_branch), new_commit_hash + '\n'); } else { // Detached HEAD, update .git/HEAD directly. write(join_paths(current_directory, '.git/HEAD'), new_commit_hash + '\n'); } } function git_rev_parse(ref) { var symbolic_ref_target = git_symbolic_ref(ref); if (symbolic_ref_target) { // symbolic ref like "ref: refs/heads/main" return git_rev_parse(symbolic_ref_target); } else if (/[0-9a-f]{40}/.test(ref)) { // hash like "0123456789abcdef0123456789abcdef01234567" return ref; } else if (ref == 'HEAD') { // user-friendly reference like "HEAD" return git_rev_parse(trim_newline(read(join_paths(current_directory, '.git/' + ref)))); } else if (ref.startsWith('refs/') && exists(join_paths(current_directory, '.git/' + ref))) { // user-friendly reference like "refs/heads/main" return git_rev_parse(trim_newline(read(join_paths(current_directory, '.git/' + ref)))); } else if (exists(join_paths(current_directory, '.git/refs/heads/' + ref))) { // user-friendly reference like "main" (a branch) return git_rev_parse(trim_newline(read(join_paths(current_directory, '.git/refs/heads/' + ref)))); } else if (exists(join_paths(current_directory, '.git/refs/tags/' + ref))) { // user-friendly reference like "v1.0" (a branch) return git_rev_parse(trim_newline(read(join_paths(current_directory, '.git/refs/tags/' + ref)))); } else { // unknown ref return false; } } function git_symbolic_ref(ref) { var ref_file = join_paths(current_directory, '.git/' + ref); if (exists(ref_file) && read(ref_file).startsWith('ref: ')) { return trim_newline(read(ref_file)).substr('ref: '.length); } else { return false; } } function trim_newline(s) { if (s.endsWith('\n')) { return s.substr(0, s.length-1); } else { return s; } } write('proj/README', 'This is my Scheme project -- with updates!'); var second_commit = git_commit(['README', 'src/main.scm'], 'Some updates');

# `git tag`

Tags are like branches, but are stored in `.git/refs/tags/the_tag_name` and a tag is not normally modified. Once created, it's supposed to always point to the same version.

GIT does offer a `git tag -f existing-tag new-hash` command, but using it should be a rare occurrence.

function git_tag(tag_name, commit_hash) { mkdir(join_paths(current_directory, '.git/refs')); mkdir(join_paths(current_directory, '.git/refs/tags')); if (exists(join_paths(current_directory, '.git/refs/tags/' + tag_name))) { alert("tag already exists"); return false; } else { write(join_paths(current_directory, '.git/refs/tags/' + tag_name), commit_hash + '\n'); return true; } } // git tag v1.0 0123456789012345678901234567890123456789 git_tag('v1.0', second_commit);

# `git checkout`

# Checkout, branches and other references

More importantly, the HEAD does not normally point to a tag. Although nothing actually prevents writing `ref: refs/tags/v1.0` into `.git/HEAD`, the GIT commands will not automatically do this. For example, `git checkout tag-or-branch-or-hash` will put a symbolic `ref:` in `.git/HEAD` only if the argument is a branch.

function git_checkout(tag_or_branch_or_hash) { if (exists(join_paths(current_directory, '.git/refs/heads/' + tag_or_branch_or_hash))) { // Normal (attached) HEAD, points to 'ref: refs/heads/the_branch_name' write(join_paths(current_directory, '.git/HEAD'), 'ref: refs/heads/' + tag_or_branch_or_hash + '\n'); } else { // Detached HEAD, points directly to commit hash write(join_paths(current_directory, '.git/HEAD'), git_rev_parse(tag_or_branch_or_hash) + '\n'); } checkout_files(git_rev_parse('HEAD')); }

# Checking out files

function checkout_files(hash) { var commit = parse_commit(hash); checkout_tree(current_directory, commit.tree); } function checkout_tree(path_prefix, hash) { var entries = parse_tree(hash); for (var i = 0; i < entries.length; i++) { var o = parse_object(entries[i].hash); var entry_path = join_paths(path_prefix, entries[i].name); if (o.type == 'blob') { write(entry_path, o.contents); } else { checkout_tree(entry_path, entries[i].hash) } } }

# Assert

The parsers will check that their input looks reasonably well-formed, using `assert()`. function assert(boolean, text) { if (! boolean) { alert("assertion failed: " + text); throw new Error(text); } }

# Reading compressed objects

function parse_object(hash) { var compressed = read(join_paths(current_directory, '.git/objects/' + hash.substr(0,2) + '/' + hash.substr(2))); var inflated = inflate(compressed); var split = inflated.match(/^([\s\S]*?) ([\s\S]*?)\0([\s\S]*)$/); assert(split, "ill-formed object"); var type = split[1]; var length = split[2]; var contents = split[3]; assert(contents.length == length, "object has incorrect length"); return { type: type, length: length, contents: contents }; }

# Parsing tree objects

function parse_tree(hash) { var tree = parse_object(hash); var split = tree.contents.split(/(?<=\0[\s\S]{20})/); assert(split, 'invalid contents of tree object'); var entries = []; for (var i = 0; i < split.length; i++) { entries.push(parse_tree_entry(split[i])); } return entries; } function parse_tree_entry(entry) { var split = entry.match(/^([0-9]+) ([\s\S]*)\0([\s\S]{20})$/); assert(split, 'invalid entry in tree object'); var mode = split[1]; var name = split[2]; var hash = to_hex(split[3]); return { mode: mode, name: name, hash: hash }; }

The `parse_tree` function above needs a small utility to convert hashes in binary form to a hexadecimal representation.

function to_hex(bin) { var bin = String(bin); var hex = ""; for (var i = 0; i < bin.length; i++) { hex += left_pad(bin.charCodeAt(i).toString(16), '0', 2); } return hex; }

# Parsing commit objects

function parse_commit(hash) { var commit = parse_object(hash); var lines = commit.contents.split('\n'); var tree = null; var parents = []; var author = null; var committer = null; var i; // A blank line separates the headers from the message. for (i = 0; i < lines.length && lines[i] != ''; i++) { var split = lines[i].match(/^(.*?) (.*)$/); assert(split, "ill-formed commit header: " + lines[i]); var header = split[1]; var value = split[2]; switch (header) { case 'tree': assert(!tree, 'duplicate tree header in commit'); assert(/^[0-9a-f]{40}$/.test(value), "invalid tree header in commit"); tree = value; break; case 'parent': assert(/^[0-9a-f]{40}$/.test(value), "invalid parent header in commit"); parents.push(value); break; case 'author': assert(!author, 'duplicate author header in commit'); author = parse_author(value, 'author'); break; case 'committer': assert(!committer, 'duplicate committer header in commit'); committer = parse_author(value, 'committer'); break; default: /* unknown field, skipping */ break; } } // The message is everything after the blank line. message = lines.splice(i+1).join('\n'); assert(tree, 'commit lacks tree header'); assert(author, 'commit lacks author header'); assert(committer, 'commit lacks committer header'); return { tree: tree, parents: parents, author: author, committer: committer, message: message }; }

# Parsing author and committer metadata

function parse_author(value, field) { var split = value.match(/^(.*?) <(.*?)> ([0-9]+) ([+-])([0-9][0-9])([0-9][0-9])$/); assert(split, 'ill-formed ' + field) var name = split[1]; var email = split[2]; var date = new Date(parseInt(split[3], 10) * 1000); var timezone_sign = (split[4] == '+' ? 1 : -1); var timezone_hours = parseInt(split[5], 10); var timezone_minutes = parseInt(split[6], 10); var timezone = timezone_sign * (timezone_hours * 60 + timezone_minutes); return { name: name, email: email, date: date, timzeone: timezone }; }

# Example checkout

git_checkout(initial_commit);

# `git init`

The `git init` command creates the `.git` directory and points `.git/HEAD` to the default branch (a file which does not exist yet, as this branch does not contain any commit at this point).

function git_init() { git_init_mkdir(); git_init_head(); }

# The index

When adding files with `git add`, GIT does not immediately create a commit object. Instead, it adds the files to the index, which uses a binary format with lots of metadata. The mock filesystem used here lacks most of these pieces of information, so thr value `0` will be used for most fields. See this blog post for a more in-depth study of the index.

function binary(val, bytes) { return from_hex(left_pad(val.toString(16), '0', bytes*2)); } function binary16(val) { return binary(val, 2); } function binary32(val) { return binary(val, 4); } function binary64(val) { return binary(val, 8); } function store_index(paths) { var magic = 'DIRC' // DIRectory Cache var version = binary32(2); var entries = binary32(paths.length); var header = magic + version + entries; index = header; for (var i = 0; i < paths.length; i++) { var ctime = binary64(0); var mtime = binary64(0); var device = binary32(0); var inode = binary32(0); // default permissions for files, in octal. var mode = binary32(0100644); var uid = binary32(0); var gid = binary32(0); var size = binary32(read(join_paths(current_directory, paths[i])).length); var hash = from_hex(hash_object(true, 'blob', false, paths[i])); // for this simple index, the flags (the 4 higher bits) are 0. assert(paths[i].length < 0xfff) var flags_and_file_path_length = binary16(paths[i].length) var file_path = paths[i] + '\0'; entry = ctime + mtime + device + inode + mode + uid + gid + size + hash + flags_and_file_path_length + file_path; while (entry.length % 8 != 0) { // pad with null bytes to a multiple of 8 bytes (64-bits). entry += '\0'; } index += entry; } index += from_hex(sha1(index)); write(join_paths(current_directory, '.git/index'), index) }

# Playground

The implementation is now sufficiently complete to create a small repository.

// Reset the filesystem to its initial state filesystem = {}; current_directory = ''; cd('proj'); mkdir('proj'); write('proj/README', 'This is my implementation of GIT.\n'); mkdir('proj/src'); write('proj/src/main.scm', "(define filesystem '())\n...\n"); git_init(); git_commit(['README', 'src/main.scm'], 'A well-understood initial commit.'); git_branch('dev', 'HEAD'); git_checkout('dev'); write('proj/src/main.scm', "(define filesystem '())\n(define current_directory \"\")\n"); git_commit(['README', 'src/main.scm'], 'What an update!'); git_checkout('main'); // update the cache of the working directory. Without this, // GIT finds an empty cache, and thinks all files are scheduled // for deletion, until "git add ." allows it to realize that // the working directory matches the contents of HEAD. store_index(['README', 'src/main.scm']);

By clicking on "Copy commands to recreate in *nix terminal.", it is possible to copy a series of `mkdir …` and `printf … > …` commands that, when executed, will recreate the virtual filesystem on a real system. The resulting folder is binary-compatible with the official `git log`, `git status`, `git checkout` etc. commands.

# Conclusion

This article shows that a large part of the core of GIT can be re-implemented in a few source lines of code* (copy all the code). * empty lines and single closing braces excluded, a few more in total.

- Some of the features which may appear mysterious at first sight (e.g. detached HEAD) should be clearer with the knowledge of how GIT works behind the scenes.
- Furthermore, branches are often associated with an intuition (containers into which commits are added) which does not match the implementation (mutable pointers to commits).
- Finally, it is tempting to think of commits as patches. While `darcs` tries to expose an interface which matches this intuition, it is clear that the implementation of GIT considers commits as copies of the entire repository, and are linked to the previous version solely by the `parent` metadata in the commit headers.

A few core commands like `git diff` and `git apply` are not described in this tutorial. They are little more than improved versions of the classical *nix commands `diff` and `patch`.

Most other commands provided by GIT are merely convenience wrappers around these commands. For example, `git cherry-pick` is simply a combination of `git diff` between the tree of a commit and the tree of its parent, followed by `git apply` to apply the patch and `git commit` to create a new commit whose diff is equivalent to the diff of the original commit. As an other example, the command `git rebase` performs as succession of `cherry-pick` operations.

By keeping in mind the internal model of GIT, it becomes easier to understand the usual commands and their quirks. By undersanding the design philosophy behind the implementation, the day-to-day usage can become, hopefully, less surprising.