Skip to main content

Overview

The GitEngine class is the heart of Learn Git Branching. It simulates all git operations in-memory, manages the commit graph structure, and coordinates with the visualization system to display changes. Location: src/js/git/index.js

Architecture

Class Structure

function GitEngine(options) {
  this.rootCommit = null;
  this.refs = {};           // All refs (commits, branches, tags)
  this.HEAD = null;         // HEAD reference
  this.origin = null;       // Remote repository (GitEngine instance)
  this.mode = 'git';        // 'git' or 'hg' (Mercurial)
  this.localRepo = null;    // For origin repos, reference to local
  
  // Collections (Backbone-style)
  this.branchCollection = options.branches;
  this.tagCollection = options.tags;
  this.commitCollection = options.collection;
  
  // Visualization reference
  this.gitVisuals = options.gitVisuals;
  
  // Event system
  this.eventBaton = options.eventBaton;
  this.eventBaton.stealBaton('processGitCommand', this.dispatch, this);
}

Key Properties

PropertyTypeDescription
refsObjectMap of all references (commits, branches, tags, HEAD) by ID
rootCommitCommitThe initial C0 commit
HEADRefPoints to current branch or commit (detached HEAD)
originGitEngineSeparate GitEngine instance simulating remote repository
modeStringEither ‘git’ or ‘hg’ for Mercurial simulation
commitCollectionCollectionBackbone collection of all commits
branchCollectionCollectionBackbone collection of all branches
tagCollectionCollectionBackbone collection of all tags
gitVisualsGitVisualsReference to visualization system
eventBatonEventBatonEvent handling system for commands

Core Operations

Initialization

init()

Creates the initial repository state:
GitEngine.prototype.init = function() {
  // Create root commit C0
  this.rootCommit = this.makeCommit(null, null, {rootCommit: true});
  this.commitCollection.add(this.rootCommit);
  
  // Create main branch
  var main = this.makeBranch('main', this.rootCommit);
  
  // Create HEAD pointing to main
  this.HEAD = new Ref({
    id: 'HEAD',
    target: main
  });
  this.refs[this.HEAD.get('id')] = this.HEAD;
  
  // Make initial commit
  this.commit();
};

Commit Operations

makeCommit(parents, id, options)

Creates a new commit with automatic ID generation:
GitEngine.prototype.makeCommit = function(parents, id, options) {
  // Generate unique ID if not provided
  if (!id) {
    id = this.getUniqueID();
  }
  
  var commit = new Commit(Object.assign({
    parents: parents,
    id: id,
    gitVisuals: this.gitVisuals
  }, options || {}));
  
  this.refs[commit.get('id')] = commit;
  this.commitCollection.add(commit);
  return commit;
};

commit(options)

Performs a git commit operation:
GitEngine.prototype.commit = function(options) {
  var targetCommit = this.getCommitFromRef(this.HEAD);
  var id = null;
  
  // Handle amend
  if (options.isAmend) {
    targetCommit = this.resolveID('HEAD~1');
    id = this.rebaseAltID(this.getCommitFromRef('HEAD').get('id'));
  }
  
  var newCommit = this.makeCommit([targetCommit], id);
  
  // Warn if detached HEAD in git mode
  if (this.getDetachedHead() && this.mode === 'git') {
    this.command.addWarning(intl.str('git-warning-detached'));
  }
  
  this.setTargetLocation(this.HEAD, newCommit);
  return newCommit;
};

Branch Management

makeBranch(id, target)

Creates a new branch:
GitEngine.prototype.makeBranch = function(id, target) {
  if (this.refs[id]) {
    throw new Error('Branch already exists: ' + id);
  }
  
  var branch = new Branch({
    target: target,
    id: id
  });
  
  this.branchCollection.add(branch);
  this.refs[branch.get('id')] = branch;
  return branch;
};

validateBranchName(name)

Validates branch names according to git rules:
GitEngine.prototype.validateBranchName = function(name) {
  // Escape special characters
  name = name.replace(///g,"\/");
  name = name.replace(/\s/g, '');
  
  // Check format: alphanumeric with optional / or -
  if (!/^(\w+[.\/\-]?)+\w+$/.test(name) || name.search('o/') === 0) {
    throw new GitError({msg: intl.str('bad-branch-name', {branch: name})});
  }
  
  // Don't allow C# pattern (reserved for commits)
  if (/^[cC]\d+$/.test(name)) {
    throw new GitError({msg: intl.str('bad-branch-name', {branch: name})});
  }
  
  // Don't allow HEAD in name
  if (/[hH][eE][aA][dD]/.test(name)) {
    throw new GitError({msg: intl.str('bad-branch-name', {branch: name})});
  }
  
  // Limit length to 9 characters
  if (name.length > 9) {
    name = name.slice(0, 9);
    this.command.addWarning(intl.str('branch-name-short', {branch: name}));
  }
  
  return name;
};

Reference Resolution

resolveID(idOrTarget)

Resolves string references to actual objects:
GitEngine.prototype.resolveID = function(idOrTarget) {
  if (typeof idOrTarget !== 'string') {
    return idOrTarget;
  }
  return this.resolveStringRef(idOrTarget);
};

resolveStringRef(ref)

Handles complex reference resolution including relative refs:
GitEngine.prototype.resolveStringRef = function(ref) {
  // Direct reference
  if (this.refs[ref]) {
    return this.refs[ref];
  }
  
  // Case-insensitive commit IDs
  if (ref.match(/^c\d+'*/) && this.refs[ref.toUpperCase()]) {
    return this.refs[ref.toUpperCase()];
  }
  
  // Parse relative references (e.g., HEAD~2, main^)
  var regex = /^([a-zA-Z0-9]+)(([~\^]\d*)*)$/;
  var matches = regex.exec(ref);
  
  if (matches) {
    var startRef = matches[1];
    var relative = matches[2];
    
    var commit = this.getCommitFromRef(startRef);
    if (relative) {
      commit = this.resolveRelativeRef(commit, relative);
    }
    return commit;
  }
  
  throw new GitError({msg: intl.str('git-error-exist', {ref: ref})});
};

resolveRelativeRef(commit, relative)

Handles ~ and ^ relative reference operators:
GitEngine.prototype.resolveRelativeRef = function(commit, relative) {
  var regex = /([~\^])(\d*)/g;
  var matches;
  
  while (matches = regex.exec(relative)) {
    var next = commit;
    var num = matches[2] ? parseInt(matches[2], 10) : 1;
    
    if (matches[1] == '^') {
      // ^ selects parent by index
      next = commit.getParent(num-1);
    } else {
      // ~ traverses first parent num times
      while (next && num--) {
        next = next.getParent(0);
      }
    }
    
    if (!next) {
      throw new GitError({
        msg: intl.str('git-error-relative-ref', {
          commit: commit.id,
          match: matches[0]
        })
      });
    }
    
    commit = next;
  }
  
  return commit;
};

Remote Operations

Origin Simulation

Remote repositories are simulated by creating a separate GitEngine instance:

makeOrigin(treeString)

Creates a remote repository:
GitEngine.prototype.makeOrigin = function(treeString) {
  if (this.hasOrigin()) {
    throw new GitError({msg: intl.str('git-error-origin-exists')});
  }
  
  treeString = treeString || this.printTree(this.exportTreeForBranch('main'));
  
  // Create origin visualization and git engine
  var mainVis = this.gitVisuals.getVisualization();
  var originVis = mainVis.makeOrigin({
    localRepo: this,
    treeString: treeString
  });
  
  // Connect when origin is ready
  originVis.customEvents.on('gitEngineReady', function() {
    this.origin = originVis.gitEngine;
    originVis.gitEngine.assignLocalRepo(this);
    this.syncRemoteBranchFills();
    this.origin.externalRefresh();
    this.animationFactory.playRefreshAnimationAndFinish(
      this.gitVisuals, 
      this.animationQueue
    );
  }, this);
  
  // Create remote tracking branches
  var originTree = JSON.parse(unescape(treeString));
  Object.keys(originTree.branches).forEach(function(branchName) {
    var originTarget = this.findCommonAncestorWithRemote(
      branchJSON.target
    );
    
    var remoteBranch = this.makeBranch(
      ORIGIN_PREFIX + branchName,
      this.getCommitFromRef(originTarget)
    );
    
    this.setLocalToTrackRemote(this.refs[branchName], remoteBranch);
  }, this);
};

Push Operation

push(options)

Implements git push with fast-forward checking:
GitEngine.prototype.push = function(options) {
  var sourceBranch = this.resolveID(options.source);
  var branchOnRemote = this.origin.resolveID(options.destination);
  
  // Check fast-forward unless --force
  if (!options.force) {
    this.checkUpstreamOfSource(
      this,
      this.origin,
      branchOnRemote,
      sourceLocation,
      intl.str('git-error-origin-push-no-ff')
    );
  }
  
  // Get commits to push
  var commitsToMake = this.getTargetGraphDifference(
    this.origin,
    this,
    branchOnRemote,
    sourceLocation,
    {dontThrowOnNoFetch: true}
  );
  
  // Filter commits already on remote
  commitsToMake = commitsToMake.filter(function(commitJSON) {
    return !this.origin.refs[commitJSON.id];
  }, this);
  
  // Create commits on remote with animation
  var chain = Promise.resolve();
  commitsToMake.forEach(function(commitJSON) {
    chain = chain.then(function() {
      return this.animationFactory.playHighlightPromiseAnimation(
        this.refs[commitJSON.id],
        branchOnRemote
      );
    }.bind(this));
    
    chain = chain.then(function() {
      var newCommit = this.origin.makeCommit(
        commitJSON.parents.map(id => this.origin.refs[id]),
        commitJSON.id
      );
      return this.animationFactory.playCommitBirthPromiseAnimation(
        newCommit,
        this.origin.gitVisuals
      );
    }.bind(this));
  }, this);
  
  // Update remote branch pointer
  chain = chain.then(function() {
    var localCommit = this.getCommitFromRef(sourceLocation);
    this.origin.setTargetLocation(branchOnRemote, 
      this.origin.refs[localCommit.get('id')]);
    return this.animationFactory.playRefreshAnimation(this.origin.gitVisuals);
  }.bind(this));
  
  this.animationQueue.thenFinish(chain);
};

Fetch Operation

fetch(options)

Downloads commits from remote:
GitEngine.prototype.fetch = function(options) {
  // Create remote branches if needed
  var sourceDestPairs = [];
  var didMakeBranch = this.makeRemoteBranchIfNeeded(options.source);
  
  // Check fast-forward unless --force
  if (!options.force) {
    sourceDestPairs.forEach(function (pair) {
      this.checkUpstreamOfSource(
        this,
        this.origin,
        pair.destination,
        pair.source
      );
    }, this);
  }
  
  // Get commits to fetch
  var commitsToMake = [];
  sourceDestPairs.forEach(function (pair) {
    commitsToMake = commitsToMake.concat(
      this.getTargetGraphDifference(
        this,
        this.origin,
        pair.destination,
        pair.source,
        {dontThrowOnNoFetch: true}
      )
    );
  }, this);
  
  // Create commits locally
  var chain = Promise.resolve();
  commitsToMake.forEach(function (commitJSON) {
    chain = chain.then(function() {
      var newCommit = this.makeCommit(
        commitJSON.parents.map(id => this.refs[id]),
        commitJSON.id
      );
      return this.animationFactory.playCommitBirthPromiseAnimation(
        newCommit,
        this.gitVisuals
      );
    }.bind(this));
  }, this);
  
  this.animationQueue.thenFinish(chain);
};

Tree Management

Tree Export/Import

exportTree()

Serializes entire git graph to JSON:
GitEngine.prototype.exportTree = function() {
  var totalExport = {
    branches: {},
    commits: {},
    tags: {},
    HEAD: null
  };
  
  // Export branches
  this.branchCollection.toJSON().forEach(function(branch) {
    branch.target = branch.target.get('id');
    delete branch.visBranch;
    totalExport.branches[branch.id] = branch;
  });
  
  // Export commits
  this.commitCollection.toJSON().forEach(function(commit) {
    Commit.constants.circularFields.forEach(function(field) {
      delete commit[field];
    });
    commit.parents = (commit.parents || []).map(par => par.get('id'));
    totalExport.commits[commit.id] = commit;
  });
  
  // Export tags
  this.tagCollection.toJSON().forEach(function(tag) {
    delete tag.visTag;
    tag.target = tag.target.get('id');
    totalExport.tags[tag.id] = tag;
  });
  
  // Export HEAD
  var HEAD = this.HEAD.toJSON();
  HEAD.target = HEAD.target.get('id');
  totalExport.HEAD = HEAD;
  
  // Include origin if present
  if (this.hasOrigin()) {
    totalExport.originTree = this.origin.exportTree();
  }
  
  return totalExport;
};

loadTree(tree)

Restores git state from exported tree:
GitEngine.prototype.loadTree = function(tree) {
  tree = JSON.parse(JSON.stringify(tree)); // Deep copy
  
  this.removeAll();
  this.instantiateFromTree(tree);
  this.reloadGraphics();
  this.initUniqueID();
};

VCS Mode Support

GitEngine supports both Git and Mercurial:

handleModeChange(vcs, callback)

GitEngine.prototype.handleModeChange = function(vcs, callback) {
  if (this.mode === vcs) {
    callback();
    return;
  }
  
  Main.getEvents().trigger('vcsModeChange', {mode: vcs});
  var chain = this.setMode(vcs);
  
  if (this.origin) {
    this.origin.setMode(vcs, function() {});
  }
  
  if (chain) {
    chain.then(callback);
  } else {
    callback();
  }
};

Event System

GitEngine uses the EventBaton pattern for command processing:
// Steal command processing responsibility
this.eventBaton.stealBaton('processGitCommand', this.dispatch, this);

// Release when done
this.eventBaton.releaseBaton('processGitCommand', this.dispatch, this);

Key Algorithms

Unique ID Generation

GitEngine.prototype.getUniqueID = function() {
  var id = this.uniqueId('C');
  
  var hasID = function(idToCheck) {
    if (this.refs[idToCheck]) return true;
    if (this.origin && this.origin.refs[idToCheck]) return true;
    return false;
  }.bind(this);
  
  while (hasID(id)) {
    id = this.uniqueId('C');
  }
  return id;
};

Graph Difference Algorithm

Finds commits to transfer between local and remote:
GitEngine.prototype.getTargetGraphDifference = function(
  target, source, targetBranch, sourceBranch, options
) {
  var targetSet = Graph.getUpstreamSet(target, targetBranch);
  var sourceStartCommit = source.getCommitFromRef(sourceBranch);
  
  // Breadth-first search for new commits
  var difference = [];
  var toExplore = [sourceStartCommitJSON];
  
  while (toExplore.length) {
    var here = toExplore.pop();
    difference.push(here);
    
    here.parents.forEach(function(parentID) {
      if (!targetSet[parentID]) {
        toExplore.push(sourceTree.commits[parentID]);
      }
    });
  }
  
  // Order by dependency
  var inOrder = [];
  while (differenceUnique.length) {
    for (var i = 0; i < differenceUnique.length; i++) {
      if (allParentsMade(differenceUnique[i])) {
        var makeThis = differenceUnique[i];
        inOrder.push(makeThis);
        differenceUnique.splice(i, 1);
        targetSet[makeThis.id] = true;
      }
    }
  }
  
  return inOrder;
};

Build docs developers (and LLMs) love