mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-23 14:00:55 +01:00
Update "arc alias" to modern workflows
Summary: Depends on D20993. Ref T13395. Merges the more-modern "alias" out of "experimental". Test Plan: Defined and invoked aliases. This has some rough edges: notably, no easy list/delete flow. Maniphest Tasks: T13395 Differential Revision: https://secure.phabricator.com/D20996
This commit is contained in:
parent
0c6ae6bbcf
commit
70c6045c7f
11 changed files with 251 additions and 297 deletions
|
@ -24,7 +24,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistAliasEngine' => 'toolset/ArcanistAliasEngine.php',
|
||||
'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php',
|
||||
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php',
|
||||
'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php',
|
||||
'ArcanistAliasWorkflow' => 'toolset/workflow/ArcanistAliasWorkflow.php',
|
||||
'ArcanistAliasesConfigOption' => 'config/option/ArcanistAliasesConfigOption.php',
|
||||
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
|
||||
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
|
||||
|
|
|
@ -22,7 +22,18 @@ final class ArcanistAliasesConfigOption
|
|||
|
||||
protected function didReadStorageValueList(array $list) {
|
||||
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
|
||||
return mpull($list, 'getValue');
|
||||
|
||||
$results = array();
|
||||
foreach ($list as $spec) {
|
||||
$source = $spec->getConfigurationSource();
|
||||
$value = $spec->getValue();
|
||||
|
||||
$value->setConfigurationSource($source);
|
||||
|
||||
$results[] = $value;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getDisplayValueFromValue($value) {
|
||||
|
|
|
@ -81,53 +81,6 @@ class ArcanistConfiguration extends Phobject {
|
|||
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
|
||||
|
|
|
@ -270,7 +270,13 @@ final class ArcanistConfigurationManager extends Phobject {
|
|||
}
|
||||
|
||||
public function readUserArcConfig() {
|
||||
return idx($this->readUserConfigurationFile(), 'config', array());
|
||||
$config = $this->readUserConfigurationFile();
|
||||
|
||||
if (isset($config['config'])) {
|
||||
$config = $config['config'];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function writeUserArcConfig(array $options) {
|
||||
|
|
|
@ -521,7 +521,7 @@ final class ArcanistRuntime {
|
|||
$message = $effect->getMessage();
|
||||
|
||||
if ($message !== null) {
|
||||
$log->writeInfo(pht('ALIAS'), $message);
|
||||
$log->writeHint(pht('ALIAS'), $message);
|
||||
}
|
||||
|
||||
if ($effect->getCommand()) {
|
||||
|
|
|
@ -36,7 +36,7 @@ final class ArcanistAlias extends Phobject {
|
|||
$is_list = false;
|
||||
$is_dict = false;
|
||||
if ($value && is_array($value)) {
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
if (phutil_is_natural_list($value)) {
|
||||
$is_list = true;
|
||||
} else {
|
||||
$is_dict = true;
|
||||
|
|
|
@ -17,6 +17,7 @@ final class ArcanistAliasEffect
|
|||
const EFFECT_NOTFOUND = 'not-found';
|
||||
const EFFECT_CYCLE = 'cycle';
|
||||
const EFFECT_STACK = 'stack';
|
||||
const EFFECT_IGNORED = 'ignored';
|
||||
|
||||
public function setType($type) {
|
||||
$this->type = $type;
|
||||
|
|
|
@ -172,15 +172,31 @@ final class ArcanistAliasEngine
|
|||
}
|
||||
|
||||
$alias = array_pop($toolset_matches);
|
||||
foreach ($toolset_matches as $ignored_match) {
|
||||
|
||||
if ($toolset_matches) {
|
||||
$source = $alias->getConfigurationSource();
|
||||
|
||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_IGNORED)
|
||||
->setMessage(
|
||||
pht(
|
||||
'Multiple configuration sources define an alias for "%s %s". '.
|
||||
'The definition in "%s" will be ignored.',
|
||||
'The last definition in the most specific source ("%s") will '.
|
||||
'be used.',
|
||||
$toolset_key,
|
||||
$command,
|
||||
$ignored_match->getConfigurationSource()->getSourceDisplayName()));
|
||||
$source->getSourceDisplayName()));
|
||||
|
||||
foreach ($toolset_matches as $ignored_match) {
|
||||
$source = $ignored_match->getConfigurationSource();
|
||||
|
||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_IGNORED)
|
||||
->setMessage(
|
||||
pht(
|
||||
'A definition of "%s %s" in "%s" will be ignored.',
|
||||
$toolset_key,
|
||||
$command,
|
||||
$source->getSourceDisplayName()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($alias->isShellCommandAlias()) {
|
||||
|
@ -227,14 +243,17 @@ final class ArcanistAliasEngine
|
|||
return $results;
|
||||
}
|
||||
|
||||
$display_argv = (string)csprintf('%LR', $alias_argv);
|
||||
|
||||
$results[] = $this->newEffect(ArcanistAliasEffect::EFFECT_ALIAS)
|
||||
->setMessage(
|
||||
pht(
|
||||
'%s %s -> %s %s',
|
||||
'%s %s -> %s %s %s',
|
||||
$toolset_key,
|
||||
$command,
|
||||
$toolset_key,
|
||||
$alias_command));
|
||||
$alias_command,
|
||||
$display_argv));
|
||||
|
||||
$argv = array_merge($alias_argv, $argv);
|
||||
|
||||
|
|
200
src/toolset/workflow/ArcanistAliasWorkflow.php
Normal file
200
src/toolset/workflow/ArcanistAliasWorkflow.php
Normal file
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Manages aliases for commands with options.
|
||||
*/
|
||||
final class ArcanistAliasWorkflow extends ArcanistWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'alias';
|
||||
}
|
||||
|
||||
public function supportsToolset(ArcanistToolset $toolset) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getWorkflowInformation() {
|
||||
$help = pht(<<<EOTEXT
|
||||
Create an alias from __command__ to __target__ (optionally, with __options__).
|
||||
|
||||
Aliases allow you to create shorthands for commands and sets of flags you
|
||||
commonly use, like defining "arc draft" as a shorthand for "arc diff --draft".
|
||||
|
||||
**Creating Aliases**
|
||||
|
||||
You can define "arc draft" as a shorthand for "arc diff --draft" like this:
|
||||
|
||||
$ arc alias draft diff -- --draft
|
||||
|
||||
Now, when you run "arc draft", the command will function like
|
||||
"arc diff --draft".
|
||||
|
||||
<bg:yellow> NOTE: </bg> Make sure you use "--" before specifying any flags you
|
||||
want to pass to the command! Otherwise, the flags will be interpreted as flags
|
||||
to "arc alias".
|
||||
|
||||
**Listing Aliases**
|
||||
|
||||
Without any arguments, "arc alias" will list aliases.
|
||||
|
||||
**Removing Aliases**
|
||||
|
||||
To remove an alias, run:
|
||||
|
||||
$ arc alias <alias-name>
|
||||
|
||||
You will be prompted to remove the alias.
|
||||
|
||||
**Shell Commands**
|
||||
|
||||
If you begin an alias with "!", the remainder of the alias will be invoked as
|
||||
a shell command. For example, if you want to implement "arc ls", you can do so
|
||||
like this:
|
||||
|
||||
$ arc alias ls '!ls'
|
||||
|
||||
When run, "arc ls" will now behave like "ls".
|
||||
|
||||
**Multiple Toolsets**
|
||||
|
||||
This workflow supports any toolset, even though the examples in this help text
|
||||
use "arc". If you are working with another toolset, use the binary for that
|
||||
toolset define aliases for it:
|
||||
|
||||
$ phage alias ...
|
||||
|
||||
Aliases are bound to the toolset which was used to define them. If you define
|
||||
an "arc draft" alias, that does not also define a "phage draft" alias.
|
||||
|
||||
**Builtins**
|
||||
|
||||
You can not overwrite the behavior of builtin workflows, including "alias"
|
||||
itself, and if you install a new workflow it will take precedence over any
|
||||
existing aliases with the same name.
|
||||
EOTEXT
|
||||
);
|
||||
|
||||
return $this->newWorkflowInformation()
|
||||
->addExample(pht('**alias**'))
|
||||
->addExample(pht('**alias** __command__'))
|
||||
->addExample(pht('**alias** __command__ __target__ -- [__options__]'))
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
public function getWorkflowArguments() {
|
||||
return array(
|
||||
$this->newWorkflowArgument('json')
|
||||
->setHelp(pht('Output aliases in JSON format.')),
|
||||
$this->newWorkflowArgument('argv')
|
||||
->setWildcard(true),
|
||||
);
|
||||
}
|
||||
|
||||
public function runWorkflow() {
|
||||
$argv = $this->getArgument('argv');
|
||||
|
||||
$is_list = false;
|
||||
$is_delete = false;
|
||||
|
||||
if (!$argv) {
|
||||
$is_list = true;
|
||||
} else if (count($argv) === 1) {
|
||||
$is_delete = true;
|
||||
}
|
||||
|
||||
$is_json = $this->getArgument('json');
|
||||
if ($is_json && !$is_list) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'The "--json" argument may only be used when listing aliases.'));
|
||||
}
|
||||
|
||||
if ($is_list) {
|
||||
return $this->runListAliases();
|
||||
}
|
||||
|
||||
if ($is_delete) {
|
||||
return $this->runDeleteAlias($argv[0]);
|
||||
}
|
||||
|
||||
return $this->runCreateAlias($argv);
|
||||
}
|
||||
|
||||
private function runListAliases() {
|
||||
// TOOLSETS: Actually list aliases.
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function runDeleteAlias($alias) {
|
||||
// TOOLSETS: Actually delete aliases.
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function runCreateAlias(array $argv) {
|
||||
$trigger = array_shift($argv);
|
||||
$this->validateAliasTrigger($trigger);
|
||||
|
||||
$alias = id(new ArcanistAlias())
|
||||
->setToolset($this->getToolsetKey())
|
||||
->setTrigger($trigger)
|
||||
->setCommand($argv);
|
||||
|
||||
$aliases = $this->readAliasesForWrite();
|
||||
|
||||
// TOOLSETS: Check if the user already has an alias for this trigger, and
|
||||
// prompt them to overwrite it. Needs prompting to work.
|
||||
|
||||
$aliases[] = $alias;
|
||||
|
||||
$this->writeAliases($aliases);
|
||||
|
||||
// TOOLSETS: Print out a confirmation that we added the alias.
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function validateAliasTrigger($trigger) {
|
||||
$workflows = $this->getRuntime()->getWorkflows();
|
||||
|
||||
if (isset($workflows[$trigger])) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You can not define an alias for "%s" because it is a builtin '.
|
||||
'workflow for the current toolset ("%s"). The "alias" workflow '.
|
||||
'can only define new commands as aliases; it can not redefine '.
|
||||
'existing commands to mean something else.',
|
||||
$trigger,
|
||||
$this->getToolsetKey()));
|
||||
}
|
||||
}
|
||||
|
||||
private function getEditScope() {
|
||||
return ArcanistConfigurationSource::SCOPE_USER;
|
||||
}
|
||||
|
||||
private function getAliasesConfigKey() {
|
||||
return ArcanistArcConfigurationEngineExtension::KEY_ALIASES;
|
||||
}
|
||||
|
||||
private function readAliasesForWrite() {
|
||||
$key = $this->getAliasesConfigKey();
|
||||
$scope = $this->getEditScope();
|
||||
$source_list = $this->getConfigurationSourceList();
|
||||
|
||||
return $source_list->getConfigFromScopes($key, array($scope));
|
||||
}
|
||||
|
||||
private function writeAliases(array $aliases) {
|
||||
assert_instances_of($aliases, 'ArcanistAlias');
|
||||
|
||||
$key = $this->getAliasesConfigKey();
|
||||
$scope = $this->getEditScope();
|
||||
|
||||
$source_list = $this->getConfigurationSourceList();
|
||||
$source = $source_list->getWritableSourceFromScope($scope);
|
||||
$option = $source_list->getConfigOption($key);
|
||||
|
||||
$option->writeValue($source, $aliases);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,240 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Manages aliases for commands with options.
|
||||
*/
|
||||
final class ArcanistAliasWorkflow extends ArcanistWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'alias';
|
||||
}
|
||||
|
||||
public function getCommandSynopses() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**alias**
|
||||
**alias** __command__
|
||||
**alias** __command__ __target__ -- [__options__]
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
Supports: cli
|
||||
Create an alias from __command__ to __target__ (optionally, with
|
||||
__options__). For example:
|
||||
|
||||
arc alias fpatch patch -- --force
|
||||
|
||||
...will create a new 'arc' command, 'arc fpatch', which invokes
|
||||
'arc patch --force ...' when run. NOTE: use "--" before specifying
|
||||
options!
|
||||
|
||||
If you start an alias with "!", the remainder of the alias will be
|
||||
invoked as a shell command. For example, if you want to implement
|
||||
'arc ls', you can do so like this:
|
||||
|
||||
arc alias ls '!ls'
|
||||
|
||||
You can now run "arc ls" and it will behave like "ls". Of course, this
|
||||
example is silly and would make your life worse.
|
||||
|
||||
You can not overwrite builtins, including 'alias' itself. The builtin
|
||||
will always execute, even if it was added after your alias.
|
||||
|
||||
To remove an alias, run:
|
||||
|
||||
arc alias fpatch
|
||||
|
||||
Without any arguments, 'arc alias' will list aliases.
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
public function getArguments() {
|
||||
return array(
|
||||
'*' => 'argv',
|
||||
);
|
||||
}
|
||||
|
||||
public static function getAliases(
|
||||
ArcanistConfigurationManager $configuration_manager) {
|
||||
$sources = $configuration_manager->getConfigFromAllSources('aliases');
|
||||
|
||||
$aliases = array();
|
||||
foreach ($sources as $source) {
|
||||
$aliases += $source;
|
||||
}
|
||||
|
||||
return $aliases;
|
||||
}
|
||||
|
||||
private function writeAliases(array $aliases) {
|
||||
$config = $this->getConfigurationManager()->readUserConfigurationFile();
|
||||
$config['aliases'] = $aliases;
|
||||
$this->getConfigurationManager()->writeUserConfigurationFile($config);
|
||||
}
|
||||
|
||||
public function run() {
|
||||
$aliases = self::getAliases($this->getConfigurationManager());
|
||||
|
||||
$argv = $this->getArgument('argv');
|
||||
if (count($argv) == 0) {
|
||||
$this->printAliases($aliases);
|
||||
} else if (count($argv) == 1) {
|
||||
$this->removeAlias($aliases, $argv[0]);
|
||||
} else {
|
||||
$arc_config = $this->getArcanistConfiguration();
|
||||
$alias = $argv[0];
|
||||
|
||||
if ($arc_config->buildWorkflow($alias)) {
|
||||
throw new ArcanistUsageException(
|
||||
pht(
|
||||
'You can not create an alias for "%s" because it is a '.
|
||||
'builtin command. "%s" can only create new commands.',
|
||||
"arc {$alias}",
|
||||
'arc alias'));
|
||||
}
|
||||
|
||||
$new_alias = array_slice($argv, 1);
|
||||
|
||||
$command = implode(' ', $new_alias);
|
||||
if (self::isShellCommandAlias($command)) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Aliased "%s" to shell command "%s".',
|
||||
"arc {$alias}",
|
||||
substr($command, 1)));
|
||||
} else {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Aliased "%s" to "%s".',
|
||||
"arc {$alias}",
|
||||
"arc {$command}"));
|
||||
}
|
||||
|
||||
$aliases[$alias] = $new_alias;
|
||||
$this->writeAliases($aliases);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function isShellCommandAlias($command) {
|
||||
return preg_match('/^!/', $command);
|
||||
}
|
||||
|
||||
public static function resolveAliases(
|
||||
$command,
|
||||
ArcanistConfiguration $config,
|
||||
array $argv,
|
||||
ArcanistConfigurationManager $configuration_manager) {
|
||||
|
||||
$aliases = self::getAliases($configuration_manager);
|
||||
if (!isset($aliases[$command])) {
|
||||
return array(null, $argv);
|
||||
}
|
||||
|
||||
$new_command = head($aliases[$command]);
|
||||
|
||||
if (self::isShellCommandAlias($new_command)) {
|
||||
return array($new_command, $argv);
|
||||
}
|
||||
|
||||
$workflow = $config->buildWorkflow($new_command);
|
||||
if (!$workflow) {
|
||||
return array(null, $argv);
|
||||
}
|
||||
|
||||
$alias_argv = array_slice($aliases[$command], 1);
|
||||
foreach (array_reverse($alias_argv) as $alias_arg) {
|
||||
if (!in_array($alias_arg, $argv)) {
|
||||
array_unshift($argv, $alias_arg);
|
||||
}
|
||||
}
|
||||
|
||||
return array($new_command, $argv);
|
||||
}
|
||||
|
||||
private function printAliases(array $aliases) {
|
||||
if (!$aliases) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('You have not defined any aliases yet.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$table = id(new PhutilConsoleTable())
|
||||
->addColumn('input', array('title' => pht('Alias')))
|
||||
->addColumn('command', array('title' => pht('Command')))
|
||||
->addColumn('type', array('title' => pht('Type')));
|
||||
|
||||
ksort($aliases);
|
||||
|
||||
foreach ($aliases as $alias => $binding) {
|
||||
$command = implode(' ', $binding);
|
||||
if (self::isShellCommandAlias($command)) {
|
||||
$command = substr($command, 1);
|
||||
$type = pht('Shell Command');
|
||||
} else {
|
||||
$command = "arc {$command}";
|
||||
$type = pht('Arcanist Command');
|
||||
}
|
||||
|
||||
$row = array(
|
||||
'input' => "arc {$alias}",
|
||||
'type' => $type,
|
||||
'command' => $command,
|
||||
);
|
||||
|
||||
$table->addRow($row);
|
||||
}
|
||||
|
||||
$table->draw();
|
||||
}
|
||||
|
||||
private function removeAlias(array $aliases, $alias) {
|
||||
if (empty($aliases[$alias])) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht('No alias "%s" to remove.', $alias));
|
||||
return;
|
||||
}
|
||||
|
||||
$command = implode(' ', $aliases[$alias]);
|
||||
|
||||
if (self::isShellCommandAlias($command)) {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'"%s" is currently aliased to shell command "%s".',
|
||||
"arc {$alias}",
|
||||
substr($command, 1)));
|
||||
} else {
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'"%s" is currently aliased to "%s".',
|
||||
"arc {$alias}",
|
||||
"arc {$command}"));
|
||||
}
|
||||
|
||||
|
||||
$ok = phutil_console_confirm(pht('Delete this alias?'));
|
||||
if (!$ok) {
|
||||
throw new ArcanistUserAbortException();
|
||||
}
|
||||
|
||||
unset($aliases[$alias]);
|
||||
$this->writeAliases($aliases);
|
||||
|
||||
echo tsprintf(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Removed alias "%s".',
|
||||
"arc {$alias}"));
|
||||
}
|
||||
|
||||
}
|
|
@ -2271,4 +2271,8 @@ abstract class ArcanistWorkflow extends Phobject {
|
|||
return $this->repositoryRef;
|
||||
}
|
||||
|
||||
final public function getToolsetKey() {
|
||||
return $this->getToolset()->getToolsetKey();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue