Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -49,9 +49,13 @@ '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 @@ '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 @@ '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 @@ '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 @@ 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistListConfigOption' => 'ArcanistConfigOption', 'ArcanistListWorkflow' => 'ArcanistWorkflow', 'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1587,6 +1599,8 @@ 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWildConfigOption' => 'ArcanistConfigOption', 'ArcanistWorkflow' => 'Phobject', + 'ArcanistWorkflowArgument' => 'Phobject', + 'ArcanistWorkflowInformation' => 'Phobject', 'ArcanistWorkingCopy' => 'Phobject', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistWorkingCopyStateRef' => 'ArcanistRef', Index: src/config/ArcanistConfigurationSourceList.php =================================================================== --- src/config/ArcanistConfigurationSourceList.php +++ src/config/ArcanistConfigurationSourceList.php @@ -15,16 +15,80 @@ 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 @@ $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); } } } Index: src/config/arc/ArcanistArcConfigurationEngineExtension.php =================================================================== --- src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -5,6 +5,8 @@ const EXTENSIONKEY = 'arc'; + const KEY_ALIASES = 'aliases'; + public function newConfigurationOptions() { // TOOLSETS: Restore "load", and maybe this other stuff. @@ -49,12 +51,6 @@ '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 @@ 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.')), ); } Index: src/config/option/ArcanistAliasesConfigOption.php =================================================================== --- /dev/null +++ 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'); + } + +} Index: src/config/option/ArcanistConfigOption.php =================================================================== --- src/config/option/ArcanistConfigOption.php +++ src/config/option/ArcanistConfigOption.php @@ -67,9 +67,17 @@ 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 @@ return $value; } + public function writeValue(ArcanistConfigurationSource $source, $value) { + $value = $this->getStorageValueFromValue($value); + $source->setStorageValueForKey($this->getKey(), $value); + } } Index: src/config/option/ArcanistListConfigOption.php =================================================================== --- /dev/null +++ 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'); + } + +} Index: src/config/option/ArcanistStringConfigOption.php =================================================================== --- src/config/option/ArcanistStringConfigOption.php +++ src/config/option/ArcanistStringConfigOption.php @@ -15,4 +15,8 @@ return $value; } + public function getStorageValueFromValue($value) { + return $value; + } + } Index: src/config/source/ArcanistConfigurationSource.php =================================================================== --- src/config/source/ArcanistConfigurationSource.php +++ 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? Index: src/config/source/ArcanistDictionaryConfigurationSource.php =================================================================== --- src/config/source/ArcanistDictionaryConfigurationSource.php +++ src/config/source/ArcanistDictionaryConfigurationSource.php @@ -29,4 +29,16 @@ 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 Index: src/config/source/ArcanistFilesystemConfigurationSource.php =================================================================== --- src/config/source/ArcanistFilesystemConfigurationSource.php +++ src/config/source/ArcanistFilesystemConfigurationSource.php @@ -35,4 +35,11 @@ return $values; } + protected function writeToStorage($values) { + $content = id(new PhutilJSON()) + ->encodeFormatted($values); + + Filesystem::writeFile($this->path, $content); + } + } \ No newline at end of file Index: src/config/source/ArcanistUserConfigurationSource.php =================================================================== --- src/config/source/ArcanistUserConfigurationSource.php +++ src/config/source/ArcanistUserConfigurationSource.php @@ -7,6 +7,14 @@ 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 Index: src/toolset/ArcanistAlias.php =================================================================== --- /dev/null +++ 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; + } + +} + Index: src/toolset/ArcanistAliasEffect.php =================================================================== --- /dev/null +++ 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 Index: src/toolset/ArcanistAliasEngine.php =================================================================== --- /dev/null +++ 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); + } + +} + Index: src/toolset/ArcanistWorkflow.php =================================================================== --- src/toolset/ArcanistWorkflow.php +++ 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 @@ 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 @@ 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 @@ 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); + } + + final protected function newWorkflowArgument($key) { + return id(new ArcanistWorkflowArgument()) + ->setKey($key); + } - return $this->arguments->getArg($key, $default); + final protected function newWorkflowInformation() { + return new ArcanistWorkflowInformation(); } } Index: src/toolset/ArcanistWorkflowArgument.php =================================================================== --- /dev/null +++ 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; + } + +} + Index: src/toolset/ArcanistWorkflowInformation.php =================================================================== --- /dev/null +++ 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; + } + +} + Index: src/toolset/workflow/ArcanistAliasWorkflow.php =================================================================== --- src/toolset/workflow/ArcanistAliasWorkflow.php +++ src/toolset/workflow/ArcanistAliasWorkflow.php @@ -13,232 +13,186 @@ 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". - %s alias ls '!ls' +**Listing Aliases** -You can now run "arc ls" and it will behave like "ls". Of course, this -example is silly and would make your life worse. +Without any arguments, "arc alias" will list aliases. -You can not overwrite builtins, including 'alias' itself. The builtin -will always execute, even if it was added after your alias. +**Removing Aliases** To remove an alias, run: - arc alias fpatch + $ 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** -Without any arguments, 'arc alias' will list aliases. +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'); + public function runWorkflow() { + $argv = $this->getArgument('argv'); - $aliases = array(); - foreach ($sources as $source) { - $aliases += $source; - } + $is_list = false; + $is_delete = false; - return $aliases; - } + if (!$argv) { + $is_list = true; + } else if (count($argv) === 1) { + $is_delete = true; + } - private function writeAliases(array $aliases) { - $config = $this->getConfigurationManager()->readUserConfigurationFile(); - $config['aliases'] = $aliases; - $this->getConfigurationManager()->writeUserConfigurationFile($config); - } + $is_json = $this->getArgument('json'); + if ($is_json && !$is_list) { + throw new PhutilArgumentUsageException( + pht( + 'The "--json" argument may only be used when listing aliases.')); + } - public function runWorkflow() { - $aliases = self::getAliases($this->getConfigurationManager()); + if ($is_list) { + return $this->runListAliases(); + } - $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); + if ($is_delete) { + return $this->runDeleteAlias($argv[1]); } - return 0; + return $this->runCreateAlias($argv); } - public static function isShellCommandAlias($command) { - return preg_match('/^!/', $command); + private function runListAliases() { + // TOOLSETS: Actually list aliases. + return 1; } - public static function resolveAliases( - $command, - ArcanistRuntime $config, - array $argv, - ArcanistConfigurationManager $configuration_manager) { + private function runDeleteAlias($alias) { + // TOOLSETS: Actually delete aliases. + return 1; + } - $aliases = self::getAliases($configuration_manager); - if (!isset($aliases[$command])) { - return array(null, $argv); - } + private function runCreateAlias(array $argv) { + $trigger = array_shift($argv); + $this->validateAliasTrigger($trigger); - $new_command = head($aliases[$command]); + $alias = id(new ArcanistAlias()) + ->setToolset($this->getToolsetKey()) + ->setTrigger($trigger) + ->setCommand($argv); - if (self::isShellCommandAlias($new_command)) { - return array($new_command, $argv); - } + $aliases = $this->readAliasesForWrite(); - $workflow = $config->buildWorkflow($new_command); - if (!$workflow) { - return array(null, $argv); - } + // TOOLSETS: Check if the user already has an alias for this trigger, and + // prompt them to overwrite it. Needs prompting to work. - $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); - } - } + $aliases[] = $alias; - return array($new_command, $argv); + $this->writeAliases($aliases); + + return 0; } - private function printAliases(array $aliases) { - if (!$aliases) { - echo tsprintf( - "%s\n", - pht('You have not defined any aliases yet.')); - return; - } + private function validateAliasTrigger($trigger) { + $workflows = $this->getRuntime()->getWorkflows(); - $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); + 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())); } + } - $table->draw(); + private function getEditScope() { + return ArcanistConfigurationSource::SCOPE_USER; } - private function removeAlias(array $aliases, $alias) { - if (empty($aliases[$alias])) { - echo tsprintf( - "%s\n", - pht('No alias "%s" to remove.', $alias)); - return; - } + private function getAliasesConfigKey() { + return ArcanistArcConfigurationEngineExtension::KEY_ALIASES; + } - $command = implode(' ', $aliases[$alias]); + private function readAliasesForWrite() { + $key = $this->getAliasesConfigKey(); + $scope = $this->getEditScope(); + $source_list = $this->getConfigurationSourceList(); - 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}")); - } + return $source_list->getConfigFromScopes($key, array($scope)); + } + private function writeAliases(array $aliases) { + assert_instances_of($aliases, 'ArcanistAlias'); - $ok = phutil_console_confirm(pht('Delete this alias?')); - if (!$ok) { - throw new ArcanistUserAbortException(); - } + $key = $this->getAliasesConfigKey(); + $scope = $this->getEditScope(); - unset($aliases[$alias]); - $this->writeAliases($aliases); + $source_list = $this->getConfigurationSourceList(); + $source = $source_list->getWritableSourceFromScope($scope); + $option = $source_list->getConfigOption($key); - echo tsprintf( - "%s\n", - pht( - 'Removed alias "%s".', - "arc {$alias}")); + $option->writeValue($source, $aliases); } } Index: src/workflow/ArcanistLiberateWorkflow.php =================================================================== --- src/workflow/ArcanistLiberateWorkflow.php +++ src/workflow/ArcanistLiberateWorkflow.php @@ -1,70 +1,32 @@ newWorkflowInformation() + ->addExample(pht('**liberate**')) + ->addExample(pht('**liberate** [__path__]')) + ->setHelp($help); } - public function getArguments() { + public function getWorkflowArguments() { return array( - 'all' => 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 @@ ); } - $is_remap = $this->getArgument('remap'); - $is_verify = $this->getArgument('verify'); - foreach ($paths as $path) { $this->liberatePath($path); } @@ -122,19 +81,10 @@ $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 @@ 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)) { Index: support/ArcanistRuntime.php =================================================================== --- support/ArcanistRuntime.php +++ support/ArcanistRuntime.php @@ -2,6 +2,8 @@ final class ArcanistRuntime { + private $workflows; + public function execute(array $argv) { try { @@ -81,12 +83,14 @@ $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 @@ 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,70 +476,45 @@ return $map; } - private function resolveAliases( - array $workflows, - array $argv, - ArcanistConfigurationSourceList $config) { - - return $argv; + private function logTrace($label, $message) { + echo tsprintf( + "** %s ** %s\n", + $label, + $message); + } - $command = head($argv); + public function getWorkflows() { + return $this->workflows; + } - // 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; - } + 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)); + } - $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; + if ($effect->getCommand()) { + $command = $effect->getCommand(); + $arguments = $effect->getArguments(); + } } - $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); + if ($command !== null) { + $argv = array_merge(array($command), $arguments); } - 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", - $label, - $message); + return $argv; } }