Changeset View
Changeset View
Standalone View
Standalone View
src/toolset/ArcanistAliasEngine.php
- This file was added.
<?php | |||||
final class ArcanistAliasEngine | |||||
extends Phobject { | |||||
private $runtime; | |||||
private $toolset; | |||||
private $workflows; | |||||
private $configurationSourceList; | |||||
public function setRuntime(ArcanistRuntime $runtime) { | |||||
$this->runtime = $runtime; | |||||
return $this; | |||||
} | |||||
public function getRuntime() { | |||||
return $this->runtime; | |||||
} | |||||
public function setToolset(ArcanistToolset $toolset) { | |||||
$this->toolset = $toolset; | |||||
return $this; | |||||
} | |||||
public function getToolset() { | |||||
return $this->toolset; | |||||
} | |||||
public function setWorkflows(array $workflows) { | |||||
assert_instances_of($workflows, 'ArcanistWorkflow'); | |||||
$this->workflows = $workflows; | |||||
return $this; | |||||
} | |||||
public function getWorkflows() { | |||||
return $this->workflows; | |||||
} | |||||
public function setConfigurationSourceList( | |||||
ArcanistConfigurationSourceList $config) { | |||||
$this->configurationSourceList = $config; | |||||
return $this; | |||||
} | |||||
public function getConfigurationSourceList() { | |||||
return $this->configurationSourceList; | |||||
} | |||||
public function resolveAliases(array $argv) { | |||||
$aliases_key = ArcanistArcConfigurationEngineExtension::KEY_ALIASES; | |||||
$source_list = $this->getConfigurationSourceList(); | |||||
$aliases = $source_list->getConfig($aliases_key); | |||||
$results = array(); | |||||
// Identify aliases which had some kind of format or specification issue | |||||
// when loading config. We could possibly do this earlier, but it's nice | |||||
// to handle all the alias stuff in one place. | |||||
foreach ($aliases as $key => $alias) { | |||||
$exception = $alias->getException(); | |||||
if (!$exception) { | |||||
continue; | |||||
} | |||||
// This alias is not defined properly, so we're going to ignore it. | |||||
unset($aliases[$key]); | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_CONFIGURATION) | |||||
->setMessage( | |||||
pht( | |||||
'Configuration source ("%s") defines an invalid alias, which '. | |||||
'will be ignored: %s', | |||||
$alias->getConfigurationSource()->getSourceDisplayName()), | |||||
$exception->getMessage()); | |||||
} | |||||
$command = array_shift($argv); | |||||
$stack = array(); | |||||
return $this->resolveAliasesForCommand( | |||||
$aliases, | |||||
$command, | |||||
$argv, | |||||
$results, | |||||
$stack); | |||||
} | |||||
private function resolveAliasesForCommand( | |||||
array $aliases, | |||||
$command, | |||||
array $argv, | |||||
array $results, | |||||
array $stack) { | |||||
$toolset = $this->getToolset(); | |||||
$toolset_key = $toolset->getToolsetKey(); | |||||
// If we have a command which resolves to a real workflow, match it and | |||||
// finish resolution. You can not overwrite a real workflow with an alias. | |||||
$workflows = $this->getWorkflows(); | |||||
if (isset($workflows[$command])) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_RESOLUTION) | |||||
->setCommand($command) | |||||
->setArguments($argv); | |||||
return $results; | |||||
} | |||||
// Find all the aliases which match whatever the user typed, like "draft". | |||||
// We look for aliases in other toolsets, too, so we can provide the user | |||||
// a hint when they type "phage draft" and mean "arc draft". | |||||
$matches = array(); | |||||
$toolset_matches = array(); | |||||
foreach ($aliases as $alias) { | |||||
if ($alias->getTrigger() === $command) { | |||||
$matches[] = $alias; | |||||
if ($alias->getToolset() == $toolset_key) { | |||||
$toolset_matches[] = $alias; | |||||
} | |||||
} | |||||
} | |||||
if (!$toolset_matches) { | |||||
// If the user typed "phage draft" and meant "arc draft", give them a | |||||
// hint that the alias exists somewhere else and they may have specified | |||||
// the wrong toolset. | |||||
foreach ($matches as $match) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_SUGGEST) | |||||
->setMessage( | |||||
pht( | |||||
'No "%s %s" alias is defined, did you mean "%s %s"?', | |||||
$toolset_key, | |||||
$command, | |||||
$match->getToolset(), | |||||
$command)); | |||||
} | |||||
// If the user misspells a command (like "arc hlep") and it doesn't match | |||||
// anything (no alias or workflow), we want to pass it through unmodified | |||||
// and let the parser try to correct the spelling into a real workflow | |||||
// later on. | |||||
// However, if the user correctly types a command (like "arc draft") that | |||||
// resolves at least once (so it hits a valid alias) but does not | |||||
// ultimately resolve into a valid workflow, we want to treat this as a | |||||
// hard failure. | |||||
// This could happen if you manually defined a bad alias, or a workflow | |||||
// you'd previously aliased to was removed, or you stacked aliases and | |||||
// then deleted one. | |||||
if ($stack) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_NOTFOUND) | |||||
->setMessage( | |||||
pht( | |||||
'Alias resolved to "%s", but this is not a valid workflow or '. | |||||
'alias name. This alias or workflow might have previously '. | |||||
'existed and been removed.', | |||||
$command)); | |||||
} else { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_RESOLUTION) | |||||
->setCommand($command) | |||||
->setArguments($argv); | |||||
} | |||||
return $results; | |||||
} | |||||
$alias = array_pop($toolset_matches); | |||||
foreach ($toolset_matches as $ignored_match) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_IGNORED) | |||||
->setMessage( | |||||
pht( | |||||
'Multiple configuration sources define an alias for "%s %s". '. | |||||
'The definition in "%s" will be ignored.', | |||||
$toolset_key, | |||||
$command, | |||||
$ignored_match->getConfigurationSource()->getSourceDisplayName())); | |||||
} | |||||
if ($alias->isShellCommandAlias()) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_SHELL) | |||||
->setMessage( | |||||
pht( | |||||
'%s %s -> $ %s', | |||||
$toolset_key, | |||||
$command, | |||||
$alias->getShellCommand())) | |||||
->setCommand($command) | |||||
->setArgv($argv); | |||||
return $results; | |||||
} | |||||
$alias_argv = $alias->getCommand(); | |||||
$alias_command = array_shift($alias_argv); | |||||
if (isset($stack[$alias_command])) { | |||||
$cycle = array_keys($stack); | |||||
$cycle[] = $alias_command; | |||||
$cycle = implode(' -> ', $cycle); | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_CYCLE) | |||||
->setMessage( | |||||
pht( | |||||
'Alias definitions form a cycle which can not be resolved: %s.', | |||||
$cycle)); | |||||
return $results; | |||||
} | |||||
$stack[$alias_command] = true; | |||||
$stack_limit = 16; | |||||
if (count($stack) >= $stack_limit) { | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_STACK) | |||||
->setMessage( | |||||
pht( | |||||
'Alias definitions form an unreasonably deep stack. A chain of '. | |||||
'aliases may not resolve more than %s times.', | |||||
new PhutilNumber($stack_limit))); | |||||
return $results; | |||||
} | |||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_ALIAS) | |||||
->setMessage( | |||||
pht( | |||||
'%s %s -> %s %s', | |||||
$toolset_key, | |||||
$command, | |||||
$toolset_key, | |||||
$alias_command)); | |||||
$argv = array_merge($alias_argv, $argv); | |||||
return $this->resolveAliasesForCommand( | |||||
$aliases, | |||||
$alias_command, | |||||
$argv, | |||||
$results, | |||||
$stack); | |||||
} | |||||
protected function newEffect($effect_type) { | |||||
return id(new ArcanistAliasEffect()) | |||||
->setType($effect_type); | |||||
} | |||||
} |