1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-22 14:52: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:
epriestley 2012-07-25 18:37:09 -07:00
parent 11d0331d21
commit 768a125b58
7 changed files with 307 additions and 110 deletions

View file

@ -120,9 +120,9 @@ try {
// Load libraries in ".arcconfig". Libraries here must load. // Load libraries in ".arcconfig". Libraries here must load.
arcanist_load_libraries( arcanist_load_libraries(
$working_copy->getConfig('phutil_libraries'), $working_copy->getConfig('load'),
$must_load = true, $must_load = true,
$lib_source = 'the "phutil_libraries" setting in ".arcconfig"', $lib_source = 'the "load" setting in ".arcconfig"',
$working_copy, $working_copy,
$config_trace_mode); $config_trace_mode);
} }

View file

@ -102,6 +102,7 @@ phutil_register_library_map(array(
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php', 'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
'ArcanistSettings' => 'configuration/ArcanistSettings.php',
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php', 'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php', 'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
'ArcanistSpellingDefaultData' => 'lint/linter/ArcanistSpellingDefaultData.php', 'ArcanistSpellingDefaultData' => 'lint/linter/ArcanistSpellingDefaultData.php',

View 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;
}
}

View file

@ -1090,9 +1090,9 @@ abstract class ArcanistBaseWorkflow {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
$working_copy = $this->getWorkingCopy(); $working_copy = $this->getWorkingCopy();
$project_config = $working_copy->getConfigFromAnySource('immutable_history'); $config = $working_copy->getConfigFromAnySource('history.immutable');
if ($project_config !== null) { if ($config !== null) {
return $project_config; return $config;
} }
return $repository_api->isHistoryDefaultImmutable(); return $repository_api->isHistoryDefaultImmutable();
@ -1264,18 +1264,6 @@ abstract class ArcanistBaseWorkflow {
return $this->repositoryEncoding; 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) { protected function newInteractiveEditor($text) {
$editor = new PhutilInteractiveEditor($text); $editor = new PhutilInteractiveEditor($text);

View file

@ -33,7 +33,7 @@ EOTEXT
public function getCommandHelp() { public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT return phutil_console_format(<<<EOTEXT
Supports: cli Supports: cli
Reads an arc configuration option. With no arugment, reads all Reads an arc configuration option. With no argument, reads all
options. options.
EOTEXT EOTEXT
); );
@ -52,10 +52,13 @@ EOTEXT
public function run() { public function run() {
$argv = $this->getArgument('argv'); $argv = $this->getArgument('argv');
$settings = new ArcanistSettings();
$configs = array( $configs = array(
'system' => self::readSystemArcConfig(), 'system' => self::readSystemArcConfig(),
'global' => self::readGlobalArcConfig(), 'global' => self::readGlobalArcConfig(),
'local' => $this->readLocalArcConfig(), 'project' => $this->getWorkingCopy()->getProjectConfig(),
'local' => $this->readLocalArcConfig(),
); );
if ($argv) { if ($argv) {
$keys = $argv; $keys = $argv;
@ -66,12 +69,16 @@ EOTEXT
} }
foreach ($keys as $key) { foreach ($keys as $key) {
echo "{$key}\n";
foreach ($configs as $name => $config) { foreach ($configs as $name => $config) {
if ($name == 'global' || isset($config[$key])) { $val = idx($config, $key);
$val = self::formatConfigValueForDisplay(idx($config, $key)); if ($val === null) {
echo "({$name}) {$key} = {$val}\n"; continue;
} }
$val = $settings->formatConfigValueForDisplay($key, $val);
printf("% 10.10s: %s\n", $name, $val);
} }
echo "\n";
} }
return 0; return 0;

View file

@ -90,6 +90,8 @@ EOTEXT
$key = $argv[0]; $key = $argv[0];
$val = $argv[1]; $val = $argv[1];
$settings = new ArcanistSettings();
$old = null; $old = null;
if (array_key_exists($key, $config)) { if (array_key_exists($key, $config)) {
$old = $config[$key]; $old = $config[$key];
@ -103,13 +105,15 @@ EOTEXT
self::writeGlobalArcConfig($config); self::writeGlobalArcConfig($config);
} }
$old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) { if ($old === null) {
echo "Deleted key '{$key}' from {$which} config.\n"; echo "Deleted key '{$key}' from {$which} config.\n";
} else { } else {
echo "Deleted key '{$key}' from {$which} config (was '{$old}').\n"; echo "Deleted key '{$key}' from {$which} config (was {$old}).\n";
} }
} else { } else {
$val = $this->parse($key, $val); $val = $settings->willWriteValue($key, $val);
$config[$key] = $val; $config[$key] = $val;
if ($is_local) { if ($is_local) {
@ -118,13 +122,13 @@ EOTEXT
self::writeGlobalArcConfig($config); self::writeGlobalArcConfig($config);
} }
$val = self::formatConfigValueForDisplay($val); $val = $settings->formatConfigValueForDisplay($key, $val);
$old = self::formatConfigValueForDisplay($old); $old = $settings->formatConfigValueForDisplay($key, $old);
if ($old === null) { if ($old === null) {
echo "Set key '{$key}' = '{$val}' in {$which} config.\n"; echo "Set key '{$key}' = {$val} in {$which} config.\n";
} else { } 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() { 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(); $config = self::readGlobalArcConfig();
foreach ($keys as $key => $spec) { $settings = new ArcanistSettings();
$type = $this->getType($key);
$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 = 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("**__%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)) { if (strlen($value)) {
echo phutil_console_format(" Global Setting: %s\n", $value); echo phutil_console_format(" Global Setting: %s\n", $value);
} }
echo "\n"; echo "\n";
echo phutil_console_wrap($spec['help'], 4); echo phutil_console_wrap($help, 4);
echo "\n\n\n"; echo "\n\n\n";
} }
return 0; 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}'!");
}
}
} }

View file

@ -157,6 +157,9 @@ final class ArcanistWorkingCopyIdentity {
/* -( Config )------------------------------------------------------------- */ /* -( Config )------------------------------------------------------------- */
public function getProjectConfig() {
return $this->projectConfig;
}
/** /**
* Read a configuration directive from project configuration. This reads ONLY * Read a configuration directive from project configuration. This reads ONLY
@ -171,7 +174,26 @@ final class ArcanistWorkingCopyIdentity {
* @task config * @task config
*/ */
public function getConfig($key, $default = null) { 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 * @task config
*/ */
public function getConfigFromAnySource($key, $default = null) { public function getConfigFromAnySource($key, $default = null) {
$settings = new ArcanistSettings();
// try local config first // try local config first
$pval = $this->getLocalConfig($key); $pval = $this->getLocalConfig($key);
@ -211,22 +234,13 @@ final class ArcanistWorkingCopyIdentity {
$pval = $this->getConfig($key); $pval = $this->getConfig($key);
} }
// Test for older names in the per-project config only, since // now try global (i.e. user-level) config
// 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
if ($pval === null) { if ($pval === null) {
$global_config = ArcanistBaseWorkflow::readGlobalArcConfig(); $global_config = ArcanistBaseWorkflow::readGlobalArcConfig();
$pval = idx($global_config, $key); $pval = idx($global_config, $key);
} }
// Finally, try system-level config.
if ($pval === null) { if ($pval === null) {
$system_config = ArcanistBaseWorkflow::readSystemArcConfig(); $system_config = ArcanistBaseWorkflow::readSystemArcConfig();
$pval = idx($system_config, $key); $pval = idx($system_config, $key);
@ -234,6 +248,8 @@ final class ArcanistWorkingCopyIdentity {
if ($pval === null) { if ($pval === null) {
$pval = $default; $pval = $default;
} else {
$pval = $settings->willReadValue($key, $pval);
} }
return $pval; return $pval;