diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php index e66b7936..8761d8cb 100644 --- a/src/configuration/ArcanistConfiguration.php +++ b/src/configuration/ArcanistConfiguration.php @@ -1,254 +1,260 @@ buildAllWorkflows(), $command); + $workflow = idx($this->buildAllWorkflows(), $command); + + if (!$workflow) { + return null; + } + + return clone $workflow; } public function buildAllWorkflows() { return id(new PhutilClassMapQuery()) ->setAncestorClass('ArcanistWorkflow') ->setUniqueMethod('getWorkflowName') ->execute(); } final public function isValidWorkflow($workflow) { return (bool)$this->buildWorkflow($workflow); } public function willRunWorkflow($command, ArcanistWorkflow $workflow) { // This is a hook. } public function didRunWorkflow($command, ArcanistWorkflow $workflow, $err) { // This is a hook. } public function didAbortWorkflow($command, $workflow, Exception $ex) { // This is a hook. } public function getCustomArgumentsForCommand($command) { return array(); } final public function selectWorkflow( &$command, array &$args, ArcanistConfigurationManager $configuration_manager, PhutilConsole $console) { // First, try to build a workflow with the exact name provided. We always // pick an exact match, and do not allow aliases to override it. $workflow = $this->buildWorkflow($command); if ($workflow) { return $workflow; } // If the user has an alias, like 'arc alias dhelp diff help', look it up // and substitute it. We do this only after trying to resolve the workflow // normally to prevent you from doing silly things like aliasing 'alias' // to something else. $aliases = ArcanistAliasWorkflow::getAliases($configuration_manager); list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases( $command, $this, $args, $configuration_manager); $full_alias = idx($aliases, $command, array()); $full_alias = implode(' ', $full_alias); // Run shell command aliases. if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) { $shell_cmd = substr($full_alias, 1); $console->writeLog( "[%s: 'arc %s' -> $ %s]", pht('alias'), $command, $shell_cmd); if ($args) { $err = phutil_passthru('%C %Ls', $shell_cmd, $args); } else { $err = phutil_passthru('%C', $shell_cmd); } exit($err); } // Run arc command aliases. if ($new_command) { $workflow = $this->buildWorkflow($new_command); if ($workflow) { $console->writeLog( "[%s: 'arc %s' -> 'arc %s']\n", pht('alias'), $command, $full_alias); $command = $new_command; return $workflow; } } $all = array_keys($this->buildAllWorkflows()); // We haven't found a real command or an alias, so try to locate a command // by unique prefix. $prefixes = $this->expandCommandPrefix($command, $all); if (count($prefixes) == 1) { $command = head($prefixes); return $this->buildWorkflow($command); } else if (count($prefixes) > 1) { $this->raiseUnknownCommand($command, $prefixes); } // We haven't found a real command, alias, or unique prefix. Try similar // spellings. $corrected = self::correctCommandSpelling($command, $all, 2); if (count($corrected) == 1) { $console->writeErr( pht( "(Assuming '%s' is the British spelling of '%s'.)", $command, head($corrected))."\n"); $command = head($corrected); return $this->buildWorkflow($command); } else if (count($corrected) > 1) { $this->raiseUnknownCommand($command, $corrected); } $this->raiseUnknownCommand($command); } private function raiseUnknownCommand($command, array $maybe = array()) { $message = pht("Unknown command '%s'. Try '%s'.", $command, 'arc help'); if ($maybe) { $message .= "\n\n".pht('Did you mean:')."\n"; sort($maybe); foreach ($maybe as $other) { $message .= " ".$other."\n"; } } throw new ArcanistUsageException($message); } private function expandCommandPrefix($command, array $options) { $is_prefix = array(); foreach ($options as $option) { if (strncmp($option, $command, strlen($command)) == 0) { $is_prefix[$option] = true; } } return array_keys($is_prefix); } public static function correctCommandSpelling( $command, array $options, $max_distance) { // Adjust to the scaled edit costs we use below, so "2" roughly means // "2 edits". $max_distance = $max_distance * 3; // These costs are somewhat made up, but the theory is that it is far more // likely you will mis-strike a key ("lans" for "land") or press two keys // out of order ("alnd" for "land") than omit keys or press extra keys. $matrix = id(new PhutilEditDistanceMatrix()) ->setInsertCost(4) ->setDeleteCost(4) ->setReplaceCost(3) ->setTransposeCost(2); return self::correctSpelling($command, $options, $matrix, $max_distance); } public static function correctArgumentSpelling($command, array $options) { $max_distance = 1; // We are stricter with arguments - we allow only one inserted or deleted // character. It is mainly to handle cases like --no-lint versus --nolint // or --reviewer versus --reviewers. $matrix = id(new PhutilEditDistanceMatrix()) ->setInsertCost(1) ->setDeleteCost(1) ->setReplaceCost(10); return self::correctSpelling($command, $options, $matrix, $max_distance); } public static function correctSpelling( $input, array $options, PhutilEditDistanceMatrix $matrix, $max_distance) { $distances = array(); $inputv = str_split($input); foreach ($options as $option) { $optionv = str_split($option); $matrix->setSequences($optionv, $inputv); $distances[$option] = $matrix->getEditDistance(); } asort($distances); $best = min($max_distance, reset($distances)); foreach ($distances as $option => $distance) { if ($distance > $best) { unset($distances[$option]); } } // Before filtering, check if we have multiple equidistant matches and // return them if we do. This prevents us from, e.g., matching "alnd" with // both "land" and "amend", then dropping "land" for being too short, and // incorrectly completing to "amend". if (count($distances) > 1) { return array_keys($distances); } foreach ($distances as $option => $distance) { if (strlen($option) < $distance) { unset($distances[$option]); } } return array_keys($distances); } }