diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -51,10 +51,10 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ '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 @@ 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistConduitCall' => 'Phobject', - 'ArcanistConduitConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension', 'ArcanistConduitEngine' => 'Phobject', 'ArcanistConfigOption' => 'Phobject', 'ArcanistConfigurationDrivenLintEngine' => 'ArcanistLintEngine', @@ -1236,6 +1239,7 @@ 'ArcanistConfigurationManager' => 'Phobject', 'ArcanistConfigurationSource' => 'Phobject', 'ArcanistConfigurationSourceList' => 'Phobject', + 'ArcanistConfigurationSourceValue' => 'Phobject', 'ArcanistConsoleLintRenderer' => 'ArcanistLintRenderer', 'ArcanistConsoleLintRendererTestCase' => 'PhutilTestCase', 'ArcanistConstructorParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1253,9 +1257,10 @@ '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 @@ 'ArcanistFileUploader' => 'Phobject', 'ArcanistFilenameLinter' => 'ArcanistLinter', 'ArcanistFilenameLinterTestCase' => 'ArcanistLinterTestCase', - 'ArcanistFilesystemConfigurationSource' => 'ArcanistConfigurationSource', + 'ArcanistFilesystemConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistFlagWorkflow' => 'ArcanistWorkflow', 'ArcanistFlake8Linter' => 'ArcanistExternalLinter', 'ArcanistFlake8LinterTestCase' => 'ArcanistExternalLinterTestCase', @@ -1496,7 +1501,8 @@ 'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRubyLinter' => 'ArcanistExternalLinter', 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', - 'ArcanistRuntimeConfigurationSource' => 'ArcanistConfigurationSource', + 'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource', + 'ArcanistScalarConfigOption' => 'ArcanistConfigOption', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSelfClassReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1517,6 +1523,7 @@ 'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistStopWorkflow' => 'ArcanistPhrequentWorkflow', + 'ArcanistStringConfigOption' => 'ArcanistScalarConfigOption', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSubversionWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', @@ -1578,6 +1585,7 @@ 'ArcanistVersionWorkflow' => 'ArcanistWorkflow', 'ArcanistWeldWorkflow' => 'ArcanistWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', + 'ArcanistWildConfigOption' => 'ArcanistConfigOption', 'ArcanistWorkflow' => 'Phobject', 'ArcanistWorkingCopy' => 'Phobject', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', diff --git a/src/config/ArcanistArcConfigurationEngineExtension.php b/src/config/ArcanistArcConfigurationEngineExtension.php deleted file mode 100644 --- a/src/config/ArcanistArcConfigurationEngineExtension.php +++ /dev/null @@ -1,6 +0,0 @@ -workingCopy = $working_copy; @@ -95,4 +96,120 @@ } } + 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(); + } + } diff --git a/src/config/ArcanistConfigurationEngineExtension.php b/src/config/ArcanistConfigurationEngineExtension.php --- a/src/config/ArcanistConfigurationEngineExtension.php +++ b/src/config/ArcanistConfigurationEngineExtension.php @@ -1,6 +1,10 @@ getPhobjectClassConstant('EXTENSIONKEY'); + } + } diff --git a/src/config/ArcanistConfigurationSourceList.php b/src/config/ArcanistConfigurationSourceList.php --- a/src/config/ArcanistConfigurationSourceList.php +++ b/src/config/ArcanistConfigurationSourceList.php @@ -4,10 +4,129 @@ 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; + } + } + + } + } \ No newline at end of file diff --git a/src/config/ArcanistConfigurationSourceValue.php b/src/config/ArcanistConfigurationSourceValue.php new file mode 100644 --- /dev/null +++ b/src/config/ArcanistConfigurationSourceValue.php @@ -0,0 +1,24 @@ +source = $source; + $this->value = $value; + } + + public function getConfigurationSource() { + return $this->source; + } + + public function getValue() { + return $this->value; + } + +} + + diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php new file mode 100644 --- /dev/null +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -0,0 +1,166 @@ + 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/', + )), + ); + } + +} diff --git a/src/config/option/ArcanistConfigOption.php b/src/config/option/ArcanistConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistConfigOption.php @@ -0,0 +1,88 @@ +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; + } + + +} diff --git a/src/config/option/ArcanistScalarConfigOption.php b/src/config/option/ArcanistScalarConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistScalarConfigOption.php @@ -0,0 +1,19 @@ +getStorageValueFromSourceValue($source_value); + + return $this->getValueFromStorageValue($storage_value); + } + + public function getValueFromStorageValue($value) { + return $value; + } + +} diff --git a/src/config/option/ArcanistStringConfigOption.php b/src/config/option/ArcanistStringConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistStringConfigOption.php @@ -0,0 +1,18 @@ +getStorageValueFromSourceValue($source_value); + + return $this->getValueFromStorageValue($storage_value); + } + + public function getValueFromStorageValue($value) { + return $value; + } + +} diff --git a/src/config/source/ArcanistConfigurationSource.php b/src/config/source/ArcanistConfigurationSource.php --- a/src/config/source/ArcanistConfigurationSource.php +++ b/src/config/source/ArcanistConfigurationSource.php @@ -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( + "** %s ** %s\n", + pht('WARNING'), + pht( + 'Ignoring unrecognized configuration option ("%s") from source: %s.', + $key, + $this->getSourceDisplayName()))); + } + } \ No newline at end of file diff --git a/src/config/source/ArcanistDefaultsConfigurationSource.php b/src/config/source/ArcanistDefaultsConfigurationSource.php --- a/src/config/source/ArcanistDefaultsConfigurationSource.php +++ b/src/config/source/ArcanistDefaultsConfigurationSource.php @@ -1,6 +1,17 @@ newDefaults(); + + parent::__construct($values); + } } \ No newline at end of file diff --git a/src/config/source/ArcanistDictionaryConfigurationSource.php b/src/config/source/ArcanistDictionaryConfigurationSource.php new file mode 100644 --- /dev/null +++ b/src/config/source/ArcanistDictionaryConfigurationSource.php @@ -0,0 +1,32 @@ +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]; + } + +} \ No newline at end of file diff --git a/src/config/source/ArcanistFileConfigurationSource.php b/src/config/source/ArcanistFileConfigurationSource.php --- a/src/config/source/ArcanistFileConfigurationSource.php +++ b/src/config/source/ArcanistFileConfigurationSource.php @@ -3,4 +3,9 @@ final class ArcanistFileConfigurationSource extends ArcanistConfigurationSource { + public function getFileKindDisplayName() { + return pht('Config File'); + } + + } \ No newline at end of file diff --git a/src/config/source/ArcanistFilesystemConfigurationSource.php b/src/config/source/ArcanistFilesystemConfigurationSource.php --- a/src/config/source/ArcanistFilesystemConfigurationSource.php +++ b/src/config/source/ArcanistFilesystemConfigurationSource.php @@ -1,6 +1,32 @@ 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(); } \ No newline at end of file diff --git a/src/config/source/ArcanistLocalConfigurationSource.php b/src/config/source/ArcanistLocalConfigurationSource.php --- a/src/config/source/ArcanistLocalConfigurationSource.php +++ b/src/config/source/ArcanistLocalConfigurationSource.php @@ -3,4 +3,8 @@ final class ArcanistLocalConfigurationSource extends ArcanistWorkingCopyConfigurationSource { + public function getFileKindDisplayName() { + return pht('Local Config File'); + } + } \ No newline at end of file diff --git a/src/config/source/ArcanistProjectConfigurationSource.php b/src/config/source/ArcanistProjectConfigurationSource.php --- a/src/config/source/ArcanistProjectConfigurationSource.php +++ b/src/config/source/ArcanistProjectConfigurationSource.php @@ -3,4 +3,8 @@ final class ArcanistProjectConfigurationSource extends ArcanistWorkingCopyConfigurationSource { + public function getFileKindDisplayName() { + return pht('Project Config File'); + } + } \ No newline at end of file diff --git a/src/config/source/ArcanistRuntimeConfigurationSource.php b/src/config/source/ArcanistRuntimeConfigurationSource.php --- a/src/config/source/ArcanistRuntimeConfigurationSource.php +++ b/src/config/source/ArcanistRuntimeConfigurationSource.php @@ -1,6 +1,49 @@ 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(); } diff --git a/src/toolset/ArcanistAliasWorkflow.php b/src/toolset/workflow/ArcanistAliasWorkflow.php rename from src/toolset/ArcanistAliasWorkflow.php rename to src/toolset/workflow/ArcanistAliasWorkflow.php diff --git a/src/toolset/workflow/ArcanistGetConfigWorkflow.php b/src/toolset/workflow/ArcanistGetConfigWorkflow.php new file mode 100644 --- /dev/null +++ b/src/toolset/workflow/ArcanistGetConfigWorkflow.php @@ -0,0 +1,137 @@ + 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; + } + +} diff --git a/src/toolset/ArcanistHelpWorkflow.php b/src/toolset/workflow/ArcanistHelpWorkflow.php rename from src/toolset/ArcanistHelpWorkflow.php rename to src/toolset/workflow/ArcanistHelpWorkflow.php diff --git a/src/workflow/ArcanistSetConfigWorkflow.php b/src/toolset/workflow/ArcanistSetConfigWorkflow.php rename from src/workflow/ArcanistSetConfigWorkflow.php rename to src/toolset/workflow/ArcanistSetConfigWorkflow.php diff --git a/src/toolset/ArcanistShellCompleteWorkflow.php b/src/toolset/workflow/ArcanistShellCompleteWorkflow.php rename from src/toolset/ArcanistShellCompleteWorkflow.php rename to src/toolset/workflow/ArcanistShellCompleteWorkflow.php diff --git a/src/toolset/ArcanistVersionWorkflow.php b/src/toolset/workflow/ArcanistVersionWorkflow.php rename from src/toolset/ArcanistVersionWorkflow.php rename to src/toolset/workflow/ArcanistVersionWorkflow.php diff --git a/src/workflow/ArcanistGetConfigWorkflow.php b/src/workflow/ArcanistGetConfigWorkflow.php deleted file mode 100644 --- a/src/workflow/ArcanistGetConfigWorkflow.php +++ /dev/null @@ -1,173 +0,0 @@ - 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; - } - -} diff --git a/support/ArcanistRuntime.php b/support/ArcanistRuntime.php --- a/support/ArcanistRuntime.php +++ b/support/ArcanistRuntime.php @@ -65,10 +65,17 @@ $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 @@ $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 @@ $engine->setWorkingCopy($working_copy); } - return $engine->newConfigurationSourceList(); + return $engine; } private function loadLibraries( @@ -462,6 +473,8 @@ array $argv, ArcanistConfigurationSourceList $config) { + return $argv; + $command = head($argv); // If this is a match for a recognized workflow, just return the arguments