From 5e707193066e290694f1f03b17aa2df3b6a26a01 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 14 Sep 2018 13:45:26 -0700 Subject: [PATCH] [Wilds] Continue toward a generalized "arc alias" workflow Summary: Ref T13098. This leaves a lot of rough edges but nothing is overtly broken so here's where we're at so far: Config sources get "scopes", like user configuration vs system configuration. The major reason for this is so that `arc set-config x y` can know where it's supposed to write. This is generalized enough that we can implement `arc set-config --system ...` and `arc alias --local ...` and so on relatively easily later, although scopes themselves are not modular (a third-party can't add a new type of config scope). Maybe we'll modularize this some day but it felt like that's probably YAGNI/overboard since we have no current use cases. For now, a source does not need to belong to any particular scope. Config may be writable (like user config in `~/.arcrc`) or nonwritable (like `--config` flags). Writable config can now specify how to write to disk. Config files can actually write to disk now, although the only pathway for doing this that exists is via `arc alias`. Aliases now parse properly and can write to disk. `arc alias` now lets you define aliases, and writes them to disk. **The first time you do this, your `~/.arcrc` file will be rewritten into a format which old `arc` can not read!** It's relatively easily to unmangle/repair these files so I'm planning to just let this happen. When a toolset is invoked, it now reads and evaluates aliases. Aliases have a lot of new guard rails like suggesting the user try `arc draft` if they type `phage draft`, allowing alias chains, detecting cycles, and limiting chain length. Workflows can provide help and argument lists in a more structured way. I've moved this to sub-objects: help is now on `WorkflowInformation` (instead of a bunch of different `getHelp()`, `getSynopsis()` methods) and arguments now have a `WorkflowArgument` object instead of a dictionary. I think this pattern is generally better for extending: it lets us add and change stuff with less impact (and greater explicitness) down the road. `arc alias` now has reasonable help text and argument documentation. The `arc alias` (list) and `arc alias x` (details/remove) flows don't work yet but `arc alias x y` does. `arc liberate` now uses the new help/argument stuff, although the help needs more beef eventually. I pruned a bunch of long-obsolete or questionable flags and renamed `--all` to `--clean` since `--all` sounds like "liberate all libraries", which is now the default behavior of `arc liberate`. Test Plan: You can now define chains of aliases. Finally! ``` $ arc draft4 WARNING Ignoring unrecognized configuration option ("hosts") from source: User Config File (/Users/epriestley/.arcrc). WARNING Ignoring unrecognized configuration option ("load") from source: Project Config File (/Users/epriestley/dev/core/.arcconfig). ALIAS arc draft4 -> arc draft3 ALIAS arc draft3 -> arc draft2 ALIAS arc draft2 -> arc diff Usage Exception: Unrecognized argument '--draft'. ``` This also works now: ``` $ phage alias deploy-secure -- deploy --hosts secure001-4 --limit 1 ``` General! Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19697 --- src/__phutil_library_map__.php | 14 + .../ArcanistConfigurationSourceList.php | 74 +++- ...rcanistArcConfigurationEngineExtension.php | 16 +- .../option/ArcanistAliasesConfigOption.php | 36 ++ src/config/option/ArcanistConfigOption.php | 14 +- .../option/ArcanistListConfigOption.php | 32 ++ .../option/ArcanistStringConfigOption.php | 4 + .../source/ArcanistConfigurationSource.php | 10 + .../ArcanistDictionaryConfigurationSource.php | 12 + .../ArcanistFilesystemConfigurationSource.php | 7 + .../ArcanistUserConfigurationSource.php | 8 + src/toolset/ArcanistAlias.php | 145 ++++++++ src/toolset/ArcanistAliasEffect.php | 57 +++ src/toolset/ArcanistAliasEngine.php | 255 +++++++++++++ src/toolset/ArcanistWorkflow.php | 70 +++- src/toolset/ArcanistWorkflowArgument.php | 50 +++ src/toolset/ArcanistWorkflowInformation.php | 28 ++ .../workflow/ArcanistAliasWorkflow.php | 338 ++++++++---------- src/workflow/ArcanistLiberateWorkflow.php | 103 +----- support/ArcanistRuntime.php | 111 +++--- 20 files changed, 1029 insertions(+), 355 deletions(-) create mode 100644 src/config/option/ArcanistAliasesConfigOption.php create mode 100644 src/config/option/ArcanistListConfigOption.php create mode 100644 src/toolset/ArcanistAlias.php create mode 100644 src/toolset/ArcanistAliasEffect.php create mode 100644 src/toolset/ArcanistAliasEngine.php create mode 100644 src/toolset/ArcanistWorkflowArgument.php create mode 100644 src/toolset/ArcanistWorkflowInformation.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1ac7b962..a7e43553 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -49,9 +49,13 @@ phutil_register_library_map(array( 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase.php', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php', 'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase.php', + 'ArcanistAlias' => 'toolset/ArcanistAlias.php', + 'ArcanistAliasEffect' => 'toolset/ArcanistAliasEffect.php', + 'ArcanistAliasEngine' => 'toolset/ArcanistAliasEngine.php', 'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php', 'ArcanistAliasWorkflow' => 'toolset/workflow/ArcanistAliasWorkflow.php', + 'ArcanistAliasesConfigOption' => 'config/option/ArcanistAliasesConfigOption.php', 'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php', 'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php', 'ArcanistArcConfigurationEngineExtension' => 'config/arc/ArcanistArcConfigurationEngineExtension.php', @@ -303,6 +307,7 @@ phutil_register_library_map(array( 'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php', 'ArcanistListAssignmentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistListAssignmentXHPASTLinterRule.php', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistListAssignmentXHPASTLinterRuleTestCase.php', + 'ArcanistListConfigOption' => 'config/option/ArcanistListConfigOption.php', 'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php', 'ArcanistLocalConfigurationSource' => 'config/source/ArcanistLocalConfigurationSource.php', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php', @@ -491,6 +496,8 @@ phutil_register_library_map(array( 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', 'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php', 'ArcanistWorkflow' => 'toolset/ArcanistWorkflow.php', + 'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php', + 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', 'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php', 'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php', 'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php', @@ -1145,9 +1152,13 @@ phutil_register_library_map(array( 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistAlias' => 'Phobject', + 'ArcanistAliasEffect' => 'Phobject', + 'ArcanistAliasEngine' => 'Phobject', 'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', + 'ArcanistAliasesConfigOption' => 'ArcanistListConfigOption', 'ArcanistAmendWorkflow' => 'ArcanistWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistWorkflow', 'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension', @@ -1399,6 +1410,7 @@ phutil_register_library_map(array( 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistListConfigOption' => 'ArcanistConfigOption', 'ArcanistListWorkflow' => 'ArcanistWorkflow', 'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1587,6 +1599,8 @@ phutil_register_library_map(array( 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWildConfigOption' => 'ArcanistConfigOption', 'ArcanistWorkflow' => 'Phobject', + 'ArcanistWorkflowArgument' => 'Phobject', + 'ArcanistWorkflowInformation' => 'Phobject', 'ArcanistWorkingCopy' => 'Phobject', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistWorkingCopyStateRef' => 'ArcanistRef', diff --git a/src/config/ArcanistConfigurationSourceList.php b/src/config/ArcanistConfigurationSourceList.php index 20fb3e08..6fd72c89 100644 --- a/src/config/ArcanistConfigurationSourceList.php +++ b/src/config/ArcanistConfigurationSourceList.php @@ -15,16 +15,80 @@ final class ArcanistConfigurationSourceList return $this->sources; } + private function getSourcesWithScopes($scopes) { + if ($scopes !== null) { + $scopes = array_fuse($scopes); + } + + $results = array(); + foreach ($this->getSources() as $source) { + if ($scopes !== null) { + $scope = $source->getConfigurationSourceScope(); + if ($scope === null) { + continue; + } + if (!isset($scopes[$scope])) { + continue; + } + } + + $results[] = $source; + } + + return $results; + } + + public function getWritableSourceFromScope($scope) { + $sources = $this->getSourcesWithScopes(array($scope)); + + $writable = array(); + foreach ($sources as $source) { + if (!$source->isWritableConfigurationSource()) { + continue; + } + + $writable[] = $source; + } + + if (!$writable) { + throw new Exception( + pht( + 'Unable to write configuration: there is no writable configuration '. + 'source in the "%s" scope.', + $scope)); + } + + if (count($writable) > 1) { + throw new Exception( + pht( + 'Unable to write configuration: more than one writable source '. + 'exists in the "%s" scope.', + $scope)); + } + + return head($writable); + } + public function getConfig($key) { $option = $this->getConfigOption($key); $values = $this->getStorageValueList($key); return $option->getValueFromStorageValueList($values); } + public function getConfigFromScopes($key, array $scopes) { + $option = $this->getConfigOption($key); + $values = $this->getStorageValueListFromScopes($key, $scopes); + return $option->getValueFromStorageValueList($values); + } + public function getStorageValueList($key) { + return $this->getStorageValueListFromScopes($key, null); + } + + private function getStorageValueListFromScopes($key, $scopes) { $values = array(); - foreach ($this->getSources() as $source) { + foreach ($this->getSourcesWithScopes($scopes) as $source) { if ($source->hasValueForKey($key)) { $value = $source->getValueForKey($key); $values[] = new ArcanistConfigurationSourceValue( @@ -113,7 +177,13 @@ final class ArcanistConfigurationSourceList $source, $raw_value); } catch (Exception $ex) { - throw $ex; + throw new PhutilProxyException( + pht( + 'Configuration value ("%s") defined in source "%s" is not '. + 'valid.', + $key, + $source->getSourceDisplayName()), + $ex); } } } diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index 71837a49..a6c2aa15 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -5,6 +5,8 @@ final class ArcanistArcConfigurationEngineExtension const EXTENSIONKEY = 'arc'; + const KEY_ALIASES = 'aliases'; + public function newConfigurationOptions() { // TOOLSETS: Restore "load", and maybe this other stuff. @@ -49,12 +51,6 @@ final class ArcanistArcConfigurationEngineExtension 'example' => 'false', ), - 'aliases' => array( - 'type' => 'aliases', - 'help' => pht( - 'Configured command aliases. Use "arc alias" to define aliases.'), - ), - 'history.immutable' => array( 'type' => 'bool', 'legacy' => 'immutable_history', @@ -160,6 +156,14 @@ final class ArcanistArcConfigurationEngineExtension array( 'https://phabricator.mycompany.com/', )), + id(new ArcanistAliasesConfigOption()) + ->setKey(self::KEY_ALIASES) + ->setDefaultValue(array()) + ->setSummary(pht('List of command aliases.')) + ->setHelp( + pht( + 'Configured command aliases. Use the "alias" workflow to define '. + 'aliases.')), ); } diff --git a/src/config/option/ArcanistAliasesConfigOption.php b/src/config/option/ArcanistAliasesConfigOption.php new file mode 100644 index 00000000..974088ea --- /dev/null +++ b/src/config/option/ArcanistAliasesConfigOption.php @@ -0,0 +1,36 @@ +'; + } + + public function getValueFromStorageValue($value) { + if (!is_array($value)) { + throw new Exception(pht('Expected a list or dictionary!')); + } + + $aliases = array(); + foreach ($value as $key => $spec) { + $aliases[] = ArcanistAlias::newFromConfig($key, $spec); + } + + return $aliases; + } + + protected function didReadStorageValueList(array $list) { + assert_instances_of($list, 'ArcanistConfigurationSourceValue'); + return mpull($list, 'getValue'); + } + + public function getDisplayValueFromValue($value) { + return pht('Use the "alias" workflow to review aliases.'); + } + + public function getStorageValueFromValue($value) { + return mpull($value, 'getStorageDictionary'); + } + +} diff --git a/src/config/option/ArcanistConfigOption.php b/src/config/option/ArcanistConfigOption.php index 0372e2b0..40ea1fcc 100644 --- a/src/config/option/ArcanistConfigOption.php +++ b/src/config/option/ArcanistConfigOption.php @@ -67,9 +67,17 @@ abstract class ArcanistConfigOption abstract public function getType(); abstract public function getValueFromStorageValueList(array $list); - abstract public function getStorageValueFromStringValue($value); abstract public function getValueFromStorageValue($value); abstract public function getDisplayValueFromValue($value); + abstract public function getStorageValueFromValue($value); + + public function getStorageValueFromStringValue($value) { + throw new Exception( + pht( + 'This configuration option ("%s") does not support runtime definition '. + 'with "--config".', + $this->getKey())); + } protected function getStorageValueFromSourceValue( ArcanistConfigurationSourceValue $source_value) { @@ -84,5 +92,9 @@ abstract class ArcanistConfigOption return $value; } + public function writeValue(ArcanistConfigurationSource $source, $value) { + $value = $this->getStorageValueFromValue($value); + $source->setStorageValueForKey($this->getKey(), $value); + } } diff --git a/src/config/option/ArcanistListConfigOption.php b/src/config/option/ArcanistListConfigOption.php new file mode 100644 index 00000000..f8d5edf0 --- /dev/null +++ b/src/config/option/ArcanistListConfigOption.php @@ -0,0 +1,32 @@ +getConfigurationSource(); + $storage_value = $this->getStorageValueFromSourceValue($source_value); + + $items = $this->getValueFromStorageValue($storage_value); + foreach ($items as $item) { + $result_list[] = new ArcanistConfigurationSourceValue( + $source, + $item); + } + } + + $result_list = $this->didReadStorageValueList($result_list); + + return $result_list; + } + + protected function didReadStorageValueList(array $list) { + assert_instances_of($list, 'ArcanistConfigurationSourceValue'); + return mpull($list, 'getValue'); + } + +} diff --git a/src/config/option/ArcanistStringConfigOption.php b/src/config/option/ArcanistStringConfigOption.php index 29076a4c..a6da4627 100644 --- a/src/config/option/ArcanistStringConfigOption.php +++ b/src/config/option/ArcanistStringConfigOption.php @@ -15,4 +15,8 @@ final class ArcanistStringConfigOption return $value; } + public function getStorageValueFromValue($value) { + return $value; + } + } diff --git a/src/config/source/ArcanistConfigurationSource.php b/src/config/source/ArcanistConfigurationSource.php index 49050171..f6ddbba3 100644 --- a/src/config/source/ArcanistConfigurationSource.php +++ b/src/config/source/ArcanistConfigurationSource.php @@ -3,15 +3,25 @@ abstract class ArcanistConfigurationSource extends Phobject { + const SCOPE_USER = 'user'; + abstract public function getSourceDisplayName(); abstract public function getAllKeys(); abstract public function hasValueForKey($key); abstract public function getValueForKey($key); + public function getConfigurationSourceScope() { + return null; + } + public function isStringSource() { return false; } + public function isWritableConfigurationSource() { + return false; + } + public function didReadUnknownOption($key) { // TOOLSETS: Standardize this kind of messaging? On ArcanistRuntime? diff --git a/src/config/source/ArcanistDictionaryConfigurationSource.php b/src/config/source/ArcanistDictionaryConfigurationSource.php index 590c6110..2fd89bc1 100644 --- a/src/config/source/ArcanistDictionaryConfigurationSource.php +++ b/src/config/source/ArcanistDictionaryConfigurationSource.php @@ -29,4 +29,16 @@ abstract class ArcanistDictionaryConfigurationSource return $this->values[$key]; } + public function setStorageValueForKey($key, $value) { + $this->values[$key] = $value; + + $this->writeToStorage($this->values); + + return $this; + } + + protected function writeToStorage($values) { + throw new PhutilMethodNotImplementedException(); + } + } \ No newline at end of file diff --git a/src/config/source/ArcanistFilesystemConfigurationSource.php b/src/config/source/ArcanistFilesystemConfigurationSource.php index 964092a1..b18f7bb9 100644 --- a/src/config/source/ArcanistFilesystemConfigurationSource.php +++ b/src/config/source/ArcanistFilesystemConfigurationSource.php @@ -35,4 +35,11 @@ abstract class ArcanistFilesystemConfigurationSource return $values; } + protected function writeToStorage($values) { + $content = id(new PhutilJSON()) + ->encodeFormatted($values); + + Filesystem::writeFile($this->path, $content); + } + } \ No newline at end of file diff --git a/src/config/source/ArcanistUserConfigurationSource.php b/src/config/source/ArcanistUserConfigurationSource.php index e9962a54..b1eebf5f 100644 --- a/src/config/source/ArcanistUserConfigurationSource.php +++ b/src/config/source/ArcanistUserConfigurationSource.php @@ -7,6 +7,14 @@ final class ArcanistUserConfigurationSource return pht('User Config File'); } + public function isWritableConfigurationSource() { + return true; + } + + public function getConfigurationSourceScope() { + return ArcanistConfigurationSource::SCOPE_USER; + } + public function didReadFilesystemValues(array $values) { // Before toolsets, the "~/.arcrc" file had separate top-level keys for // "config", "hosts", and "aliases". Transform this older file format into diff --git a/src/toolset/ArcanistAlias.php b/src/toolset/ArcanistAlias.php new file mode 100644 index 00000000..35c5dcd4 --- /dev/null +++ b/src/toolset/ArcanistAlias.php @@ -0,0 +1,145 @@ +trigger = $key; + $alias->toolset = 'arc'; + $alias->command = $value; + } else if ($is_dict) { + try { + PhutilTypeSpec::checkMap( + $value, + array( + 'trigger' => 'string', + 'toolset' => 'string', + 'command' => 'list', + )); + + $alias->trigger = idx($value, 'trigger'); + $alias->toolset = idx($value, 'toolset'); + $alias->command = idx($value, 'command'); + } catch (PhutilTypeCheckException $ex) { + $alias->exception = new PhutilProxyException( + pht( + 'Found invalid alias definition (with key "%s").', + $key), + $ex); + } + } else { + $alias->exception = new Exception( + pht( + 'Expected alias definition (with key "%s") to be a dictionary.', + $key)); + } + + return $alias; + } + + public function setToolset($toolset) { + $this->toolset = $toolset; + return $this; + } + + public function getToolset() { + return $this->toolset; + } + + public function setTrigger($trigger) { + $this->trigger = $trigger; + return $this; + } + + public function getTrigger() { + return $this->trigger; + } + + public function setCommand(array $command) { + $this->command = $command; + return $this; + } + + public function getCommand() { + return $this->command; + } + + public function setException(Exception $exception) { + $this->exception = $exception; + return $this; + } + + public function getException() { + return $this->exception; + } + + public function isShellCommandAlias() { + $command = $this->getCommand(); + if (!$command) { + return false; + } + + $head = head($command); + return preg_match('/^!/', $head); + } + + public function getStorageDictionary() { + return array( + 'trigger' => $this->getTrigger(), + 'toolset' => $this->getToolset(), + 'command' => $this->getCommand(), + ); + } + + public function setConfigurationSource( + ArcanistConfigurationSource $configuration_source) { + $this->configurationSource = $configuration_source; + return $this; + } + + public function getConfigurationSource() { + return $this->configurationSource; + } + +} + diff --git a/src/toolset/ArcanistAliasEffect.php b/src/toolset/ArcanistAliasEffect.php new file mode 100644 index 00000000..1812d835 --- /dev/null +++ b/src/toolset/ArcanistAliasEffect.php @@ -0,0 +1,57 @@ +type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setCommand($command) { + $this->command = $command; + return $this; + } + + public function getCommand() { + return $this->command; + } + + public function setArguments(array $arguments) { + $this->arguments = $arguments; + return $this; + } + + public function getArguments() { + return $this->arguments; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + +} \ No newline at end of file diff --git a/src/toolset/ArcanistAliasEngine.php b/src/toolset/ArcanistAliasEngine.php new file mode 100644 index 00000000..dc52987c --- /dev/null +++ b/src/toolset/ArcanistAliasEngine.php @@ -0,0 +1,255 @@ +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); + } + +} + diff --git a/src/toolset/ArcanistWorkflow.php b/src/toolset/ArcanistWorkflow.php index 3c74e554..58f11acb 100644 --- a/src/toolset/ArcanistWorkflow.php +++ b/src/toolset/ArcanistWorkflow.php @@ -2,6 +2,7 @@ abstract class ArcanistWorkflow extends Phobject { + private $runtime; private $toolset; private $arguments; private $configurationEngine; @@ -37,10 +38,48 @@ abstract class ArcanistWorkflow extends Phobject { return true; } + protected function getWorkflowArguments() { + // TOOLSETS: Temporary! + return array(); + } + + protected function getWorkflowInformation() { + // TOOLSETS: Temporary! + return null; + } + + public function newPhutilWorkflow() { - return id(new ArcanistPhutilWorkflow()) + $arguments = $this->getWorkflowArguments(); + assert_instances_of($arguments, 'ArcanistWorkflowArgument'); + + $specs = mpull($arguments, 'getPhutilSpecification'); + + $phutil_workflow = id(new ArcanistPhutilWorkflow()) ->setName($this->getWorkflowName()) - ->setWorkflow($this); + ->setWorkflow($this) + ->setArguments($specs); + + $information = $this->getWorkflowInformation(); + if ($information) { + + $examples = $information->getExamples(); + if ($examples) { + $examples = implode("\n", $examples); + $phutil_workflow->setExamples($examples); + } + + $help = $information->getHelp(); + if (strlen($help)) { + // Unwrap linebreaks in the help text so we don't get weird formatting. + $help = preg_replace("/(?<=\S)\n(?=\S)/", " ", $help); + + $phutil_workflow->setHelp($help); + } + + } + + return $phutil_workflow; } final public function getToolset() { @@ -52,6 +91,19 @@ abstract class ArcanistWorkflow extends Phobject { return $this; } + final public function setRuntime(ArcanistRuntime $runtime) { + $this->runtime = $runtime; + return $this; + } + + final public function getRuntime() { + return $this->runtime; + } + + final public function getConfig($key) { + return $this->getConfigurationSourceList()->getConfig($key); + } + final public function setConfigurationSourceList( ArcanistConfigurationSourceList $config) { $this->configurationSourceList = $config; @@ -99,11 +151,17 @@ abstract class ArcanistWorkflow extends Phobject { return $err; } - final public function getArgument($key, $default = null) { - // TOOLSETS: This is a stub for now. - return $default; + final public function getArgument($key) { + return $this->arguments->getArg($key); + } - return $this->arguments->getArg($key, $default); + final protected function newWorkflowArgument($key) { + return id(new ArcanistWorkflowArgument()) + ->setKey($key); + } + + final protected function newWorkflowInformation() { + return new ArcanistWorkflowInformation(); } } diff --git a/src/toolset/ArcanistWorkflowArgument.php b/src/toolset/ArcanistWorkflowArgument.php new file mode 100644 index 00000000..9c3d61bf --- /dev/null +++ b/src/toolset/ArcanistWorkflowArgument.php @@ -0,0 +1,50 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setWildcard($wildcard) { + $this->wildcard = $wildcard; + return $this; + } + + public function getWildcard() { + return $this->wildcard; + } + + public function getPhutilSpecification() { + $spec = array( + 'name' => $this->getKey(), + ); + + if ($this->getWildcard()) { + $spec['wildcard'] = true; + } + + return $spec; + } + + public function setHelp($help) { + $this->help = $help; + return $this; + } + + public function getHelp() { + return $this->help; + } + +} + diff --git a/src/toolset/ArcanistWorkflowInformation.php b/src/toolset/ArcanistWorkflowInformation.php new file mode 100644 index 00000000..8f75cd82 --- /dev/null +++ b/src/toolset/ArcanistWorkflowInformation.php @@ -0,0 +1,28 @@ +help = $help; + return $this; + } + + public function getHelp() { + return $this->help; + } + + public function addExample($example) { + $this->examples[] = $example; + return $this; + } + + public function getExamples() { + return $this->examples; + } + +} + diff --git a/src/toolset/workflow/ArcanistAliasWorkflow.php b/src/toolset/workflow/ArcanistAliasWorkflow.php index 7f0929b0..b925379b 100644 --- a/src/toolset/workflow/ArcanistAliasWorkflow.php +++ b/src/toolset/workflow/ArcanistAliasWorkflow.php @@ -13,232 +13,186 @@ final class ArcanistAliasWorkflow extends ArcanistWorkflow { return true; } - public function getWorkflowSynopses() { - return array( - pht('**alias**'), - pht('**alias** __command__'), - pht('**alias** __command__ __target__ -- [__options__]'), - ); - } - - public function getWorkflowHelp() { - return pht(<< 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 fpatch + $ arc alias -Without any arguments, 'arc alias' will list aliases. +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 - , - $this->getToolsetName()); +); + + return $this->newWorkflowInformation() + ->addExample(pht('**alias**')) + ->addExample(pht('**alias** __command__')) + ->addExample(pht('**alias** __command__ __target__ -- [__options__]')) + ->setHelp($help); } - public function getArguments() { + public function getWorkflowArguments() { return array( - '*' => 'argv', + $this->newWorkflowArgument('json') + ->setHelp(pht('Output aliases in JSON format.')), + $this->newWorkflowArgument('argv') + ->setWildcard(true), ); } - 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 runWorkflow() { - $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')); - } + $is_list = false; + $is_delete = false; - $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); + 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[1]); + } + + 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); + return 0; } - public static function isShellCommandAlias($command) { - return preg_match('/^!/', $command); - } + private function validateAliasTrigger($trigger) { + $workflows = $this->getRuntime()->getWorkflows(); - public static function resolveAliases( - $command, - ArcanistRuntime $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", + if (isset($workflows[$trigger])) { + throw new PhutilArgumentUsageException( 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}")); + '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; + } - $ok = phutil_console_confirm(pht('Delete this alias?')); - if (!$ok) { - throw new ArcanistUserAbortException(); - } + private function getAliasesConfigKey() { + return ArcanistArcConfigurationEngineExtension::KEY_ALIASES; + } - unset($aliases[$alias]); - $this->writeAliases($aliases); + private function readAliasesForWrite() { + $key = $this->getAliasesConfigKey(); + $scope = $this->getEditScope(); + $source_list = $this->getConfigurationSourceList(); - echo tsprintf( - "%s\n", - pht( - 'Removed alias "%s".', - "arc {$alias}")); + 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/ArcanistLiberateWorkflow.php b/src/workflow/ArcanistLiberateWorkflow.php index ef81793a..5e7006a1 100644 --- a/src/workflow/ArcanistLiberateWorkflow.php +++ b/src/workflow/ArcanistLiberateWorkflow.php @@ -1,70 +1,32 @@ newWorkflowInformation() + ->addExample(pht('**liberate**')) + ->addExample(pht('**liberate** [__path__]')) + ->setHelp($help); } - public function getCommandHelp() { - return phutil_console_format(<< array( - 'help' => pht( - 'Drop the module cache before liberating. This will completely '. - 'reanalyze the entire library. Thorough, but slow!'), - ), - 'force-update' => array( - 'help' => pht( - 'Force the library map to be updated, even in the presence of '. - 'lint errors.'), - ), - 'library-name' => array( - 'param' => 'name', - 'help' => - pht('Use a flag for library name rather than awaiting user input.'), - ), - 'remap' => array( - 'hide' => true, - 'help' => pht( - 'Internal. Run the remap step of liberation. You do not need to '. - 'run this unless you are debugging the workflow.'), - ), - 'verify' => array( - 'hide' => true, - 'help' => pht( - 'Internal. Run the verify step of liberation. You do not need to '. - 'run this unless you are debugging the workflow.'), - ), - 'upgrade' => array( - 'hide' => true, - 'help' => pht('Experimental. Upgrade library to v2.'), - ), - '*' => 'argv', + $this->newWorkflowArgument('clean') + ->setHelp( + pht('Perform a clean rebuild, ignoring caches. Thorough, but slow.')), + $this->newWorkflowArgument('argv') + ->setWildcard(true), ); } @@ -97,9 +59,6 @@ EOTEXT ); } - $is_remap = $this->getArgument('remap'); - $is_verify = $this->getArgument('verify'); - foreach ($paths as $path) { $this->liberatePath($path); } @@ -122,19 +81,10 @@ EOTEXT $version = $this->getLibraryFormatVersion($path); switch ($version) { case 1: - if ($this->getArgument('upgrade')) { - return $this->upgradeLibrary($path); - } throw new ArcanistUsageException( pht( - "This library is using libphutil v1, which is no ". - "longer supported. Run '%s' to upgrade to v2.", - 'arc liberate --upgrade')); + 'This very old library is no longer supported.')); case 2: - if ($this->getArgument('upgrade')) { - throw new ArcanistUsageException( - pht("Can't upgrade a v2 library!")); - } return $this->liberateVersion2($path); default: throw new ArcanistUsageException( @@ -165,25 +115,10 @@ EOTEXT return phutil_passthru( 'php %s %C %s', $bin, - $this->getArgument('all') ? '--drop-cache' : '', + $this->getArgument('clean') ? '--drop-cache' : '', $path); } - private function upgradeLibrary($path) { - $inits = id(new FileFinder($path)) - ->withPath('*/__init__.php') - ->withType('f') - ->find(); - - echo pht('Removing %s files...', '__init__.php')."\n"; - foreach ($inits as $init) { - Filesystem::remove($path.'/'.$init); - } - - echo pht('Upgrading library to v2...')."\n"; - $this->liberateVersion2($path); - } - private function liberateCreateDirectory($path) { if (Filesystem::pathExists($path)) { if (!is_dir($path)) { diff --git a/support/ArcanistRuntime.php b/support/ArcanistRuntime.php index 5121fc9d..2fd61c05 100644 --- a/support/ArcanistRuntime.php +++ b/support/ArcanistRuntime.php @@ -2,6 +2,8 @@ final class ArcanistRuntime { + private $workflows; + public function execute(array $argv) { try { @@ -81,12 +83,14 @@ final class ArcanistRuntime { $args->parsePartial($toolset->getToolsetArguments()); $workflows = $this->newWorkflows($toolset); + $this->workflows = $workflows; $phutil_workflows = array(); foreach ($workflows as $key => $workflow) { $phutil_workflows[$key] = $workflow->newPhutilWorkflow(); $workflow + ->setRuntime($this) ->setConfigurationEngine($config_engine) ->setConfigurationSourceList($config); } @@ -104,12 +108,16 @@ final class ArcanistRuntime { throw new PhutilArgumentUsageException(pht('Choose a workflow!')); } - $result = $this->resolveAliases($workflows, $unconsumed_argv, $config); - if (is_int($result)) { - return $result; - } + $alias_effects = id(new ArcanistAliasEngine()) + ->setRuntime($this) + ->setToolset($toolset) + ->setWorkflows($workflows) + ->setConfigurationSourceList($config) + ->resolveAliases($unconsumed_argv); - $args->setUnconsumedArgumentVector($result); + $result_argv = $this->applyAliasEffects($alias_effects, $unconsumed_argv); + + $args->setUnconsumedArgumentVector($result_argv); return $args->parseWorkflows($phutil_workflows); } @@ -468,65 +476,6 @@ final class ArcanistRuntime { return $map; } - private function resolveAliases( - array $workflows, - array $argv, - ArcanistConfigurationSourceList $config) { - - return $argv; - - $command = head($argv); - - // If this is a match for a recognized workflow, just return the arguments - // unmodified. You aren't allowed to alias over real workflows. - if (isset($workflows[$command])) { - return $argv; - } - - $aliases = ArcanistAliasWorkflow::getAliases($config); - list($new_command, $new_args) = ArcanistAliasWorkflow::resolveAliases( - $command, - $this, - array_slice($argv, 1), - $config); - - // You can't alias something to itself, so if the new command isn't new, - // we're all done resolving aliases. - if ($new_command === $command) { - return $argv; - } - - $full_alias = idx($aliases, $command, array()); - $full_alias = implode(' ', $full_alias); - - // Run shell command aliases. - if (ArcanistAliasWorkflow::isShellCommandAlias($new_command)) { - fwrite( - STDERR, - tsprintf( - '** %s ** arc %s -> $ %s', - pht('ALIAS'), - $command, - $shell_cmd)); - - $shell_cmd = substr($full_alias, 1); - - return phutil_passthru('%C %Ls', $shell_cmd, $args); - } - - fwrite( - STDERR, - tsprintf( - '** %s ** arc %s -> arc %s', - pht('ALIAS'), - $command, - $new_command)); - - $new_argv = array_merge(array($new_command), $new_args); - - return $this->resolveAliases($workflows, $new_argv, $config); - } - private function logTrace($label, $message) { echo tsprintf( "** %s ** %s\n", @@ -534,4 +483,38 @@ final class ArcanistRuntime { $message); } + public function getWorkflows() { + return $this->workflows; + } + + private function applyAliasEffects(array $effects, array $argv) { + assert_instances_of($effects, 'ArcanistAliasEffect'); + + $command = null; + $arguments = null; + foreach ($effects as $effect) { + $message = $effect->getMessage(); + + if ($message !== null) { + fprintf( + STDERR, + tsprintf( + "** %s ** %s\n", + pht('ALIAS'), + $message)); + } + + if ($effect->getCommand()) { + $command = $effect->getCommand(); + $arguments = $effect->getArguments(); + } + } + + if ($command !== null) { + $argv = array_merge(array($command), $arguments); + } + + return $argv; + } + }