1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2025-01-01 10:20:58 +01:00

[Wilds] Flesh out most of the new Config objects

Summary:
Ref T13098. This is pretty rough, but sketches out the general shape of a more modern configuration flow. The new flow is very similar to the Phabricator flow.

Each configuration option is typed (string, bool, list of dictionaries, etc) so we can typecheck it, and each type is a class so the types are modular and can do fancy stuff. Some of this "fancy stuff" that I want to do includes:

  - transparently rewriting/reformatting various options for modernness/consistency;
  - having some options exposed as objects instead of raw JSON values (in particular, aliases);
  - merging "list" options (like "aliases") in a modular way instead of by having hard-coded stuff that says "this particular option is magic gets merged instead of getting replaced when defined in multiple places".

Generally, this makes everything modular and extensible and gets rid of the hard-coded `switch (...)` stuff.

Test Plan: Ran `arc get-config`, it sort of almost worked.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13098

Differential Revision: https://secure.phabricator.com/D19695
This commit is contained in:
epriestley 2018-09-18 13:46:12 -07:00
parent 23aaf85eaf
commit c64f86c2f6
32 changed files with 945 additions and 211 deletions

View file

@ -51,10 +51,10 @@ phutil_register_library_map(array(
'ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractPrivateMethodXHPASTLinterRuleTestCase.php',
'ArcanistAliasFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAliasFunctionXHPASTLinterRule.php',
'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAliasFunctionXHPASTLinterRuleTestCase.php',
'ArcanistAliasWorkflow' => 'toolset/ArcanistAliasWorkflow.php',
'ArcanistAliasWorkflow' => 'toolset/workflow/ArcanistAliasWorkflow.php',
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
'ArcanistAnoidWorkflow' => 'workflow/ArcanistAnoidWorkflow.php',
'ArcanistArcConfigurationEngineExtension' => 'config/ArcanistArcConfigurationEngineExtension.php',
'ArcanistArcConfigurationEngineExtension' => 'config/arc/ArcanistArcConfigurationEngineExtension.php',
'ArcanistArcToolset' => 'toolset/ArcanistArcToolset.php',
'ArcanistArcWorkflow' => 'workflow/ArcanistArcWorkflow.php',
'ArcanistArrayCombineXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistArrayCombineXHPASTLinterRule.php',
@ -134,9 +134,8 @@ phutil_register_library_map(array(
'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php',
'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php',
'ArcanistConduitCall' => 'conduit/ArcanistConduitCall.php',
'ArcanistConduitConfigurationEngineExtension' => 'config/ArcanistConduitConfigurationEngineExtension.php',
'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php',
'ArcanistConfigOption' => 'config/ArcanistConfigOption.php',
'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php',
'ArcanistConfigurationDrivenLintEngine' => 'lint/engine/ArcanistConfigurationDrivenLintEngine.php',
'ArcanistConfigurationDrivenUnitTestEngine' => 'unit/engine/ArcanistConfigurationDrivenUnitTestEngine.php',
'ArcanistConfigurationEngine' => 'config/ArcanistConfigurationEngine.php',
@ -144,6 +143,7 @@ phutil_register_library_map(array(
'ArcanistConfigurationManager' => 'configuration/ArcanistConfigurationManager.php',
'ArcanistConfigurationSource' => 'config/source/ArcanistConfigurationSource.php',
'ArcanistConfigurationSourceList' => 'config/ArcanistConfigurationSourceList.php',
'ArcanistConfigurationSourceValue' => 'config/ArcanistConfigurationSourceValue.php',
'ArcanistConsoleLintRenderer' => 'lint/renderer/ArcanistConsoleLintRenderer.php',
'ArcanistConsoleLintRendererTestCase' => 'lint/renderer/__tests__/ArcanistConsoleLintRendererTestCase.php',
'ArcanistConstructorParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConstructorParenthesesXHPASTLinterRule.php',
@ -164,6 +164,7 @@ phutil_register_library_map(array(
'ArcanistDefaultsConfigurationSource' => 'config/source/ArcanistDefaultsConfigurationSource.php',
'ArcanistDeprecationXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDeprecationXHPASTLinterRule.php',
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDeprecationXHPASTLinterRuleTestCase.php',
'ArcanistDictionaryConfigurationSource' => 'config/source/ArcanistDictionaryConfigurationSource.php',
'ArcanistDiffByteSizeException' => 'exception/ArcanistDiffByteSizeException.php',
'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php',
'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php',
@ -217,7 +218,7 @@ phutil_register_library_map(array(
'ArcanistFutureLinter' => 'lint/linter/ArcanistFutureLinter.php',
'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php',
'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGetConfigWorkflow' => 'toolset/workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
'ArcanistGitCommitMessageHardpointLoader' => 'loader/ArcanistGitCommitMessageHardpointLoader.php',
'ArcanistGitHardpointLoader' => 'loader/ArcanistGitHardpointLoader.php',
@ -234,7 +235,7 @@ phutil_register_library_map(array(
'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php',
'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php',
'ArcanistHardpointLoader' => 'loader/ArcanistHardpointLoader.php',
'ArcanistHelpWorkflow' => 'toolset/ArcanistHelpWorkflow.php',
'ArcanistHelpWorkflow' => 'toolset/workflow/ArcanistHelpWorkflow.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase.php',
'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php',
@ -405,6 +406,7 @@ phutil_register_library_map(array(
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
'ArcanistRubyLinterTestCase' => 'lint/linter/__tests__/ArcanistRubyLinterTestCase.php',
'ArcanistRuntimeConfigurationSource' => 'config/source/ArcanistRuntimeConfigurationSource.php',
'ArcanistScalarConfigOption' => 'config/option/ArcanistScalarConfigOption.php',
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
'ArcanistSelfClassReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfClassReferenceXHPASTLinterRule.php',
'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfClassReferenceXHPASTLinterRuleTestCase.php',
@ -412,10 +414,10 @@ phutil_register_library_map(array(
'ArcanistSelfMemberReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSelfMemberReferenceXHPASTLinterRuleTestCase.php',
'ArcanistSemicolonSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSemicolonSpacingXHPASTLinterRule.php',
'ArcanistSemicolonSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSemicolonSpacingXHPASTLinterRuleTestCase.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
'ArcanistSetConfigWorkflow' => 'toolset/workflow/ArcanistSetConfigWorkflow.php',
'ArcanistSetting' => 'configuration/ArcanistSetting.php',
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
'ArcanistShellCompleteWorkflow' => 'toolset/ArcanistShellCompleteWorkflow.php',
'ArcanistShellCompleteWorkflow' => 'toolset/workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php',
'ArcanistSlownessXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSlownessXHPASTLinterRuleTestCase.php',
@ -425,6 +427,7 @@ phutil_register_library_map(array(
'ArcanistStaticThisXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php',
'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistStaticThisXHPASTLinterRuleTestCase.php',
'ArcanistStopWorkflow' => 'workflow/ArcanistStopWorkflow.php',
'ArcanistStringConfigOption' => 'config/option/ArcanistStringConfigOption.php',
'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php',
'ArcanistSubversionWorkingCopy' => 'workingcopy/ArcanistSubversionWorkingCopy.php',
'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php',
@ -483,9 +486,10 @@ phutil_register_library_map(array(
'ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableReferenceSpacingXHPASTLinterRuleTestCase.php',
'ArcanistVariableVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistVariableVariableXHPASTLinterRule.php',
'ArcanistVariableVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistVariableVariableXHPASTLinterRuleTestCase.php',
'ArcanistVersionWorkflow' => 'toolset/ArcanistVersionWorkflow.php',
'ArcanistVersionWorkflow' => 'toolset/workflow/ArcanistVersionWorkflow.php',
'ArcanistWeldWorkflow' => 'workflow/ArcanistWeldWorkflow.php',
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php',
'ArcanistWorkflow' => 'toolset/ArcanistWorkflow.php',
'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php',
'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php',
@ -1226,7 +1230,6 @@ phutil_register_library_map(array(
'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistConduitCall' => 'Phobject',
'ArcanistConduitConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension',
'ArcanistConduitEngine' => 'Phobject',
'ArcanistConfigOption' => 'Phobject',
'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine',
@ -1236,6 +1239,7 @@ phutil_register_library_map(array(
'ArcanistConfigurationManager' => 'Phobject',
'ArcanistConfigurationSource' => 'Phobject',
'ArcanistConfigurationSourceList' => 'Phobject',
'ArcanistConfigurationSourceValue' => 'Phobject',
'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer',
'ArcanistConsoleLintRendererTestCase' => 'PhutilTestCase',
'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -1253,9 +1257,10 @@ phutil_register_library_map(array(
'ArcanistDeclarationParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDefaultParametersXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistDefaultParametersXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDefaultsConfigurationSource' => 'ArcanistConfigurationSource',
'ArcanistDefaultsConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
'ArcanistDeprecationXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistDeprecationXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistDictionaryConfigurationSource' => 'ArcanistConfigurationSource',
'ArcanistDiffByteSizeException' => 'Exception',
'ArcanistDiffChange' => 'Phobject',
'ArcanistDiffChangeType' => 'Phobject',
@ -1298,7 +1303,7 @@ phutil_register_library_map(array(
'ArcanistFileUploader' => 'Phobject',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistFilesystemConfigurationSource' => 'ArcanistConfigurationSource',
'ArcanistFilesystemConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
'ArcanistFlagWorkflow' => 'ArcanistWorkflow',
'ArcanistFlake8Linter' => 'ArcanistExternalLinter',
'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase',
@ -1496,7 +1501,8 @@ phutil_register_library_map(array(
'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRubyLinter' => 'ArcanistExternalLinter',
'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRuntimeConfigurationSource' => 'ArcanistConfigurationSource',
'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource',
'ArcanistScalarConfigOption' => 'ArcanistConfigOption',
'ArcanistScriptAndRegexLinter' => 'ArcanistLinter',
'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@ -1517,6 +1523,7 @@ phutil_register_library_map(array(
'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistStopWorkflow' => 'ArcanistPhrequentWorkflow',
'ArcanistStringConfigOption' => 'ArcanistScalarConfigOption',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSubversionWorkingCopy' => 'ArcanistWorkingCopy',
'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer',
@ -1578,6 +1585,7 @@ phutil_register_library_map(array(
'ArcanistVersionWorkflow' => 'ArcanistWorkflow',
'ArcanistWeldWorkflow' => 'ArcanistWorkflow',
'ArcanistWhichWorkflow' => 'ArcanistWorkflow',
'ArcanistWildConfigOption' => 'ArcanistConfigOption',
'ArcanistWorkflow' => 'Phobject',
'ArcanistWorkingCopy' => 'Phobject',
'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource',

View file

@ -1,6 +0,0 @@
<?php
final class ArcanistArcConfigurationEngineExtension
extends ArcanistConfigurationEngineExtension {
}

View file

@ -1,6 +0,0 @@
<?php
final class ArcanistConduitConfigurationEngineExtension
extends ArcanistConfigurationEngineExtension {
}

View file

@ -1,6 +0,0 @@
<?php
abstract class ArcanistConfigOption
extends Phobject {
}

View file

@ -5,6 +5,7 @@ final class ArcanistConfigurationEngine
private $workingCopy;
private $arguments;
private $toolset;
public function setWorkingCopy(ArcanistWorkingCopy $working_copy) {
$this->workingCopy = $working_copy;
@ -95,4 +96,120 @@ final class ArcanistConfigurationEngine
}
}
public function newDefaults() {
$map = $this->newConfigOptionsMap();
return mpull($map, 'getDefaultValue');
}
public function newConfigOptionsMap() {
$extensions = $this->newEngineExtensions();
$map = array();
$alias_map = array();
foreach ($extensions as $extension) {
$options = $extension->newConfigurationOptions();
foreach ($options as $option) {
$key = $option->getKey();
$this->validateConfigOptionKey($key, $extension);
if (isset($map[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'conflicts with an existing option. Each option must have '.
'a unique key.',
$key,
get_class($extension)));
}
if (isset($alias_map[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'conflicts with an alias for another option ("%s"). The '.
'key and aliases of each option must be unique.',
$key,
get_class($extension),
$alias_map[$key]->getKey()));
}
$map[$key] = $option;
foreach ($option->getAliases() as $alias) {
$this->validateConfigOptionKey($alias, $extension, $key);
if (isset($map[$alias])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'has an alias ("%s") which conflicts with an existing '.
'option. The key and aliases of each option must be '.
'unique.',
$key,
get_class($extension),
$alias));
}
if (isset($alias_map[$alias])) {
throw new Exception(
pht(
'Configuration option ("%s") defined by extension "%s" '.
'has an alias ("%s") which conflicts with the alias of '.
'another configuration option ("%s"). The key and aliases '.
'of each option must be unique.',
$key,
get_class($extension),
$alias,
$alias_map[$alias]->getKey()));
}
$alias_map[$alias] = $option;
}
}
}
return $map;
}
private function validateConfigOptionKey(
$key,
ArcanistConfigurationEngineExtension $extension,
$is_alias_of = null) {
$is_ok = preg_match('(^[a-z][a-z0-9._-]{2,}\z)', $key);
if (!$is_ok) {
if ($is_alias_of === null) {
throw new Exception(
pht(
'Extension ("%s") defines invalid configuration with key "%s". '.
'Configuration keys: may only contain lowercase letters, '.
'numbers, hyphens, underscores, and periods; must start with a '.
'letter; and must be at least three characters long.',
get_class($extension),
$key));
} else {
throw new Exception(
pht(
'Extension ("%s") defines invalid alias ("%s") for configuration '.
'key ("%s"). Configuration keys and aliases: may only contain '.
'lowercase letters, numbers, hyphens, underscores, and periods; '.
'must start with a letter; and must be at least three characters '.
'long.',
get_class($extension),
$key,
$is_alias_of));
}
}
}
private function newEngineExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass('ArcanistConfigurationEngineExtension')
->setUniqueMethod('getExtensionKey')
->setContinueOnFailure(true)
->execute();
}
}

View file

@ -1,6 +1,10 @@
<?php
final class ArcanistConfigurationEngineExtension
abstract class ArcanistConfigurationEngineExtension
extends Phobject {
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('EXTENSIONKEY');
}
}

View file

@ -4,10 +4,129 @@ final class ArcanistConfigurationSourceList
extends Phobject {
private $sources = array();
private $configOptions;
public function addSource(ArcanistConfigurationSource $source) {
$this->sources[] = $source;
return $this;
}
public function getSources() {
return $this->sources;
}
public function getConfig($key) {
$option = $this->getConfigOption($key);
$values = $this->getStorageValueList($key);
return $option->getValueFromStorageValueList($values);
}
public function getStorageValueList($key) {
$values = array();
foreach ($this->getSources() as $source) {
if ($source->hasValueForKey($key)) {
$value = $source->getValueForKey($key);
$values[] = new ArcanistConfigurationSourceValue(
$source,
$source->getValueForKey($key));
}
}
return $values;
}
public function getConfigOption($key) {
$options = $this->getConfigOptions();
if (!isset($options[$key])) {
throw new Exception(
pht(
'Configuration option ("%s") is unrecognized. You can only read '.
'recognized configuration options.',
$key));
}
return $options[$key];
}
public function setConfigOptions(array $config_options) {
assert_instances_of($config_options, 'ArcanistConfigOption');
$config_options = mpull($config_options, null, 'getKey');
$this->configOptions = $config_options;
return $this;
}
public function getConfigOptions() {
if ($this->configOptions === null) {
throw new PhutilInvalidStateException('setConfigOptions');
}
return $this->configOptions;
}
public function validateConfiguration() {
$options = $this->getConfigOptions();
$aliases = array();
foreach ($options as $key => $option) {
foreach ($option->getAliases() as $alias) {
$aliases[$alias] = $key;
}
}
// TOOLSETS: Handle the case where config specifies both a value and an
// alias for that value. The alias should be ignored and we should emit
// a warning. This also needs to be implemented when actually reading
// configuration.
$value_lists = array();
foreach ($this->getSources() as $source) {
$keys = $source->getAllKeys();
foreach ($keys as $key) {
$resolved_key = idx($aliases, $key, $key);
$option = idx($options, $resolved_key);
// If there's no option object for this config, this value is
// unrecognized. Sources are free to handle this however they want:
// for config files we emit a warning; for "--config" we fatal.
if (!$option) {
$source->didReadUnknownOption($key);
continue;
}
$raw_value = $source->getValueForKey($key);
// Make sure we can convert whatever value the configuration source is
// providing into a legitimate runtime value.
try {
$value = $raw_value;
if ($source->isStringSource()) {
$value = $option->getStorageValueFromStringValue($value);
}
$option->getValueFromStorageValue($value);
$value_lists[$resolved_key][] = new ArcanistConfigurationSourceValue(
$source,
$raw_value);
} catch (Exception $ex) {
throw $ex;
}
}
}
// Make sure each value list can be merged.
foreach ($value_lists as $key => $value_list) {
try {
$options[$key]->getValueFromStorageValueList($value_list);
} catch (Exception $ex) {
throw $ex;
}
}
}
}

View file

@ -0,0 +1,24 @@
<?php
final class ArcanistConfigurationSourceValue
extends Phobject {
private $source;
private $value;
public function __construct(ArcanistConfigurationSource $source, $value) {
$this->source = $source;
$this->value = $value;
}
public function getConfigurationSource() {
return $this->source;
}
public function getValue() {
return $this->value;
}
}

View file

@ -0,0 +1,166 @@
<?php
final class ArcanistArcConfigurationEngineExtension
extends ArcanistConfigurationEngineExtension {
const EXTENSIONKEY = 'arc';
public function newConfigurationOptions() {
// TOOLSETS: Restore "load", and maybe this other stuff.
/*
'load' => array(
'type' => 'list',
'legacy' => 'phutil_libraries',
'help' => pht(
'A list of paths to phutil libraries that should be loaded at '.
'startup. This can be used to make classes available, like lint '.
'or unit test engines.'),
'default' => array(),
'example' => '["/var/arc/customlib/src"]',
),
'arc.feature.start.default' => array(
'type' => 'string',
'help' => pht(
'The name of the default branch to create the new feature branch '.
'off of.'),
'example' => '"develop"',
),
'arc.land.onto.default' => array(
'type' => 'string',
'help' => pht(
'The name of the default branch to land changes onto when '.
'`%s` is run.',
'arc land'),
'example' => '"develop"',
),
'arc.autostash' => array(
'type' => 'bool',
'help' => pht(
'Whether %s should permit the automatic stashing of changes in the '.
'working directory when requiring a clean working copy. This option '.
'should only be used when users understand how to restore their '.
'working directory from the local stash if an Arcanist operation '.
'causes an unrecoverable error.',
'arc'),
'default' => false,
'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',
'help' => pht(
'If true, %s will never change repository history (e.g., through '.
'amending or rebasing). Defaults to true in Mercurial and false in '.
'Git. This setting has no effect in Subversion.',
'arc'),
'example' => 'false',
),
'editor' => array(
'type' => 'string',
'help' => pht(
'Command to use to invoke an interactive editor, like `%s` or `%s`. '.
'This setting overrides the %s environmental variable.',
'nano',
'vim',
'EDITOR'),
'example' => '"nano"',
),
'https.cabundle' => array(
'type' => 'string',
'help' => pht(
"Path to a custom CA bundle file to be used for arcanist's cURL ".
"calls. This is used primarily when your conduit endpoint is ".
"behind HTTPS signed by your organization's internal CA."),
'example' => 'support/yourca.pem',
),
'https.blindly-trust-domains' => array(
'type' => 'list',
'help' => pht(
'List of domains to blindly trust SSL certificates for. '.
'Disables peer verification.'),
'default' => array(),
'example' => '["secure.mycompany.com"]',
),
'browser' => array(
'type' => 'string',
'help' => pht('Command to use to invoke a web browser.'),
'example' => '"gnome-www-browser"',
),
'http.basicauth.user' => array(
'type' => 'string',
'help' => pht('Username to use for basic auth over HTTP transports.'),
'example' => '"bob"',
),
'http.basicauth.pass' => array(
'type' => 'string',
'help' => pht('Password to use for basic auth over HTTP transports.'),
'example' => '"bobhasasecret"',
),
*/
return array(
id(new ArcanistStringConfigOption())
->setKey('base')
->setSummary(pht('Ruleset for selecting commit ranges.'))
->setHelp(
pht(
'Base commit ruleset to invoke when determining the start of a '.
'commit range. See "Arcanist User Guide: Commit Ranges" for '.
'details.'))
->setExamples(
array(
'arc:amended, arc:prompt',
)),
id(new ArcanistStringConfigOption())
->setKey('repository')
->setAliases(
array(
'repository.callsign',
))
->setSummary(pht('Repository for the current working copy.'))
->setHelp(
pht(
'Associate the working copy with a specific Phabricator '.
'repository. Normally, `arc` can figure this association out on '.
'its own, but if your setup is unusual you can use this option '.
'to tell it what the desired value is.'))
->setExamples(
array(
'libexample',
'XYZ',
'R123',
'123',
)),
id(new ArcanistStringConfigOption())
->setKey('phabricator.uri')
->setAliases(
array(
'conduit_uri',
'default',
))
->setSummary(pht('Phabricator install to connect to.'))
->setHelp(
pht(
'Associates this working copy with a specific installation of '.
'Phabricator.'))
->setExamples(
array(
'https://phabricator.mycompany.com/',
)),
);
}
}

View file

@ -0,0 +1,88 @@
<?php
abstract class ArcanistConfigOption
extends Phobject {
private $key;
private $help;
private $summary;
private $aliases = array();
private $examples = array();
private $defaultValue;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setAliases($aliases) {
$this->aliases = $aliases;
return $this;
}
public function getAliases() {
return $this->aliases;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
return $this->summary;
}
public function setHelp($help) {
$this->help = $help;
return $this;
}
public function getHelp() {
return $this->help;
}
public function setExamples(array $examples) {
$this->examples = $examples;
return $this;
}
public function getExamples() {
return $this->examples;
}
public function setDefaultValue($default_value) {
$this->defaultValue = $default_value;
return $this;
}
public function getDefaultValue() {
return $this->defaultValue;
}
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);
protected function getStorageValueFromSourceValue(
ArcanistConfigurationSourceValue $source_value) {
$value = $source_value->getValue();
$source = $source_value->getConfigurationSource();
if ($source->isStringSource()) {
$value = $this->getStorageValueFromStringValue($value);
}
return $value;
}
}

View file

@ -0,0 +1,19 @@
<?php
abstract class ArcanistScalarConfigOption
extends ArcanistConfigOption {
public function getValueFromStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$source_value = last($list);
$storage_value = $this->getStorageValueFromSourceValue($source_value);
return $this->getValueFromStorageValue($storage_value);
}
public function getValueFromStorageValue($value) {
return $value;
}
}

View file

@ -0,0 +1,18 @@
<?php
final class ArcanistStringConfigOption
extends ArcanistScalarConfigOption {
public function getType() {
return 'string';
}
public function getStorageValueFromStringValue($value) {
return (string)$value;
}
public function getDisplayValueFromValue($value) {
return $value;
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* This option type makes it easier to manage unknown options with unknown
* types.
*/
final class ArcanistWildConfigOption
extends ArcanistConfigOption {
public function getType() {
return 'wild';
}
public function getStorageValueFromStringValue($value) {
return (string)$value;
}
public function getDisplayValueFromValue($value) {
return json_encode($value);
}
public function getValueFromStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$source_value = last($list);
$storage_value = $this->getStorageValueFromSourceValue($source_value);
return $this->getValueFromStorageValue($storage_value);
}
public function getValueFromStorageValue($value) {
return $value;
}
}

View file

@ -3,4 +3,27 @@
abstract class ArcanistConfigurationSource
extends Phobject {
abstract public function getSourceDisplayName();
abstract public function getAllKeys();
abstract public function hasValueForKey($key);
abstract public function getValueForKey($key);
public function isStringSource() {
return false;
}
public function didReadUnknownOption($key) {
// TOOLSETS: Standardize this kind of messaging? On ArcanistRuntime?
fprintf(
STDERR,
tsprintf(
"<bg:yellow>** %s **</bg> %s\n",
pht('WARNING'),
pht(
'Ignoring unrecognized configuration option ("%s") from source: %s.',
$key,
$this->getSourceDisplayName())));
}
}

View file

@ -1,6 +1,17 @@
<?php
final class ArcanistDefaultsConfigurationSource
extends ArcanistConfigurationSource {
extends ArcanistDictionaryConfigurationSource {
public function getSourceDisplayName() {
return pht('Builtin Defaults');
}
public function __construct() {
$values = id(new ArcanistConfigurationEngine())
->newDefaults();
parent::__construct($values);
}
}

View file

@ -0,0 +1,32 @@
<?php
abstract class ArcanistDictionaryConfigurationSource
extends ArcanistConfigurationSource {
private $values;
public function __construct(array $dictionary) {
$this->values = $dictionary;
}
public function getAllKeys() {
return array_keys($this->values);
}
public function hasValueForKey($key) {
return array_key_exists($key, $this->values);
}
public function getValueForKey($key) {
if (!$this->hasValueForKey($key)) {
throw new Exception(
pht(
'Configuration source ("%s") has no value for key ("%s").',
get_class($this),
$key));
}
return $this->values[$key];
}
}

View file

@ -3,4 +3,9 @@
final class ArcanistFileConfigurationSource
extends ArcanistConfigurationSource {
public function getFileKindDisplayName() {
return pht('Config File');
}
}

View file

@ -1,6 +1,32 @@
<?php
abstract class ArcanistFilesystemConfigurationSource
extends ArcanistConfigurationSource {
extends ArcanistDictionaryConfigurationSource {
private $path;
public function __construct($path) {
$this->path = $path;
$values = array();
if (Filesystem::pathExists($path)) {
$contents = Filesystem::readFile($path);
if (strlen(trim($contents))) {
$values = phutil_json_decode($contents);
}
}
parent::__construct($values);
}
public function getPath() {
return $this->path;
}
public function getSourceDisplayName() {
return pht('%s (%s)', $this->getFileKindDisplayName(), $this->getPath());
}
abstract public function getFileKindDisplayName();
}

View file

@ -3,4 +3,8 @@
final class ArcanistLocalConfigurationSource
extends ArcanistWorkingCopyConfigurationSource {
public function getFileKindDisplayName() {
return pht('Local Config File');
}
}

View file

@ -3,4 +3,8 @@
final class ArcanistProjectConfigurationSource
extends ArcanistWorkingCopyConfigurationSource {
public function getFileKindDisplayName() {
return pht('Project Config File');
}
}

View file

@ -1,6 +1,49 @@
<?php
final class ArcanistRuntimeConfigurationSource
extends ArcanistConfigurationSource {
extends ArcanistDictionaryConfigurationSource {
public function __construct(array $argv) {
$map = array();
foreach ($argv as $raw) {
$parts = explode('=', $raw, 2);
if (count($parts) !== 2) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option "%s" is not valid. Configuration options '.
'passed with command line flags must be in the form "name=value".',
$raw));
}
list($key, $value) = $parts;
if (isset($map[$key])) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option "%s" was provided multiple times with '.
'"--config" flags. Specify each option no more than once.',
$key));
}
$map[$key] = $value;
}
parent::__construct($map);
}
public function didReadUnknownOption($key) {
throw new PhutilArgumentUsageException(
pht(
'Configuration option ("%s") specified with "--config" flag is not '.
'a recognized option.',
$key));
}
public function getSourceDisplayName() {
return pht('Runtime "--config" Flags');
}
public function isStringSource() {
return true;
}
}

View file

@ -3,4 +3,8 @@
final class ArcanistSystemConfigurationSource
extends ArcanistFilesystemConfigurationSource {
public function getFileKindDisplayName() {
return pht('System Config File');
}
}

View file

@ -3,4 +3,8 @@
final class ArcanistUserConfigurationSource
extends ArcanistFilesystemConfigurationSource {
public function getFileKindDisplayName() {
return pht('User Config File');
}
}

View file

@ -4,7 +4,8 @@ abstract class ArcanistWorkflow extends Phobject {
private $toolset;
private $arguments;
private $configurationEngine;
private $configurationSourceList;
/**
* Return the command used to invoke this workflow from the command like,
@ -51,6 +52,26 @@ abstract class ArcanistWorkflow extends Phobject {
return $this;
}
final public function setConfigurationSourceList(
ArcanistConfigurationSourceList $config) {
$this->configurationSourceList = $config;
return $this;
}
final public function getConfigurationSourceList() {
return $this->configurationSourceList;
}
final public function setConfigurationEngine(
ArcanistConfigurationEngine $configuration_engine) {
$this->configurationEngine = $configuration_engine;
return $this;
}
final public function getConfigurationEngine() {
return $this->configurationEngine;
}
final protected function getToolsetKey() {
return $this->getToolset()->getToolsetKey();
}

View file

@ -0,0 +1,137 @@
<?php
/**
* Read configuration settings.
*/
final class ArcanistGetConfigWorkflow
extends ArcanistWorkflow {
public function getWorkflowName() {
return 'get-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**get-config** [__options__] -- [__name__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Reads an arc configuration option. With no argument, reads all
options.
With __--verbose__, shows detailed information about one or more
options.
EOTEXT
);
}
public function getArguments() {
return array(
'verbose' => array(
'help' => pht('Show detailed information about options.'),
),
'*' => 'argv',
);
}
public function runWorkflow() {
$argv = $this->getArgument('argv');
$is_verbose = $this->getArgument('verbose');
$source_list = $this->getConfigurationSourceList();
$config_engine = $this->getConfigurationEngine();
$options_map = $config_engine->newConfigOptionsMap();
$all_keys = array();
$alias_map = array();
foreach ($options_map as $key => $config_option) {
$all_keys[$key] = $key;
foreach ($config_option->getAliases() as $alias) {
$alias_map[$alias] = $key;
}
}
foreach ($source_list->getSources() as $source) {
foreach ($source->getAllKeys() as $key) {
$all_keys[$key] = $key;
}
}
ksort($all_keys);
$defaults_map = $config_engine->newDefaults();
foreach ($all_keys as $key) {
$option = idx($options_map, $key);
if ($option) {
$option_summary = $option->getSummary();
$option_help = $option->getHelp();
} else {
$option_summary = pht('(This option is unrecognized.)');
$option_help = $option_summary;
}
if ($option) {
$formatter = $option;
} else {
$formatter = new ArcanistWildConfigOption();
}
if (!$is_verbose) {
echo tsprintf(
"**%s**\n%R\n\n",
$key,
$option_summary);
} else {
echo tsprintf(
"**%s**\n\n%R\n\n",
$key,
$option_help);
}
// NOTE: We can only get configuration from a SourceList if the option is
// a recognized option, so skip this part if the option isn't known.
if ($option) {
$value = $source_list->getConfig($key);
$display_value = $formatter->getDisplayValueFromValue($value);
echo tsprintf("%s: %s\n", pht('Value'), $display_value);
$default_value = idx($defaults_map, $key);
$display_default = $formatter->getDisplayValueFromValue($value);
echo tsprintf("%s: %s\n", pht('Default'), $display_default);
}
foreach ($source_list->getSources() as $source) {
if ($source->hasValueForKey($key)) {
$source_value = $source->getValueForKey($key);
$source_value = $formatter->getValueFromStorageValue($source_value);
$source_display = $formatter->getDisplayValueFromValue($source_value);
} else {
$source_display = pht('-');
}
echo tsprintf(
"%s: %s\n",
$source->getSourceDisplayName(),
$source_display);
}
}
// if (!$verbose) {
// $console->writeOut(
// "(%s)\n",
// pht('Run with %s for more details.', '--verbose'));
// }
return 0;
}
}

View file

@ -1,173 +0,0 @@
<?php
/**
* Read configuration settings.
*/
final class ArcanistGetConfigWorkflow extends ArcanistWorkflow {
public function getWorkflowName() {
return 'get-config';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**get-config** [__options__] -- [__name__ ...]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: cli
Reads an arc configuration option. With no argument, reads all
options.
With __--verbose__, shows detailed information about one or more
options.
EOTEXT
);
}
public function getArguments() {
return array(
'verbose' => array(
'help' => pht('Show detailed information about options.'),
),
'*' => 'argv',
);
}
public function desiresRepositoryAPI() {
return true;
}
public function run() {
$argv = $this->getArgument('argv');
$verbose = $this->getArgument('verbose');
$settings = new ArcanistSettings();
$configuration_manager = $this->getConfigurationManager();
$configs = array(
ArcanistConfigurationManager::CONFIG_SOURCE_LOCAL =>
$configuration_manager->readLocalArcConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT =>
$this->getWorkingCopy()->readProjectConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_USER =>
$configuration_manager->readUserArcConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_SYSTEM =>
$configuration_manager->readSystemArcConfig(),
ArcanistConfigurationManager::CONFIG_SOURCE_DEFAULT =>
$configuration_manager->readDefaultConfig(),
);
if ($argv) {
$keys = $argv;
} else {
$keys = array_mergev(array_map('array_keys', $configs));
$keys = array_merge($keys, $settings->getAllKeys());
$keys = array_unique($keys);
sort($keys);
}
$console = PhutilConsole::getConsole();
$multi = (count($keys) > 1);
foreach ($keys as $key) {
$console->writeOut("**%s**\n\n", $key);
if ($verbose) {
$help = $settings->getHelp($key);
if (!$help) {
$help = pht(
'(This configuration value is not recognized by arc. It may '.
'be misspelled or out of date.)');
}
$console->writeOut("%s\n\n", phutil_console_wrap($help, 4));
$console->writeOut(
"%s: %s\n\n",
sprintf('% 20.20s', pht('Example Value')),
$settings->getExample($key));
}
$values = array();
foreach ($configs as $config_key => $config) {
if (array_key_exists($key, $config)) {
$values[$config_key] = $config[$key];
} else {
// If we didn't find a value, look for a legacy value.
$source_project = ArcanistConfigurationManager::CONFIG_SOURCE_PROJECT;
if ($config_key === $source_project) {
$legacy_name = $settings->getLegacyName($key);
if (array_key_exists($legacy_name, $config)) {
$values[$config_key] = $config[$legacy_name];
}
}
}
}
$console->writeOut(
'%s: ',
sprintf('% 20.20s', pht('Current Value')));
if ($values) {
$value = head($values);
$value = $settings->formatConfigValueForDisplay($key, $value);
$console->writeOut("%s\n", $value);
} else {
$console->writeOut("-\n");
}
$console->writeOut(
'%s: ',
sprintf('% 20.20s', pht('Current Source')));
if ($values) {
$source = head_key($values);
$console->writeOut("%s\n", $source);
} else {
$console->writeOut("-\n");
}
if ($verbose) {
$console->writeOut("\n");
foreach ($configs as $name => $config) {
$have_value = false;
if (array_key_exists($name, $values)) {
$have_value = true;
$value = $values[$name];
}
$console->writeOut(
'%s: ',
sprintf('% 20.20s', pht('%s Value', $name)));
if ($have_value) {
$console->writeOut(
"%s\n",
$settings->formatConfigValueForDisplay($key, $value));
} else {
$console->writeOut("-\n");
}
}
}
if ($multi) {
echo "\n";
}
}
if (!$verbose) {
$console->writeOut(
"(%s)\n",
pht('Run with %s for more details.', '--verbose'));
}
return 0;
}
}

View file

@ -65,10 +65,17 @@ final class ArcanistRuntime {
$args->parsePartial($config_args, true);
$config = $this->loadConfiguration($args);
$config_engine = $this->loadConfiguration($args);
$config = $config_engine->newConfigurationSourceList();
$this->loadLibraries($args, $config);
// Now that we've loaded libraries, we can validate configuration.
// Do this before continuing since configuration can impact other
// behaviors immediately and we want to catch any issues right away.
$config->setConfigOptions($config_engine->newConfigOptionsMap());
$config->validateConfiguration();
$toolset = $this->newToolset($argv);
$args->parsePartial($toolset->getToolsetArguments());
@ -78,6 +85,10 @@ final class ArcanistRuntime {
$phutil_workflows = array();
foreach ($workflows as $key => $workflow) {
$phutil_workflows[$key] = $workflow->newPhutilWorkflow();
$workflow
->setConfigurationEngine($config_engine)
->setConfigurationSourceList($config);
}
$unconsumed_argv = $args->getUnconsumedArgumentVector();
@ -218,7 +229,7 @@ final class ArcanistRuntime {
$engine->setWorkingCopy($working_copy);
}
return $engine->newConfigurationSourceList();
return $engine;
}
private function loadLibraries(
@ -462,6 +473,8 @@ final class ArcanistRuntime {
array $argv,
ArcanistConfigurationSourceList $config) {
return $argv;
$command = head($argv);
// If this is a match for a recognized workflow, just return the arguments