4
* Runtime workflow configuration. In Arcanist, commands you type like
5
* "arc diff" or "arc lint" are called "workflows". This class allows you to add
6
* new workflows (and extend existing workflows) by subclassing it and then
7
* pointing to your subclass in your project configuration.
9
* When specified as the **arcanist_configuration** class in your project's
10
* ##.arcconfig##, your subclass will be instantiated (instead of this class)
11
* and be able to handle all the method calls. In particular, you can:
13
* - create, replace, or disable workflows by overriding buildWorkflow()
14
* and buildAllWorkflows();
15
* - add additional steps before or after workflows run by overriding
16
* willRunWorkflow() or didRunWorkflow() or didAbortWorkflow(); and
17
* - add new flags to existing workflows by overriding
18
* getCustomArgumentsForCommand().
20
* @concrete-extensible
22
class ArcanistConfiguration {
24
public function buildWorkflow($command) {
25
if ($command == '--help') {
26
// Special-case "arc --help" to behave like "arc help" instead of telling
27
// you to type "arc help" without being helpful.
29
} else if ($command == '--version') {
30
// Special-case "arc --version" to behave like "arc version".
34
return idx($this->buildAllWorkflows(), $command);
37
public function buildAllWorkflows() {
38
$workflows_by_name = array();
40
$workflows_by_class_name = id(new PhutilSymbolLoader())
41
->setAncestorClass('ArcanistWorkflow')
43
foreach ($workflows_by_class_name as $class => $workflow) {
44
$name = $workflow->getWorkflowName();
46
if (isset($workflows_by_name[$name])) {
47
$other = get_class($workflows_by_name[$name]);
49
"Workflows {$class} and {$other} both implement workflows named ".
53
$workflows_by_name[$name] = $workflow;
56
return $workflows_by_name;
59
final public function isValidWorkflow($workflow) {
60
return (bool)$this->buildWorkflow($workflow);
63
public function willRunWorkflow($command, ArcanistWorkflow $workflow) {
67
public function didRunWorkflow($command, ArcanistWorkflow $workflow, $err) {
72
public function didAbortWorkflow($command, $workflow, Exception $ex) {
76
public function getCustomArgumentsForCommand($command) {
80
final public function selectWorkflow(
83
ArcanistConfigurationManager $configuration_manager,
84
PhutilConsole $console) {
86
// First, try to build a workflow with the exact name provided. We always
87
// pick an exact match, and do not allow aliases to override it.
88
$workflow = $this->buildWorkflow($command);
93
// If the user has an alias, like 'arc alias dhelp diff help', look it up
94
// and substitute it. We do this only after trying to resolve the workflow
95
// normally to prevent you from doing silly things like aliasing 'alias'
97
$aliases = ArcanistAliasWorkflow::getAliases($configuration_manager);
98
list($new_command, $args) = ArcanistAliasWorkflow::resolveAliases(
102
$configuration_manager);
104
$full_alias = idx($aliases, $command, array());
105
$full_alias = implode(' ', $full_alias);
107
// Run shell command aliases.
108
if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) {
109
$shell_cmd = substr($full_alias, 1);
112
"[alias: 'arc %s' -> $ %s]",
117
$err = phutil_passthru('%C %Ls', $shell_cmd, $args);
119
$err = phutil_passthru('%C', $shell_cmd);
125
// Run arc command aliases.
127
$workflow = $this->buildWorkflow($new_command);
130
"[alias: 'arc %s' -> 'arc %s']\n",
133
$command = $new_command;
138
$all = array_keys($this->buildAllWorkflows());
140
// We haven't found a real command or an alias, so try to locate a command
142
$prefixes = $this->expandCommandPrefix($command, $all);
144
if (count($prefixes) == 1) {
145
$command = head($prefixes);
146
return $this->buildWorkflow($command);
147
} else if (count($prefixes) > 1) {
148
$this->raiseUnknownCommand($command, $prefixes);
152
// We haven't found a real command, alias, or unique prefix. Try similar
154
$corrected = self::correctCommandSpelling($command, $all, 2);
155
if (count($corrected) == 1) {
158
"(Assuming '%s' is the British spelling of '%s'.)",
160
head($corrected))."\n");
161
$command = head($corrected);
162
return $this->buildWorkflow($command);
163
} else if (count($corrected) > 1) {
164
$this->raiseUnknownCommand($command, $corrected);
167
$this->raiseUnknownCommand($command);
170
private function raiseUnknownCommand($command, array $maybe = array()) {
171
$message = pht("Unknown command '%s'. Try 'arc help'.", $command);
173
$message .= "\n\n".pht('Did you mean:')."\n";
175
foreach ($maybe as $other) {
176
$message .= " ".$other."\n";
179
throw new ArcanistUsageException($message);
182
private function expandCommandPrefix($command, array $options) {
183
$is_prefix = array();
184
foreach ($options as $option) {
185
if (strncmp($option, $command, strlen($command)) == 0) {
186
$is_prefix[$option] = true;
190
return array_keys($is_prefix);
193
public static function correctCommandSpelling(
198
// Adjust to the scaled edit costs we use below, so "2" roughly means
200
$max_distance = $max_distance * 3;
202
// These costs are somewhat made up, but the theory is that it is far more
203
// likely you will mis-strike a key ("lans" for "land") or press two keys
204
// out of order ("alnd" for "land") than omit keys or press extra keys.
205
$matrix = id(new PhutilEditDistanceMatrix())
209
->setTransposeCost(2);
211
return self::correctSpelling($command, $options, $matrix, $max_distance);
214
public static function correctArgumentSpelling($command, array $options) {
217
// We are stricter with arguments - we allow only one inserted or deleted
218
// character. It is mainly to handle cases like --no-lint versus --nolint
219
// or --reviewer versus --reviewers.
220
$matrix = id(new PhutilEditDistanceMatrix())
223
->setReplaceCost(10);
225
return self::correctSpelling($command, $options, $matrix, $max_distance);
228
public static function correctSpelling(
231
PhutilEditDistanceMatrix $matrix,
234
$distances = array();
235
$inputv = str_split($input);
236
foreach ($options as $option) {
237
$optionv = str_split($option);
238
$matrix->setSequences($optionv, $inputv);
239
$distances[$option] = $matrix->getEditDistance();
243
$best = min($max_distance, reset($distances));
244
foreach ($distances as $option => $distance) {
245
if ($distance > $best) {
246
unset($distances[$option]);
250
// Before filtering, check if we have multiple equidistant matches and
251
// return them if we do. This prevents us from, e.g., matching "alnd" with
252
// both "land" and "amend", then dropping "land" for being too short, and
253
// incorrectly completing to "amend".
254
if (count($distances) > 1) {
255
return array_keys($distances);
258
foreach ($distances as $option => $distance) {
259
if (strlen($option) < $distance) {
260
unset($distances[$option]);
264
return array_keys($distances);