1
0
Fork 0
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:
epriestley 2020-02-13 15:21:29 -08:00
parent 0c6ae6bbcf
commit 70c6045c7f
11 changed files with 251 additions and 297 deletions

View file

@ -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',

View file

@ -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) {

View file

@ -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

View file

@ -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) {

View file

@ -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()) {

View file

@ -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;

View file

@ -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;

View file

@ -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);

View 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);
}
}

View file

@ -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}"));
}
}

View file

@ -2271,4 +2271,8 @@ abstract class ArcanistWorkflow extends Phobject {
return $this->repositoryRef;
}
final public function getToolsetKey() {
return $this->getToolset()->getToolsetKey();
}
}