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:
parent
f3f31155b7
commit
c5192bde34
10 changed files with 377 additions and 48 deletions
|
@ -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',
|
||||
|
|
|
@ -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())
|
||||
|
|
51
src/config/option/ArcanistPromptsConfigOption.php
Normal file
51
src/config/option/ArcanistPromptsConfigOption.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
59
src/toolset/ArcanistPromptResponse.php
Normal file
59
src/toolset/ArcanistPromptResponse.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.')),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue