Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995673
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
9 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php
index eed355d3..0cf66ed1 100644
--- a/src/configuration/ArcanistConfiguration.php
+++ b/src/configuration/ArcanistConfiguration.php
@@ -1,229 +1,245 @@
<?php
/**
* Runtime workflow configuration. In Arcanist, commands you type like
* "arc diff" or "arc lint" are called "workflows". This class allows you to add
* new workflows (and extend existing workflows) by subclassing it and then
* pointing to your subclass in your project configuration.
*
* When specified as the **arcanist_configuration** class in your project's
* ##.arcconfig##, your subclass will be instantiated (instead of this class)
* and be able to handle all the method calls. In particular, you can:
*
* - create, replace, or disable workflows by overriding buildWorkflow()
* and buildAllWorkflows();
* - add additional steps before or after workflows run by overriding
* willRunWorkflow() or didRunWorkflow() or didAbortWorkflow(); and
* - add new flags to existing workflows by overriding
* getCustomArgumentsForCommand().
*
* @group config
* @concrete-extensible
*/
class ArcanistConfiguration {
public function buildWorkflow($command) {
if ($command == '--help') {
// Special-case "arc --help" to behave like "arc help" instead of telling
// you to type "arc help" without being helpful.
$command = 'help';
}
return idx($this->buildAllWorkflows(), $command);
}
public function buildAllWorkflows() {
$symbols = id(new PhutilSymbolLoader())
->setType('class')
->setAncestorClass('ArcanistBaseWorkflow')
->selectAndLoadSymbols();
$workflows = array();
foreach ($symbols as $symbol) {
$class = $symbol['name'];
$workflow = newv($class, array());
$name = $workflow->getWorkflowName();
if (isset($workflows[$name])) {
$other = get_class($workflows[$name]);
throw new Exception(
"Workflows {$class} and {$other} both implement workflows named ".
"{$name}.");
}
$workflows[$workflow->getWorkflowName()] = $workflow;
}
return $workflows;
}
final public function isValidWorkflow($workflow) {
return (bool)$this->buildWorkflow($workflow);
}
public function willRunWorkflow($command, ArcanistBaseWorkflow $workflow) {
// This is a hook.
}
public function didRunWorkflow($command, ArcanistBaseWorkflow $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,
ArcanistWorkingCopyIdentity $working_copy,
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($working_copy);
list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
$command,
$this,
$args,
$working_copy);
$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(
"[alias: 'arc %s' -> $ %s]",
$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(
"[alias: 'arc %s' -> 'arc %s']\n",
$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 'arc help'.", $command);
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);
+
$distances = array();
+ $commandv = str_split($command);
foreach ($options as $option) {
- $distances[$option] = levenshtein($option, $command);
+ $optionv = str_split($option);
+ $matrix->setSequences($optionv, $commandv);
+ $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) <= 2 * $distance) {
+ if (strlen($option) < $distance) {
unset($distances[$option]);
}
}
return array_keys($distances);
}
}
diff --git a/src/configuration/__tests__/ArcanistBritishTestCase.php b/src/configuration/__tests__/ArcanistBritishTestCase.php
index 67e219e2..3fe34688 100644
--- a/src/configuration/__tests__/ArcanistBritishTestCase.php
+++ b/src/configuration/__tests__/ArcanistBritishTestCase.php
@@ -1,40 +1,60 @@
<?php
final class ArcanistBritishTestCase extends ArcanistTestCase {
public function testCompletion() {
$this->assertCompletion(
- array('land', 'amend'),
+ array('land'),
'alnd',
array('land', 'amend'));
$this->assertCompletion(
array('branch'),
'brnach',
array('branch', 'browse'));
$this->assertCompletion(
array(),
'test',
array('list', 'unit'));
+
+ $this->assertCompletion(
+ array('list'),
+ 'lists',
+ array('list'));
+
+ $this->assertCompletion(
+ array('diff'),
+ 'dfif',
+ array('diff'));
+
+ $this->assertCompletion(
+ array('unit'),
+ 'uint',
+ array('unit', 'lint', 'list'));
+
+ $this->assertCompletion(
+ array('list', 'lint'),
+ 'nilt',
+ array('unit', 'lint', 'list'));
}
private function assertCompletion($expect, $input, $commands) {
$result = ArcanistConfiguration::correctCommandSpelling(
$input,
$commands,
2);
sort($result);
sort($expect);
$commands = implode(', ', $commands);
$this->assertEqual(
$expect,
$result,
"Correction of {$input} against: {$commands}");
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 13:04 (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556906
Default Alt Text
(9 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment