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 @@ -95,6 +95,7 @@ 'ArcanistClosureLinterTestCase' => 'lint/linter/__tests__/ArcanistClosureLinterTestCase.php', 'ArcanistCoffeeLintLinter' => 'lint/linter/ArcanistCoffeeLintLinter.php', 'ArcanistCoffeeLintLinterTestCase' => 'lint/linter/__tests__/ArcanistCoffeeLintLinterTestCase.php', + 'ArcanistCommand' => 'toolset/command/ArcanistCommand.php', 'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php', 'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php', 'ArcanistCommentSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php', @@ -1036,6 +1037,7 @@ 'ArcanistClosureLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistCoffeeLintLinter' => 'ArcanistExternalLinter', 'ArcanistCoffeeLintLinterTestCase' => 'ArcanistExternalLinterTestCase', + 'ArcanistCommand' => 'Phobject', 'ArcanistCommentRemover' => 'Phobject', 'ArcanistCommentRemoverTestCase' => 'PhutilTestCase', 'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1399,7 +1401,7 @@ 'ArcanistUnnecessarySymbolAliasXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUnsafeDynamicStringXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistUnsafeDynamicStringXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistUpgradeWorkflow' => 'ArcanistWorkflow', + 'ArcanistUpgradeWorkflow' => 'ArcanistArcWorkflow', 'ArcanistUploadWorkflow' => 'ArcanistWorkflow', 'ArcanistUsageException' => 'Exception', 'ArcanistUseStatementNamespacePrefixXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -33,7 +33,6 @@ private $stdoutPos = 0; private $stderrPos = 0; - private $command = null; private $readBufferSize; private $stdoutSizeLimit = PHP_INT_MAX; @@ -55,42 +54,13 @@ 2 => array('pipe', 'w'), // stderr ); - -/* -( Creating ExecFutures )----------------------------------------------- */ - - - /** - * Create a new ExecFuture. - * - * $future = new ExecFuture('wc -l %s', $file_path); - * - * @param string `sprintf()`-style command string which will be passed - * through @{function:csprintf} with the rest of the arguments. - * @param ... Zero or more additional arguments for @{function:csprintf}. - * @return ExecFuture ExecFuture for running the specified command. - * @task create - */ - public function __construct($command) { - $argv = func_get_args(); - $this->command = call_user_func_array('csprintf', $argv); + protected function didConstruct() { $this->stdin = new PhutilRope(); } - /* -( Command Information )------------------------------------------------ */ - /** - * Retrieve the raw command to be executed. - * - * @return string Raw command. - * @task info - */ - public function getCommand() { - return $this->command; - } - - /** * Retrieve the byte limit for the stderr buffer. * @@ -349,7 +319,7 @@ public function resolvex($timeout = null) { list($err, $stdout, $stderr) = $this->resolve($timeout); if ($err) { - $cmd = $this->command; + $cmd = $this->getCommand(); if ($this->getWasKilledByTimeout()) { // NOTE: The timeout can be a float and PhutilNumber only handles @@ -385,7 +355,7 @@ public function resolveJSON($timeout = null) { list($stdout, $stderr) = $this->resolvex($timeout); if (strlen($stderr)) { - $cmd = $this->command; + $cmd = $this->getCommand(); throw new CommandException( pht( "JSON command '%s' emitted text to stderr when none was expected: %d", @@ -399,7 +369,7 @@ try { return phutil_json_decode($stdout); } catch (PhutilJSONParserException $ex) { - $cmd = $this->command; + $cmd = $this->getCommand(); throw new CommandException( pht( "JSON command '%s' did not produce a valid JSON object on stdout: %s", @@ -579,7 +549,7 @@ $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'exec', - 'command' => (string)$this->command, + 'command' => phutil_string_cast($this->getCommand()), )); } @@ -588,10 +558,8 @@ $this->start = microtime(true); } - $unmasked_command = $this->command; - if ($unmasked_command instanceof PhutilCommandString) { - $unmasked_command = $unmasked_command->getUnmaskedString(); - } + $unmasked_command = $this->getCommand(); + $unmasked_command = $unmasked_command->getUnmaskedString(); $pipes = array(); @@ -674,7 +642,7 @@ pht( 'Call to "proc_open()" to open a subprocess failed: %s', $err), - $this->command, + $this->getCommand(), 1, '', ''); diff --git a/src/future/exec/PhutilExecPassthru.php b/src/future/exec/PhutilExecPassthru.php --- a/src/future/exec/PhutilExecPassthru.php +++ b/src/future/exec/PhutilExecPassthru.php @@ -20,30 +20,12 @@ */ final class PhutilExecPassthru extends PhutilExecutableFuture { - - private $command; private $passthruResult; /* -( Executing Passthru Commands )---------------------------------------- */ - /** - * Build a new passthru command. - * - * $exec = new PhutilExecPassthru('ls %s', $dir); - * - * @param string Command pattern. See @{function:csprintf}. - * @param ... Pattern arguments. - * - * @task command - */ - public function __construct($pattern /* , ... */) { - $args = func_get_args(); - $this->command = call_user_func_array('csprintf', $args); - } - - /** * Execute this command. * @@ -52,7 +34,7 @@ * @task command */ public function execute() { - $command = $this->command; + $command = $this->getCommand(); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( @@ -65,11 +47,7 @@ $spec = array(STDIN, STDOUT, STDERR); $pipes = array(); - if ($command instanceof PhutilCommandString) { - $unmasked_command = $command->getUnmaskedString(); - } else { - $unmasked_command = $command; - } + $unmasked_command = $command->getUnmaskedString(); if ($this->hasEnv()) { $env = $this->getEnv(); diff --git a/src/future/exec/PhutilExecutableFuture.php b/src/future/exec/PhutilExecutableFuture.php --- a/src/future/exec/PhutilExecutableFuture.php +++ b/src/future/exec/PhutilExecutableFuture.php @@ -5,10 +5,38 @@ */ abstract class PhutilExecutableFuture extends Future { - + private $command; private $env; private $cwd; + final public function __construct($pattern /* , ... */) { + $args = func_get_args(); + + if ($pattern instanceof PhutilCommandString) { + if (count($args) !== 1) { + throw new Exception( + pht( + 'Command (of class "%s") was constructed with a '. + '"PhutilCommandString", but also passed arguments. '. + 'When using a preprebuilt command, you must not pass '. + 'arguments.', + get_class($this))); + } + $this->command = $pattern; + } else { + $this->command = call_user_func_array('csprintf', $args); + } + + $this->didConstruct(); + } + + protected function didConstruct() { + return; + } + + final public function getCommand() { + return $this->command; + } /** * Set environmental variables for the command. diff --git a/src/toolset/command/ArcanistCommand.php b/src/toolset/command/ArcanistCommand.php new file mode 100644 --- /dev/null +++ b/src/toolset/command/ArcanistCommand.php @@ -0,0 +1,59 @@ +executableFuture = $future; + return $this; + } + + public function getExecutableFuture() { + return $this->executableFuture; + } + + public function setLogEngine(ArcanistLogEngine $log_engine) { + $this->logEngine = $log_engine; + return $this; + } + + public function getLogEngine() { + return $this->logEngine; + } + + public function execute() { + $log = $this->getLogEngine(); + $future = $this->getExecutableFuture(); + $command = $future->getCommand(); + + $log->writeNewline(); + + $log->writeStatus( + ' $ ', + tsprintf('**%s**', phutil_string_cast($command))); + + $log->writeNewline(); + + $err = $future->resolve(); + + $log->writeNewline(); + + if ($err) { + $log->writeError( + pht('ERROR'), + pht( + 'Command exited with error code %d.', + $err)); + + throw new CommandException( + pht('Command exited with nonzero error code.'), + $command, + $err, + '', + ''); + } + } +} diff --git a/src/workflow/ArcanistUpgradeWorkflow.php b/src/workflow/ArcanistUpgradeWorkflow.php --- a/src/workflow/ArcanistUpgradeWorkflow.php +++ b/src/workflow/ArcanistUpgradeWorkflow.php @@ -1,30 +1,31 @@ newWorkflowInformation() + ->setSynopsis(pht('Upgrade Arcanist to the latest version.')) + ->addExample(pht('**upgrade**')) + ->setHelp($help); } - public function getCommandHelp() { - return phutil_console_format(<<getLogEngine(); + $roots = array( 'arcanist' => dirname(phutil_get_library_root('arcanist')), ); @@ -32,41 +33,46 @@ $supported_branches = array( 'master', 'stable', - 'experimental', ); $supported_branches = array_fuse($supported_branches); - foreach ($roots as $lib => $root) { - echo phutil_console_format( - "%s\n", - pht('Upgrading %s...', $lib)); - - $working_copy = ArcanistWorkingCopyIdentity::newFromPath($root); - $configuration_manager = clone $this->getConfigurationManager(); - $configuration_manager->setWorkingCopyIdentity($working_copy); - $repository = ArcanistRepositoryAPI::newAPIFromConfigurationManager( - $configuration_manager); + foreach ($roots as $library => $root) { + $log->writeStatus( + pht('PREPARING'), + pht( + 'Preparing to upgrade "%s"...', + $library)); + + $is_git = false; + + $working_copy = ArcanistWorkingCopy::newFromWorkingDirectory($root); + if ($working_copy) { + $repository_api = $working_copy->newRepositoryAPI(); + if ($repository_api instanceof ArcanistGitAPI) { + $is_git = true; + } + } - if (!Filesystem::pathExists($repository->getMetadataPath())) { - throw new ArcanistUsageException( + if (!$is_git) { + throw new PhutilArgumentUsageException( pht( - "%s must be in its git working copy to be automatically upgraded. ". - "This copy of %s (in '%s') is not in a git working copy.", - $lib, - $lib, + 'The "arc upgrade" workflow uses "git pull" to upgrade '. + 'Arcanist, but the "arcanist/" directory (in "%s") is not a Git '. + 'working copy. You must leave "arcanist/" as a Git '. + 'working copy to use "arc upgrade".', $root)); } - $this->setRepositoryAPI($repository); - // NOTE: Don't use requireCleanWorkingCopy() here because it tries to // amend changes and generally move the workflow forward. We just want to // abort if there are local changes and make the user sort things out. - $uncommitted = $repository->getUncommittedStatus(); + $uncommitted = $repository_api->getUncommittedStatus(); if ($uncommitted) { $message = pht( - 'You have uncommitted changes in the working copy for this '. - 'library:'); + 'You have uncommitted changes in the working copy ("%s") for this '. + 'library ("%s"):', + $root, + $library); $list = id(new PhutilConsoleList()) ->setWrap(false) @@ -75,29 +81,45 @@ id(new PhutilConsoleBlock()) ->addParagraph($message) ->addList($list) + ->addParagraph( + pht( + 'Discard these changes before running "arc upgrade".')) ->draw(); - throw new ArcanistUsageException( - pht('`arc upgrade` can only upgrade clean working copies.')); + throw new PhutilArgumentUsageException( + pht('"arc upgrade" can only upgrade clean working copies.')); } - $branch_name = $repository->getBranchName(); + $branch_name = $repository_api->getBranchName(); if (!isset($supported_branches[$branch_name])) { - throw new ArcanistUsageException( + throw new PhutilArgumentUsageException( pht( 'Library "%s" (in "%s") is on branch "%s", but this branch is '. 'not supported for automatic upgrades. Supported branches are: '. '%s.', - $lib, + $library, $root, $branch_name, implode(', ', array_keys($supported_branches)))); } - chdir($root); + $log->writeStatus( + pht('UPGRADING'), + pht( + 'Upgrading "%s" (on branch "%s").', + $library, + $branch_name)); + + $command = csprintf( + 'git pull --rebase origin -- %R', + $branch_name); + + $future = (new PhutilExecPassthru($command)) + ->setCWD($root); try { - execx('git pull --rebase'); + $this->newCommand($future) + ->execute(); } catch (Exception $ex) { // If we failed, try to go back to the old state, then throw the // original exception. @@ -106,10 +128,10 @@ } } - echo phutil_console_format( - "**%s** %s\n", - pht('Updated!'), - pht('Your copy of arc is now up to date.')); + $log->writeSuccess( + pht('UPGRADED'), + pht('Your copy of Arcanist is now up to date.')); + return 0; } diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2243,4 +2243,10 @@ return false; } + final public function newCommand(PhutilExecutableFuture $future) { + return id(new ArcanistCommand()) + ->setLogEngine($this->getLogEngine()) + ->setExecutableFuture($future); + } + }