Skip to main content

Overview

The command processing system parses text commands entered by users and executes them against the git simulation. It supports both git and Mercurial command sets with a flexible regex-based parsing system. Location: src/js/commands/

Architecture

Command Flow

User Input

Command String

Parser (parse function)

Command Object

Command Executor

GitEngine Method

Visualization Update

Command Structure

Location: src/js/commands/index.js

Command Configuration

Commands are defined with regex patterns and execution functions:
var commandConfigs = {
  'git': GitCommands.commandConfig,
  'hg': MercurialCommands.commandConfig
};

// Example from GitCommands.commandConfig
{
  commit: {
    regex: /^git +commit($|\s)/,
    options: ['--amend'],
    execute: function(engine, command) {
      var options = command.getSupportedMap();
      engine.commit({
        isAmend: !!options['--amend']
      });
    }
  },
  
  branch: {
    regex: /^git +branch($|\s)/,
    options: ['-d', '-D', '-f', '--contains'],
    execute: function(engine, command) {
      var args = command.getGeneralArgs();
      var options = command.getSupportedMap();
      
      if (options['-d'] || options['-D']) {
        engine.deleteBranch(args[0]);
      } else if (options['--contains']) {
        engine.printBranchesWithout(args[0]);
      } else if (args.length === 0) {
        engine.printBranches(engine.getBranches());
      } else {
        engine.validateAndMakeBranch(args[0], engine.getCommitFromRef('HEAD'));
      }
    }
  }
}

Command Object

Parsed commands create a command object with metadata:
var commandData = {
  generalArgs: parsedOptions.generalArgs,
  supportedMap: parsedOptions.supportedMap,
  error: error,
  vcs: vcs,
  method: method,
  options: options,
  eventName: 'processGitCommand'
};

Parser Implementation

Location: src/js/commands/index.js

Main Parse Function

var parse = function(str) {
  var vcs;
  var method;
  var options;
  
  // Match against all command regexes
  var regexMap = commands.getRegexMap();
  Object.keys(regexMap).forEach(function (thisVCS) {
    var map = regexMap[thisVCS];
    Object.keys(map).forEach(function(thisMethod) {
      var regex = map[thisMethod];
      if (regex.exec(str)) {
        vcs = thisVCS;
        method = thisMethod;
        
        // Parse options: split on space-groups, skip first 2 (vcs + command)
        options = str.match(/('.*?'|".*?"|\S+)/g).slice(2);
      }
    });
  });
  
  if (!method) {
    return false;
  }
  
  // Parse options and arguments
  var parsedOptions = new CommandOptionParser(vcs, method, options);
  var error = parsedOptions.explodeAndSet();
  
  return {
    toSet: {
      generalArgs: parsedOptions.generalArgs,
      supportedMap: parsedOptions.supportedMap,
      error: error,
      vcs: vcs,
      method: method,
      options: options,
      eventName: 'processGitCommand'
    }
  };
};

CommandOptionParser

Parses command options and arguments:
function CommandOptionParser(vcs, method, options) {
  this.vcs = vcs;
  this.method = method;
  this.rawOptions = options;
  
  // Get supported options for this command
  this.supportedMap = commands.getOptionMap()[vcs][method];
  if (this.supportedMap === undefined) {
    throw new Error('No option map for ' + method);
  }
  
  this.generalArgs = [];
}

CommandOptionParser.prototype.explodeAndSet = function() {
  for (var i = 0; i < this.rawOptions.length; i++) {
    var part = this.rawOptions[i];
    
    if (part.slice(0,1) == '-') {
      // It's an option
      if (this.supportedMap[part] === undefined) {
        return new CommandProcessError({
          msg: intl.str('option-not-supported', {option: part})
        });
      }
      
      var next = this.rawOptions[i + 1];
      var optionArgs = [];
      
      // If next arg doesn't start with -, it's the option value
      if (next && next.slice(0,1) !== '-') {
        i++;
        optionArgs = [next];
      }
      
      this.supportedMap[part] = optionArgs;
    } else {
      // General argument
      this.generalArgs.push(part);
    }
  }
};

Command Execution

Location: src/js/commands/index.js

Execute Method

var commands = {
  execute: function(vcs, name, engine, commandObj) {
    if (!commandConfigs[vcs][name]) {
      throw new Error('i don\'t have a command for ' + name);
    }
    
    var config = commandConfigs[vcs][name];
    
    if (config.delegate) {
      return this.delegateExecute(config, engine, commandObj);
    }
    
    config.execute.call(this, engine, commandObj);
  }
};

Command Delegation

Some commands delegate to others:
delegateExecute: function(config, engine, commandObj) {
  var result = config.delegate.call(this, engine, commandObj);
  
  if (result.multiDelegate) {
    // Execute multiple commands in sequence
    result.multiDelegate.forEach(function(delConfig) {
      commandObj.setOptionsMap(delConfig.options || {});
      commandObj.setGeneralArgs(delConfig.args || []);
      
      commandConfigs[delConfig.vcs][delConfig.name]
        .execute.call(this, engine, commandObj);
    }, this);
  } else {
    // Single delegation
    commandConfigs[result.vcs][result.name]
      .execute.call(this, engine, commandObj);
  }
}

Common Git Commands

Location: src/js/git/commands.js

Commit

commit: {
  regex: /^git +commit($|\s)/,
  options: ['--amend'],
  execute: function(engine, command) {
    var options = command.getSupportedMap();
    engine.commit({
      isAmend: !!options['--amend']
    });
  }
}

Checkout

checkout: {
  regex: /^git +checkout($|\s)/,
  options: ['-b', '-B', '--detach'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    if (options['-b'] || options['-B']) {
      // Create and checkout new branch
      var branch = engine.validateAndMakeBranch(
        args[0],
        engine.getCommitFromRef('HEAD')
      );
      engine.checkout(branch);
    } else {
      // Checkout existing ref
      engine.checkout(args[0]);
    }
  }
}

Branch

branch: {
  regex: /^git +branch($|\s)/,
  options: ['-d', '-D', '-f', '--contains'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    if (options['-d'] || options['-D']) {
      // Delete branch
      engine.deleteBranch(args[0]);
    } else if (options['--contains']) {
      // List branches containing ref
      engine.printBranchesWithout(args[0]);
    } else if (args.length === 0) {
      // List all branches
      engine.printBranches(engine.getBranches());
    } else {
      // Create new branch
      engine.validateAndMakeBranch(
        args[0],
        engine.getCommitFromRef('HEAD')
      );
    }
  }
}

Merge

merge: {
  regex: /^git +merge($|\s)/,
  options: ['--no-ff'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    engine.merge(args[0], {
      noFF: !!options['--no-ff']
    });
  }
}

Rebase

rebase: {
  regex: /^git +rebase($|\s)/,
  options: ['-i', '--interactive', '--aboveAll'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    if (options['-i'] || options['--interactive']) {
      engine.rebaseInteractive(args[0]);
    } else {
      engine.rebase(args[0]);
    }
  }
}

Cherry-pick

cherrypick: {
  regex: /^git +cherry-pick($|\s)/,
  options: [],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    
    var toCherrypick = args.map(function(arg) {
      return engine.getCommitFromRef(arg);
    });
    
    engine.setupCherrypickChain(toCherrypick);
  }
}

Reset

reset: {
  regex: /^git +reset($|\s)/,
  options: ['--hard', '--soft'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    engine.reset(args[0]);
  }
}

Remote Commands

Clone

clone: {
  regex: /^git +clone($|\s)/,
  options: [],
  execute: function(engine, command) {
    engine.makeOrigin();
  }
}

Fetch

fetch: {
  regex: /^git +fetch($|\s)/,
  options: ['--force'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    engine.fetch({
      source: args[0],
      destination: args[1],
      force: !!options['--force']
    });
  }
}

Pull

pull: {
  regex: /^git +pull($|\s)/,
  options: ['--rebase', '--force'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    engine.pull({
      source: args[0],
      destination: args[1],
      isRebase: !!options['--rebase'],
      force: !!options['--force']
    });
  }
}

Push

push: {
  regex: /^git +push($|\s)/,
  options: ['--force', '-f', '-u', '--set-upstream'],
  execute: function(engine, command) {
    var args = command.getGeneralArgs();
    var options = command.getSupportedMap();
    
    engine.push({
      source: args[0] || 'HEAD',
      destination: args[1] || args[0],
      force: !!(options['--force'] || options['-f'])
    });
  }
}

Utility Methods

Location: src/js/commands/index.js

getRegexMap()

Returns map of command patterns:
getRegexMap: function() {
  var map = this.blankMap();
  this.loop(function(config, name, vcs) {
    var displayName = config.displayName || name;
    map[vcs][displayName] = config.regex;
  });
  return map;
}

getOptionMap()

Returns supported options for each command:
getOptionMap: function() {
  var optionMap = this.blankMap();
  this.loop(function(config, name, vcs) {
    var displayName = config.displayName || name;
    var thisMap = {};
    
    (config.options || []).forEach(function(option) {
      thisMap[option] = false;
    });
    
    optionMap[vcs][displayName] = thisMap;
  });
  return optionMap;
}

getCommandsThatCount()

Returns commands that count toward level score:
getCommandsThatCount: function() {
  var counted = this.blankMap();
  this.loop(function(config, name, vcs) {
    if (config.dontCountForGolf) {
      return;
    }
    counted[vcs][name] = config.regex;
  });
  return counted;
}

Command Shortcuts

Commands can define shortcuts:
commit: {
  regex: /^git +commit($|\s)/,
  sc: /^(gc|git ci)($|\s)/,
  options: ['--amend'],
  execute: function(engine, command) {
    // ...
  }
}

// Usage: "gc" instead of "git commit"

Error Handling

CommandProcessError

function CommandProcessError(options) {
  this.msg = options.msg;
}

CommandProcessError.prototype.getMsg = function() {
  return this.msg;
};

CommandResult

For non-error results:
function CommandResult(options) {
  this.msg = options.msg;
}

CommandResult.prototype.getMsg = function() {
  return this.msg;
};

Command Descriptions

Commands can include descriptions for help:
getDescriptionMap: function() {
  var map = this.blankMap();
  this.loop(function(config, name, vcs) {
    var displayName = config.displayName || name;
    if (config.description) {
      map[vcs][displayName] = config.description;
    }
  });
  return map;
}

Level-Specific Commands

Levels can disable certain commands:
// In level definition
var level = {
  name: 'Example Level',
  disabledMap: {
    'git rebase': true,
    'git cherry-pick': true
  },
  // ...
};

// DisabledMap implementation
class DisabledMap {
  getInstantCommands() {
    return Object.keys(this.disabledMap).map(function(command) {
      return [
        new RegExp('^' + command),
        function() {
          throw new CommandResult({
            msg: intl.str('disabled-command', {command: command})
          });
        }
      ];
    });
  }
}

Build docs developers (and LLMs) love