mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-29 18:22:41 +01:00
[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
This commit is contained in:
parent
5ef6599239
commit
5e70719306
20 changed files with 1029 additions and 355 deletions
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.')),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
36
src/config/option/ArcanistAliasesConfigOption.php
Normal file
36
src/config/option/ArcanistAliasesConfigOption.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistAliasesConfigOption
|
||||
extends ArcanistListConfigOption {
|
||||
|
||||
public function getType() {
|
||||
return 'list<alias>';
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
32
src/config/option/ArcanistListConfigOption.php
Normal file
32
src/config/option/ArcanistListConfigOption.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
abstract class ArcanistListConfigOption
|
||||
extends ArcanistConfigOption {
|
||||
|
||||
public function getValueFromStorageValueList(array $list) {
|
||||
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
|
||||
|
||||
$result_list = array();
|
||||
foreach ($list as $source_value) {
|
||||
$source = $source_value->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');
|
||||
}
|
||||
|
||||
}
|
|
@ -15,4 +15,8 @@ final class ArcanistStringConfigOption
|
|||
return $value;
|
||||
}
|
||||
|
||||
public function getStorageValueFromValue($value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -35,4 +35,11 @@ abstract class ArcanistFilesystemConfigurationSource
|
|||
return $values;
|
||||
}
|
||||
|
||||
protected function writeToStorage($values) {
|
||||
$content = id(new PhutilJSON())
|
||||
->encodeFormatted($values);
|
||||
|
||||
Filesystem::writeFile($this->path, $content);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
145
src/toolset/ArcanistAlias.php
Normal file
145
src/toolset/ArcanistAlias.php
Normal file
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistAlias extends Phobject {
|
||||
|
||||
private $toolset;
|
||||
private $trigger;
|
||||
private $command;
|
||||
private $exception;
|
||||
private $configurationSource;
|
||||
|
||||
public static function newFromConfig($key, $value) {
|
||||
$alias = new self();
|
||||
|
||||
// Parse older style aliases which were always for the "arc" toolset.
|
||||
// When we next write these back into the config file, we'll update them
|
||||
// to the modern format.
|
||||
|
||||
// The old format looked like this:
|
||||
//
|
||||
// {
|
||||
// "draft": ["diff", "--draft"]
|
||||
// }
|
||||
//
|
||||
// The new format looks like this:
|
||||
//
|
||||
// {
|
||||
// [
|
||||
// "toolset": "arc",
|
||||
// "trigger": "draft",
|
||||
// "command": ["diff", "--draft"]
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// For now, we parse the older format and fill in the toolset as "arc".
|
||||
|
||||
$is_list = false;
|
||||
$is_dict = false;
|
||||
if ($value && is_array($value)) {
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$is_list = true;
|
||||
} else {
|
||||
$is_dict = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_list) {
|
||||
$alias->trigger = $key;
|
||||
$alias->toolset = 'arc';
|
||||
$alias->command = $value;
|
||||
} else if ($is_dict) {
|
||||
try {
|
||||
PhutilTypeSpec::checkMap(
|
||||
$value,
|
||||
array(
|
||||
'trigger' => 'string',
|
||||
'toolset' => 'string',
|
||||
'command' => 'list<string>',
|
||||
));
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
|
57
src/toolset/ArcanistAliasEffect.php
Normal file
57
src/toolset/ArcanistAliasEffect.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistAliasEffect
|
||||
extends Phobject {
|
||||
|
||||
private $type;
|
||||
private $command;
|
||||
private $arguments;
|
||||
private $message;
|
||||
|
||||
const EFFECT_MISCONFIGURATION = 'misconfiguration';
|
||||
const EFFECT_SHELL = 'shell';
|
||||
const EFFECT_RESOLUTION = 'resolution';
|
||||
const EFFECT_SUGGEST = 'suggest';
|
||||
const EFFECT_OVERRIDDE = 'override';
|
||||
const EFFECT_ALIAS = 'alias';
|
||||
const EFFECT_NOTFOUND = 'not-found';
|
||||
const EFFECT_CYCLE = 'cycle';
|
||||
const EFFECT_STACK = 'stack';
|
||||
|
||||
public function setType($type) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
255
src/toolset/ArcanistAliasEngine.php
Normal file
255
src/toolset/ArcanistAliasEngine.php
Normal file
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistAliasEngine
|
||||
extends Phobject {
|
||||
|
||||
private $runtime;
|
||||
private $toolset;
|
||||
private $workflows;
|
||||
private $configurationSourceList;
|
||||
|
||||
public function setRuntime(ArcanistRuntime $runtime) {
|
||||
$this->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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
50
src/toolset/ArcanistWorkflowArgument.php
Normal file
50
src/toolset/ArcanistWorkflowArgument.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistWorkflowArgument
|
||||
extends Phobject {
|
||||
|
||||
private $key;
|
||||
private $help;
|
||||
private $wildcard;
|
||||
|
||||
public function setKey($key) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
||||
|
28
src/toolset/ArcanistWorkflowInformation.php
Normal file
28
src/toolset/ArcanistWorkflowInformation.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistWorkflowInformation
|
||||
extends Phobject {
|
||||
|
||||
private $help;
|
||||
private $examples = array();
|
||||
|
||||
public function setHelp($help) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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(<<<EOTEXT
|
||||
Supports: cli
|
||||
public function getWorkflowInformation() {
|
||||
$help = pht(<<<EOTEXT
|
||||
Create an alias from __command__ to __target__ (optionally, with __options__).
|
||||
For example:
|
||||
|
||||
%s alias fpatch patch -- --force
|
||||
Aliases allow you to create shorthands for commands and sets of flags you
|
||||
commonly use, like defining "arc draft" as a shorthand for "arc diff --draft".
|
||||
|
||||
...will create a new 'arc' command, 'arc fpatch', which invokes
|
||||
'arc patch --force ...' when run. NOTE: use "--" before specifying
|
||||
options!
|
||||
**Creating Aliases**
|
||||
|
||||
If you start an alias with "!", the remainder of the alias will be
|
||||
invoked as a shell command. For example, if you want to implement
|
||||
'arc ls', you can do so like this:
|
||||
You can define "arc draft" as a shorthand for "arc diff --draft" like this:
|
||||
|
||||
%s alias ls '!ls'
|
||||
$ arc alias draft diff -- --draft
|
||||
|
||||
You can now run "arc ls" and it will behave like "ls". Of course, this
|
||||
example is silly and would make your life worse.
|
||||
Now, when you run "arc draft", the command will function like
|
||||
"arc diff --draft".
|
||||
|
||||
You can not overwrite builtins, including 'alias' itself. The builtin
|
||||
will always execute, even if it was added after your alias.
|
||||
<bg:yellow> NOTE: </bg> Make sure you use "--" before specifying any flags you
|
||||
want to pass to the command! Otherwise, the flags will be interpreted as flags
|
||||
to "arc alias".
|
||||
|
||||
**Listing Aliases**
|
||||
|
||||
Without any arguments, "arc alias" will list aliases.
|
||||
|
||||
**Removing Aliases**
|
||||
|
||||
To remove an alias, run:
|
||||
|
||||
arc alias fpatch
|
||||
$ arc alias <alias-name>
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,70 +1,32 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Create and update libphutil libraries.
|
||||
*
|
||||
* This workflow is unusual and involves re-executing 'arc liberate' as a
|
||||
* subprocess with `--remap` and `--verify`. This is because there is no way
|
||||
* to unload or reload a library, so every process is stuck with the library
|
||||
* definition it had when it first loaded. This is normally fine, but
|
||||
* problematic in this case because `arc liberate` modifies library definitions.
|
||||
*/
|
||||
final class ArcanistLiberateWorkflow extends ArcanistWorkflow {
|
||||
|
||||
public function getWorkflowName() {
|
||||
return 'liberate';
|
||||
}
|
||||
|
||||
public function getCommandSynopses() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**liberate** [__path__]
|
||||
public function getWorkflowInformation() {
|
||||
// TOOLSETS: Expand this help.
|
||||
|
||||
$help = pht(<<<EOTEXT
|
||||
Create or update an Arcanist library.
|
||||
EOTEXT
|
||||
);
|
||||
);
|
||||
|
||||
return $this->newWorkflowInformation()
|
||||
->addExample(pht('**liberate**'))
|
||||
->addExample(pht('**liberate** [__path__]'))
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
Supports: libphutil
|
||||
Create or update a libphutil library, generating required metadata
|
||||
files like \__init__.php.
|
||||
EOTEXT
|
||||
);
|
||||
}
|
||||
|
||||
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 @@ 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)) {
|
||||
|
|
|
@ -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(
|
||||
'**<bg:green> %s </bg>** 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(
|
||||
'**<bg:green> %s </bg>** 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(
|
||||
"**<bg:magenta> %s </bg>** %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(
|
||||
"**<bg:yellow> %s </bg>** %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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue