mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-02 03:32: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',
|
'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase.php',
|
||||||
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php',
|
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php',
|
||||||
'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase.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',
|
'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php',
|
||||||
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php',
|
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php',
|
||||||
'ArcanistAliasWorkflow' => 'toolset/workflow/ArcanistAliasWorkflow.php',
|
'ArcanistAliasWorkflow' => 'toolset/workflow/ArcanistAliasWorkflow.php',
|
||||||
|
'ArcanistAliasesConfigOption' => 'config/option/ArcanistAliasesConfigOption.php',
|
||||||
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
|
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
|
||||||
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
|
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
|
||||||
'ArcanistArcConfigurationEngineExtension' => 'config/arc/ArcanistArcConfigurationEngineExtension.php',
|
'ArcanistArcConfigurationEngineExtension' => 'config/arc/ArcanistArcConfigurationEngineExtension.php',
|
||||||
|
@ -303,6 +307,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php',
|
'ArcanistLintersWorkflow' => 'workflow/ArcanistLintersWorkflow.php',
|
||||||
'ArcanistListAssignmentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistListAssignmentXHPASTLinterRule.php',
|
'ArcanistListAssignmentXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistListAssignmentXHPASTLinterRule.php',
|
||||||
'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistListAssignmentXHPASTLinterRuleTestCase.php',
|
'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistListAssignmentXHPASTLinterRuleTestCase.php',
|
||||||
|
'ArcanistListConfigOption' => 'config/option/ArcanistListConfigOption.php',
|
||||||
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
|
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
|
||||||
'ArcanistLocalConfigurationSource' => 'config/source/ArcanistLocalConfigurationSource.php',
|
'ArcanistLocalConfigurationSource' => 'config/source/ArcanistLocalConfigurationSource.php',
|
||||||
'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php',
|
'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php',
|
||||||
|
@ -491,6 +496,8 @@ phutil_register_library_map(array(
|
||||||
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
|
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
|
||||||
'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php',
|
'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php',
|
||||||
'ArcanistWorkflow' => 'toolset/ArcanistWorkflow.php',
|
'ArcanistWorkflow' => 'toolset/ArcanistWorkflow.php',
|
||||||
|
'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php',
|
||||||
|
'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php',
|
||||||
'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php',
|
'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php',
|
||||||
'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php',
|
'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php',
|
||||||
'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php',
|
'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php',
|
||||||
|
@ -1145,9 +1152,13 @@ phutil_register_library_map(array(
|
||||||
'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
|
'ArcanistAlias' => 'Phobject',
|
||||||
|
'ArcanistAliasEffect' => 'Phobject',
|
||||||
|
'ArcanistAliasEngine' => 'Phobject',
|
||||||
'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
'ArcanistAliasWorkflow' => 'ArcanistWorkflow',
|
'ArcanistAliasWorkflow' => 'ArcanistWorkflow',
|
||||||
|
'ArcanistAliasesConfigOption' => 'ArcanistListConfigOption',
|
||||||
'ArcanistAmendWorkflow' => 'ArcanistWorkflow',
|
'ArcanistAmendWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistAnoidWorkflow' => 'ArcanistWorkflow',
|
'ArcanistAnoidWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension',
|
'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension',
|
||||||
|
@ -1399,6 +1410,7 @@ phutil_register_library_map(array(
|
||||||
'ArcanistLintersWorkflow' => 'ArcanistWorkflow',
|
'ArcanistLintersWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
|
||||||
|
'ArcanistListConfigOption' => 'ArcanistConfigOption',
|
||||||
'ArcanistListWorkflow' => 'ArcanistWorkflow',
|
'ArcanistListWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
|
'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
|
||||||
'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
|
||||||
|
@ -1587,6 +1599,8 @@ phutil_register_library_map(array(
|
||||||
'ArcanistWhichWorkflow' => 'ArcanistWorkflow',
|
'ArcanistWhichWorkflow' => 'ArcanistWorkflow',
|
||||||
'ArcanistWildConfigOption' => 'ArcanistConfigOption',
|
'ArcanistWildConfigOption' => 'ArcanistConfigOption',
|
||||||
'ArcanistWorkflow' => 'Phobject',
|
'ArcanistWorkflow' => 'Phobject',
|
||||||
|
'ArcanistWorkflowArgument' => 'Phobject',
|
||||||
|
'ArcanistWorkflowInformation' => 'Phobject',
|
||||||
'ArcanistWorkingCopy' => 'Phobject',
|
'ArcanistWorkingCopy' => 'Phobject',
|
||||||
'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource',
|
'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource',
|
||||||
'ArcanistWorkingCopyStateRef' => 'ArcanistRef',
|
'ArcanistWorkingCopyStateRef' => 'ArcanistRef',
|
||||||
|
|
|
@ -15,16 +15,80 @@ final class ArcanistConfigurationSourceList
|
||||||
return $this->sources;
|
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) {
|
public function getConfig($key) {
|
||||||
$option = $this->getConfigOption($key);
|
$option = $this->getConfigOption($key);
|
||||||
$values = $this->getStorageValueList($key);
|
$values = $this->getStorageValueList($key);
|
||||||
return $option->getValueFromStorageValueList($values);
|
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) {
|
public function getStorageValueList($key) {
|
||||||
|
return $this->getStorageValueListFromScopes($key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getStorageValueListFromScopes($key, $scopes) {
|
||||||
$values = array();
|
$values = array();
|
||||||
|
|
||||||
foreach ($this->getSources() as $source) {
|
foreach ($this->getSourcesWithScopes($scopes) as $source) {
|
||||||
if ($source->hasValueForKey($key)) {
|
if ($source->hasValueForKey($key)) {
|
||||||
$value = $source->getValueForKey($key);
|
$value = $source->getValueForKey($key);
|
||||||
$values[] = new ArcanistConfigurationSourceValue(
|
$values[] = new ArcanistConfigurationSourceValue(
|
||||||
|
@ -113,7 +177,13 @@ final class ArcanistConfigurationSourceList
|
||||||
$source,
|
$source,
|
||||||
$raw_value);
|
$raw_value);
|
||||||
} catch (Exception $ex) {
|
} 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 EXTENSIONKEY = 'arc';
|
||||||
|
|
||||||
|
const KEY_ALIASES = 'aliases';
|
||||||
|
|
||||||
public function newConfigurationOptions() {
|
public function newConfigurationOptions() {
|
||||||
// TOOLSETS: Restore "load", and maybe this other stuff.
|
// TOOLSETS: Restore "load", and maybe this other stuff.
|
||||||
|
|
||||||
|
@ -49,12 +51,6 @@ final class ArcanistArcConfigurationEngineExtension
|
||||||
'example' => 'false',
|
'example' => 'false',
|
||||||
),
|
),
|
||||||
|
|
||||||
'aliases' => array(
|
|
||||||
'type' => 'aliases',
|
|
||||||
'help' => pht(
|
|
||||||
'Configured command aliases. Use "arc alias" to define aliases.'),
|
|
||||||
),
|
|
||||||
|
|
||||||
'history.immutable' => array(
|
'history.immutable' => array(
|
||||||
'type' => 'bool',
|
'type' => 'bool',
|
||||||
'legacy' => 'immutable_history',
|
'legacy' => 'immutable_history',
|
||||||
|
@ -160,6 +156,14 @@ final class ArcanistArcConfigurationEngineExtension
|
||||||
array(
|
array(
|
||||||
'https://phabricator.mycompany.com/',
|
'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 getType();
|
||||||
|
|
||||||
abstract public function getValueFromStorageValueList(array $list);
|
abstract public function getValueFromStorageValueList(array $list);
|
||||||
abstract public function getStorageValueFromStringValue($value);
|
|
||||||
abstract public function getValueFromStorageValue($value);
|
abstract public function getValueFromStorageValue($value);
|
||||||
abstract public function getDisplayValueFromValue($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(
|
protected function getStorageValueFromSourceValue(
|
||||||
ArcanistConfigurationSourceValue $source_value) {
|
ArcanistConfigurationSourceValue $source_value) {
|
||||||
|
@ -84,5 +92,9 @@ abstract class ArcanistConfigOption
|
||||||
return $value;
|
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;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getStorageValueFromValue($value) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,25 @@
|
||||||
abstract class ArcanistConfigurationSource
|
abstract class ArcanistConfigurationSource
|
||||||
extends Phobject {
|
extends Phobject {
|
||||||
|
|
||||||
|
const SCOPE_USER = 'user';
|
||||||
|
|
||||||
abstract public function getSourceDisplayName();
|
abstract public function getSourceDisplayName();
|
||||||
abstract public function getAllKeys();
|
abstract public function getAllKeys();
|
||||||
abstract public function hasValueForKey($key);
|
abstract public function hasValueForKey($key);
|
||||||
abstract public function getValueForKey($key);
|
abstract public function getValueForKey($key);
|
||||||
|
|
||||||
|
public function getConfigurationSourceScope() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function isStringSource() {
|
public function isStringSource() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isWritableConfigurationSource() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public function didReadUnknownOption($key) {
|
public function didReadUnknownOption($key) {
|
||||||
// TOOLSETS: Standardize this kind of messaging? On ArcanistRuntime?
|
// TOOLSETS: Standardize this kind of messaging? On ArcanistRuntime?
|
||||||
|
|
||||||
|
|
|
@ -29,4 +29,16 @@ abstract class ArcanistDictionaryConfigurationSource
|
||||||
return $this->values[$key];
|
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;
|
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');
|
return pht('User Config File');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isWritableConfigurationSource() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfigurationSourceScope() {
|
||||||
|
return ArcanistConfigurationSource::SCOPE_USER;
|
||||||
|
}
|
||||||
|
|
||||||
public function didReadFilesystemValues(array $values) {
|
public function didReadFilesystemValues(array $values) {
|
||||||
// Before toolsets, the "~/.arcrc" file had separate top-level keys for
|
// Before toolsets, the "~/.arcrc" file had separate top-level keys for
|
||||||
// "config", "hosts", and "aliases". Transform this older file format into
|
// "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 {
|
abstract class ArcanistWorkflow extends Phobject {
|
||||||
|
|
||||||
|
private $runtime;
|
||||||
private $toolset;
|
private $toolset;
|
||||||
private $arguments;
|
private $arguments;
|
||||||
private $configurationEngine;
|
private $configurationEngine;
|
||||||
|
@ -37,10 +38,48 @@ abstract class ArcanistWorkflow extends Phobject {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getWorkflowArguments() {
|
||||||
|
// TOOLSETS: Temporary!
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getWorkflowInformation() {
|
||||||
|
// TOOLSETS: Temporary!
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function newPhutilWorkflow() {
|
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())
|
->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() {
|
final public function getToolset() {
|
||||||
|
@ -52,6 +91,19 @@ abstract class ArcanistWorkflow extends Phobject {
|
||||||
return $this;
|
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(
|
final public function setConfigurationSourceList(
|
||||||
ArcanistConfigurationSourceList $config) {
|
ArcanistConfigurationSourceList $config) {
|
||||||
$this->configurationSourceList = $config;
|
$this->configurationSourceList = $config;
|
||||||
|
@ -99,11 +151,17 @@ abstract class ArcanistWorkflow extends Phobject {
|
||||||
return $err;
|
return $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function getArgument($key, $default = null) {
|
final public function getArgument($key) {
|
||||||
// TOOLSETS: This is a stub for now.
|
return $this->arguments->getArg($key);
|
||||||
return $default;
|
}
|
||||||
|
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getWorkflowSynopses() {
|
public function getWorkflowInformation() {
|
||||||
return array(
|
$help = pht(<<<EOTEXT
|
||||||
pht('**alias**'),
|
|
||||||
pht('**alias** __command__'),
|
|
||||||
pht('**alias** __command__ __target__ -- [__options__]'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getWorkflowHelp() {
|
|
||||||
return pht(<<<EOTEXT
|
|
||||||
Supports: cli
|
|
||||||
Create an alias from __command__ to __target__ (optionally, with __options__).
|
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
|
**Creating Aliases**
|
||||||
'arc patch --force ...' when run. NOTE: use "--" before specifying
|
|
||||||
options!
|
|
||||||
|
|
||||||
If you start an alias with "!", the remainder of the alias will be
|
You can define "arc draft" as a shorthand for "arc diff --draft" like this:
|
||||||
invoked as a shell command. For example, if you want to implement
|
|
||||||
'arc ls', you can do so 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
|
Now, when you run "arc draft", the command will function like
|
||||||
example is silly and would make your life worse.
|
"arc diff --draft".
|
||||||
|
|
||||||
You can not overwrite builtins, including 'alias' itself. The builtin
|
<bg:yellow> NOTE: </bg> Make sure you use "--" before specifying any flags you
|
||||||
will always execute, even if it was added after your alias.
|
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:
|
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
|
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(
|
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() {
|
public function runWorkflow() {
|
||||||
$aliases = self::getAliases($this->getConfigurationManager());
|
|
||||||
|
|
||||||
$argv = $this->getArgument('argv');
|
$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)) {
|
$is_list = false;
|
||||||
throw new ArcanistUsageException(
|
$is_delete = false;
|
||||||
pht(
|
|
||||||
'You can not create an alias for "%s" because it is a '.
|
if (!$argv) {
|
||||||
'builtin command. "%s" can only create new commands.',
|
$is_list = true;
|
||||||
"arc {$alias}",
|
} else if (count($argv) === 1) {
|
||||||
'arc alias'));
|
$is_delete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$new_alias = array_slice($argv, 1);
|
$is_json = $this->getArgument('json');
|
||||||
|
if ($is_json && !$is_list) {
|
||||||
$command = implode(' ', $new_alias);
|
throw new PhutilArgumentUsageException(
|
||||||
if (self::isShellCommandAlias($command)) {
|
|
||||||
echo tsprintf(
|
|
||||||
"%s\n",
|
|
||||||
pht(
|
pht(
|
||||||
'Aliased "%s" to shell command "%s".',
|
'The "--json" argument may only be used when listing aliases.'));
|
||||||
"arc {$alias}",
|
|
||||||
substr($command, 1)));
|
|
||||||
} else {
|
|
||||||
echo tsprintf(
|
|
||||||
"%s\n",
|
|
||||||
pht(
|
|
||||||
'Aliased "%s" to "%s".',
|
|
||||||
"arc {$alias}",
|
|
||||||
"arc {$command}"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$aliases[$alias] = $new_alias;
|
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);
|
$this->writeAliases($aliases);
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isShellCommandAlias($command) {
|
private function validateAliasTrigger($trigger) {
|
||||||
return preg_match('/^!/', $command);
|
$workflows = $this->getRuntime()->getWorkflows();
|
||||||
}
|
|
||||||
|
|
||||||
public static function resolveAliases(
|
if (isset($workflows[$trigger])) {
|
||||||
$command,
|
throw new PhutilArgumentUsageException(
|
||||||
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",
|
|
||||||
pht(
|
pht(
|
||||||
'"%s" is currently aliased to shell command "%s".',
|
'You can not define an alias for "%s" because it is a builtin '.
|
||||||
"arc {$alias}",
|
'workflow for the current toolset ("%s"). The "alias" workflow '.
|
||||||
substr($command, 1)));
|
'can only define new commands as aliases; it can not redefine '.
|
||||||
} else {
|
'existing commands to mean something else.',
|
||||||
echo tsprintf(
|
$trigger,
|
||||||
"%s\n",
|
$this->getToolsetKey()));
|
||||||
pht(
|
}
|
||||||
'"%s" is currently aliased to "%s".',
|
|
||||||
"arc {$alias}",
|
|
||||||
"arc {$command}"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getEditScope() {
|
||||||
$ok = phutil_console_confirm(pht('Delete this alias?'));
|
return ArcanistConfigurationSource::SCOPE_USER;
|
||||||
if (!$ok) {
|
|
||||||
throw new ArcanistUserAbortException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($aliases[$alias]);
|
private function getAliasesConfigKey() {
|
||||||
$this->writeAliases($aliases);
|
return ArcanistArcConfigurationEngineExtension::KEY_ALIASES;
|
||||||
|
}
|
||||||
|
|
||||||
echo tsprintf(
|
private function readAliasesForWrite() {
|
||||||
"%s\n",
|
$key = $this->getAliasesConfigKey();
|
||||||
pht(
|
$scope = $this->getEditScope();
|
||||||
'Removed alias "%s".',
|
$source_list = $this->getConfigurationSourceList();
|
||||||
"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
|
<?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 {
|
final class ArcanistLiberateWorkflow extends ArcanistWorkflow {
|
||||||
|
|
||||||
public function getWorkflowName() {
|
public function getWorkflowName() {
|
||||||
return 'liberate';
|
return 'liberate';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommandSynopses() {
|
public function getWorkflowInformation() {
|
||||||
return phutil_console_format(<<<EOTEXT
|
// TOOLSETS: Expand this help.
|
||||||
**liberate** [__path__]
|
|
||||||
|
$help = pht(<<<EOTEXT
|
||||||
|
Create or update an Arcanist library.
|
||||||
EOTEXT
|
EOTEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return $this->newWorkflowInformation()
|
||||||
|
->addExample(pht('**liberate**'))
|
||||||
|
->addExample(pht('**liberate** [__path__]'))
|
||||||
|
->setHelp($help);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommandHelp() {
|
public function getWorkflowArguments() {
|
||||||
return phutil_console_format(<<<EOTEXT
|
|
||||||
Supports: libphutil
|
|
||||||
Create or update a libphutil library, generating required metadata
|
|
||||||
files like \__init__.php.
|
|
||||||
EOTEXT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getArguments() {
|
|
||||||
return array(
|
return array(
|
||||||
'all' => array(
|
$this->newWorkflowArgument('clean')
|
||||||
'help' => pht(
|
->setHelp(
|
||||||
'Drop the module cache before liberating. This will completely '.
|
pht('Perform a clean rebuild, ignoring caches. Thorough, but slow.')),
|
||||||
'reanalyze the entire library. Thorough, but slow!'),
|
$this->newWorkflowArgument('argv')
|
||||||
),
|
->setWildcard(true),
|
||||||
'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',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,9 +59,6 @@ EOTEXT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$is_remap = $this->getArgument('remap');
|
|
||||||
$is_verify = $this->getArgument('verify');
|
|
||||||
|
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
$this->liberatePath($path);
|
$this->liberatePath($path);
|
||||||
}
|
}
|
||||||
|
@ -122,19 +81,10 @@ EOTEXT
|
||||||
$version = $this->getLibraryFormatVersion($path);
|
$version = $this->getLibraryFormatVersion($path);
|
||||||
switch ($version) {
|
switch ($version) {
|
||||||
case 1:
|
case 1:
|
||||||
if ($this->getArgument('upgrade')) {
|
|
||||||
return $this->upgradeLibrary($path);
|
|
||||||
}
|
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
pht(
|
pht(
|
||||||
"This library is using libphutil v1, which is no ".
|
'This very old library is no longer supported.'));
|
||||||
"longer supported. Run '%s' to upgrade to v2.",
|
|
||||||
'arc liberate --upgrade'));
|
|
||||||
case 2:
|
case 2:
|
||||||
if ($this->getArgument('upgrade')) {
|
|
||||||
throw new ArcanistUsageException(
|
|
||||||
pht("Can't upgrade a v2 library!"));
|
|
||||||
}
|
|
||||||
return $this->liberateVersion2($path);
|
return $this->liberateVersion2($path);
|
||||||
default:
|
default:
|
||||||
throw new ArcanistUsageException(
|
throw new ArcanistUsageException(
|
||||||
|
@ -165,25 +115,10 @@ EOTEXT
|
||||||
return phutil_passthru(
|
return phutil_passthru(
|
||||||
'php %s %C %s',
|
'php %s %C %s',
|
||||||
$bin,
|
$bin,
|
||||||
$this->getArgument('all') ? '--drop-cache' : '',
|
$this->getArgument('clean') ? '--drop-cache' : '',
|
||||||
$path);
|
$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) {
|
private function liberateCreateDirectory($path) {
|
||||||
if (Filesystem::pathExists($path)) {
|
if (Filesystem::pathExists($path)) {
|
||||||
if (!is_dir($path)) {
|
if (!is_dir($path)) {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
final class ArcanistRuntime {
|
final class ArcanistRuntime {
|
||||||
|
|
||||||
|
private $workflows;
|
||||||
|
|
||||||
public function execute(array $argv) {
|
public function execute(array $argv) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -81,12 +83,14 @@ final class ArcanistRuntime {
|
||||||
$args->parsePartial($toolset->getToolsetArguments());
|
$args->parsePartial($toolset->getToolsetArguments());
|
||||||
|
|
||||||
$workflows = $this->newWorkflows($toolset);
|
$workflows = $this->newWorkflows($toolset);
|
||||||
|
$this->workflows = $workflows;
|
||||||
|
|
||||||
$phutil_workflows = array();
|
$phutil_workflows = array();
|
||||||
foreach ($workflows as $key => $workflow) {
|
foreach ($workflows as $key => $workflow) {
|
||||||
$phutil_workflows[$key] = $workflow->newPhutilWorkflow();
|
$phutil_workflows[$key] = $workflow->newPhutilWorkflow();
|
||||||
|
|
||||||
$workflow
|
$workflow
|
||||||
|
->setRuntime($this)
|
||||||
->setConfigurationEngine($config_engine)
|
->setConfigurationEngine($config_engine)
|
||||||
->setConfigurationSourceList($config);
|
->setConfigurationSourceList($config);
|
||||||
}
|
}
|
||||||
|
@ -104,12 +108,16 @@ final class ArcanistRuntime {
|
||||||
throw new PhutilArgumentUsageException(pht('Choose a workflow!'));
|
throw new PhutilArgumentUsageException(pht('Choose a workflow!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->resolveAliases($workflows, $unconsumed_argv, $config);
|
$alias_effects = id(new ArcanistAliasEngine())
|
||||||
if (is_int($result)) {
|
->setRuntime($this)
|
||||||
return $result;
|
->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);
|
return $args->parseWorkflows($phutil_workflows);
|
||||||
}
|
}
|
||||||
|
@ -468,65 +476,6 @@ final class ArcanistRuntime {
|
||||||
return $map;
|
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) {
|
private function logTrace($label, $message) {
|
||||||
echo tsprintf(
|
echo tsprintf(
|
||||||
"**<bg:magenta> %s </bg>** %s\n",
|
"**<bg:magenta> %s </bg>** %s\n",
|
||||||
|
@ -534,4 +483,38 @@ final class ArcanistRuntime {
|
||||||
$message);
|
$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