diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -24,7 +24,7 @@ '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', diff --git a/src/config/option/ArcanistAliasesConfigOption.php b/src/config/option/ArcanistAliasesConfigOption.php --- a/src/config/option/ArcanistAliasesConfigOption.php +++ b/src/config/option/ArcanistAliasesConfigOption.php @@ -22,7 +22,18 @@ 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) { diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php --- a/src/configuration/ArcanistConfiguration.php +++ b/src/configuration/ArcanistConfiguration.php @@ -81,53 +81,6 @@ 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 diff --git a/src/configuration/ArcanistConfigurationManager.php b/src/configuration/ArcanistConfigurationManager.php --- a/src/configuration/ArcanistConfigurationManager.php +++ b/src/configuration/ArcanistConfigurationManager.php @@ -270,7 +270,13 @@ } 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) { diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -521,7 +521,7 @@ $message = $effect->getMessage(); if ($message !== null) { - $log->writeInfo(pht('ALIAS'), $message); + $log->writeHint(pht('ALIAS'), $message); } if ($effect->getCommand()) { diff --git a/src/toolset/ArcanistAlias.php b/src/toolset/ArcanistAlias.php --- a/src/toolset/ArcanistAlias.php +++ b/src/toolset/ArcanistAlias.php @@ -36,7 +36,7 @@ $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; diff --git a/src/toolset/ArcanistAliasEffect.php b/src/toolset/ArcanistAliasEffect.php --- a/src/toolset/ArcanistAliasEffect.php +++ b/src/toolset/ArcanistAliasEffect.php @@ -17,6 +17,7 @@ const EFFECT_NOTFOUND = 'not-found'; const EFFECT_CYCLE = 'cycle'; const EFFECT_STACK = 'stack'; + const EFFECT_IGNORED = 'ignored'; public function setType($type) { $this->type = $type; diff --git a/src/toolset/ArcanistAliasEngine.php b/src/toolset/ArcanistAliasEngine.php --- a/src/toolset/ArcanistAliasEngine.php +++ b/src/toolset/ArcanistAliasEngine.php @@ -172,15 +172,31 @@ } $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 @@ 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); diff --git a/src/toolset/workflow/ArcanistAliasWorkflow.php b/src/toolset/workflow/ArcanistAliasWorkflow.php new file mode 100644 --- /dev/null +++ b/src/toolset/workflow/ArcanistAliasWorkflow.php @@ -0,0 +1,200 @@ + NOTE: 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 + +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); + } + +} diff --git a/src/workflow/ArcanistAliasWorkflow.php b/src/workflow/ArcanistAliasWorkflow.php deleted file mode 100644 --- a/src/workflow/ArcanistAliasWorkflow.php +++ /dev/null @@ -1,240 +0,0 @@ - '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}")); - } - -} diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2271,4 +2271,8 @@ return $this->repositoryRef; } + final public function getToolsetKey() { + return $this->getToolset()->getToolsetKey(); + } + }