1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-21 22:32:41 +01:00

Allow users to save prompt responses in "arc" workflows

Summary: Ref T13546. Permit users to answer "y*" to mean "y, and don't ask me again".

Test Plan: Answered "y*" to some prompts, re-ran workflows, got auto-responses.

Maniphest Tasks: T13546

Differential Revision: https://secure.phabricator.com/D21331
This commit is contained in:
epriestley 2020-06-08 05:03:18 -07:00
parent f3f31155b7
commit c5192bde34
10 changed files with 377 additions and 48 deletions

View file

@ -396,6 +396,8 @@ phutil_register_library_map(array(
'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase.php',
'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php',
'ArcanistPrompt' => 'toolset/ArcanistPrompt.php',
'ArcanistPromptResponse' => 'toolset/ArcanistPromptResponse.php',
'ArcanistPromptsConfigOption' => 'config/option/ArcanistPromptsConfigOption.php',
'ArcanistPromptsWorkflow' => 'toolset/workflow/ArcanistPromptsWorkflow.php',
'ArcanistPublicPropertyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPublicPropertyXHPASTLinterRule.php',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPublicPropertyXHPASTLinterRuleTestCase.php',
@ -1420,6 +1422,8 @@ phutil_register_library_map(array(
'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
'ArcanistPrompt' => 'Phobject',
'ArcanistPromptResponse' => 'Phobject',
'ArcanistPromptsConfigOption' => 'ArcanistMultiSourceConfigOption',
'ArcanistPromptsWorkflow' => 'ArcanistWorkflow',
'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',

View file

@ -6,6 +6,7 @@ final class ArcanistArcConfigurationEngineExtension
const EXTENSIONKEY = 'arc';
const KEY_ALIASES = 'aliases';
const KEY_PROMPTS = 'prompts';
public function newConfigurationOptions() {
// TOOLSETS: Restore "load", and maybe this other stuff.
@ -113,6 +114,14 @@ final class ArcanistArcConfigurationEngineExtension
pht(
'Configured command aliases. Use the "alias" workflow to define '.
'aliases.')),
id(new ArcanistPromptsConfigOption())
->setKey(self::KEY_PROMPTS)
->setDefaultValue(array())
->setSummary(pht('List of prompt responses.'))
->setHelp(
pht(
'Configured prompt aliases. Use the "prompts" workflow to '.
'show prompts and responses.')),
id(new ArcanistStringListConfigOption())
->setKey('arc.land.onto')
->setDefaultValue(array())

View file

@ -0,0 +1,51 @@
<?php
final class ArcanistPromptsConfigOption
extends ArcanistMultiSourceConfigOption {
public function getType() {
return 'map<string, prompt>';
}
public function getValueFromStorageValue($value) {
if (!is_array($value)) {
throw new Exception(pht('Expected a list!'));
}
if (!phutil_is_natural_list($value)) {
throw new Exception(pht('Expected a natural list!'));
}
$responses = array();
foreach ($value as $spec) {
$responses[] = ArcanistPromptResponse::newFromConfig($spec);
}
return $responses;
}
protected function didReadStorageValueList(array $list) {
assert_instances_of($list, 'ArcanistConfigurationSourceValue');
$results = array();
foreach ($list as $spec) {
$source = $spec->getConfigurationSource();
$value = $spec->getValue();
$value->setConfigurationSource($source);
$results[] = $value;
}
return $results;
}
public function getDisplayValueFromValue($value) {
return pht('Use the "prompts" workflow to review prompt responses.');
}
public function getStorageValueFromValue($value) {
return mpull($value, 'getStorageDictionary');
}
}

View file

@ -4,6 +4,7 @@ abstract class ArcanistConfigurationSource
extends Phobject {
const SCOPE_USER = 'user';
const SCOPE_WORKING_COPY = 'working-copy';
abstract public function getSourceDisplayName();
abstract public function getAllKeys();

View file

@ -7,4 +7,12 @@ final class ArcanistLocalConfigurationSource
return pht('Local Config File');
}
public function isWritableConfigurationSource() {
return true;
}
public function getConfigurationSourceScope() {
return ArcanistConfigurationSource::SCOPE_WORKING_COPY;
}
}

View file

@ -884,7 +884,6 @@ final class ArcanistRuntime {
$legacy[] = new ArcanistGetConfigWorkflow();
$legacy[] = new ArcanistSetConfigWorkflow();
$legacy[] = new ArcanistInstallCertificateWorkflow();
$legacy[] = new ArcanistLandWorkflow();
$legacy[] = new ArcanistLintersWorkflow();
$legacy[] = new ArcanistLintWorkflow();
$legacy[] = new ArcanistListWorkflow();

View file

@ -70,8 +70,10 @@ final class ArcanistPrompt
$this->getKey()));
}
$options = '[y/N]';
$default = 'N';
$options = '[y/N/?]';
$default = 'n';
$saved_response = $this->getSavedResponse();
try {
phutil_console_require_tty();
@ -101,54 +103,78 @@ final class ArcanistPrompt
echo "\n";
$result = null;
$is_saved = false;
while (true) {
echo tsprintf(
'**<bg:cyan> %s </bg>** %s %s ',
'>>>',
$query,
$options);
if ($saved_response !== null) {
$is_saved = true;
while (true) {
$read = array($stdin);
$write = array();
$except = array();
$response = $saved_response;
$saved_response = null;
} else {
echo tsprintf(
'**<bg:cyan> %s </bg>** %s %s ',
'>>>',
$query,
$options);
$ok = @stream_select($read, $write, $except, 1);
if ($ok === false) {
// NOTE: We may be interrupted by a system call, particularly if
// the window is resized while a prompt is shown and the terminal
// sends SIGWINCH.
// If we are, just continue below and try to read from stdin. If
// we were interrupted, we should read nothing and continue
// normally. If the pipe is broken, the read should fail.
}
$response = '';
while (true) {
$bytes = fread($stdin, 8192);
if ($bytes === false) {
throw new Exception(
pht('fread() from stdin failed with an error.'));
$is_saved = false;
$read = array($stdin);
$write = array();
$except = array();
$ok = @stream_select($read, $write, $except, 1);
if ($ok === false) {
// NOTE: We may be interrupted by a system call, particularly if
// the window is resized while a prompt is shown and the terminal
// sends SIGWINCH.
// If we are, just continue below and try to read from stdin. If
// we were interrupted, we should read nothing and continue
// normally. If the pipe is broken, the read should fail.
}
if (!strlen($bytes)) {
break;
$response = '';
while (true) {
$bytes = fread($stdin, 8192);
if ($bytes === false) {
throw new Exception(
pht('fread() from stdin failed with an error.'));
}
if (!strlen($bytes)) {
break;
}
$response .= $bytes;
}
$response .= $bytes;
if (!strlen($response)) {
continue;
}
break;
}
$response = trim($response);
if (!strlen($response)) {
continue;
$response = $default;
}
break;
}
$response = trim($response);
if (!strlen($response)) {
$response = $default;
$save_scope = null;
if (!$is_saved) {
$matches = null;
if (preg_match('(^(.*)([!*])\z)', $response, $matches)) {
$response = $matches[1];
if ($matches[2] === '*') {
$save_scope = ArcanistConfigurationSource::SCOPE_USER;
} else {
$save_scope = ArcanistConfigurationSource::SCOPE_WORKING_COPY;
}
}
}
if (phutil_utf8_strtolower($response) == 'y') {
@ -160,12 +186,127 @@ final class ArcanistPrompt
$result = false;
break;
}
if (phutil_utf8_strtolower($response) == '?') {
echo tsprintf(
"\n<bg:green>** %s **</bg> **%s**\n\n",
pht('PROMPT'),
$this->getKey());
echo tsprintf(
"%s\n",
$this->getDescription());
echo tsprintf("\n");
echo tsprintf(
"%s\n",
pht(
'The default response to this prompt is "%s".',
$default));
echo tsprintf("\n");
echo tsprintf(
"%?\n",
pht(
'Use "*" after a response to save it in user configuration.'));
echo tsprintf(
"%?\n",
pht(
'Use "!" after a response to save it in working copy '.
'configuration.'));
echo tsprintf(
"%?\n",
pht(
'Run "arc help prompts" for detailed help on configuring '.
'responses.'));
echo tsprintf("\n");
continue;
}
}
if ($save_scope !== null) {
$this->saveResponse($save_scope, $response);
}
if ($is_saved) {
echo tsprintf(
"<bg:cyan>** %s **</bg> %s **<%s>**\n".
"<bg:cyan>** %s **</bg> (%s)\n\n",
'>>>',
$query,
$response,
'>>>',
pht(
'Using saved response to prompt "%s".',
$this->getKey()));
}
if (!$result) {
throw new ArcanistUserAbortException();
}
}
private function getSavedResponse() {
$config_key = ArcanistArcConfigurationEngineExtension::KEY_PROMPTS;
$workflow = $this->getWorkflow();
$config = $workflow->getConfig($config_key);
$prompt_key = $this->getKey();
$prompt_response = null;
foreach ($config as $response) {
if ($response->getPrompt() === $prompt_key) {
$prompt_response = $response;
}
}
if ($prompt_response === null) {
return null;
}
return $prompt_response->getResponse();
}
private function saveResponse($scope, $response_value) {
$config_key = ArcanistArcConfigurationEngineExtension::KEY_PROMPTS;
$workflow = $this->getWorkflow();
echo tsprintf(
"<bg:green>** %s **</bg> %s\n",
pht('SAVE PROMPT'),
pht(
'Saving response "%s" to prompt "%s".',
$response_value,
$this->getKey()));
$source_list = $workflow->getConfigurationSourceList();
$source = $source_list->getWritableSourceFromScope($scope);
$response_list = $source_list->getConfigFromScopes(
$config_key,
array($scope));
foreach ($response_list as $key => $response) {
if ($response->getPrompt() === $this->getKey()) {
unset($response_list[$key]);
}
}
if ($response_value !== null) {
$response_list[] = id(new ArcanistPromptResponse())
->setPrompt($this->getKey())
->setResponse($response_value);
}
$option = $source_list->getConfigOption($config_key);
$option->writeValue($source, $response_list);
}
}

View file

@ -0,0 +1,59 @@
<?php
final class ArcanistPromptResponse
extends Phobject {
private $prompt;
private $response;
private $configurationSource;
public static function newFromConfig($map) {
PhutilTypeSpec::checkMap(
$map,
array(
'prompt' => 'string',
'response' => 'string',
));
return id(new self())
->setPrompt($map['prompt'])
->setResponse($map['response']);
}
public function getStorageDictionary() {
return array(
'prompt' => $this->getPrompt(),
'response' => $this->getResponse(),
);
}
public function setPrompt($prompt) {
$this->prompt = $prompt;
return $this;
}
public function getPrompt() {
return $this->prompt;
}
public function setResponse($response) {
$this->response = $response;
return $this;
}
public function getResponse() {
return $this->response;
}
public function setConfigurationSource(
ArcanistConfigurationSource $configuration_source) {
$this->configurationSource = $configuration_source;
return $this;
}
public function getConfigurationSource() {
return $this->configurationSource;
}
}

View file

@ -1,6 +1,7 @@
<?php
final class ArcanistPromptsWorkflow extends ArcanistWorkflow {
final class ArcanistPromptsWorkflow
extends ArcanistWorkflow {
public function supportsToolset(ArcanistToolset $toolset) {
return true;
@ -12,14 +13,34 @@ final class ArcanistPromptsWorkflow extends ArcanistWorkflow {
public function getWorkflowInformation() {
$help = pht(<<<EOTEXT
Show information about prompts a workflow may execute and configure default
Show information about prompts a workflow may execute, and review saved
responses.
**Show Prompts**
To show possible prompts a workflow may execute, run:
$ arc prompts <workflow>
$ arc prompts __workflow__
**Saving Responses**
If you always want to answer a particular prompt in a certain way, you can
save your response to the prompt. When you encounter the prompt again, your
saved response will be used automatically.
To save a response, add "*" or "!" to the end of the response you want to save
when you answer the prompt:
- Using "*" will save the response in user configuration. In the future,
the saved answer will be used any time you encounter the prompt (in any
project).
- Using "!" will save the response in working copy configuration. In the
future, the saved answer will be used when you encounter the prompt in
the current working copy.
For example, if you would like to always answer "y" to a particular prompt,
respond with "y*" or "y!" to save your response.
EOTEXT
);
@ -65,16 +86,51 @@ EOTEXT
return 0;
}
$prompts = msort($prompts, 'getKey');
$blocks = array();
foreach ($prompts as $prompt) {
echo tsprintf(
"**%s**\n",
$block = array();
$block[] = tsprintf(
"<bg:green>** %s **</bg> **%s**\n\n",
pht('PROMPT'),
$prompt->getKey());
echo tsprintf(
"%s\n",
$block[] = tsprintf(
"%W\n",
$prompt->getDescription());
$responses = $this->getSavedResponses($prompt->getKey());
if ($responses) {
$block[] = tsprintf("\n");
foreach ($responses as $response) {
$block[] = tsprintf(
" <bg:cyan>** > **</bg> %s\n",
pht(
'You have saved the response "%s" to this prompt.',
$response->getResponse()));
}
}
$blocks[] = $block;
}
echo tsprintf('%B', phutil_glue($blocks, tsprintf("\n")));
return 0;
}
private function getSavedResponses($prompt_key) {
$config_key = ArcanistArcConfigurationEngineExtension::KEY_PROMPTS;
$config = $this->getConfig($config_key);
$responses = array();
foreach ($config as $response) {
if ($response->getPrompt() === $prompt_key) {
$responses[] = $response;
}
}
return $responses;
}
}

View file

@ -237,12 +237,13 @@ EOTEXT
$this->newPrompt('arc.land.confirm')
->setDescription(
pht(
'Confirms that the correct changes have been selected.')),
'Confirms that the correct changes have been selected to '.
'land.')),
$this->newPrompt('arc.land.implicit')
->setDescription(
pht(
'Confirms that local commits which are not associated with '.
'a revision should land.')),
'a revision have been associated correctly and should land.')),
$this->newPrompt('arc.land.unauthored')
->setDescription(
pht(
@ -267,11 +268,11 @@ EOTEXT
$this->newPrompt('arc.land.failed-builds')
->setDescription(
pht(
'Confirms that revisions with failed builds.')),
'Confirms that revisions with failed builds should land.')),
$this->newPrompt('arc.land.ongoing-builds')
->setDescription(
pht(
'Confirms that revisions with ongoing builds.')),
'Confirms that revisions with ongoing builds should land.')),
);
}