Page MenuHomePhabricator

D19706.diff
No OneTemporary

D19706.diff

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
@@ -385,6 +385,8 @@
'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPregQuoteMisuseXHPASTLinterRule.php',
'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase.php',
'ArcanistProjectConfigurationSource' => 'config/source/ArcanistProjectConfigurationSource.php',
+ 'ArcanistPrompt' => 'toolset/ArcanistPrompt.php',
+ 'ArcanistPromptsWorkflow' => 'toolset/workflow/ArcanistPromptsWorkflow.php',
'ArcanistPublicPropertyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistPublicPropertyXHPASTLinterRule.php',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistPublicPropertyXHPASTLinterRuleTestCase.php',
'ArcanistPuppetLintLinter' => 'lint/linter/ArcanistPuppetLintLinter.php',
@@ -1491,6 +1493,8 @@
'ArcanistPregQuoteMisuseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPregQuoteMisuseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistProjectConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource',
+ 'ArcanistPrompt' => 'Phobject',
+ 'ArcanistPromptsWorkflow' => 'ArcanistWorkflow',
'ArcanistPublicPropertyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistPublicPropertyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistPuppetLintLinter' => 'ArcanistExternalLinter',
diff --git a/src/log/ArcanistLogEngine.php b/src/log/ArcanistLogEngine.php
--- a/src/log/ArcanistLogEngine.php
+++ b/src/log/ArcanistLogEngine.php
@@ -18,12 +18,19 @@
return new ArcanistLogMessage();
}
+ private function writeBytes($bytes) {
+ fprintf(STDERR, '%s', $bytes);
+ return $this;
+ }
+
+ public function writeNewline() {
+ return $this->writeBytes("\n");
+ }
+
public function writeMessage(ArcanistLogMessage $message) {
$color = $message->getColor();
- fprintf(
- STDERR,
- '%s',
+ $this->writeBytes(
tsprintf(
"**<bg:".$color."> %s </bg>** %s\n",
$message->getLabel(),
diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php
new file mode 100644
--- /dev/null
+++ b/src/toolset/ArcanistPrompt.php
@@ -0,0 +1,152 @@
+<?php
+
+final class ArcanistPrompt
+ extends Phobject {
+
+ private $key;
+ private $workflow;
+ private $description;
+ private $query;
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public function setWorkflow(ArcanistWorkflow $workflow) {
+ $this->workflow = $workflow;
+ return $this;
+ }
+
+ public function getWorkflow() {
+ return $this->workflow;
+ }
+
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function getDescription() {
+ return $this->description;
+ }
+
+ public function setQuery($query) {
+ $this->query = $query;
+ return $this;
+ }
+
+ public function getQuery() {
+ return $this->query;
+ }
+
+ public function execute() {
+ $workflow = $this->getWorkflow();
+ if ($workflow) {
+ $workflow_ok = $workflow->hasPrompt($this->getKey());
+ } else {
+ $workflow_ok = false;
+ }
+
+ if (!$workflow_ok) {
+ throw new Exception(
+ pht(
+ 'Prompt ("%s") is executing, but it is not properly bound to the '.
+ 'invoking workflow. You may have called "newPrompt()" to execute a '.
+ 'prompt instead of "getPrompt()". Use "newPrompt()" when defining '.
+ 'prompts and "getPrompt()" when executing them.',
+ $this->getKey()));
+ }
+
+ $query = $this->getQuery();
+ if (!strlen($query)) {
+ throw new Exception(
+ pht(
+ 'Prompt ("%s") has no query text!',
+ $this->getKey()));
+ }
+
+ $options = '[y/N]';
+ $default = 'N';
+
+ try {
+ phutil_console_require_tty();
+ } catch (PhutilConsoleStdinNotInteractiveException $ex) {
+ // TOOLSETS: Clean this up to provide more details to the user about how
+ // they can configure prompts to be answered.
+
+ // Throw after echoing the prompt so the user has some idea what happened.
+ echo $query."\n";
+ throw $ex;
+ }
+
+ // NOTE: We're making stdin nonblocking so that we can respond to signals
+ // immediately. If we don't, and you ^C during a prompt, the program does
+ // not handle the signal until fgets() returns.
+
+ $stdin = fopen('php://stdin', 'r');
+ if (!$stdin) {
+ throw new Exception(pht('Failed to open stdin for reading.'));
+ }
+
+ $ok = stream_set_blocking($stdin, false);
+ if (!$ok) {
+ throw new Exception(pht('Unable to set stdin nonblocking.'));
+ }
+
+ echo "\n";
+
+ $result = null;
+ while (true) {
+ echo tsprintf(
+ '**<bg:cyan> %s </bg>** %s %s ',
+ '>>>',
+ $query,
+ $options);
+
+ while (true) {
+ $read = array($stdin);
+ $write = array();
+ $except = array();
+
+ $ok = stream_select($read, $write, $except, 1);
+ if ($ok === false) {
+ throw new Exception(pht('stream_select() failed!'));
+ }
+
+ $response = fgets($stdin);
+ if (!strlen($response)) {
+ continue;
+ }
+
+ break;
+ }
+
+ $response = trim($response);
+ if (!strlen($response)) {
+ $response = $default;
+ }
+
+ if (phutil_utf8_strtolower($response) == 'y') {
+ $result = true;
+ break;
+ }
+
+ if (phutil_utf8_strtolower($response) == 'n') {
+ $result = false;
+ break;
+ }
+ }
+
+ if (!$result) {
+ throw new ArcanistUserAbortException();
+ }
+
+ }
+
+}
+
diff --git a/src/toolset/ArcanistWorkflow.php b/src/toolset/ArcanistWorkflow.php
--- a/src/toolset/ArcanistWorkflow.php
+++ b/src/toolset/ArcanistWorkflow.php
@@ -8,6 +8,7 @@
private $configurationEngine;
private $configurationSourceList;
private $conduitEngine;
+ private $promptMap;
/**
* Return the command used to invoke this workflow from the command like,
@@ -203,4 +204,63 @@
throw new PhutilMethodNotImplementedException();
}
+ protected function newPrompts() {
+ return array();
+ }
+
+ protected function newPrompt($key) {
+ return id(new ArcanistPrompt())
+ ->setWorkflow($this)
+ ->setKey($key);
+ }
+
+ public function hasPrompt($key) {
+ $map = $this->getPromptMap();
+ return isset($map[$key]);
+ }
+
+ public function getPromptMap() {
+ if ($this->promptMap === null) {
+ $prompts = $this->newPrompts();
+ assert_instances_of($prompts, 'ArcanistPrompt');
+
+ $map = array();
+ foreach ($prompts as $prompt) {
+ $key = $prompt->getKey();
+
+ if (isset($map[$key])) {
+ throw new Exception(
+ pht(
+ 'Workflow ("%s") generates two prompts with the same '.
+ 'key ("%s"). Each prompt a workflow generates must have a '.
+ 'unique key.',
+ get_class($this),
+ $key));
+ }
+
+ $map[$key] = $prompt;
+ }
+
+ $this->promptMap = $map;
+ }
+
+ return $this->promptMap;
+ }
+
+ protected function getPrompt($key) {
+ $map = $this->getPromptMap();
+
+ $prompt = idx($map, $key);
+ if (!$prompt) {
+ throw new Exception(
+ pht(
+ 'Workflow ("%s") is requesting a prompt ("%s") but it did not '.
+ 'generate any prompt with that name in "newPrompts()".',
+ get_class($this),
+ $key));
+ }
+
+ return clone $prompt;
+ }
+
}
diff --git a/src/toolset/workflow/ArcanistPromptsWorkflow.php b/src/toolset/workflow/ArcanistPromptsWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/toolset/workflow/ArcanistPromptsWorkflow.php
@@ -0,0 +1,80 @@
+<?php
+
+final class ArcanistPromptsWorkflow extends ArcanistWorkflow {
+
+ public function getWorkflowName() {
+ return 'prompts';
+ }
+
+ public function supportsToolset(ArcanistToolset $toolset) {
+ return true;
+ }
+
+ public function getWorkflowInformation() {
+ $help = pht(<<<EOTEXT
+Show information about prompts a workflow may execute and configure default
+responses.
+
+**Show Prompts**
+
+To show possible prompts a workflow may execute, run:
+
+ $ arc prompts <workflow>
+EOTEXT
+);
+
+ return $this->newWorkflowInformation()
+ ->addExample(pht('**prompts** __workflow__'))
+ ->setHelp($help);
+ }
+
+ public function getWorkflowArguments() {
+ return array(
+ $this->newWorkflowArgument('argv')
+ ->setWildcard(true),
+ );
+ }
+
+ public function runWorkflow() {
+ $argv = $this->getArgument('argv');
+
+ if (!$argv) {
+ throw new PhutilArgumentUsageException(
+ pht('Provide a workflow to list prompts for.'));
+ }
+
+ $runtime = $this->getRuntime();
+ $workflows = $runtime->getWorkflows();
+
+ $workflow_key = array_shift($argv);
+ $workflow = idx($workflows, $workflow_key);
+
+ if (!$workflow) {
+ throw new PhutilArgumentUsageException(
+ pht(
+ 'Workflow "%s" is unknown. Supported workflows are: %s.',
+ $workflow_key,
+ implode(', ', array_keys($workflows))));
+ }
+
+ $prompts = $workflow->getPromptMap();
+ if (!$prompts) {
+ echo tsprintf(
+ "%s\n",
+ pht('This workflow can not prompt.'));
+ return 0;
+ }
+
+ foreach ($prompts as $prompt) {
+ echo tsprintf(
+ "**%s**\n",
+ $prompt->getKey());
+ echo tsprintf(
+ "%s\n",
+ $prompt->getDescription());
+ }
+
+ return 0;
+ }
+
+}
diff --git a/src/toolset/workflow/ArcanistShellCompleteWorkflow.php b/src/toolset/workflow/ArcanistShellCompleteWorkflow.php
--- a/src/toolset/workflow/ArcanistShellCompleteWorkflow.php
+++ b/src/toolset/workflow/ArcanistShellCompleteWorkflow.php
@@ -155,6 +155,16 @@
$this->runAutocomplete();
}
+ protected function newPrompts() {
+ return array(
+ $this->newPrompt('arc.shell-complete.install')
+ ->setDescription(
+ pht(
+ 'Confirms writing to to "~/.profile" (or another similar file) '.
+ 'to install shell completion.')),
+ );
+ }
+
private function runInstall() {
$log = $this->getLogEngine();
@@ -281,11 +291,9 @@
}
}
- // TOOLSETS: Generalize prompting.
-
- if (!phutil_console_confirm($prompt, false)) {
- throw new PhutilArgumentUsageException(pht('Aborted.'));
- }
+ $this->getPrompt('arc.shell-complete.install')
+ ->setQuery($prompt)
+ ->execute();
Filesystem::writeFile($file_path, $new_data);
diff --git a/support/ArcanistRuntime.php b/support/ArcanistRuntime.php
--- a/support/ArcanistRuntime.php
+++ b/support/ArcanistRuntime.php
@@ -32,6 +32,8 @@
$log->writeError(pht('CONDUIT'), $ex->getMessage());
} catch (PhutilArgumentUsageException $ex) {
$log->writeError(pht('USAGE EXCEPTION'), $ex->getMessage());
+ } catch (ArcanistUserAbortException $ex) {
+ $log->writeError(pht('---'), $ex->getMessage());
}
return 1;
@@ -590,6 +592,11 @@
}
}
+ // It's common for users to ^C on prompts. Write a newline before writing
+ // a response to the interrupt so the behavior is a little cleaner. This
+ // also avoids lines that read "^C [ INTERRUPT ] ...".
+ $log->writeNewline();
+
if ($should_exit) {
$log->writeHint(
pht('INTERRUPT'),

File Metadata

Mime Type
text/plain
Expires
Mon, Nov 25, 9:39 AM (19 h, 28 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6785576
Default Alt Text
D19706.diff (11 KB)

Event Timeline