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 @@ -396,6 +396,8 @@ '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 @@ 'ArcanistPlusOperatorOnStringsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistPrompt' => 'Phobject', + 'ArcanistPromptResponse' => 'Phobject', + 'ArcanistPromptsConfigOption' => 'ArcanistMultiSourceConfigOption', 'ArcanistPromptsWorkflow' => 'ArcanistWorkflow', 'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -6,6 +6,7 @@ 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 @@ 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()) diff --git a/src/config/option/ArcanistPromptsConfigOption.php b/src/config/option/ArcanistPromptsConfigOption.php new file mode 100644 --- /dev/null +++ b/src/config/option/ArcanistPromptsConfigOption.php @@ -0,0 +1,51 @@ +'; + } + + 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'); + } + +} 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 @@ -4,6 +4,7 @@ extends Phobject { const SCOPE_USER = 'user'; + const SCOPE_WORKING_COPY = 'working-copy'; abstract public function getSourceDisplayName(); abstract public function getAllKeys(); 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 @@ -7,4 +7,12 @@ return pht('Local Config File'); } + public function isWritableConfigurationSource() { + return true; + } + + public function getConfigurationSourceScope() { + return ArcanistConfigurationSource::SCOPE_WORKING_COPY; + } + } diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -884,7 +884,6 @@ $legacy[] = new ArcanistGetConfigWorkflow(); $legacy[] = new ArcanistSetConfigWorkflow(); $legacy[] = new ArcanistInstallCertificateWorkflow(); - $legacy[] = new ArcanistLandWorkflow(); $legacy[] = new ArcanistLintersWorkflow(); $legacy[] = new ArcanistLintWorkflow(); $legacy[] = new ArcanistListWorkflow(); diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php --- a/src/toolset/ArcanistPrompt.php +++ b/src/toolset/ArcanistPrompt.php @@ -70,8 +70,10 @@ $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 @@ echo "\n"; $result = null; + $is_saved = false; while (true) { - echo tsprintf( - '** %s ** %s %s ', - '>>>', - $query, - $options); - - while (true) { - $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 ($saved_response !== null) { + $is_saved = true; + + $response = $saved_response; + $saved_response = null; + } else { + echo tsprintf( + '** %s ** %s %s ', + '>>>', + $query, + $options); - $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; + } + + if (!strlen($response)) { + continue; } - $response .= $bytes; + 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 @@ $result = false; break; } + + if (phutil_utf8_strtolower($response) == '?') { + echo tsprintf( + "\n** %s ** **%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( + "** %s ** %s **<%s>**\n". + "** %s ** (%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( + "** %s ** %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); } } diff --git a/src/toolset/ArcanistPromptResponse.php b/src/toolset/ArcanistPromptResponse.php new file mode 100644 --- /dev/null +++ b/src/toolset/ArcanistPromptResponse.php @@ -0,0 +1,59 @@ + '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; + } + +} diff --git a/src/toolset/workflow/ArcanistPromptsWorkflow.php b/src/toolset/workflow/ArcanistPromptsWorkflow.php --- a/src/toolset/workflow/ArcanistPromptsWorkflow.php +++ b/src/toolset/workflow/ArcanistPromptsWorkflow.php @@ -1,6 +1,7 @@ + $ 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 @@ return 0; } + $prompts = msort($prompts, 'getKey'); + + $blocks = array(); foreach ($prompts as $prompt) { - echo tsprintf( - "**%s**\n", + $block = array(); + $block[] = tsprintf( + "** %s ** **%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( + " ** > ** %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; + } + } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -237,12 +237,13 @@ $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 @@ $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.')), ); }