mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-25 08:12:40 +01:00
Enrich arc configuration and add stronger typing
Summary: See <https://github.com/facebook/arcanist/issues/45> Currently, when the user types `arc set-config x false`, we set it as the string "false", which is usually not desirable. We have some steps toward typed config already, but expand on what we have and move as much stuff as possible into it, including all the config settings that aren't currently documented (there are still some lint-specific and project-specific settings not present here, but this is most of it). Also make the `phutil_libraries` key a legacy name for `load`, and `immutable_history` a legacy name for `history.immutable`. Generally the goal here is to make config simpler and bring it more in-line with Git/Mercurial, which use dotted hierarchies. I'll add some documentation here but I think most of the changes should be fairly straightforward. Test Plan: - `arc set-config history.immutable on` (And similar -- sets to boolean true.) - `arc set-config history.immutable off` (And similar -- sets to boolean false.) - `arc set-config history.immutable derp` (And similar -- raises exception.) - `arc set-config history.immutable ''` (And similar -- removes setting value.) - `arc set-config --show` - `arc get-config` - `arc get-config base` Reviewers: dschleimer, bos, btrahan, vrana Reviewed By: dschleimer CC: aran Maniphest Tasks: T1546 Differential Revision: https://secure.phabricator.com/D3045
This commit is contained in:
parent
11d0331d21
commit
768a125b58
7 changed files with 307 additions and 110 deletions
|
@ -120,9 +120,9 @@ try {
|
|||
|
||||
// Load libraries in ".arcconfig". Libraries here must load.
|
||||
arcanist_load_libraries(
|
||||
$working_copy->getConfig('phutil_libraries'),
|
||||
$working_copy->getConfig('load'),
|
||||
$must_load = true,
|
||||
$lib_source = 'the "phutil_libraries" setting in ".arcconfig"',
|
||||
$lib_source = 'the "load" setting in ".arcconfig"',
|
||||
$working_copy,
|
||||
$config_trace_mode);
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
|
||||
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
|
||||
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
|
||||
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
|
||||
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
|
||||
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
|
||||
'ArcanistSpellingDefaultData' => 'lint/linter/ArcanistSpellingDefaultData.php',
|
||||
|
|
236
src/configuration/ArcanistSettings.php
Normal file
236
src/configuration/ArcanistSettings.php
Normal file
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @group config
|
||||
*/
|
||||
final class ArcanistSettings {
|
||||
|
||||
private function getOptions() {
|
||||
return array(
|
||||
'default' => array(
|
||||
'type' => 'string',
|
||||
'help' =>
|
||||
'The URI of a Phabricator install to connect to by default, if '.
|
||||
'arc is run in a project without a Phabricator URI or run outside '.
|
||||
'of a project.',
|
||||
'example' => '"http://phabricator.example.com/"',
|
||||
),
|
||||
'base' => array(
|
||||
'type' => 'string',
|
||||
'help' =>
|
||||
'Base commit ruleset to invoke when determining the start of a '.
|
||||
'commit range. See "Arcanist User Guide: Commit Ranges" for '.
|
||||
'details.',
|
||||
'example' => '"arc:amended, arc:prompt"',
|
||||
),
|
||||
'load' => array(
|
||||
'type' => 'list',
|
||||
'legacy' => 'phutil_libraries',
|
||||
'help' =>
|
||||
'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.',
|
||||
'example' => '["/var/arc/customlib/src"]',
|
||||
),
|
||||
'lint.engine' => array(
|
||||
'type' => 'string',
|
||||
'legacy' => 'lint_engine',
|
||||
'help' =>
|
||||
'The name of a default lint engine to use, if no lint engine is '.
|
||||
'specified by the current project.',
|
||||
'example' => '"ExampleLintEngine"',
|
||||
),
|
||||
'unit.engine' => array(
|
||||
'type' => 'string',
|
||||
'legacy' => 'unit_engine',
|
||||
'help' =>
|
||||
'The name of a default unit test engine to use, if no unit test '.
|
||||
'engine is specified by the current project.',
|
||||
'example' => '"ExampleUnitTestEngine"',
|
||||
),
|
||||
'arc.land.onto.default' => array(
|
||||
'type' => 'string',
|
||||
'help' =>
|
||||
'The name of the default branch to land changes onto when '.
|
||||
'`arc land` is run.',
|
||||
'example' => '"develop"',
|
||||
),
|
||||
'history.immutable' => array(
|
||||
'type' => 'bool',
|
||||
'legacy' => 'immutable_history',
|
||||
'help' =>
|
||||
'If true, arc 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.',
|
||||
'example' => 'false',
|
||||
),
|
||||
'editor' => array(
|
||||
'type' => 'string',
|
||||
'help' =>
|
||||
"Command to use to invoke an interactive editor, like 'nano' or ".
|
||||
"'vim'. This setting overrides the EDITOR environmental variable.",
|
||||
'example' => '"nano"',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function getOption($key) {
|
||||
return idx($this->getOptions(), $key, array());
|
||||
}
|
||||
|
||||
public function getAllKeys() {
|
||||
return array_keys($this->getOptions());
|
||||
}
|
||||
|
||||
public function getHelp($key) {
|
||||
return idx($this->getOption($key), 'help');
|
||||
}
|
||||
|
||||
public function getExample($key) {
|
||||
return idx($this->getOption($key), 'example');
|
||||
}
|
||||
|
||||
public function getType($key) {
|
||||
return idx($this->getOption($key), 'type', 'wild');
|
||||
}
|
||||
|
||||
public function getLegacyName($key) {
|
||||
return idx($this->getOption($key), 'legacy');
|
||||
}
|
||||
|
||||
public function willWriteValue($key, $value) {
|
||||
$type = $this->getType($key);
|
||||
switch ($type) {
|
||||
case 'bool':
|
||||
if (strtolower($value) === 'false' ||
|
||||
strtolower($value) === 'no' ||
|
||||
strtolower($value) === 'off' ||
|
||||
$value === '' ||
|
||||
$value === '0' ||
|
||||
$value === 0 ||
|
||||
$value === false) {
|
||||
$value = false;
|
||||
} else if (strtolower($value) === 'true' ||
|
||||
strtolower($value) === 'yes' ||
|
||||
strtolower($value) === 'on' ||
|
||||
$value === '1' ||
|
||||
$value === 1 ||
|
||||
$value === true) {
|
||||
$value = true;
|
||||
} else {
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be boolean, like 'true' or ".
|
||||
"'false'.");
|
||||
}
|
||||
break;
|
||||
case 'list':
|
||||
if (is_array($value)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$list = json_decode($value, true);
|
||||
if (is_array($list)) {
|
||||
$value = $list;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$list_example = '["apple", "banana", "cherry"]';
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be list. You can specify a list ".
|
||||
"in JSON, like: {$list_example}");
|
||||
|
||||
case 'string':
|
||||
if (!is_scalar($value)) {
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be string.");
|
||||
}
|
||||
$value = (string)$value;
|
||||
break;
|
||||
case 'wild':
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function willReadValue($key, $value) {
|
||||
$type = $this->getType($key);
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
if (!is_string($value)) {
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be string.");
|
||||
}
|
||||
break;
|
||||
case 'bool':
|
||||
if ($value !== true && $value !== false) {
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be boolean.");
|
||||
}
|
||||
break;
|
||||
case 'list':
|
||||
if (!is_array($value)) {
|
||||
throw new ArcanistUsageException(
|
||||
"Type of setting '{$key}' must be list.");
|
||||
}
|
||||
break;
|
||||
case 'wild':
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function formatConfigValueForDisplay($key, $value) {
|
||||
if ($value === false) {
|
||||
return 'false';
|
||||
}
|
||||
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return '"'.$value.'"';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
// TODO: Both json_encode() and PhutilJSON do a bad job with one-liners.
|
||||
// PhutilJSON splits them across a bunch of lines, while json_encode()
|
||||
// escapes all kinds of stuff like "/". It would be nice if PhutilJSON
|
||||
// had a mode for pretty one-liners.
|
||||
$value = json_encode($value);
|
||||
|
||||
// json_encode() unnecessarily escapes "/" to prevent "</script>" stuff,
|
||||
// optimistically unescape it for display to improve readability.
|
||||
$value = preg_replace('@(?<!\\\\)\\\\/@', '/', $value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1090,9 +1090,9 @@ abstract class ArcanistBaseWorkflow {
|
|||
$repository_api = $this->getRepositoryAPI();
|
||||
$working_copy = $this->getWorkingCopy();
|
||||
|
||||
$project_config = $working_copy->getConfigFromAnySource('immutable_history');
|
||||
if ($project_config !== null) {
|
||||
return $project_config;
|
||||
$config = $working_copy->getConfigFromAnySource('history.immutable');
|
||||
if ($config !== null) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return $repository_api->isHistoryDefaultImmutable();
|
||||
|
@ -1264,18 +1264,6 @@ abstract class ArcanistBaseWorkflow {
|
|||
return $this->repositoryEncoding;
|
||||
}
|
||||
|
||||
protected static function formatConfigValueForDisplay($value) {
|
||||
if (is_array($value)) {
|
||||
// TODO: Both json_encode() and PhutilJSON do a bad job with one-liners.
|
||||
// PhutilJSON splits them across a bunch of lines, while json_encode()
|
||||
// escapes all kinds of stuff like "/". It would be nice if PhutilJSON
|
||||
// had a mode for pretty one-liners.
|
||||
$value = json_encode($value);
|
||||
return $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function newInteractiveEditor($text) {
|
||||
$editor = new PhutilInteractiveEditor($text);
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ EOTEXT
|
|||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
Supports: cli
|
||||
Reads an arc configuration option. With no arugment, reads all
|
||||
Reads an arc configuration option. With no argument, reads all
|
||||
options.
|
||||
EOTEXT
|
||||
);
|
||||
|
@ -52,10 +52,13 @@ EOTEXT
|
|||
public function run() {
|
||||
$argv = $this->getArgument('argv');
|
||||
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
$configs = array(
|
||||
'system' => self::readSystemArcConfig(),
|
||||
'global' => self::readGlobalArcConfig(),
|
||||
'local' => $this->readLocalArcConfig(),
|
||||
'system' => self::readSystemArcConfig(),
|
||||
'global' => self::readGlobalArcConfig(),
|
||||
'project' => $this->getWorkingCopy()->getProjectConfig(),
|
||||
'local' => $this->readLocalArcConfig(),
|
||||
);
|
||||
if ($argv) {
|
||||
$keys = $argv;
|
||||
|
@ -66,12 +69,16 @@ EOTEXT
|
|||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
echo "{$key}\n";
|
||||
foreach ($configs as $name => $config) {
|
||||
if ($name == 'global' || isset($config[$key])) {
|
||||
$val = self::formatConfigValueForDisplay(idx($config, $key));
|
||||
echo "({$name}) {$key} = {$val}\n";
|
||||
$val = idx($config, $key);
|
||||
if ($val === null) {
|
||||
continue;
|
||||
}
|
||||
$val = $settings->formatConfigValueForDisplay($key, $val);
|
||||
printf("% 10.10s: %s\n", $name, $val);
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
@ -90,6 +90,8 @@ EOTEXT
|
|||
$key = $argv[0];
|
||||
$val = $argv[1];
|
||||
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
$old = null;
|
||||
if (array_key_exists($key, $config)) {
|
||||
$old = $config[$key];
|
||||
|
@ -103,13 +105,15 @@ EOTEXT
|
|||
self::writeGlobalArcConfig($config);
|
||||
}
|
||||
|
||||
$old = $settings->formatConfigValueForDisplay($key, $old);
|
||||
|
||||
if ($old === null) {
|
||||
echo "Deleted key '{$key}' from {$which} config.\n";
|
||||
} else {
|
||||
echo "Deleted key '{$key}' from {$which} config (was '{$old}').\n";
|
||||
echo "Deleted key '{$key}' from {$which} config (was {$old}).\n";
|
||||
}
|
||||
} else {
|
||||
$val = $this->parse($key, $val);
|
||||
$val = $settings->willWriteValue($key, $val);
|
||||
|
||||
$config[$key] = $val;
|
||||
if ($is_local) {
|
||||
|
@ -118,13 +122,13 @@ EOTEXT
|
|||
self::writeGlobalArcConfig($config);
|
||||
}
|
||||
|
||||
$val = self::formatConfigValueForDisplay($val);
|
||||
$old = self::formatConfigValueForDisplay($old);
|
||||
$val = $settings->formatConfigValueForDisplay($key, $val);
|
||||
$old = $settings->formatConfigValueForDisplay($key, $old);
|
||||
|
||||
if ($old === null) {
|
||||
echo "Set key '{$key}' = '{$val}' in {$which} config.\n";
|
||||
echo "Set key '{$key}' = {$val} in {$which} config.\n";
|
||||
} else {
|
||||
echo "Set key '{$key}' = '{$val}' in {$which} config (was '{$old}').\n";
|
||||
echo "Set key '{$key}' = {$val} in {$which} config (was {$old}).\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,88 +136,33 @@ EOTEXT
|
|||
}
|
||||
|
||||
private function show() {
|
||||
$keys = array(
|
||||
'default' => array(
|
||||
'help' =>
|
||||
'The URI of a Phabricator install to connect to by default, if '.
|
||||
'arc is run in a project without a Phabricator URI or run outside '.
|
||||
'of a project.',
|
||||
'example' => 'http://phabricator.example.com/',
|
||||
),
|
||||
'load' => array(
|
||||
'help' =>
|
||||
'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.',
|
||||
'example' => '["/var/arc/customlib/src"]',
|
||||
),
|
||||
'lint.engine' => array(
|
||||
'help' =>
|
||||
'The name of a default lint engine to use, if no lint engine is '.
|
||||
'specified by the current project.',
|
||||
'example' => 'ExampleLintEngine',
|
||||
),
|
||||
'unit.engine' => array(
|
||||
'help' =>
|
||||
'The name of a default unit test engine to use, if no unit test '.
|
||||
'engine is specified by the current project.',
|
||||
'example' => 'ExampleUnitTestEngine',
|
||||
),
|
||||
'arc.land.onto.default' => array(
|
||||
'help' =>
|
||||
'The name of the default branch to land changes onto when '.
|
||||
'`arc land` is run.',
|
||||
'example' => 'develop',
|
||||
),
|
||||
);
|
||||
|
||||
$config = self::readGlobalArcConfig();
|
||||
|
||||
foreach ($keys as $key => $spec) {
|
||||
$type = $this->getType($key);
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
$keys = $settings->getAllKeys();
|
||||
sort($keys);
|
||||
foreach ($keys as $key) {
|
||||
$type = $settings->getType($key);
|
||||
$example = $settings->getExample($key);
|
||||
$help = $settings->getHelp($key);
|
||||
|
||||
$value = idx($config, $key);
|
||||
$value = self::formatConfigValueForDisplay($value);
|
||||
$value = $settings->formatConfigValueForDisplay($key, $value);
|
||||
|
||||
echo phutil_console_format("**__%s__** (%s)\n\n", $key, $type);
|
||||
echo phutil_console_format(" Example: %s\n", $spec['example']);
|
||||
if ($example !== null) {
|
||||
echo phutil_console_format(" Example: %s\n", $example);
|
||||
}
|
||||
if (strlen($value)) {
|
||||
echo phutil_console_format(" Global Setting: %s\n", $value);
|
||||
}
|
||||
echo "\n";
|
||||
echo phutil_console_wrap($spec['help'], 4);
|
||||
echo phutil_console_wrap($help, 4);
|
||||
echo "\n\n\n";
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getType($key) {
|
||||
static $types = array(
|
||||
'load' => 'list',
|
||||
);
|
||||
|
||||
return idx($types, $key, 'string');
|
||||
}
|
||||
|
||||
private function parse($key, $val) {
|
||||
$type = $this->getType($key);
|
||||
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
return $val;
|
||||
case 'list':
|
||||
$val = json_decode($val, true);
|
||||
if (!is_array($val)) {
|
||||
$example = '["apple", "banana", "cherry"]';
|
||||
throw new ArcanistUsageException(
|
||||
"Value for key '{$key}' must be specified as a JSON-encoded ".
|
||||
"list. Example: {$example}");
|
||||
}
|
||||
return $val;
|
||||
default:
|
||||
throw new Exception("Unknown config key type '{$type}'!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -157,6 +157,9 @@ final class ArcanistWorkingCopyIdentity {
|
|||
|
||||
/* -( Config )------------------------------------------------------------- */
|
||||
|
||||
public function getProjectConfig() {
|
||||
return $this->projectConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a configuration directive from project configuration. This reads ONLY
|
||||
|
@ -171,7 +174,26 @@ final class ArcanistWorkingCopyIdentity {
|
|||
* @task config
|
||||
*/
|
||||
public function getConfig($key, $default = null) {
|
||||
return idx($this->projectConfig, $key, $default);
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
$pval = idx($this->projectConfig, $key);
|
||||
|
||||
// Test for older names in the per-project config only, since
|
||||
// they've only been used there.
|
||||
if ($pval === null) {
|
||||
$legacy = $settings->getLegacyName($key);
|
||||
if ($legacy) {
|
||||
$pval = $this->getConfig($legacy);
|
||||
}
|
||||
}
|
||||
|
||||
if ($pval === null) {
|
||||
$pval = $default;
|
||||
} else {
|
||||
$pval = $settings->willReadValue($key, $pval);
|
||||
}
|
||||
|
||||
return $pval;
|
||||
}
|
||||
|
||||
|
||||
|
@ -202,6 +224,7 @@ final class ArcanistWorkingCopyIdentity {
|
|||
* @task config
|
||||
*/
|
||||
public function getConfigFromAnySource($key, $default = null) {
|
||||
$settings = new ArcanistSettings();
|
||||
|
||||
// try local config first
|
||||
$pval = $this->getLocalConfig($key);
|
||||
|
@ -211,22 +234,13 @@ final class ArcanistWorkingCopyIdentity {
|
|||
$pval = $this->getConfig($key);
|
||||
}
|
||||
|
||||
// Test for older names in the per-project config only, since
|
||||
// they've only been used there
|
||||
static $deprecated_names = array(
|
||||
'lint.engine' => 'lint_engine',
|
||||
'unit.engine' => 'unit_engine',
|
||||
);
|
||||
if ($pval === null && isset($deprecated_names[$key])) {
|
||||
$pval = $this->getConfig($deprecated_names[$key]);
|
||||
}
|
||||
|
||||
// lastly, try global (i.e. user-level) config
|
||||
// now try global (i.e. user-level) config
|
||||
if ($pval === null) {
|
||||
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
|
||||
$pval = idx($global_config, $key);
|
||||
}
|
||||
|
||||
// Finally, try system-level config.
|
||||
if ($pval === null) {
|
||||
$system_config = ArcanistBaseWorkflow::readSystemArcConfig();
|
||||
$pval = idx($system_config, $key);
|
||||
|
@ -234,6 +248,8 @@ final class ArcanistWorkingCopyIdentity {
|
|||
|
||||
if ($pval === null) {
|
||||
$pval = $default;
|
||||
} else {
|
||||
$pval = $settings->willReadValue($key, $pval);
|
||||
}
|
||||
|
||||
return $pval;
|
||||
|
|
Loading…
Reference in a new issue