From 7378e2baad69173dbff70041987b6489b2281ea1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 2 Jun 2020 08:52:42 -0700 Subject: [PATCH 01/82] Remove special casing of "arc --version" Summary: See PHI1765. You can find version information with "arc version". Remove this undocumented special-case. Test Plan: - Ran `arc --version`, no longer got a workflow API exception. - Searched the documentation for references to `arc --version` (instead of `arc version`), found none. Differential Revision: https://secure.phabricator.com/D21306 --- src/configuration/ArcanistConfiguration.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/configuration/ArcanistConfiguration.php b/src/configuration/ArcanistConfiguration.php index d2e8ae5c..9662feec 100644 --- a/src/configuration/ArcanistConfiguration.php +++ b/src/configuration/ArcanistConfiguration.php @@ -26,9 +26,6 @@ class ArcanistConfiguration extends Phobject { // Special-case "arc --help" to behave like "arc help" instead of telling // you to type "arc help" without being helpful. $command = 'help'; - } else if ($command == '--version') { - // Special-case "arc --version" to behave like "arc version". - $command = 'version'; } $workflow = idx($this->buildAllWorkflows(), $command); From fc3974ed70c67de9c56dd86573bd8e900467938a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 09:40:18 -0700 Subject: [PATCH 02/82] Impose a HardpointEngine future parallelism limit Summary: Ref T13546. If we try to resolve several hundred hardpoint queries which execute subprocesses, we can currently hit system limits. For now, limit resolution to 32 simultaneous futures. In the future, this should switch to `FuturePool` and possibly become more nuanced. Test Plan: In a future change, ran `arc land --into-empty ...` to land thousands of commits. Before change, got a "proc_open()" error when launching too many simultaneous subprocesses. After change, this "worked". Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21307 --- src/hardpoint/ArcanistHardpointEngine.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hardpoint/ArcanistHardpointEngine.php b/src/hardpoint/ArcanistHardpointEngine.php index ad15bcd5..b77341cd 100644 --- a/src/hardpoint/ArcanistHardpointEngine.php +++ b/src/hardpoint/ArcanistHardpointEngine.php @@ -192,7 +192,8 @@ final class ArcanistHardpointEngine $wait_futures = $this->waitFutures; if ($wait_futures) { if (!$this->futureIterator) { - $iterator = new FutureIterator(array()); + $iterator = id(new FutureIterator(array())) + ->limit(32); foreach ($wait_futures as $wait_future) { $iterator->addFuture($wait_future); } From 0e8247400713812cdc661ea97479fb2404d0b7bd Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 09:40:43 -0700 Subject: [PATCH 03/82] Support appending arbitrary lines to DisplayRef output Summary: Ref T13546. Several substeps in the new "arc land" flow benefit from this. For example: - When prompting "land revisions you don't own?", it's used to show authors. - When prompting "land revisions in the wrong state?", it's used to show the current states. Test Plan: Ran future "arc land" workflows, got relevant contextual information via this mechanism. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21308 --- src/ref/ArcanistDisplayRef.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ref/ArcanistDisplayRef.php b/src/ref/ArcanistDisplayRef.php index c4e7a5d5..631b65b4 100644 --- a/src/ref/ArcanistDisplayRef.php +++ b/src/ref/ArcanistDisplayRef.php @@ -7,6 +7,7 @@ final class ArcanistDisplayRef private $ref; private $uri; + private $lines = array(); public function setRef(ArcanistRef $ref) { $this->ref = $ref; @@ -26,6 +27,11 @@ final class ArcanistDisplayRef return $this->uri; } + public function appendLine($line) { + $this->lines[] = $line; + return $this; + } + public function newTerminalString() { $ref = $this->getRef(); @@ -83,6 +89,10 @@ final class ArcanistDisplayRef $uri); } + foreach ($this->lines as $line) { + $output[] = tsprintf(" %s\n", $line); + } + return $output; } From 7c80a9006d2aaab53a47d3a34bd03052480aac03 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 09:41:00 -0700 Subject: [PATCH 04/82] Add a "%?" ("hint") conversion to "tsprintf()" Summary: Ref T13546. Future "arc land" workflows use this element to provide "hint" or "next step" suggestions to make error resolution easier. Test Plan: Ran future "arc land" workflows, saw tips like "use --revision to do such-and-such" or "add these files to .gitignore". Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21309 --- src/xsprintf/tsprintf.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/xsprintf/tsprintf.php b/src/xsprintf/tsprintf.php index aea1e7f4..7b8e8934 100644 --- a/src/xsprintf/tsprintf.php +++ b/src/xsprintf/tsprintf.php @@ -48,6 +48,11 @@ function xsprintf_terminal($userdata, &$pattern, &$pos, &$value, &$length) { $value = PhutilTerminalString::escapeStringValue($value, false); $type = 's'; break; + case '?': + $value = tsprintf('** ? ** %s', $value); + $value = PhutilTerminalString::escapeStringValue($value, false); + $type = 's'; + break; case 'd': $type = 'd'; break; From 6af46f289a145e02eb5f8a4d49c13805c1da6e96 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 09:41:58 -0700 Subject: [PATCH 05/82] Support short aliases and repeatable arguments in Arcanist Workflow arguments Summary: Ref T13546. Add support for short "-x" flags and repeatable "--say moo --say quack" flags. Test Plan: In future changes, used "arc diff -m" and "arc land --onto ... --onto ...". Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21310 --- src/toolset/ArcanistWorkflowArgument.php | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/toolset/ArcanistWorkflowArgument.php b/src/toolset/ArcanistWorkflowArgument.php index 9a936b33..d56b10fb 100644 --- a/src/toolset/ArcanistWorkflowArgument.php +++ b/src/toolset/ArcanistWorkflowArgument.php @@ -8,6 +8,8 @@ final class ArcanistWorkflowArgument private $wildcard; private $parameter; private $isPathArgument; + private $shortFlag; + private $repeatable; public function setKey($key) { $this->key = $key; @@ -27,6 +29,24 @@ final class ArcanistWorkflowArgument return $this->wildcard; } + public function setShortFlag($short_flag) { + $this->shortFlag = $short_flag; + return $this; + } + + public function getShortFlag() { + return $this->shortFlag; + } + + public function setRepeatable($repeatable) { + $this->repeatable = $repeatable; + return $this; + } + + public function getRepeatable() { + return $this->repeatable; + } + public function getPhutilSpecification() { $spec = array( 'name' => $this->getKey(), @@ -46,6 +66,16 @@ final class ArcanistWorkflowArgument $spec['help'] = $help; } + $short = $this->getShortFlag(); + if ($short !== null) { + $spec['short'] = $short; + } + + $repeatable = $this->getRepeatable(); + if ($repeatable !== null) { + $spec['repeat'] = $repeatable; + } + return $spec; } From ff3cea78ee3a44ba31ff95bf0dceb18242dc737e Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 14:30:07 -0700 Subject: [PATCH 06/82] Remove "--use-commit-message/-C" from "arc diff" Summary: Ref T13544. This flag was introduced in D1385 (2012) as part of a workflow which no longer exists. I can't recall anyone ever reporting an issue which involves its use and believe it is likely unused. It's not obvious to me why someone would use it in modern "arc". (The same goal can be accomplished with "--message-file ...", although this requires more steps.) Test Plan: Grepped for "use-commit-message" and "getCommitMessageFromCommit", ran "arc diff". Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21296 --- src/workflow/ArcanistDiffWorkflow.php | 39 --------------------------- 1 file changed, 39 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index 028adbe9..f0044625 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -78,10 +78,6 @@ EOTEXT return true; } - if ($this->getArgument('use-commit-message')) { - return true; - } - return false; } @@ -106,19 +102,6 @@ EOTEXT 'When creating a revision, read revision information '. 'from this file.'), ), - 'use-commit-message' => array( - 'supports' => array( - 'git', - // TODO: Support mercurial. - ), - 'short' => 'C', - 'param' => 'commit', - 'help' => pht('Read revision information from a specific commit.'), - 'conflicts' => array( - 'only' => null, - 'update' => null, - ), - ), 'edit' => array( 'supports' => array( 'git', @@ -332,7 +315,6 @@ EOTEXT 'git', ), 'conflicts' => array( - 'use-commit-message' => true, 'update' => true, 'only' => true, 'raw' => true, @@ -823,10 +805,6 @@ EOTEXT return false; } - if ($this->getArgument('use-commit-message')) { - return false; - } - if ($this->isRawDiffSource()) { return true; } @@ -1482,13 +1460,8 @@ EOTEXT $is_create = $this->getArgument('create'); $is_update = $this->getArgument('update'); $is_raw = $this->isRawDiffSource(); - $is_message = $this->getArgument('use-commit-message'); $is_verbatim = $this->getArgument('verbatim'); - if ($is_message) { - return $this->getCommitMessageFromCommit($is_message); - } - if ($is_verbatim) { return $this->getCommitMessageFromUser(); } @@ -1543,18 +1516,6 @@ EOTEXT } - /** - * @task message - */ - private function getCommitMessageFromCommit($commit) { - $text = $this->getRepositoryAPI()->getCommitMessage($commit); - $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text); - $message->pullDataFromConduit($this->getConduit()); - $this->validateCommitMessage($message); - return $message; - } - - /** * @task message */ From 76dc1549554b727b4e3b4ccc5861fff6ff399d2a Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 14:38:04 -0700 Subject: [PATCH 07/82] Remove lint and unit excuses and "--advice" and "--excuse" flags from "arc diff" Summary: Ref T13544. Long ago, "arc diff" started prompting the user to provide "excuses" when they submitted changes with failing lint or unit tests. At the time, "arc" was generally more heavy-handed and the review workflow had fewer points where it provided feedback about lint and test issues. As the workflow has evolved, there is now significantly more feedback (promotion behavior from Draft in Differential, warnings on "arc land", etc). These days, these prompts feel archaic and like they're just getting in the way. When lint/unit have Harbormaster-triggered components, this prompt is also too early (since Harbormaster tests may fail or raise lint messages later). A modern version of this would look more like putting revisions in some kind of locked state until authors explain issues. It's possible that's worth building, but I'd like to see more interest in it. I suspect this feature is largely just a "nag" feature these days with few benefits. Test Plan: Grepped for "advice", "excuse", "handleServerMessage", "sendMessage", "getSkipExcuse", "getErrorExcuse", got no hits. Generated this revision. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21297 --- src/workflow/ArcanistDiffWorkflow.php | 131 ++------------------------ 1 file changed, 8 insertions(+), 123 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index f0044625..fc203ff2 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -13,7 +13,6 @@ final class ArcanistDiffWorkflow extends ArcanistWorkflow { private $console; private $hasWarnedExternals = false; private $unresolvedLint; - private $excuses = array('lint' => null, 'unit' => null); private $testResults; private $diffID; private $revisionID; @@ -215,12 +214,6 @@ EOTEXT 'allow-untracked' => array( 'help' => pht('Skip checks for untracked files in the working copy.'), ), - 'excuse' => array( - 'param' => 'excuse', - 'help' => pht( - 'Provide a prepared in advance excuse for any lints/tests '. - 'shall they fail.'), - ), 'less-context' => array( 'help' => pht( "Normally, files are diffed with full context: the entire file is ". @@ -236,11 +229,6 @@ EOTEXT 'lint' => true, ), ), - 'advice' => array( - 'help' => pht( - 'Require excuse for lint advice in addition to lint warnings and '. - 'errors.'), - ), 'only-new' => array( 'param' => 'bool', 'help' => pht( @@ -418,8 +406,6 @@ EOTEXT $revision = $this->buildRevisionFromCommitMessage($commit_message); } - $server = $this->console->getServer(); - $server->setHandler(array($this, 'handleServerMessage')); $data = $this->runLintUnit(); $lint_result = $data['lintResult']; @@ -427,20 +413,6 @@ EOTEXT $unit_result = $data['unitResult']; $this->testResults = $data['testResults']; - if ($this->getArgument('nolint')) { - $this->excuses['lint'] = $this->getSkipExcuse( - pht('Provide explanation for skipping lint or press Enter to abort:'), - 'lint-excuses'); - } - - if ($this->getArgument('nounit')) { - $this->excuses['unit'] = $this->getSkipExcuse( - pht( - 'Provide explanation for skipping unit tests '. - 'or press Enter to abort:'), - 'unit-excuses'); - } - $changes = $this->generateChanges(); if (!$changes) { throw new ArcanistUsageException( @@ -1256,34 +1228,22 @@ EOTEXT switch ($lint_result) { case ArcanistLintWorkflow::RESULT_OKAY: - if ($this->getArgument('advice') && - $lint_workflow->getUnresolvedMessages()) { - $this->getErrorExcuse( - 'lint', - pht('Lint issued unresolved advice.'), - 'lint-excuses'); - } else { - $this->console->writeOut( - "** %s ** %s\n", - pht('LINT OKAY'), - pht('No lint problems.')); - } + $this->console->writeOut( + "** %s ** %s\n", + pht('LINT OKAY'), + pht('No lint problems.')); break; case ArcanistLintWorkflow::RESULT_WARNINGS: - $this->getErrorExcuse( - 'lint', - pht('Lint issued unresolved warnings.'), - 'lint-excuses'); + $this->console->writeOut( + "** %s ** %s\n", + pht('LINT MESSAGES'), + pht('Lint issued unresolved warnings.')); break; case ArcanistLintWorkflow::RESULT_ERRORS: $this->console->writeOut( "** %s ** %s\n", pht('LINT ERRORS'), pht('Lint raised errors!')); - $this->getErrorExcuse( - 'lint', - pht('Lint issued unresolved errors!'), - 'lint-excuses'); break; } @@ -1357,10 +1317,6 @@ EOTEXT "** %s ** %s\n", pht('UNIT ERRORS'), pht('Unit testing raised errors!')); - $this->getErrorExcuse( - 'unit', - pht('Unit test results include failures!'), - 'unit-excuses'); break; } @@ -1385,66 +1341,6 @@ EOTEXT return $this->testResults; } - private function getSkipExcuse($prompt, $history) { - $excuse = $this->getArgument('excuse'); - - if ($excuse === null) { - $history = $this->getRepositoryAPI()->getScratchFilePath($history); - $excuse = phutil_console_prompt($prompt, $history); - if ($excuse == '') { - throw new ArcanistUserAbortException(); - } - } - - return $excuse; - } - - private function getErrorExcuse($type, $prompt, $history) { - if ($this->getArgument('excuse')) { - $this->console->sendMessage(array( - 'type' => $type, - 'confirm' => $prompt.' '.pht('Ignore them?'), - )); - return; - } - - $history = $this->getRepositoryAPI()->getScratchFilePath($history); - - $prompt .= ' '. - pht('Provide explanation to continue or press Enter to abort.'); - $this->console->writeOut("\n\n%s", phutil_console_wrap($prompt)); - $this->console->sendMessage(array( - 'type' => $type, - 'prompt' => pht('Explanation:'), - 'history' => $history, - )); - } - - public function handleServerMessage(PhutilConsoleMessage $message) { - $data = $message->getData(); - - if ($this->getArgument('excuse')) { - try { - phutil_console_require_tty(); - } catch (PhutilConsoleStdinNotInteractiveException $ex) { - $this->excuses[$data['type']] = $this->getArgument('excuse'); - return null; - } - } - - $response = ''; - if (isset($data['prompt'])) { - $response = phutil_console_prompt($data['prompt'], idx($data, 'history')); - } else if (phutil_console_confirm($data['confirm'])) { - $response = $this->getArgument('excuse'); - } - if ($response == '') { - throw new ArcanistUserAbortException(); - } - $this->excuses[$data['type']] = $response; - return null; - } - /* -( Commit and Update Messages )----------------------------------------- */ @@ -2450,12 +2346,6 @@ EOTEXT * @task diffprop */ private function updateLintDiffProperty() { - if (strlen($this->excuses['lint'])) { - $this->updateDiffProperty( - 'arc:lint-excuse', - json_encode($this->excuses['lint'])); - } - if (!$this->hitAutotargets) { if ($this->unresolvedLint) { $this->updateDiffProperty( @@ -2474,11 +2364,6 @@ EOTEXT * @task diffprop */ private function updateUnitDiffProperty() { - if (strlen($this->excuses['unit'])) { - $this->updateDiffProperty('arc:unit-excuse', - json_encode($this->excuses['unit'])); - } - if (!$this->hitAutotargets) { if ($this->testResults) { $this->updateDiffProperty('arc:unit', json_encode($this->testResults)); From d0eb822e37d1df68e6af7e0e5ba9d7fa2793eaad Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 14:50:04 -0700 Subject: [PATCH 08/82] Remove "--lintall" and "--only-new" flags to "arc diff" Summary: Ref T13544. These flags change the behavior of the "arc lint" subprocess. I believe there is no reason to ever use "arc diff --lintall". If you want to find extra warnings to fix, you can use "arc lint --lintall" to express this intent. Use of "arc diff --only-new" almost certainly means your linters are raising messages at "error" severity which should instead be raised at "warning" severity. If you only care about fixing a particular type of error in changed code, it should be raised as a "warning". The correct remedy is to adjust the severity, not use "--only-new", which is a very broad, slow, complicated hammer. Test Plan: Searched for "lintall" and "only-new" in this workflow. These flags still exist in "arc lint", but may be changed in the future. Generated this change. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21298 --- src/workflow/ArcanistDiffWorkflow.php | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index fc203ff2..70ac1de8 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -122,8 +122,6 @@ EOTEXT 'less-context' => null, 'apply-patches' => pht('%s disables lint.', '--raw'), 'never-apply-patches' => pht('%s disables lint.', '--raw'), - 'advice' => pht('%s disables lint.', '--raw'), - 'lintall' => pht('%s disables lint.', '--raw'), 'create' => pht( '%s and %s both need stdin. Use %s.', @@ -148,8 +146,6 @@ EOTEXT 'less-context' => null, 'apply-patches' => pht('%s disables lint.', '--raw-command'), 'never-apply-patches' => pht('%s disables lint.', '--raw-command'), - 'advice' => pht('%s disables lint.', '--raw-command'), - 'lintall' => pht('%s disables lint.', '--raw-command'), ), ), 'create' => array( @@ -191,8 +187,6 @@ EOTEXT 'nolint' => array( 'help' => pht('Do not run lint.'), 'conflicts' => array( - 'lintall' => pht('%s suppresses lint.', '--nolint'), - 'advice' => pht('%s suppresses lint.', '--nolint'), 'apply-patches' => pht('%s suppresses lint.', '--nolint'), 'never-apply-patches' => pht('%s suppresses lint.', '--nolint'), ), @@ -222,21 +216,6 @@ EOTEXT "of lines, this may not work well. With this flag, a diff will ". "be created that has only a few lines of context."), ), - 'lintall' => array( - 'help' => pht( - 'Raise all lint warnings, not just those on lines you changed.'), - 'passthru' => array( - 'lint' => true, - ), - ), - 'only-new' => array( - 'param' => 'bool', - 'help' => pht( - 'Display only lint messages not present in the original code.'), - 'passthru' => array( - 'lint' => true, - ), - ), 'apply-patches' => array( 'help' => pht( 'Apply patches suggested by lint to the working copy without '. @@ -374,10 +353,6 @@ EOTEXT 'svn' => pht('Subversion does not support commit ranges.'), 'hg' => pht('Mercurial does not support %s yet.', '--head'), ), - 'conflicts' => array( - 'lintall' => pht('%s suppresses lint.', '--head'), - 'advice' => pht('%s suppresses lint.', '--head'), - ), ), ); From bb81172eb7fde9e0e20250407cc6602c937cad62 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 15:25:43 -0700 Subject: [PATCH 09/82] Remove "haveUncommittedChanges" property from "arc diff" Summary: Ref T13544. This property is private and has no writers. Test Plan: Grepped for symbol. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21299 --- src/workflow/ArcanistDiffWorkflow.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index 70ac1de8..2ec74a5b 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -16,7 +16,6 @@ final class ArcanistDiffWorkflow extends ArcanistWorkflow { private $testResults; private $diffID; private $revisionID; - private $haveUncommittedChanges = false; private $diffPropertyFutures = array(); private $commitMessageFromRevision; private $hitAutotargets; @@ -1132,10 +1131,6 @@ EOTEXT return false; } - if ($this->haveUncommittedChanges) { - return false; - } - if ($this->getArgument('no-amend')) { return false; } From 466de2d2e1ec81c6a51b88aacb355e9db05edb5d Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 15:29:12 -0700 Subject: [PATCH 10/82] Remove "--encoding" flag from "arc diff" Summary: Ref T13544. This flag is generally questionable, likely has no actual uses, and is slated for obsoletion and replacement elsewhere (see T13338). Test Plan: Grepped for "encoding", found no relevant hits. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21300 --- src/workflow/ArcanistDiffWorkflow.php | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index 2ec74a5b..2832a9e9 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -199,11 +199,6 @@ EOTEXT 'message' => pht('%s does not update any revision.', '--only'), ), ), - 'encoding' => array( - 'param' => 'encoding', - 'help' => pht( - 'Attempt to convert non UTF-8 hunks into specified encoding.'), - ), 'allow-untracked' => array( 'help' => pht('Skip checks for untracked files in the working copy.'), ), @@ -956,8 +951,6 @@ EOTEXT } } - $try_encoding = nonempty($this->getArgument('encoding'), null); - $utf8_problems = array(); foreach ($changes as $change) { foreach ($change->getHunks() as $hunk) { @@ -970,17 +963,15 @@ EOTEXT $is_binary = ArcanistDiffUtils::isHeuristicBinaryFile($corpus); if (!$is_binary) { - if (!$try_encoding) { - try { - $try_encoding = $this->getRepositoryEncoding(); - } catch (ConduitClientException $e) { - if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') { - echo phutil_console_wrap( - pht('Lookup of encoding in arcanist project failed: %s', - $e->getMessage())."\n"); - } else { - throw $e; - } + try { + $try_encoding = $this->getRepositoryEncoding(); + } catch (ConduitClientException $e) { + if ($e->getErrorCode() == 'ERR-BAD-ARCANIST-PROJECT') { + echo phutil_console_wrap( + pht('Lookup of encoding in arcanist project failed: %s', + $e->getMessage())."\n"); + } else { + throw $e; } } From 0a4a841f8f1d6201e551fe51a2fd1702b6eb99d0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 15:40:44 -0700 Subject: [PATCH 11/82] Remove the "--less-context" flag from "arc diff" Summary: Ref T13544. This is an obscure flag and almost never useful. You can accomplish the same goal, roughly, with "git diff | arc diff --raw -". See also PHI675. See also T13338. Test Plan: Grepped for "less-context", created this revision. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21301 --- src/workflow/ArcanistDiffWorkflow.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index 2832a9e9..cc0d7d60 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -118,7 +118,6 @@ EOTEXT 'many Arcanist/Phabricator features which depend on having access '. 'to the working copy.'), 'conflicts' => array( - 'less-context' => null, 'apply-patches' => pht('%s disables lint.', '--raw'), 'never-apply-patches' => pht('%s disables lint.', '--raw'), @@ -142,7 +141,6 @@ EOTEXT 'working copy. This disables many Arcanist/Phabricator features '. 'which depend on having access to the working copy.'), 'conflicts' => array( - 'less-context' => null, 'apply-patches' => pht('%s disables lint.', '--raw-command'), 'never-apply-patches' => pht('%s disables lint.', '--raw-command'), ), @@ -202,14 +200,6 @@ EOTEXT 'allow-untracked' => array( 'help' => pht('Skip checks for untracked files in the working copy.'), ), - 'less-context' => array( - 'help' => pht( - "Normally, files are diffed with full context: the entire file is ". - "sent to Differential so reviewers can 'show more' and see it. If ". - "you are making changes to very large files with tens of thousands ". - "of lines, this may not work well. With this flag, a diff will ". - "be created that has only a few lines of context."), - ), 'apply-patches' => array( 'help' => pht( 'Apply patches suggested by lint to the working copy without '. @@ -597,9 +587,6 @@ EOTEXT } $repository_api = $this->getRepositoryAPI(); - if ($this->getArgument('less-context')) { - $repository_api->setDiffLinesOfContext(3); - } $repository_api->setBaseCommitArgumentRules( $this->getArgument('base', '')); @@ -925,11 +912,6 @@ EOTEXT "Generally, source changes should not be this large.", $change->getCurrentPath(), new PhutilNumber($size)); - if (!$this->getArgument('less-context')) { - $byte_warning .= ' '.pht( - "If this file is a huge text file, try using the '%s' flag.", - '--less-context'); - } if ($repository_api instanceof ArcanistSubversionAPI) { throw new ArcanistUsageException( $byte_warning.' '. From 8b973bf4390cad2338618df1c691ba6289657e42 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 06:38:53 -0700 Subject: [PATCH 12/82] Alias newer "--library" to "--load-phutil-library" in legacy workflows Summary: See PHI1772. This is a quick patch to make some debugging/testing tasks easier until remaining workflows modernize. See T13490. Test Plan: Ran `scripts/arcanist.php --library ...`, got the same behavior as `scripts/arcanist.php --load-phutil-library ...`. Differential Revision: https://secure.phabricator.com/D21323 --- scripts/arcanist.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/arcanist.php b/scripts/arcanist.php index 76aacb55..af296a11 100755 --- a/scripts/arcanist.php +++ b/scripts/arcanist.php @@ -55,6 +55,12 @@ $base_args->parsePartial( 'help' => pht('Load a libphutil library.'), 'repeat' => true, ), + array( + 'name' => 'library', + 'param' => 'path', + 'help' => pht('Load a library (same as --load-phutil-library).'), + 'repeat' => true, + ), array( 'name' => 'arcrc-file', 'param' => 'filename', @@ -89,7 +95,9 @@ $force_conduit = $base_args->getArg('conduit-uri'); $force_token = $base_args->getArg('conduit-token'); $custom_arcrc = $base_args->getArg('arcrc-file'); $is_anonymous = $base_args->getArg('anonymous'); -$load = $base_args->getArg('load-phutil-library'); +$load = array_merge( + $base_args->getArg('load-phutil-library'), + $base_args->getArg('library')); $help = $base_args->getArg('help'); $args = array_values($base_args->getUnconsumedArgumentVector()); From c1a4bee4a1787f11292aa978ec42df68005cccc7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 09:42:50 -0700 Subject: [PATCH 13/82] Add "Author" and "Parent Revision" hardpoints to RevisionRefs Summary: Ref T13546. These are used by a future "arc land" workflow to support the "Land changes you don't own?" and "Land changes with open dependencies?" prompts. Test Plan: Ran a future "arc land" flow, hit both prompts. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21311 --- src/__phutil_library_map__.php | 4 + .../ArcanistRevisionAuthorHardpointQuery.php | 35 ++++++++ ...visionParentRevisionRefsHardpointQuery.php | 82 +++++++++++++++++++ src/ref/revision/ArcanistRevisionRef.php | 39 +++++++++ 4 files changed, 160 insertions(+) create mode 100644 src/ref/revision/ArcanistRevisionAuthorHardpointQuery.php create mode 100644 src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d4f29121..0fd75f9c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -400,7 +400,9 @@ phutil_register_library_map(array( 'ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase.php', 'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorXHPASTLinterRuleTestCase.php', + 'ArcanistRevisionAuthorHardpointQuery' => 'ref/revision/ArcanistRevisionAuthorHardpointQuery.php', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ref/revision/ArcanistRevisionCommitMessageHardpointQuery.php', + 'ArcanistRevisionParentRevisionRefsHardpointQuery' => 'ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php', 'ArcanistRevisionRef' => 'ref/revision/ArcanistRevisionRef.php', 'ArcanistRevisionRefSource' => 'ref/ArcanistRevisionRefSource.php', 'ArcanistRevisionSymbolRef' => 'ref/revision/ArcanistRevisionSymbolRef.php', @@ -1390,7 +1392,9 @@ phutil_register_library_map(array( 'ArcanistReusedIteratorReferenceXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistRevisionAuthorHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ArcanistRuntimeHardpointQuery', + 'ArcanistRevisionParentRevisionRefsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', diff --git a/src/ref/revision/ArcanistRevisionAuthorHardpointQuery.php b/src/ref/revision/ArcanistRevisionAuthorHardpointQuery.php new file mode 100644 index 00000000..df6e1261 --- /dev/null +++ b/src/ref/revision/ArcanistRevisionAuthorHardpointQuery.php @@ -0,0 +1,35 @@ + $ref) { + $symbols[$key] = id(new ArcanistUserSymbolRef()) + ->setSymbol($ref->getAuthorPHID()); + } + + yield $this->yieldRequests( + $symbols, + array( + ArcanistSymbolRef::HARDPOINT_OBJECT, + )); + + $results = mpull($symbols, 'getObject'); + + yield $this->yieldMap($results); + } + +} diff --git a/src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php b/src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php new file mode 100644 index 00000000..0fde2dd6 --- /dev/null +++ b/src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php @@ -0,0 +1,82 @@ + mpull($refs, 'getPHID'), + 'types' => array( + 'revision.parent', + ), + ); + + $data = array(); + while (true) { + $results = (yield $this->yieldConduit( + 'edge.search', + $parameters)); + + foreach ($results['data'] as $item) { + $data[] = $item; + } + + if ($results['cursor']['after'] === null) { + break; + } + + $parameters['after'] = $results['cursor']['after']; + } + + if (!$data) { + yield $this->yieldValue($refs, array()); + } + + $map = array(); + $symbols = array(); + foreach ($data as $edge) { + $src = $edge['sourcePHID']; + $dst = $edge['destinationPHID']; + + $map[$src][$dst] = $dst; + + $symbols[$dst] = id(new ArcanistRevisionSymbolRef()) + ->setSymbol($dst); + } + + yield $this->yieldRequests( + $symbols, + array( + ArcanistSymbolRef::HARDPOINT_OBJECT, + )); + + $objects = array(); + foreach ($symbols as $key => $symbol) { + $object = $symbol->getObject(); + if ($object) { + $objects[$key] = $object; + } + } + + $results = array_fill_keys(array_keys($refs), array()); + foreach ($refs as $ref_key => $ref) { + $revision_phid = $ref->getPHID(); + $parent_phids = idx($map, $revision_phid, array()); + $parent_refs = array_select_keys($objects, $parent_phids); + $results[$ref_key] = $parent_refs; + } + + yield $this->yieldMap($results); + } + +} diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index 96b05d45..4c0fd5f6 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -6,6 +6,8 @@ final class ArcanistRevisionRef ArcanistDisplayRefInterface { const HARDPOINT_COMMITMESSAGE = 'ref.revision.commitmessage'; + const HARDPOINT_AUTHORREF = 'ref.revision.authorRef'; + const HARDPOINT_PARENTREVISIONREFS = 'ref.revision.parentRevisionRefs'; private $parameters; private $sources = array(); @@ -15,8 +17,13 @@ final class ArcanistRevisionRef } protected function newHardpoints() { + $object_list = new ArcanistObjectListHardpoint(); return array( $this->newHardpoint(self::HARDPOINT_COMMITMESSAGE), + $this->newHardpoint(self::HARDPOINT_AUTHORREF), + $this->newTemplateHardpoint( + self::HARDPOINT_PARENTREVISIONREFS, + $object_list), ); } @@ -63,6 +70,30 @@ final class ArcanistRevisionRef return idxv($this->parameters, array('fields', 'status', 'name')); } + public function isStatusChangesPlanned() { + $status = $this->getStatus(); + return ($status === 'changes-planned'); + } + + public function isStatusAbandoned() { + $status = $this->getStatus(); + return ($status === 'abandoned'); + } + + public function isStatusClosed() { + $status = $this->getStatus(); + return ($status === 'closed'); + } + + public function isStatusAccepted() { + $status = $this->getStatus(); + return ($status === 'accepted'); + } + + public function getStatus() { + return idxv($this->parameters, array('fields', 'status', 'value')); + } + public function isClosed() { return idxv($this->parameters, array('fields', 'status', 'closed')); } @@ -114,6 +145,14 @@ final class ArcanistRevisionRef return $this->getHardpoint(self::HARDPOINT_COMMITMESSAGE); } + public function getAuthorRef() { + return $this->getHardpoint(self::HARDPOINT_AUTHORREF); + } + + public function getParentRevisionRefs() { + return $this->getHardpoint(self::HARDPOINT_PARENTREVISIONREFS); + } + public function getDisplayRefObjectName() { return $this->getMonogram(); } From de607e9fbc3f4f2414270709b684260eeaff46b2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 10:14:05 -0700 Subject: [PATCH 14/82] Add modern refs and hardpoints for buildables, builds, and build plans Summary: Ref T13546. Prepares "arc land" to use hardpoint queries to load build information. Test Plan: Ran `arc inspect --explore revision(1234)`, got a full related object tree including build information. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21312 --- src/__phutil_library_map__.php | 35 ++++- src/ref/ArcanistBuildPlanRef.php | 25 ---- src/ref/ArcanistBuildRef.php | 140 ------------------ .../ArcanistBuildBuildplanHardpointQuery.php | 44 ++++++ src/ref/build/ArcanistBuildRef.php | 106 +++++++++++++ src/ref/build/ArcanistBuildSymbolRef.php | 30 ++++ .../ArcanistBuildableBuildsHardpointQuery.php | 43 ++++++ src/ref/buildable/ArcanistBuildableRef.php | 63 ++++++++ .../buildable/ArcanistBuildableSymbolRef.php | 30 ++++ src/ref/buildplan/ArcanistBuildPlanRef.php | 47 ++++++ .../buildplan/ArcanistBuildPlanSymbolRef.php | 30 ++++ ...rcanistRevisionBuildableHardpointQuery.php | 60 ++++++++ ...RevisionParentRevisionsHardpointQuery.php} | 2 +- src/ref/revision/ArcanistRevisionRef.php | 10 ++ src/ref/simple/ArcanistSimpleSymbolRef.php | 9 +- 15 files changed, 501 insertions(+), 173 deletions(-) delete mode 100644 src/ref/ArcanistBuildPlanRef.php delete mode 100644 src/ref/ArcanistBuildRef.php create mode 100644 src/ref/build/ArcanistBuildBuildplanHardpointQuery.php create mode 100644 src/ref/build/ArcanistBuildRef.php create mode 100644 src/ref/build/ArcanistBuildSymbolRef.php create mode 100644 src/ref/buildable/ArcanistBuildableBuildsHardpointQuery.php create mode 100644 src/ref/buildable/ArcanistBuildableRef.php create mode 100644 src/ref/buildable/ArcanistBuildableSymbolRef.php create mode 100644 src/ref/buildplan/ArcanistBuildPlanRef.php create mode 100644 src/ref/buildplan/ArcanistBuildPlanSymbolRef.php create mode 100644 src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php rename src/ref/revision/{ArcanistRevisionParentRevisionRefsHardpointQuery.php => ArcanistRevisionParentRevisionsHardpointQuery.php} (96%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0fd75f9c..b6108cca 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -64,8 +64,14 @@ phutil_register_library_map(array( 'ArcanistBrowseURIHardpointQuery' => 'browse/query/ArcanistBrowseURIHardpointQuery.php', 'ArcanistBrowseURIRef' => 'browse/ref/ArcanistBrowseURIRef.php', 'ArcanistBrowseWorkflow' => 'browse/workflow/ArcanistBrowseWorkflow.php', - 'ArcanistBuildPlanRef' => 'ref/ArcanistBuildPlanRef.php', - 'ArcanistBuildRef' => 'ref/ArcanistBuildRef.php', + 'ArcanistBuildBuildplanHardpointQuery' => 'ref/build/ArcanistBuildBuildplanHardpointQuery.php', + 'ArcanistBuildPlanRef' => 'ref/buildplan/ArcanistBuildPlanRef.php', + 'ArcanistBuildPlanSymbolRef' => 'ref/buildplan/ArcanistBuildPlanSymbolRef.php', + 'ArcanistBuildRef' => 'ref/build/ArcanistBuildRef.php', + 'ArcanistBuildSymbolRef' => 'ref/build/ArcanistBuildSymbolRef.php', + 'ArcanistBuildableBuildsHardpointQuery' => 'ref/buildable/ArcanistBuildableBuildsHardpointQuery.php', + 'ArcanistBuildableRef' => 'ref/buildable/ArcanistBuildableRef.php', + 'ArcanistBuildableSymbolRef' => 'ref/buildable/ArcanistBuildableSymbolRef.php', 'ArcanistBundle' => 'parser/ArcanistBundle.php', 'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php', 'ArcanistCSSLintLinter' => 'lint/linter/ArcanistCSSLintLinter.php', @@ -401,8 +407,9 @@ phutil_register_library_map(array( 'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorXHPASTLinterRuleTestCase.php', 'ArcanistRevisionAuthorHardpointQuery' => 'ref/revision/ArcanistRevisionAuthorHardpointQuery.php', + 'ArcanistRevisionBuildableHardpointQuery' => 'ref/revision/ArcanistRevisionBuildableHardpointQuery.php', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ref/revision/ArcanistRevisionCommitMessageHardpointQuery.php', - 'ArcanistRevisionParentRevisionRefsHardpointQuery' => 'ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php', + 'ArcanistRevisionParentRevisionsHardpointQuery' => 'ref/revision/ArcanistRevisionParentRevisionsHardpointQuery.php', 'ArcanistRevisionRef' => 'ref/revision/ArcanistRevisionRef.php', 'ArcanistRevisionRefSource' => 'ref/ArcanistRevisionRefSource.php', 'ArcanistRevisionSymbolRef' => 'ref/revision/ArcanistRevisionSymbolRef.php', @@ -1048,8 +1055,23 @@ phutil_register_library_map(array( 'ArcanistBrowseURIHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBrowseURIRef' => 'ArcanistRef', 'ArcanistBrowseWorkflow' => 'ArcanistArcWorkflow', - 'ArcanistBuildPlanRef' => 'Phobject', - 'ArcanistBuildRef' => 'Phobject', + 'ArcanistBuildBuildplanHardpointQuery' => 'ArcanistRuntimeHardpointQuery', + 'ArcanistBuildPlanRef' => array( + 'ArcanistRef', + 'ArcanistDisplayRefInterface', + ), + 'ArcanistBuildPlanSymbolRef' => 'ArcanistSimpleSymbolRef', + 'ArcanistBuildRef' => array( + 'ArcanistRef', + 'ArcanistDisplayRefInterface', + ), + 'ArcanistBuildSymbolRef' => 'ArcanistSimpleSymbolRef', + 'ArcanistBuildableBuildsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', + 'ArcanistBuildableRef' => array( + 'ArcanistRef', + 'ArcanistDisplayRefInterface', + ), + 'ArcanistBuildableSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBundle' => 'Phobject', 'ArcanistBundleTestCase' => 'PhutilTestCase', 'ArcanistCSSLintLinter' => 'ArcanistExternalLinter', @@ -1393,8 +1415,9 @@ phutil_register_library_map(array( 'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistRevisionAuthorHardpointQuery' => 'ArcanistRuntimeHardpointQuery', + 'ArcanistRevisionBuildableHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ArcanistRuntimeHardpointQuery', - 'ArcanistRevisionParentRevisionRefsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', + 'ArcanistRevisionParentRevisionsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionRef' => array( 'ArcanistRef', 'ArcanistDisplayRefInterface', diff --git a/src/ref/ArcanistBuildPlanRef.php b/src/ref/ArcanistBuildPlanRef.php deleted file mode 100644 index bc4a66d6..00000000 --- a/src/ref/ArcanistBuildPlanRef.php +++ /dev/null @@ -1,25 +0,0 @@ -parameters = $data; - return $ref; - } - - public function getPHID() { - return $this->parameters['phid']; - } - - public function getBehavior($behavior_key, $default = null) { - return idxv( - $this->parameters, - array('fields', 'behaviors', $behavior_key, 'value'), - $default); - } - -} diff --git a/src/ref/ArcanistBuildRef.php b/src/ref/ArcanistBuildRef.php deleted file mode 100644 index e6ca3855..00000000 --- a/src/ref/ArcanistBuildRef.php +++ /dev/null @@ -1,140 +0,0 @@ -parameters = $data; - return $ref; - } - - private function getStatusMap() { - // The modern "harbormaster.build.search" API method returns this in the - // "fields" list; the older API method returns it at the root level. - if (isset($this->parameters['fields']['buildStatus'])) { - $status = $this->parameters['fields']['buildStatus']; - } else if (isset($this->parameters['buildStatus'])) { - $status = $this->parameters['buildStatus']; - } else { - $status = 'unknown'; - } - - // We may either have an array or a scalar here. The array comes from - // "harbormaster.build.search", or from "harbormaster.querybuilds" if - // the server is newer than August 2016. The scalar comes from older - // versions of that method. See PHI261. - if (is_array($status)) { - $map = $status; - } else { - $map = array( - 'value' => $status, - ); - } - - // If we don't have a name, try to fill one in. - if (!isset($map['name'])) { - $name_map = array( - 'inactive' => pht('Inactive'), - 'pending' => pht('Pending'), - 'building' => pht('Building'), - 'passed' => pht('Passed'), - 'failed' => pht('Failed'), - 'aborted' => pht('Aborted'), - 'error' => pht('Error'), - 'paused' => pht('Paused'), - 'deadlocked' => pht('Deadlocked'), - 'unknown' => pht('Unknown'), - ); - - $map['name'] = idx($name_map, $map['value'], $map['value']); - } - - // If we don't have an ANSI color code, try to fill one in. - if (!isset($map['color.ansi'])) { - $color_map = array( - 'failed' => 'red', - 'passed' => 'green', - ); - - $map['color.ansi'] = idx($color_map, $map['value'], 'yellow'); - } - - return $map; - } - - public function getID() { - return $this->parameters['id']; - } - - public function getPHID() { - return $this->parameters['phid']; - } - - public function getName() { - if (isset($this->parameters['fields']['name'])) { - return $this->parameters['fields']['name']; - } - - return $this->parameters['name']; - } - - public function getStatus() { - $map = $this->getStatusMap(); - return $map['value']; - } - - public function getStatusName() { - $map = $this->getStatusMap(); - return $map['name']; - } - - public function getStatusANSIColor() { - $map = $this->getStatusMap(); - return $map['color.ansi']; - } - - public function getObjectName() { - return pht('Build %d', $this->getID()); - } - - public function getBuildPlanPHID() { - return idxv($this->parameters, array('fields', 'buildPlanPHID')); - } - - public function isComplete() { - switch ($this->getStatus()) { - case 'passed': - case 'failed': - case 'aborted': - case 'error': - case 'deadlocked': - return true; - default: - return false; - } - } - - public function isPassed() { - return ($this->getStatus() === 'passed'); - } - - public function getStatusSortVector() { - $status = $this->getStatus(); - - // For now, just sort passed builds first. - if ($this->isPassed()) { - $status_class = 1; - } else { - $status_class = 2; - } - - return id(new PhutilSortVector()) - ->addInt($status_class) - ->addString($status); - } - - -} diff --git a/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php b/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php new file mode 100644 index 00000000..3491c210 --- /dev/null +++ b/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php @@ -0,0 +1,44 @@ +yieldConduitSearch( + 'harbormaster.buildplan.search', + array( + 'phids' => $plan_phids, + ))); + + $plan_refs = array(); + foreach ($plans as $plan) { + $plan_ref = ArcanistBuildPlanRef::newFromConduit($plan); + $plan_refs[] = $plan_ref; + } + $plan_refs = mpull($plan_refs, 'getPHID'); + + $results = array(); + foreach ($refs as $key => $build_ref) { + $plan_phid = $build_ref->getBuildPlanPHID(); + $plan = idx($plan_refs, $plan_phid); + $results[$key] = $plan; + } + + yield $this->yieldMap($results); + } + +} diff --git a/src/ref/build/ArcanistBuildRef.php b/src/ref/build/ArcanistBuildRef.php new file mode 100644 index 00000000..f2ffcd90 --- /dev/null +++ b/src/ref/build/ArcanistBuildRef.php @@ -0,0 +1,106 @@ +newHardpoint(self::HARDPOINT_BUILDPLANREF), + ); + } + + public function getRefDisplayName() { + return $this->getDisplayRefObjectName(); + } + + public static function newFromConduit(array $parameters) { + $ref = new self(); + $ref->parameters = $parameters; + return $ref; + } + + public function getID() { + return idx($this->parameters, 'id'); + } + + public function getPHID() { + return idx($this->parameters, 'phid'); + } + + public function getName() { + return idxv($this->parameters, array('fields', 'name')); + } + + public function getDisplayRefObjectName() { + return pht('Build %d', $this->getID()); + } + + public function getDisplayRefTitle() { + return $this->getName(); + } + + public function getBuildPlanRef() { + return $this->getHardpoint(self::HARDPOINT_BUILDPLANREF); + } + + public function getBuildablePHID() { + return idxv($this->parameters, array('fields', 'buildablePHID')); + } + + public function getBuildPlanPHID() { + return idxv($this->parameters, array('fields', 'buildPlanPHID')); + } + + public function getStatus() { + return idxv($this->parameters, array('fields', 'buildStatus', 'value')); + } + + public function getStatusName() { + return idxv($this->parameters, array('fields', 'buildStatus', 'name')); + } + + public function getStatusANSIColor() { + return idxv( + $this->parameters, + array('fields', 'buildStatus', 'color.ansi')); + } + + public function isComplete() { + switch ($this->getStatus()) { + case 'passed': + case 'failed': + case 'aborted': + case 'error': + case 'deadlocked': + return true; + default: + return false; + } + } + + public function isPassed() { + return ($this->getStatus() === 'passed'); + } + + public function getStatusSortVector() { + $status = $this->getStatus(); + + // For now, just sort passed builds first. + if ($this->isPassed()) { + $status_class = 1; + } else { + $status_class = 2; + } + + return id(new PhutilSortVector()) + ->addInt($status_class) + ->addString($status); + } + +} diff --git a/src/ref/build/ArcanistBuildSymbolRef.php b/src/ref/build/ArcanistBuildSymbolRef.php new file mode 100644 index 00000000..06122764 --- /dev/null +++ b/src/ref/build/ArcanistBuildSymbolRef.php @@ -0,0 +1,30 @@ +getSymbol()); + } + + protected function getSimpleSymbolPHIDType() { + return 'HMBD'; + } + + public function getSimpleSymbolConduitSearchMethodName() { + return 'harbormaster.build.search'; + } + + public function getSimpleSymbolConduitSearchAttachments() { + return array(); + } + + public function getSimpleSymbolInspectFunctionName() { + return 'build'; + } + + public function newSimpleSymbolObjectRef() { + return new ArcanistBuildRef(); + } + +} diff --git a/src/ref/buildable/ArcanistBuildableBuildsHardpointQuery.php b/src/ref/buildable/ArcanistBuildableBuildsHardpointQuery.php new file mode 100644 index 00000000..11ad03d7 --- /dev/null +++ b/src/ref/buildable/ArcanistBuildableBuildsHardpointQuery.php @@ -0,0 +1,43 @@ +yieldConduitSearch( + 'harbormaster.build.search', + array( + 'buildables' => $buildable_phids, + ))); + + $build_refs = array(); + foreach ($builds as $build) { + $build_ref = ArcanistBuildRef::newFromConduit($build); + $build_refs[] = $build_ref; + } + + $build_refs = mgroup($build_refs, 'getBuildablePHID'); + + $results = array(); + foreach ($refs as $key => $buildable_ref) { + $buildable_phid = $buildable_ref->getPHID(); + $buildable_builds = idx($build_refs, $buildable_phid, array()); + $results[$key] = $buildable_builds; + } + + yield $this->yieldMap($results); + } + +} diff --git a/src/ref/buildable/ArcanistBuildableRef.php b/src/ref/buildable/ArcanistBuildableRef.php new file mode 100644 index 00000000..e9b376cc --- /dev/null +++ b/src/ref/buildable/ArcanistBuildableRef.php @@ -0,0 +1,63 @@ +newTemplateHardpoint( + self::HARDPOINT_BUILDREFS, + $object_list), + ); + } + + public function getRefDisplayName() { + return pht('Buildable "%s"', $this->getMonogram()); + } + + public static function newFromConduit(array $parameters) { + $ref = new self(); + $ref->parameters = $parameters; + return $ref; + } + + public function getID() { + return idx($this->parameters, 'id'); + } + + public function getPHID() { + return idx($this->parameters, 'phid'); + } + + public function getName() { + return idxv($this->parameters, array('fields', 'name')); + } + + public function getObjectPHID() { + return idxv($this->parameters, array('fields', 'objectPHID')); + } + + public function getMonogram() { + return 'B'.$this->getID(); + } + + public function getDisplayRefObjectName() { + return $this->getMonogram(); + } + + public function getDisplayRefTitle() { + return $this->getName(); + } + + public function getBuildRefs() { + return $this->getHardpoint(self::HARDPOINT_BUILDREFS); + } + +} diff --git a/src/ref/buildable/ArcanistBuildableSymbolRef.php b/src/ref/buildable/ArcanistBuildableSymbolRef.php new file mode 100644 index 00000000..e16df50f --- /dev/null +++ b/src/ref/buildable/ArcanistBuildableSymbolRef.php @@ -0,0 +1,30 @@ +getSymbol()); + } + + protected function getSimpleSymbolPHIDType() { + return 'HMBB'; + } + + public function getSimpleSymbolConduitSearchMethodName() { + return 'harbormaster.buildable.search'; + } + + public function getSimpleSymbolConduitSearchAttachments() { + return array(); + } + + public function getSimpleSymbolInspectFunctionName() { + return 'buildable'; + } + + public function newSimpleSymbolObjectRef() { + return new ArcanistBuildableRef(); + } + +} diff --git a/src/ref/buildplan/ArcanistBuildPlanRef.php b/src/ref/buildplan/ArcanistBuildPlanRef.php new file mode 100644 index 00000000..37570513 --- /dev/null +++ b/src/ref/buildplan/ArcanistBuildPlanRef.php @@ -0,0 +1,47 @@ +getDisplayRefObjectName(); + } + + public static function newFromConduit(array $parameters) { + $ref = new self(); + $ref->parameters = $parameters; + return $ref; + } + + public function getID() { + return idx($this->parameters, 'id'); + } + + public function getPHID() { + return idx($this->parameters, 'phid'); + } + + public function getName() { + return idxv($this->parameters, array('fields', 'name')); + } + + public function getDisplayRefObjectName() { + return pht('Build Plan %d', $this->getID()); + } + + public function getDisplayRefTitle() { + return $this->getName(); + } + + public function getBehavior($behavior_key, $default = null) { + return idxv( + $this->parameters, + array('fields', 'behaviors', $behavior_key, 'value'), + $default); + } + +} diff --git a/src/ref/buildplan/ArcanistBuildPlanSymbolRef.php b/src/ref/buildplan/ArcanistBuildPlanSymbolRef.php new file mode 100644 index 00000000..9d242268 --- /dev/null +++ b/src/ref/buildplan/ArcanistBuildPlanSymbolRef.php @@ -0,0 +1,30 @@ +getSymbol()); + } + + protected function getSimpleSymbolPHIDType() { + return 'HMCP'; + } + + public function getSimpleSymbolConduitSearchMethodName() { + return 'harbormaster.buildplan.search'; + } + + public function getSimpleSymbolConduitSearchAttachments() { + return array(); + } + + public function getSimpleSymbolInspectFunctionName() { + return 'buildplan'; + } + + public function newSimpleSymbolObjectRef() { + return new ArcanistBuildPlanRef(); + } + +} diff --git a/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php new file mode 100644 index 00000000..df23f01a --- /dev/null +++ b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php @@ -0,0 +1,60 @@ + $revision_ref) { + $diff_phid = $revision_ref->getDiffPHID(); + if ($diff_phid) { + $diff_map[$key] = $diff_phid; + } + } + + if (!$diff_map) { + yield $this->yieldValue($refs, null); + } + + $buildables = (yield $this->yieldConduitSearch( + 'harbormaster.buildable.search', + array( + 'objectPHIDs' => $diff_map, + 'manual' => false, + ))); + + $buildable_refs = array(); + foreach ($buildables as $buildable) { + $buildable_ref = ArcanistBuildableRef::newFromConduit($buildable); + $object_phid = $buildable_ref->getObjectPHID(); + $buildable_refs[$object_phid] = $buildable_ref; + } + + $results = array_fill_keys(array_keys($refs), null); + foreach ($refs as $key => $revision_ref) { + if (!isset($diff_map[$key])) { + continue; + } + + $diff_phid = $diff_map[$key]; + if (!isset($buildable_refs[$diff_phid])) { + continue; + } + + $results[$key] = $buildable_refs[$diff_phid]; + } + + yield $this->yieldMap($results); + } + +} diff --git a/src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php b/src/ref/revision/ArcanistRevisionParentRevisionsHardpointQuery.php similarity index 96% rename from src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php rename to src/ref/revision/ArcanistRevisionParentRevisionsHardpointQuery.php index 0fde2dd6..141e2d64 100644 --- a/src/ref/revision/ArcanistRevisionParentRevisionRefsHardpointQuery.php +++ b/src/ref/revision/ArcanistRevisionParentRevisionsHardpointQuery.php @@ -1,6 +1,6 @@ newHardpoint(self::HARDPOINT_COMMITMESSAGE), $this->newHardpoint(self::HARDPOINT_AUTHORREF), + $this->newHardpoint(self::HARDPOINT_BUILDABLEREF), $this->newTemplateHardpoint( self::HARDPOINT_PARENTREVISIONREFS, $object_list), @@ -124,6 +126,10 @@ final class ArcanistRevisionRef return idx($this->parameters, 'phid'); } + public function getDiffPHID() { + return idxv($this->parameters, array('fields', 'diffPHID')); + } + public function getName() { return idxv($this->parameters, array('fields', 'title')); } @@ -153,6 +159,10 @@ final class ArcanistRevisionRef return $this->getHardpoint(self::HARDPOINT_PARENTREVISIONREFS); } + public function getBuildableRef() { + return $this->getHardpoint(self::HARDPOINT_BUILDABLEREF); + } + public function getDisplayRefObjectName() { return $this->getMonogram(); } diff --git a/src/ref/simple/ArcanistSimpleSymbolRef.php b/src/ref/simple/ArcanistSimpleSymbolRef.php index 05f01238..d6b3142f 100644 --- a/src/ref/simple/ArcanistSimpleSymbolRef.php +++ b/src/ref/simple/ArcanistSimpleSymbolRef.php @@ -22,6 +22,10 @@ abstract class ArcanistSimpleSymbolRef $matches = null; $prefix_pattern = $this->getSimpleSymbolPrefixPattern(); + if ($prefix_pattern === null) { + $prefix_pattern = ''; + } + $id_pattern = '(^'.$prefix_pattern.'([1-9]\d*)\z)'; $is_id = preg_match($id_pattern, $symbol, $matches); @@ -46,7 +50,10 @@ abstract class ArcanistSimpleSymbolRef $symbol)); } - abstract protected function getSimpleSymbolPrefixPattern(); + protected function getSimpleSymbolPrefixPattern() { + return null; + } + abstract protected function getSimpleSymbolPHIDType(); abstract public function getSimpleSymbolConduitSearchMethodName(); abstract public function getSimpleSymbolInspectFunctionName(); From 7ac3b791b05a854930b632b51ca85f43a73b21b4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 17:27:02 -0700 Subject: [PATCH 15/82] Provide modern config options for "arc land" configuration Summary: Ref T13546. Adds modern support for "arc.land.onto", "arc.land.onto-remote", and "history.immutable". Test Plan: Read configuration in a future change. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21313 --- src/__phutil_library_map__.php | 16 ++-- ...rcanistArcConfigurationEngineExtension.php | 74 ++++++++++--------- .../option/ArcanistAliasesConfigOption.php | 2 +- .../option/ArcanistBoolConfigOption.php | 35 +++++++++ .../option/ArcanistListConfigOption.php | 66 ++++++++++++----- .../ArcanistMultiSourceConfigOption.php | 32 ++++++++ ...p => ArcanistSingleSourceConfigOption.php} | 2 +- .../option/ArcanistStringConfigOption.php | 2 +- .../option/ArcanistStringListConfigOption.php | 20 +++++ 9 files changed, 186 insertions(+), 63 deletions(-) create mode 100644 src/config/option/ArcanistBoolConfigOption.php create mode 100644 src/config/option/ArcanistMultiSourceConfigOption.php rename src/config/option/{ArcanistScalarConfigOption.php => ArcanistSingleSourceConfigOption.php} (89%) create mode 100644 src/config/option/ArcanistStringListConfigOption.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b6108cca..064f07c1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -50,6 +50,7 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase.php', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'configuration/ArcanistBlindlyTrustHTTPEngineExtension.php', 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', + 'ArcanistBoolConfigOption' => 'config/option/ArcanistBoolConfigOption.php', 'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php', 'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php', @@ -328,6 +329,7 @@ phutil_register_library_map(array( 'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php', 'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php', + 'ArcanistMultiSourceConfigOption' => 'config/option/ArcanistMultiSourceConfigOption.php', 'ArcanistNamespaceFirstStatementXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNamespaceFirstStatementXHPASTLinterRule.php', 'ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase.php', 'ArcanistNamingConventionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistNamingConventionsXHPASTLinterRule.php', @@ -420,7 +422,6 @@ phutil_register_library_map(array( 'ArcanistRuntime' => 'runtime/ArcanistRuntime.php', 'ArcanistRuntimeConfigurationSource' => 'config/source/ArcanistRuntimeConfigurationSource.php', 'ArcanistRuntimeHardpointQuery' => 'toolset/query/ArcanistRuntimeHardpointQuery.php', - 'ArcanistScalarConfigOption' => 'config/option/ArcanistScalarConfigOption.php', 'ArcanistScalarHardpoint' => 'hardpoint/ArcanistScalarHardpoint.php', 'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSelfClassReferenceXHPASTLinterRule.php', @@ -437,6 +438,7 @@ phutil_register_library_map(array( 'ArcanistSimpleSymbolRef' => 'ref/simple/ArcanistSimpleSymbolRef.php', 'ArcanistSimpleSymbolRefInspector' => 'ref/simple/ArcanistSimpleSymbolRefInspector.php', 'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php', + 'ArcanistSingleSourceConfigOption' => 'config/option/ArcanistSingleSourceConfigOption.php', 'ArcanistSlownessXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistSlownessXHPASTLinterRule.php', 'ArcanistSlownessXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistSlownessXHPASTLinterRuleTestCase.php', 'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php', @@ -444,6 +446,7 @@ phutil_register_library_map(array( 'ArcanistStaticThisXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistStaticThisXHPASTLinterRule.php', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistStaticThisXHPASTLinterRuleTestCase.php', 'ArcanistStringConfigOption' => 'config/option/ArcanistStringConfigOption.php', + 'ArcanistStringListConfigOption' => 'config/option/ArcanistStringListConfigOption.php', 'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php', 'ArcanistSubversionWorkingCopy' => 'workingcopy/ArcanistSubversionWorkingCopy.php', 'ArcanistSummaryLintRenderer' => 'lint/renderer/ArcanistSummaryLintRenderer.php', @@ -1016,7 +1019,7 @@ phutil_register_library_map(array( 'ArcanistAliasFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAliasFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAliasWorkflow' => 'ArcanistWorkflow', - 'ArcanistAliasesConfigOption' => 'ArcanistListConfigOption', + 'ArcanistAliasesConfigOption' => 'ArcanistMultiSourceConfigOption', 'ArcanistAmendWorkflow' => 'ArcanistArcWorkflow', 'ArcanistAnoidWorkflow' => 'ArcanistArcWorkflow', 'ArcanistArcConfigurationEngineExtension' => 'ArcanistConfigurationEngineExtension', @@ -1041,6 +1044,7 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'PhutilHTTPEngineExtension', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureBaseWorkflow', + 'ArcanistBoolConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBranchRef' => 'ArcanistRef', @@ -1313,7 +1317,7 @@ phutil_register_library_map(array( 'ArcanistLintersWorkflow' => 'ArcanistWorkflow', 'ArcanistListAssignmentXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistListAssignmentXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistListConfigOption' => 'ArcanistConfigOption', + 'ArcanistListConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistListWorkflow' => 'ArcanistWorkflow', 'ArcanistLocalConfigurationSource' => 'ArcanistWorkingCopyConfigurationSource', 'ArcanistLogEngine' => 'Phobject', @@ -1333,6 +1337,7 @@ phutil_register_library_map(array( 'ArcanistMissingLinterException' => 'Exception', 'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistMultiSourceConfigOption' => 'ArcanistConfigOption', 'ArcanistNamespaceFirstStatementXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistNamespaceFirstStatementXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistNamingConventionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1430,7 +1435,6 @@ phutil_register_library_map(array( 'ArcanistRubyLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRuntimeConfigurationSource' => 'ArcanistDictionaryConfigurationSource', 'ArcanistRuntimeHardpointQuery' => 'ArcanistHardpointQuery', - 'ArcanistScalarConfigOption' => 'ArcanistConfigOption', 'ArcanistScalarHardpoint' => 'ArcanistHardpoint', 'ArcanistScriptAndRegexLinter' => 'ArcanistLinter', 'ArcanistSelfClassReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1447,13 +1451,15 @@ phutil_register_library_map(array( 'ArcanistSimpleSymbolRef' => 'ArcanistSymbolRef', 'ArcanistSimpleSymbolRefInspector' => 'ArcanistRefInspector', 'ArcanistSingleLintEngine' => 'ArcanistLintEngine', + 'ArcanistSingleSourceConfigOption' => 'ArcanistConfigOption', 'ArcanistSlownessXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSlownessXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistSpellingLinter' => 'ArcanistLinter', 'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistStaticThisXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistStaticThisXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistStringConfigOption' => 'ArcanistScalarConfigOption', + 'ArcanistStringConfigOption' => 'ArcanistSingleSourceConfigOption', + 'ArcanistStringListConfigOption' => 'ArcanistListConfigOption', 'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI', 'ArcanistSubversionWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistSummaryLintRenderer' => 'ArcanistLintRenderer', diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index a7219be8..e7f6d379 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -29,38 +29,6 @@ final class ArcanistArcConfigurationEngineExtension 'off of.'), 'example' => '"develop"', ), - 'arc.land.onto.default' => array( - 'type' => 'string', - 'help' => pht( - 'The name of the default branch to land changes onto when '. - '`%s` is run.', - 'arc land'), - 'example' => '"develop"', - ), - - 'arc.autostash' => array( - 'type' => 'bool', - 'help' => pht( - 'Whether %s should permit the automatic stashing of changes in the '. - 'working directory when requiring a clean working copy. This option '. - 'should only be used when users understand how to restore their '. - 'working directory from the local stash if an Arcanist operation '. - 'causes an unrecoverable error.', - 'arc'), - 'default' => false, - 'example' => 'false', - ), - - 'history.immutable' => array( - 'type' => 'bool', - 'legacy' => 'immutable_history', - 'help' => pht( - 'If true, %s will never change repository history (e.g., through '. - 'amending or rebasing). Defaults to true in Mercurial and false in '. - 'Git. This setting has no effect in Subversion.', - 'arc'), - 'example' => 'false', - ), 'editor' => array( 'type' => 'string', 'help' => pht( @@ -111,9 +79,9 @@ final class ArcanistArcConfigurationEngineExtension ->setHelp( pht( 'Associate the working copy with a specific Phabricator '. - 'repository. Normally, `arc` can figure this association out on '. - 'its own, but if your setup is unusual you can use this option '. - 'to tell it what the desired value is.')) + 'repository. Normally, Arcanist can figure this association '. + 'out on its own, but if your setup is unusual you can use '. + 'this option to tell it what the desired value is.')) ->setExamples( array( 'libexample', @@ -145,6 +113,42 @@ final class ArcanistArcConfigurationEngineExtension pht( 'Configured command aliases. Use the "alias" workflow to define '. 'aliases.')), + id(new ArcanistStringListConfigOption()) + ->setKey('arc.land.onto') + ->setDefaultValue(array()) + ->setSummary(pht('Default list of "onto" refs for "arc land".')) + ->setHelp( + pht( + 'Specifies the default behavior when "arc land" is run with '. + 'no "--onto" flag.')) + ->setExamples( + array( + '["master"]', + )), + id(new ArcanistStringConfigOption()) + ->setKey('arc.land.onto-remote') + ->setSummary(pht('Default list of "onto" remote for "arc land".')) + ->setHelp( + pht( + 'Specifies the default behavior when "arc land" is run with '. + 'no "--onto-remote" flag.')) + ->setExamples( + array( + 'origin', + )), + id(new ArcanistBoolConfigOption()) + ->setKey('history.immutable') + ->setSummary( + pht( + 'Configure use of history mutation operations like amends '. + 'and rebases.')) + ->setHelp( + pht( + 'If this option is set to "true", Arcanist will treat the '. + 'repository history as immutable and will never issue '. + 'commands which rewrite repository history (like amends or '. + 'rebases). This option defaults to "true" in Mercurial, '. + '"false" in Git, and has no effect in Subversion.')), ); } diff --git a/src/config/option/ArcanistAliasesConfigOption.php b/src/config/option/ArcanistAliasesConfigOption.php index 536650dc..6859ebde 100644 --- a/src/config/option/ArcanistAliasesConfigOption.php +++ b/src/config/option/ArcanistAliasesConfigOption.php @@ -1,7 +1,7 @@ '; diff --git a/src/config/option/ArcanistBoolConfigOption.php b/src/config/option/ArcanistBoolConfigOption.php new file mode 100644 index 00000000..74f18a78 --- /dev/null +++ b/src/config/option/ArcanistBoolConfigOption.php @@ -0,0 +1,35 @@ +getConfigurationSource(); - $storage_value = $this->getStorageValueFromSourceValue($source_value); - - $items = $this->getValueFromStorageValue($storage_value); - foreach ($items as $item) { - $result_list[] = new ArcanistConfigurationSourceValue( - $source, - $item); - } + final public function getStorageValueFromStringValue($value) { + try { + $json_value = phutil_json_decode($value); + } catch (PhutilJSONParserException $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Value "%s" is not valid, specify a JSON list: %s', + $value, + $ex->getMessage())); } - $result_list = $this->didReadStorageValueList($result_list); + if (!is_array($json_value) || !phutil_is_natural_list($json_value)) { + throw new PhutilArgumentUsageException( + pht( + 'Value "%s" is not valid: expected a list, got "%s".', + $value, + phutil_describe_type($json_value))); + } - return $result_list; + foreach ($json_value as $idx => $item) { + $this->validateListItem($idx, $item); + } + + return $json_value; } - protected function didReadStorageValueList(array $list) { - assert_instances_of($list, 'ArcanistConfigurationSourceValue'); - return mpull($list, 'getValue'); + final 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!')); + } + + foreach ($value as $idx => $item) { + $this->validateListItem($idx, $item); + } + + return $value; } + public function getDisplayValueFromValue($value) { + return json_encode($value); + } + + public function getStorageValueFromValue($value) { + return $value; + } + + abstract protected function validateListItem($idx, $item); + } diff --git a/src/config/option/ArcanistMultiSourceConfigOption.php b/src/config/option/ArcanistMultiSourceConfigOption.php new file mode 100644 index 00000000..822b4ab3 --- /dev/null +++ b/src/config/option/ArcanistMultiSourceConfigOption.php @@ -0,0 +1,32 @@ +getConfigurationSource(); + $storage_value = $this->getStorageValueFromSourceValue($source_value); + + $items = $this->getValueFromStorageValue($storage_value); + foreach ($items as $item) { + $result_list[] = new ArcanistConfigurationSourceValue( + $source, + $item); + } + } + + $result_list = $this->didReadStorageValueList($result_list); + + return $result_list; + } + + protected function didReadStorageValueList(array $list) { + assert_instances_of($list, 'ArcanistConfigurationSourceValue'); + return mpull($list, 'getValue'); + } + +} diff --git a/src/config/option/ArcanistScalarConfigOption.php b/src/config/option/ArcanistSingleSourceConfigOption.php similarity index 89% rename from src/config/option/ArcanistScalarConfigOption.php rename to src/config/option/ArcanistSingleSourceConfigOption.php index 9ccda1e6..058bc5c6 100644 --- a/src/config/option/ArcanistScalarConfigOption.php +++ b/src/config/option/ArcanistSingleSourceConfigOption.php @@ -1,6 +1,6 @@ '; + } + + protected function validateListItem($idx, $item) { + if (!is_string($item)) { + throw new PhutilArgumentUsageException( + pht( + 'Expected a string (at index "%s"), found "%s".', + $idx, + phutil_describe_type($item))); + } + } + +} From 0da395ffe4c9edb4248efbf696d21d7b6db28c7a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 17:27:29 -0700 Subject: [PATCH 16/82] Introduce "RepositoryLocalState", a modern version of "requireCleanWorkingCopy()" Summary: Ref T13546. Introduces a more structured construct for saving and restoring local repository state. This is similar to old behavior, except that: - "arc.autostash" is no longer respected; - untracked changes are stashed; and - we do not offer to amend. Test Plan: In future changes, saved and restored various permutations of local state. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21314 --- src/__phutil_library_map__.php | 4 + .../state/ArcanistGitLocalState.php | 147 +++++++++++ .../state/ArcanistRepositoryLocalState.php | 245 ++++++++++++++++++ 3 files changed, 396 insertions(+) create mode 100644 src/repository/state/ArcanistGitLocalState.php create mode 100644 src/repository/state/ArcanistRepositoryLocalState.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 064f07c1..f923b0bd 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -219,6 +219,7 @@ phutil_register_library_map(array( 'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php', 'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php', + 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php', @@ -401,6 +402,7 @@ phutil_register_library_map(array( 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', + 'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php', 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php', @@ -1227,6 +1229,7 @@ phutil_register_library_map(array( 'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitLandEngine' => 'ArcanistLandEngine', + 'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistGitUpstreamPath' => 'Phobject', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', @@ -1412,6 +1415,7 @@ phutil_register_library_map(array( 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', + 'ArcanistRepositoryLocalState' => 'Phobject', 'ArcanistRepositoryRef' => 'ArcanistRef', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php new file mode 100644 index 00000000..d9487c1d --- /dev/null +++ b/src/repository/state/ArcanistGitLocalState.php @@ -0,0 +1,147 @@ +localRef; + } + + public function getLocalPath() { + return $this->localPath; + } + + protected function executeSaveLocalState() { + $api = $this->getRepositoryAPI(); + + $commit = $api->getWorkingCopyRevision(); + + list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD'); + $ref = trim($ref); + if ($ref === 'HEAD') { + $ref = null; + $where = pht( + 'Saving local state (at detached commit "%s").', + $this->getDisplayHash($commit)); + } else { + $where = pht( + 'Saving local state (on ref "%s" at commit "%s").', + $ref, + $this->getDisplayHash($commit)); + } + + $this->localRef = $ref; + $this->localCommit = $commit; + + if ($ref !== null) { + $this->localPath = $api->getPathToUpstream($ref); + } + + $log = $this->getWorkflow()->getLogEngine(); + $log->writeStatus(pht('SAVE STATE'), $where); + } + + protected function executeRestoreLocalState() { + $api = $this->getRepositoryAPI(); + + $log = $this->getWorkflow()->getLogEngine(); + + $ref = $this->localRef; + $commit = $this->localCommit; + + if ($ref !== null) { + $where = pht( + 'Restoring local state (to ref "%s" at commit "%s").', + $ref, + $this->getDisplayHash($commit)); + } else { + $where = pht( + 'Restoring local state (to detached commit "%s").', + $this->getDisplayHash($commit)); + } + + $log->writeStatus(pht('LOAD STATE'), $where); + + if ($ref !== null) { + $api->execxLocal('checkout -B %s %s --', $ref, $commit); + + // TODO: We save, but do not restore, the upstream configuration of + // this branch. + + } else { + $api->execxLocal('checkout %s --', $commit); + } + + $api->execxLocal('submodule update --init --recursive'); + } + + protected function executeDiscardLocalState() { + // We don't have anything to clean up in Git. + return; + } + + protected function canStashChanges() { + return true; + } + + protected function getIgnoreHints() { + return array( + pht( + 'To configure Git to ignore certain files in this working copy, '. + 'add the file paths to "%s".', + '.git/info/exclude'), + ); + } + + protected function saveStash() { + $api = $this->getRepositoryAPI(); + + // NOTE: We'd prefer to "git stash create" here, because using "push" + // and "pop" means we're affecting the stash list as a side effect. + + // However, under Git 2.21.1, "git stash create" exits with no output, + // no error, and no effect if the working copy contains only untracked + // files. For now, accept mutations to the stash list. + + $api->execxLocal('stash push --include-untracked --'); + + $log = $this->getWorkflow()->getLogEngine(); + $log->writeStatus( + pht('SAVE STASH'), + pht('Saved uncommitted changes from working copy.')); + + return true; + } + + protected function restoreStash($stash_ref) { + $api = $this->getRepositoryAPI(); + + $log = $this->getWorkflow()->getLogEngine(); + $log->writeStatus( + pht('LOAD STASH'), + pht('Restoring uncommitted changes to working copy.')); + + // NOTE: Under Git 2.21.1, "git stash apply" does not accept "--". + $api->execxLocal('stash apply'); + } + + protected function discardStash($stash_ref) { + $api = $this->getRepositoryAPI(); + + // NOTE: Under Git 2.21.1, "git stash drop" does not accept "--". + $api->execxLocal('stash drop'); + } + + private function getDisplayStashRef($stash_ref) { + return substr($stash_ref, 0, 12); + } + + private function getDisplayHash($hash) { + return substr($hash, 0, 12); + } + +} diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php new file mode 100644 index 00000000..eb5883fe --- /dev/null +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -0,0 +1,245 @@ +workflow = $workflow; + return $this; + } + + final public function getWorkflow() { + return $this->workflow; + } + + final public function setRepositoryAPI(ArcanistRepositoryAPI $api) { + $this->repositoryAPI = $api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + final public function saveLocalState() { + $api = $this->getRepositoryAPI(); + + $working_copy_display = tsprintf( + " %s: %s\n", + pht('Working Copy'), + $api->getPath()); + + $conflicts = $api->getMergeConflicts(); + if ($conflicts) { + echo tsprintf( + "\n%!\n%W\n\n%s\n", + pht('MERGE CONFLICTS'), + pht('You have merge conflicts in this working copy.'), + $working_copy_display); + + $lists = array(); + + $lists[] = $this->newDisplayFileList( + pht('Merge conflicts in working copy:'), + $conflicts); + + $this->printFileLists($lists); + + throw new PhutilArgumentUsageException( + pht( + 'Resolve merge conflicts before proceeding.')); + } + + $externals = $api->getDirtyExternalChanges(); + if ($externals) { + $message = pht( + '%s submodule(s) have uncommitted or untracked changes:', + new PhutilNumber(count($externals))); + + $prompt = pht( + 'Ignore the changes to these %s submodule(s) and continue?', + new PhutilNumber(count($externals))); + + $list = id(new PhutilConsoleList()) + ->setWrap(false) + ->addItems($externals); + + id(new PhutilConsoleBlock()) + ->addParagraph($message) + ->addList($list) + ->draw(); + + $ok = phutil_console_confirm($prompt, $default_no = false); + if (!$ok) { + throw new ArcanistUserAbortException(); + } + } + + $uncommitted = $api->getUncommittedChanges(); + $unstaged = $api->getUnstagedChanges(); + $untracked = $api->getUntrackedChanges(); + + // We already dealt with externals. + $unstaged = array_diff($unstaged, $externals); + + // We only want files which are purely uncommitted. + $uncommitted = array_diff($uncommitted, $unstaged); + $uncommitted = array_diff($uncommitted, $externals); + + if ($untracked || $unstaged || $uncommitted) { + echo tsprintf( + "\n%!\n%W\n\n%s\n", + pht('UNCOMMITTED CHANGES'), + pht('You have uncommitted changes in this working copy.'), + $working_copy_display); + + $lists = array(); + + $lists[] = $this->newDisplayFileList( + pht('Untracked changes in working copy:'), + $untracked); + + $lists[] = $this->newDisplayFileList( + pht('Unstaged changes in working copy:'), + $unstaged); + + $lists[] = $this->newDisplayFileList( + pht('Uncommitted changes in working copy:'), + $uncommitted); + + $this->printFileLists($lists); + + if ($untracked) { + $hints = $this->getIgnoreHints(); + foreach ($hints as $hint) { + echo tsprintf("%?\n", $hint); + } + } + + if ($this->canStashChanges()) { + + $query = pht('Stash these changes and continue?'); + + $this->getWorkflow() + ->getPrompt('arc.state.stash') + ->setQuery($query) + ->execute(); + + $stash_ref = $this->saveStash(); + + if ($stash_ref === null) { + throw new Exception( + pht( + 'Expected a non-null return from call to "%s->saveStash()".', + get_class($this))); + } + + $this->stashRef = $stash_ref; + } else { + throw new PhutilArgumentUsageException( + pht( + 'You can not continue with uncommitted changes. Commit or '. + 'discard them before proceeding.')); + } + } + + $this->executeSaveLocalState(); + $this->shouldRestore = true; + + return $this; + } + + final public function restoreLocalState() { + $this->shouldRestore = false; + + $this->executeRestoreLocalState(); + if ($this->stashRef !== null) { + $this->restoreStash($this->stashRef); + } + + return $this; + } + + final public function discardLocalState() { + $this->shouldRestore = false; + + $this->executeDiscardLocalState(); + if ($this->stashRef !== null) { + $this->restoreStash($this->stashRef); + $this->discardStash($this->stashRef); + $this->stashRef = null; + } + + return $this; + } + + final public function __destruct() { + if ($this->shouldRestore) { + $this->restoreLocalState(); + } + + $this->discardLocalState(); + } + + protected function canStashChanges() { + return false; + } + + protected function saveStash() { + throw new PhutilMethodNotImplementedException(); + } + + protected function restoreStash($ref) { + throw new PhutilMethodNotImplementedException(); + } + + protected function discardStash($ref) { + throw new PhutilMethodNotImplementedException(); + } + + abstract protected function executeSaveLocalState(); + abstract protected function executeRestoreLocalState(); + abstract protected function executeDiscardLocalState(); + + protected function getIgnoreHints() { + return array(); + } + + final protected function newDisplayFileList($title, array $files) { + if (!$files) { + return null; + } + + $items = array(); + $items[] = tsprintf("%s\n\n", $title); + foreach ($files as $file) { + $items[] = tsprintf( + " %s\n", + $file); + } + + return $items; + } + + final protected function printFileLists(array $lists) { + $lists = array_filter($lists); + + $last_key = last_key($lists); + foreach ($lists as $key => $list) { + foreach ($list as $item) { + echo tsprintf('%B', $item); + } + if ($key !== $last_key) { + echo tsprintf("\n\n"); + } + } + + echo tsprintf("\n"); + } + +} From 3ed81d35a23a4f905ebdaf7e43254edb952d753b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 13:08:58 -0700 Subject: [PATCH 17/82] When "arc" receives SIGWINCH or other signals during display of a prompt, recover Summary: Ref T13546. Resizing the terminal window to send SIGWINCH, or other signals, may interrupt "stream_select()" with an error which upgrades to a RuntimeException. When "stream_select()" fails, continue and test the stream itself. Test Plan: Waited for a prompt, resized the window. Before patch: SIGWINCH interruption. After patch: undisturbed prompt. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21317 --- src/toolset/ArcanistPrompt.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php index 8cd4a912..eab366d4 100644 --- a/src/toolset/ArcanistPrompt.php +++ b/src/toolset/ArcanistPrompt.php @@ -113,12 +113,32 @@ final class ArcanistPrompt $write = array(); $except = array(); - $ok = stream_select($read, $write, $except, 1); + $ok = @stream_select($read, $write, $except, 1); if ($ok === false) { - throw new Exception(pht('stream_select() failed!')); + // 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.')); + } + + if (!strlen($bytes)) { + break; + } + + $response .= $bytes; } - $response = fgets($stdin); if (!strlen($response)) { continue; } From 86471fc0feac8663cea0fa3e2013b81971f86583 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 May 2020 15:48:47 -0700 Subject: [PATCH 18/82] Remove "--ignore-unsound-tests" from "arc diff" Summary: Ref T13544. This flag only disables a warning and should be a prompt default, not a flag. Test Plan: Grepped for "ignore-unsound-tests", created this revision. Maniphest Tasks: T13544 Differential Revision: https://secure.phabricator.com/D21303 --- src/workflow/ArcanistDiffWorkflow.php | 31 +++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/workflow/ArcanistDiffWorkflow.php b/src/workflow/ArcanistDiffWorkflow.php index cc0d7d60..7997458d 100644 --- a/src/workflow/ArcanistDiffWorkflow.php +++ b/src/workflow/ArcanistDiffWorkflow.php @@ -295,9 +295,6 @@ EOTEXT 'skip-staging' => array( 'help' => pht('Do not copy changes to the staging area.'), ), - 'ignore-unsound-tests' => array( - 'help' => pht('Ignore unsound test failures without prompting.'), - ), 'base' => array( 'param' => 'rules', 'help' => pht('Additional rules for determining base revision.'), @@ -1238,22 +1235,20 @@ EOTEXT pht('No unit test failures.')); break; case ArcanistUnitWorkflow::RESULT_UNSOUND: - if ($this->getArgument('ignore-unsound-tests')) { - echo phutil_console_format( - "** %s ** %s\n", - pht('UNIT UNSOUND'), - pht( - 'Unit testing raised errors, but all '. - 'failing tests are unsound.')); - } else { - $continue = phutil_console_confirm( - pht( - 'Unit test results included failures, but all failing tests '. - 'are known to be unsound. Ignore unsound test failures?')); - if (!$continue) { - throw new ArcanistUserAbortException(); - } + $continue = phutil_console_confirm( + pht( + 'Unit test results included failures, but all failing tests '. + 'are known to be unsound. Ignore unsound test failures?')); + if (!$continue) { + throw new ArcanistUserAbortException(); } + + echo phutil_console_format( + "** %s ** %s\n", + pht('UNIT UNSOUND'), + pht( + 'Unit testing raised errors, but all '. + 'failing tests are unsound.')); break; case ArcanistUnitWorkflow::RESULT_FAIL: $this->console->writeOut( From 7d615a97e24054bf7cb3509fc4da7d3217dd8905 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 12:55:55 -0700 Subject: [PATCH 19/82] In "arc branch" output, sort branches updated in the same second by name Summary: Ref T13546. The new "arc land" workflow can rebase several branches per second. With branches like "feature1", "feature2", etc., this leads to out-of-order listing in "arc branch". When two branches would otherwise sort to the same position, sort them by name. Test Plan: Ran "arc branch" after a cascading rebase by "arc land", saw "land5", "land7", "land8", etc., instead of an arbitrary order. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21316 --- src/workflow/ArcanistFeatureBaseWorkflow.php | 31 ++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/workflow/ArcanistFeatureBaseWorkflow.php b/src/workflow/ArcanistFeatureBaseWorkflow.php index 94756c2a..b6224d34 100644 --- a/src/workflow/ArcanistFeatureBaseWorkflow.php +++ b/src/workflow/ArcanistFeatureBaseWorkflow.php @@ -201,7 +201,13 @@ EOHELP $epoch = $commit->getCommitEpoch(); $color = idx($color_map, $status, 'default'); - $ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch); + + $epoch_vector = id(new PhutilSortVector()) + ->addInt($epoch); + + $status_vector = id(new PhutilSortVector()) + ->addInt(idx($ssort_map, $status, 0)) + ->addInt($epoch); if ($revision) { $desc = $revision->getFullName(); @@ -216,9 +222,10 @@ EOHELP 'desc' => $desc, 'revision' => $revision ? $revision->getID() : null, 'color' => $color, - 'esort' => $epoch, 'epoch' => $epoch, - 'ssort' => $ssort, + + 'esort' => $epoch_vector, + 'ssort' => $status_vector, ); } @@ -230,11 +237,25 @@ EOHELP $len_name = max(array_map('strlen', ipull($out, 'name'))) + 2; $len_status = max(array_map('strlen', ipull($out, 'status'))) + 2; + // Sort the list in natural order first. When we apply a stable sort to + // the list below, branches which were last updated at the same time will + // retain this ordering. This allows "feature1", "feature2", etc., to + // display in the correct order if they were touched at the same second, + // which is common when "arc land" performs a cascading rebase. + + $name_map = ipull($out, 'name'); + natcasesort($name_map); + $out = array_select_keys($out, array_keys($name_map)); + if ($this->getArgument('by-status')) { - $out = isort($out, 'ssort'); + $vectors = ipull($out, 'ssort'); } else { - $out = isort($out, 'esort'); + $vectors = ipull($out, 'esort'); } + + $vectors = msortv($vectors, 'getSelf'); + $out = array_select_keys($out, array_keys($vectors)); + if ($this->getArgument('output') == 'json') { foreach ($out as &$feature) { unset($feature['color'], $feature['ssort'], $feature['esort']); From 68f28a1718887eb386d97b452a679a950d48a70e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 4 Jun 2020 17:29:16 -0700 Subject: [PATCH 20/82] Substantially modernize the "arc land" workflow Summary: Ref T13546. This has a lot of dangerously rough edges, but has managed to land at least one commit in each Git and Mercurial. Test Plan: - Landed one commit under ideal conditions in Git and Mercurial. - See followups. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21315 --- src/__phutil_library_map__.php | 24 +- ...rcanistArcConfigurationEngineExtension.php | 14 +- src/land/ArcanistGitLandEngine.php | 913 --------- src/land/ArcanistLandCommit.php | 149 ++ src/land/ArcanistLandCommitSet.php | 52 + src/land/ArcanistLandEngine.php | 182 -- src/land/ArcanistLandSymbol.php | 27 + src/land/ArcanistLandTarget.php | 41 + src/land/engine/ArcanistGitLandEngine.php | 1362 ++++++++++++ src/land/engine/ArcanistLandEngine.php | 1430 +++++++++++++ .../engine/ArcanistMercurialLandEngine.php | 603 ++++++ ...stMercurialCommitMessageHardpointQuery.php | 36 + ...urialWorkingCopyRevisionHardpointQuery.php | 76 + ...rcanistWorkflowMercurialHardpointQuery.php | 11 + ...rcanistRevisionBuildableHardpointQuery.php | 2 +- src/repository/api/ArcanistGitAPI.php | 14 + src/repository/api/ArcanistMercurialAPI.php | 64 +- src/repository/api/ArcanistRepositoryAPI.php | 15 + .../state/ArcanistMercurialLocalState.php | 60 + .../state/ArcanistRepositoryLocalState.php | 3 + src/workflow/ArcanistLandWorkflow.php | 1821 +++-------------- src/workflow/ArcanistWorkflow.php | 13 +- 22 files changed, 4226 insertions(+), 2686 deletions(-) delete mode 100644 src/land/ArcanistGitLandEngine.php create mode 100644 src/land/ArcanistLandCommit.php create mode 100644 src/land/ArcanistLandCommitSet.php delete mode 100644 src/land/ArcanistLandEngine.php create mode 100644 src/land/ArcanistLandSymbol.php create mode 100644 src/land/ArcanistLandTarget.php create mode 100644 src/land/engine/ArcanistGitLandEngine.php create mode 100644 src/land/engine/ArcanistLandEngine.php create mode 100644 src/land/engine/ArcanistMercurialLandEngine.php create mode 100644 src/query/ArcanistMercurialCommitMessageHardpointQuery.php create mode 100644 src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php create mode 100644 src/query/ArcanistWorkflowMercurialHardpointQuery.php create mode 100644 src/repository/state/ArcanistMercurialLocalState.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f923b0bd..31e4bd10 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -218,7 +218,7 @@ phutil_register_library_map(array( 'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php', 'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php', - 'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php', + 'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php', 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', @@ -288,7 +288,11 @@ phutil_register_library_map(array( 'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistKeywordCasingXHPASTLinterRuleTestCase.php', 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLambdaFuncFunctionXHPASTLinterRule.php', 'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase.php', - 'ArcanistLandEngine' => 'land/ArcanistLandEngine.php', + 'ArcanistLandCommit' => 'land/ArcanistLandCommit.php', + 'ArcanistLandCommitSet' => 'land/ArcanistLandCommitSet.php', + 'ArcanistLandEngine' => 'land/engine/ArcanistLandEngine.php', + 'ArcanistLandSymbol' => 'land/ArcanistLandSymbol.php', + 'ArcanistLandTarget' => 'land/ArcanistLandTarget.php', 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase.php', @@ -320,9 +324,13 @@ phutil_register_library_map(array( 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', + 'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php', + 'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php', + 'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', + 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', 'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php', 'ArcanistMessageRevisionHardpointQuery' => 'query/ArcanistMessageRevisionHardpointQuery.php', @@ -524,6 +532,7 @@ phutil_register_library_map(array( 'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php', 'ArcanistWorkflowGitHardpointQuery' => 'query/ArcanistWorkflowGitHardpointQuery.php', 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', + 'ArcanistWorkflowMercurialHardpointQuery' => 'query/ArcanistWorkflowMercurialHardpointQuery.php', 'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php', 'ArcanistWorkingCopyCommitHardpointQuery' => 'query/ArcanistWorkingCopyCommitHardpointQuery.php', 'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php', @@ -1298,8 +1307,12 @@ phutil_register_library_map(array( 'ArcanistKeywordCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLambdaFuncFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistLandCommit' => 'Phobject', + 'ArcanistLandCommitSet' => 'Phobject', 'ArcanistLandEngine' => 'Phobject', - 'ArcanistLandWorkflow' => 'ArcanistWorkflow', + 'ArcanistLandSymbol' => 'Phobject', + 'ArcanistLandTarget' => 'Phobject', + 'ArcanistLandWorkflow' => 'ArcanistArcWorkflow', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLesscLinter' => 'ArcanistExternalLinter', @@ -1330,9 +1343,13 @@ phutil_register_library_map(array( 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', + 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', + 'ArcanistMercurialLandEngine' => 'ArcanistLandEngine', + 'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', + 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', 'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistMessageRevisionHardpointQuery' => 'ArcanistRuntimeHardpointQuery', @@ -1544,6 +1561,7 @@ phutil_register_library_map(array( 'ArcanistWorkflowArgument' => 'Phobject', 'ArcanistWorkflowGitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkflowInformation' => 'Phobject', + 'ArcanistWorkflowMercurialHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopy' => 'Phobject', 'ArcanistWorkingCopyCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index e7f6d379..62e88c12 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -136,19 +136,15 @@ final class ArcanistArcConfigurationEngineExtension array( 'origin', )), - id(new ArcanistBoolConfigOption()) - ->setKey('history.immutable') + id(new ArcanistStringConfigOption()) + ->setKey('arc.land.strategy') ->setSummary( pht( - 'Configure use of history mutation operations like amends '. - 'and rebases.')) + 'Configure a default merge strategy for "arc land".')) ->setHelp( pht( - 'If this option is set to "true", Arcanist will treat the '. - 'repository history as immutable and will never issue '. - 'commands which rewrite repository history (like amends or '. - 'rebases). This option defaults to "true" in Mercurial, '. - '"false" in Git, and has no effect in Subversion.')), + 'Specifies the default behavior when "arc land" is run with '. + 'no "--strategy" flag.')), ); } diff --git a/src/land/ArcanistGitLandEngine.php b/src/land/ArcanistGitLandEngine.php deleted file mode 100644 index c0a7a870..00000000 --- a/src/land/ArcanistGitLandEngine.php +++ /dev/null @@ -1,913 +0,0 @@ -isGitPerforce = $is_git_perforce; - return $this; - } - - private function getIsGitPerforce() { - return $this->isGitPerforce; - } - - public function parseArguments() { - $api = $this->getRepositoryAPI(); - - $onto = $this->getEngineOnto(); - $this->setTargetOnto($onto); - - $remote = $this->getEngineRemote(); - - $is_pushable = $api->isPushableRemote($remote); - $is_perforce = $api->isPerforceRemote($remote); - - if (!$is_pushable && !$is_perforce) { - throw new PhutilArgumentUsageException( - pht( - 'No pushable remote "%s" exists. Use the "--remote" flag to choose '. - 'a valid, pushable remote to land changes onto.', - $remote)); - } - - if ($is_perforce) { - $this->setIsGitPerforce(true); - $this->writeWarn( - pht('P4 MODE'), - pht( - 'Operating in Git/Perforce mode after selecting a Perforce '. - 'remote.')); - - if (!$this->getShouldSquash()) { - throw new PhutilArgumentUsageException( - pht( - 'Perforce mode does not support the "merge" land strategy. '. - 'Use the "squash" land strategy when landing to a Perforce '. - 'remote (you can use "--squash" to select this strategy).')); - } - } - - $this->setTargetRemote($remote); - } - - public function execute() { - $this->verifySourceAndTargetExist(); - $this->fetchTarget(); - - $this->printLandingCommits(); - - if ($this->getShouldPreview()) { - $this->writeInfo( - pht('PREVIEW'), - pht('Completed preview of operation.')); - return; - } - - $this->saveLocalState(); - - try { - $this->identifyRevision(); - $this->updateWorkingCopy(); - - if ($this->getShouldHold()) { - $this->didHoldChanges(); - } else { - $this->pushChange(); - $this->reconcileLocalState(); - - $api = $this->getRepositoryAPI(); - $api->execxLocal('submodule update --init --recursive'); - - if ($this->getShouldKeep()) { - echo tsprintf( - "%s\n", - pht('Keeping local branch.')); - } else { - $this->destroyLocalBranch(); - } - - $this->writeOkay( - pht('DONE'), - pht('Landed changes.')); - } - - $this->restoreWhenDestroyed = false; - } catch (Exception $ex) { - $this->restoreLocalState(); - throw $ex; - } - } - - public function __destruct() { - if ($this->restoreWhenDestroyed) { - $this->writeWarn( - pht('INTERRUPTED!'), - pht('Restoring working copy to its original state.')); - - $this->restoreLocalState(); - } - } - - protected function getLandingCommits() { - $api = $this->getRepositoryAPI(); - - list($out) = $api->execxLocal( - 'log --oneline %s..%s --', - $this->getTargetFullRef(), - $this->sourceCommit); - - $out = trim($out); - - if (!strlen($out)) { - return array(); - } else { - return phutil_split_lines($out, false); - } - } - - private function identifyRevision() { - $api = $this->getRepositoryAPI(); - $api->execxLocal('checkout %s --', $this->getSourceRef()); - call_user_func($this->getBuildMessageCallback(), $this); - } - - private function verifySourceAndTargetExist() { - $api = $this->getRepositoryAPI(); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getTargetFullRef()); - - if ($err) { - $this->writeWarn( - pht('TARGET'), - pht( - 'No local ref exists for branch "%s" in remote "%s", attempting '. - 'fetch...', - $this->getTargetOnto(), - $this->getTargetRemote())); - - $api->execManualLocal( - 'fetch %s %s --', - $this->getTargetRemote(), - $this->getTargetOnto()); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getTargetFullRef()); - if ($err) { - throw new Exception( - pht( - 'Branch "%s" does not exist in remote "%s".', - $this->getTargetOnto(), - $this->getTargetRemote())); - } - - $this->writeInfo( - pht('FETCHED'), - pht( - 'Fetched branch "%s" from remote "%s".', - $this->getTargetOnto(), - $this->getTargetRemote())); - } - - list($err, $stdout) = $api->execManualLocal( - 'rev-parse --verify %s', - $this->getSourceRef()); - - if ($err) { - throw new Exception( - pht( - 'Branch "%s" does not exist in the local working copy.', - $this->getSourceRef())); - } - - $this->sourceCommit = trim($stdout); - } - - private function fetchTarget() { - $api = $this->getRepositoryAPI(); - - $ref = $this->getTargetFullRef(); - - // NOTE: Although this output isn't hugely useful, we need to passthru - // instead of using a subprocess here because `git fetch` may prompt the - // user to enter a password if they're fetching over HTTP with basic - // authentication. See T10314. - - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('P4 SYNC'), - pht('Synchronizing "%s" from Perforce...', $ref)); - - $sync_ref = sprintf( - 'refs/remotes/%s/%s', - $this->getTargetRemote(), - $this->getTargetOnto()); - - $err = $api->execPassthru( - 'p4 sync --silent --branch %R --', - $sync_ref); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Perforce sync failed! Fix the error and run "arc land" again.')); - } - } else { - $this->writeInfo( - pht('FETCH'), - pht('Fetching "%s"...', $ref)); - - $err = $api->execPassthru( - 'fetch --quiet -- %s %s', - $this->getTargetRemote(), - $this->getTargetOnto()); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Fetch failed! Fix the error and run "arc land" again.')); - } - } - } - - private function updateWorkingCopy() { - $api = $this->getRepositoryAPI(); - $source = $this->sourceCommit; - - $api->execxLocal( - 'checkout %s --', - $this->getTargetFullRef()); - - list($original_author, $original_date) = $this->getAuthorAndDate($source); - - try { - if ($this->getShouldSquash()) { - // NOTE: We're explicitly specifying "--ff" to override the presence - // of "merge.ff" options in user configuration. - - $api->execxLocal( - 'merge --no-stat --no-commit --ff --squash -- %s', - $source); - } else { - $api->execxLocal( - 'merge --no-stat --no-commit --no-ff -- %s', - $source); - } - } catch (Exception $ex) { - $api->execManualLocal('merge --abort'); - $api->execManualLocal('reset --hard HEAD --'); - - throw new Exception( - pht( - 'Local "%s" does not merge cleanly into "%s". Merge or rebase '. - 'local changes so they can merge cleanly.', - $this->getSourceRef(), - $this->getTargetFullRef())); - } - - // TODO: This could probably be cleaner by asking the API a question - // about working copy status instead of running a raw diff command. See - // discussion in T11435. - list($changes) = $api->execxLocal('diff --no-ext-diff HEAD --'); - $changes = trim($changes); - if (!strlen($changes)) { - throw new Exception( - pht( - 'Merging local "%s" into "%s" produces an empty diff. '. - 'This usually means these changes have already landed.', - $this->getSourceRef(), - $this->getTargetFullRef())); - } - - $api->execxLocal( - 'commit --author %s --date %s -F %s --', - $original_author, - $original_date, - $this->getCommitMessageFile()); - - $this->getWorkflow()->didCommitMerge(); - - list($stdout) = $api->execxLocal( - 'rev-parse --verify %s', - 'HEAD'); - $this->mergedRef = trim($stdout); - } - - private function pushChange() { - $api = $this->getRepositoryAPI(); - - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('SUBMITTING'), - pht('Submitting changes to "%s".', $this->getTargetFullRef())); - - $config_argv = array(); - - // Skip the "git p4 submit" interactive editor workflow. We expect - // the commit message that "arc land" has built to be satisfactory. - $config_argv[] = '-c'; - $config_argv[] = 'git-p4.skipSubmitEdit=true'; - - // Skip the "git p4 submit" confirmation prompt if the user does not edit - // the submit message. - $config_argv[] = '-c'; - $config_argv[] = 'git-p4.skipSubmitEditCheck=true'; - - $flags_argv = array(); - - // Disable implicit "git p4 rebase" as part of submit. We're allowing - // the implicit "git p4 sync" to go through since this puts us in a - // state which is generally similar to the state after "git push", with - // updated remotes. - - // We could do a manual "git p4 sync" with a more narrow "--branch" - // instead, but it's not clear that this is beneficial. - $flags_argv[] = '--disable-rebase'; - - // Detect moves and submit them to Perforce as move operations. - $flags_argv[] = '-M'; - - // If we run into a conflict, abort the operation. We expect users to - // fix conflicts and run "arc land" again. - $flags_argv[] = '--conflict=quit'; - - $err = $api->execPassthru( - '%LR p4 submit %LR --commit %R --', - $config_argv, - $flags_argv, - $this->mergedRef); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Submit failed! Fix the error and run "arc land" again.')); - } - } else { - $this->writeInfo( - pht('PUSHING'), - pht('Pushing changes to "%s".', $this->getTargetFullRef())); - - $err = $api->execPassthru( - 'push -- %s %s:%s', - $this->getTargetRemote(), - $this->mergedRef, - $this->getTargetOnto()); - - if ($err) { - throw new ArcanistUsageException( - pht( - 'Push failed! Fix the error and run "arc land" again.')); - } - } - } - - private function reconcileLocalState() { - $api = $this->getRepositoryAPI(); - - // Try to put the user into the best final state we can. This is very - // complicated because users are incredibly creative and their local - // branches may have the same names as branches in the remote but no - // relationship to them. - - if ($this->localRef != $this->getSourceRef()) { - // The user ran `arc land X` but was on a different branch, so just put - // them back wherever they were before. - $this->writeInfo( - pht('RESTORE'), - pht('Switching back to "%s".', $this->localRef)); - $this->restoreLocalState(); - return; - } - - // We're going to try to find a path to the upstream target branch. We - // try in two different ways: - // - // - follow the source branch directly along tracking branches until - // we reach the upstream; or - // - follow a local branch with the same name as the target branch until - // we reach the upstream. - - // First, get the path from whatever we landed to wherever it goes. - $local_branch = $this->getSourceRef(); - - $path = $api->getPathToUpstream($local_branch); - if ($path->getLength()) { - // We may want to discard the thing we landed from the path, if we're - // going to delete it. In this case, we don't want to update it or worry - // if it's dirty. - if ($this->getSourceRef() == $this->getTargetOnto()) { - // In this case, we've done something like land "master" onto itself, - // so we do want to update the actual branch. We're going to use the - // entire path. - } else { - // Otherwise, we're going to delete the branch at the end of the - // workflow, so throw it away the most-local branch that isn't long - // for this world. - $path->removeUpstream($local_branch); - - if (!$path->getLength()) { - // The local branch tracked upstream directly; however, it - // may not be the only one to do so. If there's a local - // branch of the same name that tracks the remote, try - // switching to that. - $local_branch = $this->getTargetOnto(); - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $local_branch); - if (!$err) { - $path = $api->getPathToUpstream($local_branch); - } - if (!$path->isConnectedToRemote()) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" directly tracks remote, staying on '. - 'detached HEAD.', - $local_branch)); - return; - } - } - - $local_branch = head($path->getLocalBranches()); - } - } else { - // The source branch has no upstream, so look for a local branch with - // the same name as the target branch. This corresponds to the common - // case where you have "master" and checkout local branches from it - // with "git checkout -b feature", then land onto "master". - - $local_branch = $this->getTargetOnto(); - - list($err) = $api->execManualLocal( - 'rev-parse --verify %s', - $local_branch); - if ($err) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" does not exist, staying on detached HEAD.', - $local_branch)); - return; - } - - $path = $api->getPathToUpstream($local_branch); - } - - if ($path->getCycle()) { - $this->writeWarn( - pht('LOCAL CYCLE'), - pht( - 'Local branch "%s" tracks an upstream but following it leads to '. - 'a local cycle, staying on detached HEAD.', - $local_branch)); - return; - } - - $is_perforce = $this->getIsGitPerforce(); - - if ($is_perforce) { - // If we're in Perforce mode, we don't expect to have a meaningful - // path to the remote: the "p4" remote is not a real remote, and - // "git p4" commands do not configure branch upstreams to provide - // a path. - - // Just pretend the target branch is connected directly to the remote, - // since this is effectively the behavior of Perforce and appears to - // do the right thing. - $cascade_branches = array($local_branch); - } else { - if (!$path->isConnectedToRemote()) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" is not connected to a remote, staying on '. - 'detached HEAD.', - $local_branch)); - return; - } - - $remote_remote = $path->getRemoteRemoteName(); - $remote_branch = $path->getRemoteBranchName(); - - $remote_actual = $remote_remote.'/'.$remote_branch; - $remote_expect = $this->getTargetFullRef(); - if ($remote_actual != $remote_expect) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local branch "%s" is connected to a remote ("%s") other than '. - 'the target remote ("%s"), staying on detached HEAD.', - $local_branch, - $remote_actual, - $remote_expect)); - return; - } - - // If we get this far, we have a sequence of branches which ultimately - // connect to the remote. We're going to try to update them all in reverse - // order, from most-upstream to most-local. - - $cascade_branches = $path->getLocalBranches(); - $cascade_branches = array_reverse($cascade_branches); - } - - // First, check if any of them are ahead of the remote. - - $ahead_of_remote = array(); - foreach ($cascade_branches as $cascade_branch) { - list($stdout) = $api->execxLocal( - 'log %s..%s --', - $this->mergedRef, - $cascade_branch); - $stdout = trim($stdout); - - if (strlen($stdout)) { - $ahead_of_remote[$cascade_branch] = $cascade_branch; - } - } - - // We're going to handle the last branch (the thing we ultimately intend - // to check out) differently. It's OK if it's ahead of the remote, as long - // as we just landed it. - - $local_ahead = isset($ahead_of_remote[$local_branch]); - unset($ahead_of_remote[$local_branch]); - $land_self = ($this->getTargetOnto() === $this->getSourceRef()); - - // We aren't going to pull anything if anything upstream from us is ahead - // of the remote, or the local is ahead of the remote and we didn't land - // it onto itself. - $skip_pull = ($ahead_of_remote || ($local_ahead && !$land_self)); - - if ($skip_pull) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local "%s" is ahead of remote "%s". Checking out "%s" but '. - 'not pulling changes.', - nonempty(head($ahead_of_remote), $local_branch), - $this->getTargetFullRef(), - $local_branch)); - - $this->writeInfo( - pht('CHECKOUT'), - pht( - 'Checking out "%s".', - $local_branch)); - - $api->execxLocal('checkout %s --', $local_branch); - - return; - } - - // If nothing upstream from our nearest branch is ahead of the remote, - // pull it all. - - $cascade_targets = array(); - if (!$ahead_of_remote) { - foreach ($cascade_branches as $cascade_branch) { - if ($local_ahead && ($local_branch == $cascade_branch)) { - continue; - } - $cascade_targets[] = $cascade_branch; - } - } - - if ($is_perforce) { - // In Perforce, we've already set the remote to the right state with an - // implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a - // meaningful operation. We're going to skip this step and jump down to - // the "git reset --hard" below to get everything into the right state. - } else if ($cascade_targets) { - $this->writeInfo( - pht('UPDATE'), - pht( - 'Local "%s" tracks target remote "%s", checking out and '. - 'pulling changes.', - $local_branch, - $this->getTargetFullRef())); - - foreach ($cascade_targets as $cascade_branch) { - $this->writeInfo( - pht('PULL'), - pht( - 'Checking out and pulling "%s".', - $cascade_branch)); - - $api->execxLocal('checkout %s --', $cascade_branch); - $api->execxLocal( - 'pull %s %s --', - $this->getTargetRemote(), - $cascade_branch); - } - - if (!$local_ahead) { - return; - } - } - - // In this case, the user did something like land a branch onto itself, - // and the branch is tracking the correct remote. We're going to discard - // the local state and reset it to the state we just pushed. - - $this->writeInfo( - pht('RESET'), - pht( - 'Local "%s" landed into remote "%s", resetting local branch to '. - 'remote state.', - $this->getTargetOnto(), - $this->getTargetFullRef())); - - $api->execxLocal('checkout %s --', $local_branch); - $api->execxLocal('reset --hard %s --', $this->getTargetFullRef()); - - return; - } - - private function destroyLocalBranch() { - $api = $this->getRepositoryAPI(); - $source_ref = $this->getSourceRef(); - - if ($source_ref == $this->getTargetOnto()) { - // If we landed a branch into a branch with the same name, so don't - // destroy it. This prevents us from cleaning up "master" if you're - // landing master into itself. - return; - } - - // TODO: Maybe this should also recover the proper upstream? - - // See T10321. If we were not landing a branch, don't try to clean it up. - // This happens most often when landing from a detached HEAD. - $is_branch = $this->isBranch($source_ref); - if (!$is_branch) { - echo tsprintf( - "%s\n", - pht( - '(Source "%s" is not a branch, leaving working copy as-is.)', - $source_ref)); - return; - } - - $recovery_command = csprintf( - 'git checkout -b %R %R', - $source_ref, - $this->sourceCommit); - - echo tsprintf( - "%s\n", - pht('Cleaning up branch "%s"...', $source_ref)); - - echo tsprintf( - "%s\n", - pht('(Use `%s` if you want it back.)', $recovery_command)); - - $api->execxLocal('branch -D -- %s', $source_ref); - } - - /** - * Save the local working copy state so we can restore it later. - */ - private function saveLocalState() { - $api = $this->getRepositoryAPI(); - - $this->localCommit = $api->getWorkingCopyRevision(); - - list($ref) = $api->execxLocal('rev-parse --abbrev-ref HEAD'); - $ref = trim($ref); - if ($ref === 'HEAD') { - $ref = $this->localCommit; - } - - $this->localRef = $ref; - - $this->restoreWhenDestroyed = true; - } - - /** - * Restore the working copy to the state it was in before we started - * performing writes. - */ - private function restoreLocalState() { - $api = $this->getRepositoryAPI(); - - $api->execxLocal('checkout %s --', $this->localRef); - $api->execxLocal('reset --hard %s --', $this->localCommit); - $api->execxLocal('submodule update --init --recursive'); - - $this->restoreWhenDestroyed = false; - } - - private function getTargetFullRef() { - return $this->getTargetRemote().'/'.$this->getTargetOnto(); - } - - private function getAuthorAndDate($commit) { - $api = $this->getRepositoryAPI(); - - // TODO: This is working around Windows escaping problems, see T8298. - - list($info) = $api->execxLocal( - 'log -n1 --format=%C %s --', - '%aD%n%an%n%ae', - $commit); - - $info = trim($info); - list($date, $author, $email) = explode("\n", $info, 3); - - return array( - "$author <{$email}>", - $date, - ); - } - - private function didHoldChanges() { - if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('HOLD'), - pht( - 'Holding change locally, it has not been submitted.')); - - $push_command = csprintf( - '$ git p4 submit -M --commit %R --', - $this->mergedRef); - } else { - $this->writeInfo( - pht('HOLD'), - pht( - 'Holding change locally, it has not been pushed.')); - - $push_command = csprintf( - '$ git push -- %R %R:%R', - $this->getTargetRemote(), - $this->mergedRef, - $this->getTargetOnto()); - } - - $restore_command = csprintf( - '$ git checkout %R --', - $this->localRef); - - echo tsprintf( - "\n%s\n\n". - "%s\n\n". - " **%s**\n\n". - "%s\n\n". - " **%s**\n\n". - "%s\n", - pht( - 'This local working copy now contains the merged changes in a '. - 'detached state.'), - pht('You can push the changes manually with this command:'), - $push_command, - pht( - 'You can go back to how things were before you ran "arc land" with '. - 'this command:'), - $restore_command, - pht( - 'Local branches have not been changed, and are still in exactly the '. - 'same state as before.')); - } - - private function isBranch($ref) { - $api = $this->getRepositoryAPI(); - - list($err) = $api->execManualLocal( - 'show-ref --verify --quiet -- %R', - 'refs/heads/'.$ref); - - return !$err; - } - - private function getEngineOnto() { - $source_ref = $this->getSourceRef(); - - $onto = $this->getOntoArgument(); - if ($onto !== null) { - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected with the "--onto" flag.', - $onto)); - return $onto; - } - - $api = $this->getRepositoryAPI(); - $path = $api->getPathToUpstream($source_ref); - - if ($path->getLength()) { - $cycle = $path->getCycle(); - if ($cycle) { - $this->writeWarn( - pht('LOCAL CYCLE'), - pht( - 'Local branch tracks an upstream, but following it leads to a '. - 'local cycle; ignoring branch upstream.')); - - echo tsprintf( - "\n %s\n\n", - implode(' -> ', $cycle)); - - } else { - if ($path->isConnectedToRemote()) { - $onto = $path->getRemoteBranchName(); - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected by following tracking branches '. - 'upstream to the closest remote.', - $onto)); - return $onto; - } else { - $this->writeInfo( - pht('NO PATH TO UPSTREAM'), - pht( - 'Local branch tracks an upstream, but there is no path '. - 'to a remote; ignoring branch upstream.')); - } - } - } - - $workflow = $this->getWorkflow(); - - $config_key = 'arc.land.onto.default'; - $onto = $workflow->getConfigFromAnySource($config_key); - if ($onto !== null) { - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", selected by "%s" configuration.', - $onto, - $config_key)); - return $onto; - } - - $onto = 'master'; - $this->writeInfo( - pht('TARGET'), - pht( - 'Landing onto "%s", the default target under git.', - $onto)); - - return $onto; - } - - private function getEngineRemote() { - $source_ref = $this->getSourceRef(); - - $remote = $this->getRemoteArgument(); - if ($remote !== null) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", selected with the "--remote" flag.', - $remote)); - return $remote; - } - - $api = $this->getRepositoryAPI(); - $path = $api->getPathToUpstream($source_ref); - - $remote = $path->getRemoteRemoteName(); - if ($remote !== null) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", selected by following tracking branches '. - 'upstream to the closest remote.', - $remote)); - return $remote; - } - - $remote = 'p4'; - if ($api->isPerforceRemote($remote)) { - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using Perforce remote "%s". The existence of this remote implies '. - 'this working copy was synchronized from a Perforce repository.', - $remote)); - return $remote; - } - - $remote = 'origin'; - $this->writeInfo( - pht('REMOTE'), - pht( - 'Using remote "%s", the default remote under Git.', - $remote)); - - return $remote; - } - -} diff --git a/src/land/ArcanistLandCommit.php b/src/land/ArcanistLandCommit.php new file mode 100644 index 00000000..527611ce --- /dev/null +++ b/src/land/ArcanistLandCommit.php @@ -0,0 +1,149 @@ +hash = $hash; + return $this; + } + + public function getHash() { + return $this->hash; + } + + public function setSummary($summary) { + $this->summary = $summary; + return $this; + } + + public function getSummary() { + return $this->summary; + } + + public function getDisplaySummary() { + if ($this->displaySummary === null) { + $this->displaySummary = id(new PhutilUTF8StringTruncator()) + ->setMaximumGlyphs(64) + ->truncateString($this->getSummary()); + } + return $this->displaySummary; + } + + public function setParents(array $parents) { + $this->parents = $parents; + return $this; + } + + public function getParents() { + return $this->parents; + } + + public function addSymbol(ArcanistLandSymbol $symbol) { + $this->symbols[] = $symbol; + return $this; + } + + public function getSymbols() { + return $this->symbols; + } + + public function setExplicitRevisionref(ArcanistRevisionRef $ref) { + $this->explicitRevisionRef = $ref; + return $this; + } + + public function getExplicitRevisionref() { + return $this->explicitRevisionRef; + } + + public function setParentCommits(array $parent_commits) { + $this->parentCommits = $parent_commits; + return $this; + } + + public function getParentCommits() { + return $this->parentCommits; + } + + public function setIsHeadCommit($is_head_commit) { + $this->isHeadCommit = $is_head_commit; + return $this; + } + + public function getIsHeadCommit() { + return $this->isHeadCommit; + } + + public function setIsImplicitCommit($is_implicit_commit) { + $this->isImplicitCommit = $is_implicit_commit; + return $this; + } + + public function getIsImplicitCommit() { + return $this->isImplicitCommit; + } + + public function getAncestorRevisionPHIDs() { + $phids = array(); + + foreach ($this->getParentCommits() as $parent_commit) { + $phids += $parent_commit->getAncestorRevisionPHIDs(); + } + + $revision_ref = $this->getRevisionRef(); + if ($revision_ref) { + $phids[$revision_ref->getPHID()] = $revision_ref->getPHID(); + } + + return $phids; + } + + public function getRevisionRef() { + if ($this->revisionRef === false) { + $this->revisionRef = $this->newRevisionRef(); + } + + return $this->revisionRef; + } + + private function newRevisionRef() { + $revision_ref = $this->getExplicitRevisionRef(); + if ($revision_ref) { + return $revision_ref; + } + + $parent_refs = array(); + foreach ($this->getParentCommits() as $parent_commit) { + $parent_ref = $parent_commit->getRevisionRef(); + if ($parent_ref) { + $parent_refs[$parent_ref->getPHID()] = $parent_ref; + } + } + + if (count($parent_refs) > 1) { + throw new Exception( + pht( + 'Too many distinct parent refs!')); + } + + if ($parent_refs) { + return head($parent_refs); + } + + return null; + } + + +} diff --git a/src/land/ArcanistLandCommitSet.php b/src/land/ArcanistLandCommitSet.php new file mode 100644 index 00000000..bdf34ce0 --- /dev/null +++ b/src/land/ArcanistLandCommitSet.php @@ -0,0 +1,52 @@ +revisionRef = $revision_ref; + return $this; + } + + public function getRevisionRef() { + return $this->revisionRef; + } + + public function setCommits(array $commits) { + assert_instances_of($commits, 'ArcanistLandCommit'); + $this->commits = $commits; + + $revision_phid = $this->getRevisionRef()->getPHID(); + foreach ($commits as $commit) { + $revision_ref = $commit->getExplicitRevisionRef(); + + if ($revision_ref) { + if ($revision_ref->getPHID() === $revision_phid) { + continue; + } + } + + $commit->setIsImplicitCommit(true); + } + + return $this; + } + + public function getCommits() { + return $this->commits; + } + + public function hasImplicitCommits() { + foreach ($this->commits as $commit) { + if ($commit->getIsImplicitCommit()) { + return true; + } + } + + return false; + } + +} diff --git a/src/land/ArcanistLandEngine.php b/src/land/ArcanistLandEngine.php deleted file mode 100644 index e81349f8..00000000 --- a/src/land/ArcanistLandEngine.php +++ /dev/null @@ -1,182 +0,0 @@ -workflow = $workflow; - return $this; - } - - final public function getWorkflow() { - return $this->workflow; - } - - final public function setRepositoryAPI( - ArcanistRepositoryAPI $repository_api) { - $this->repositoryAPI = $repository_api; - return $this; - } - - final public function getRepositoryAPI() { - return $this->repositoryAPI; - } - - final public function setShouldHold($should_hold) { - $this->shouldHold = $should_hold; - return $this; - } - - final public function getShouldHold() { - return $this->shouldHold; - } - - final public function setShouldKeep($should_keep) { - $this->shouldKeep = $should_keep; - return $this; - } - - final public function getShouldKeep() { - return $this->shouldKeep; - } - - final public function setShouldSquash($should_squash) { - $this->shouldSquash = $should_squash; - return $this; - } - - final public function getShouldSquash() { - return $this->shouldSquash; - } - - final public function setShouldPreview($should_preview) { - $this->shouldPreview = $should_preview; - return $this; - } - - final public function getShouldPreview() { - return $this->shouldPreview; - } - - final public function setTargetRemote($target_remote) { - $this->targetRemote = $target_remote; - return $this; - } - - final public function getTargetRemote() { - return $this->targetRemote; - } - - final public function setTargetOnto($target_onto) { - $this->targetOnto = $target_onto; - return $this; - } - - final public function getTargetOnto() { - return $this->targetOnto; - } - - final public function setSourceRef($source_ref) { - $this->sourceRef = $source_ref; - return $this; - } - - final public function getSourceRef() { - return $this->sourceRef; - } - - final public function setBuildMessageCallback($build_message_callback) { - $this->buildMessageCallback = $build_message_callback; - return $this; - } - - final public function getBuildMessageCallback() { - return $this->buildMessageCallback; - } - - final public function setCommitMessageFile($commit_message_file) { - $this->commitMessageFile = $commit_message_file; - return $this; - } - - final public function getCommitMessageFile() { - return $this->commitMessageFile; - } - - final public function setRemoteArgument($remote_argument) { - $this->remoteArgument = $remote_argument; - return $this; - } - - final public function getRemoteArgument() { - return $this->remoteArgument; - } - - final public function setOntoArgument($onto_argument) { - $this->ontoArgument = $onto_argument; - return $this; - } - - final public function getOntoArgument() { - return $this->ontoArgument; - } - - abstract public function parseArguments(); - abstract public function execute(); - - abstract protected function getLandingCommits(); - - protected function printLandingCommits() { - $logs = $this->getLandingCommits(); - - if (!$logs) { - throw new ArcanistUsageException( - pht( - 'There are no commits on "%s" which are not already present on '. - 'the target.', - $this->getSourceRef())); - } - - $list = id(new PhutilConsoleList()) - ->setWrap(false) - ->addItems($logs); - - id(new PhutilConsoleBlock()) - ->addParagraph( - pht( - 'These %s commit(s) will be landed:', - new PhutilNumber(count($logs)))) - ->addList($list) - ->draw(); - } - - protected function writeWarn($title, $message) { - return $this->getWorkflow()->writeWarn($title, $message); - } - - protected function writeInfo($title, $message) { - return $this->getWorkflow()->writeInfo($title, $message); - } - - protected function writeOkay($title, $message) { - return $this->getWorkflow()->writeOkay($title, $message); - } - - -} diff --git a/src/land/ArcanistLandSymbol.php b/src/land/ArcanistLandSymbol.php new file mode 100644 index 00000000..672f792e --- /dev/null +++ b/src/land/ArcanistLandSymbol.php @@ -0,0 +1,27 @@ +symbol = $symbol; + return $this; + } + + public function getSymbol() { + return $this->symbol; + } + + public function setCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function getCommit() { + return $this->commit; + } + +} diff --git a/src/land/ArcanistLandTarget.php b/src/land/ArcanistLandTarget.php new file mode 100644 index 00000000..6a258e6b --- /dev/null +++ b/src/land/ArcanistLandTarget.php @@ -0,0 +1,41 @@ +remote = $remote; + return $this; + } + + public function getRemote() { + return $this->remote; + } + + public function setRef($ref) { + $this->ref = $ref; + return $this; + } + + public function getRef() { + return $this->ref; + } + + public function getLandTargetKey() { + return sprintf('%s/%s', $this->getRemote(), $this->getRef()); + } + + public function setLandTargetCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function getLandTargetCommit() { + return $this->commit; + } + +} diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php new file mode 100644 index 00000000..48b2d95a --- /dev/null +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -0,0 +1,1362 @@ +isGitPerforce = $is_git_perforce; + return $this; + } + + private function getIsGitPerforce() { + return $this->isGitPerforce; + } + + protected function pruneBranches(array $sets) { + $old_commits = array(); + foreach ($sets as $set) { + $hash = last($set->getCommits())->getHash(); + $old_commits[] = $hash; + } + + $branch_map = $this->getBranchesForCommits( + $old_commits, + $is_contains = false); + + $api = $this->getRepositoryAPI(); + foreach ($branch_map as $branch_name => $branch_hash) { + $recovery_command = csprintf( + 'git checkout -b %s %s', + $branch_name, + $this->getDisplayHash($branch_hash)); + + echo tsprintf( + "%s\n", + pht('Cleaning up branch "%s"...', $branch_name)); + + echo tsprintf( + "%s\n", + pht('(Use `%s` if you want it back.)', $recovery_command)); + + $api->execxLocal('branch -D -- %s', $branch_name); + $this->deletedBranches[$branch_name] = true; + } + } + + private function getBranchesForCommits(array $hashes, $is_contains) { + $api = $this->getRepositoryAPI(); + + $format = '%(refname) %(objectname)'; + + $result = array(); + foreach ($hashes as $hash) { + if ($is_contains) { + $command = csprintf( + 'for-each-ref --contains %s --format %s --', + $hash, + $format); + } else { + $command = csprintf( + 'for-each-ref --points-at %s --format %s --', + $hash, + $format); + } + + list($foreach_lines) = $api->execxLocal('%C', $command); + $foreach_lines = phutil_split_lines($foreach_lines, false); + + foreach ($foreach_lines as $line) { + if (!strlen($line)) { + continue; + } + + $expect_parts = 2; + $parts = explode(' ', $line, $expect_parts); + if (count($parts) !== $expect_parts) { + throw new Exception( + pht( + 'Failed to explode line "%s".', + $line)); + } + + $ref_name = $parts[0]; + $ref_hash = $parts[1]; + + $matches = null; + $ok = preg_match('(^refs/heads/(.*)\z)', $ref_name, $matches); + if ($ok === false) { + throw new Exception( + pht( + 'Failed to match against branch pattern "%s".', + $line)); + } + + if (!$ok) { + continue; + } + + $result[$matches[1]] = $ref_hash; + } + } + + return $result; + } + + protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $old_commit = last($set->getCommits())->getHash(); + $new_commit = $into_commit; + + $branch_map = $this->getBranchesForCommits( + array($old_commit), + $is_contains = true); + + $log = $this->getLogEngine(); + foreach ($branch_map as $branch_name => $branch_head) { + // If this branch just points at the old state, don't bother rebasing + // it. We'll update or delete it later. + if ($branch_head === $old_commit) { + continue; + } + + $log->writeStatus( + pht('CASCADE'), + pht( + 'Rebasing "%s" onto landed state...', + $branch_name)); + + try { + $api->execxLocal( + 'rebase --onto %s -- %s %s', + $new_commit, + $old_commit, + $branch_name); + } catch (CommandException $ex) { + // TODO: If we have a stashed state or are not running in incremental + // mode: abort the rebase, restore the local state, and pop the stash. + // Otherwise, drop the user out here. + throw $ex; + } + } + } + + private function fetchTarget(ArcanistLandTarget $target) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // NOTE: Although this output isn't hugely useful, we need to passthru + // instead of using a subprocess here because `git fetch` may prompt the + // user to enter a password if they're fetching over HTTP with basic + // authentication. See T10314. + + if ($this->getIsGitPerforce()) { + $log->writeStatus( + pht('P4 SYNC'), + pht( + 'Synchronizing "%s" from Perforce...', + $target->getRef())); + + $err = $api->execPassthru( + 'p4 sync --silent --branch %s --', + $target->getRemote().'/'.$target->getRef()); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Perforce sync failed! Fix the error and run "arc land" again.')); + } + + return $this->getLandTargetLocalCommit($target); + } + + $exists = $this->getLandTargetLocalExists($target); + if (!$exists) { + $log->writeWarning( + pht('TARGET'), + pht( + 'No local copy of ref "%s" in remote "%s" exists, attempting '. + 'fetch...', + $target->getRef(), + $target->getRemote())); + + $this->fetchLandTarget($target, $ignore_failure = true); + + $exists = $this->getLandTargetLocalExists($target); + if (!$exists) { + return null; + } + + $log->writeStatus( + pht('FETCHED'), + pht( + 'Fetched ref "%s" from remote "%s".', + $target->getRef(), + $target->getRemote())); + + return $this->getLandTargetLocalCommit($target); + } + + $log->writeStatus( + pht('FETCH'), + pht( + 'Fetching "%s" from remote "%s"...', + $target->getRef(), + $target->getRemote())); + + $this->fetchLandTarget($target, $ignore_failure = false); + + return $this->getLandTargetLocalCommit($target); + } + + private function updateWorkingCopy($into_commit) { + $api = $this->getRepositoryAPI(); + if ($into_commit === null) { + throw new Exception('TODO: Author a new empty state.'); + } else { + $api->execxLocal('checkout %s --', $into_commit); + } + } + + protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + + $this->updateWorkingCopy($into_commit); + + $commits = $set->getCommits(); + $source_commit = last($commits)->getHash(); + + // NOTE: See T11435 for some history. See PHI1727 for a case where a user + // modified their working copy while running "arc land". This attempts to + // resist incorrectly detecting simultaneous working copy modifications + // as changes. + + list($changes) = $api->execxLocal( + 'diff --no-ext-diff HEAD..%s --', + $source_commit); + $changes = trim($changes); + if (!strlen($changes)) { + + // TODO: We could make a more significant effort to identify the + // human-readable symbol which led us to try to land this ref. + + throw new PhutilArgumentUsageException( + pht( + 'Merging local "%s" into "%s" produces an empty diff. '. + 'This usually means these changes have already landed.', + $this->getDisplayHash($source_commit), + $this->getDisplayHash($into_commit))); + } + + list($original_author, $original_date) = $this->getAuthorAndDate( + $source_commit); + + try { + if ($this->isSquashStrategy()) { + // NOTE: We're explicitly specifying "--ff" to override the presence + // of "merge.ff" options in user configuration. + + $api->execxLocal( + 'merge --no-stat --no-commit --ff --squash -- %s', + $source_commit); + } else { + $api->execxLocal( + 'merge --no-stat --no-commit --no-ff -- %s', + $source_commit); + } + } catch (CommandException $ex) { + $api->execManualLocal('merge --abort'); + $api->execManualLocal('reset --hard HEAD --'); + + throw new PhutilArgumentUsageException( + pht( + 'Local "%s" does not merge cleanly into "%s". Merge or rebase '. + 'local changes so they can merge cleanly.', + $source_commit, + $into_commit)); + } + + $revision_ref = $set->getRevisionRef(); + $commit_message = $revision_ref->getCommitMessage(); + + $future = $api->execFutureLocal( + 'commit --author %s --date %s -F - --', + $original_author, + $original_date); + $future->write($commit_message); + $future->resolvex(); + + list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD'); + $new_cursor = trim($stdout); + + if ($into_commit === null) { + if ($this->isSquashStrategy()) { + throw new Exception( + pht('TODO: Rewrite HEAD to have no parents.')); + } else { + throw new Exception( + pht('TODO: Rewrite HEAD to have only source as a parent.')); + } + } + + return $new_cursor; + } + + protected function pushChange($into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIsGitPerforce()) { + + // TODO: Specifying "--onto" more than once is almost certainly an error + // in Perforce. + + $log->writeStatus( + pht('SUBMITTING'), + pht( + 'Submitting changes to "%s".', + $this->getOntoRemote())); + + $config_argv = array(); + + // Skip the "git p4 submit" interactive editor workflow. We expect + // the commit message that "arc land" has built to be satisfactory. + $config_argv[] = '-c'; + $config_argv[] = 'git-p4.skipSubmitEdit=true'; + + // Skip the "git p4 submit" confirmation prompt if the user does not edit + // the submit message. + $config_argv[] = '-c'; + $config_argv[] = 'git-p4.skipSubmitEditCheck=true'; + + $flags_argv = array(); + + // Disable implicit "git p4 rebase" as part of submit. We're allowing + // the implicit "git p4 sync" to go through since this puts us in a + // state which is generally similar to the state after "git push", with + // updated remotes. + + // We could do a manual "git p4 sync" with a more narrow "--branch" + // instead, but it's not clear that this is beneficial. + $flags_argv[] = '--disable-rebase'; + + // Detect moves and submit them to Perforce as move operations. + $flags_argv[] = '-M'; + + // If we run into a conflict, abort the operation. We expect users to + // fix conflicts and run "arc land" again. + $flags_argv[] = '--conflict=quit'; + + $err = $api->execPassthru( + '%LR p4 submit %LR --commit %R --', + $config_argv, + $flags_argv, + $into_commit); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Submit failed! Fix the error and run "arc land" again.')); + } + + return; + } + + $log->writeStatus( + pht('PUSHING'), + pht('Pushing changes to "%s".', $this->getOntoRemote())); + + $refspecs = array(); + foreach ($this->getOntoRefs() as $onto_ref) { + $refspecs[] = sprintf( + '%s:%s', + $into_commit, + $onto_ref); + } + + $err = $api->execPassthru( + 'push -- %s %Ls', + $this->getOntoRemote(), + $refspecs); + + if ($err) { + throw new ArcanistUsageException( + pht( + 'Push failed! Fix the error and run "arc land" again.')); + } + + // TODO + // if ($this->isGitSvn) { + // $err = phutil_passthru('git svn dcommit'); + // $cmd = 'git svn dcommit'; + + } + + protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state) { + + $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); + + // Try to put the user into the best final state we can. This is very + // complicated because users are incredibly creative and their local + // branches may, for example, have the same names as branches in the + // remote but no relationship to them. + + // First, we're going to try to update these local branches: + // + // - the branch we started on originally; and + // - the local upstreams of the branch we started on originally; and + // - the local branch with the same name as the "into" ref; and + // - the local branch with the same name as the "onto" ref. + // + // These branches may not all exist and may not all be unique. + // + // To be updated, these branches must: + // + // - exist; + // - have not been deleted; and + // - be connected to the remote we pushed into. + + $update_branches = array(); + + $local_ref = $state->getLocalRef(); + if ($local_ref !== null) { + $update_branches[] = $local_ref; + } + + $local_path = $state->getLocalPath(); + if ($local_path) { + foreach ($local_path->getLocalBranches() as $local_branch) { + $update_branches[] = $local_branch; + } + } + + if (!$this->getIntoEmpty() && !$this->getIntoLocal()) { + $update_branches[] = $this->getIntoRef(); + } + + foreach ($this->getOntoRefs() as $onto_ref) { + $update_branches[] = $onto_ref; + } + + $update_branches = array_fuse($update_branches); + + // Remove any branches we know we deleted. + foreach ($update_branches as $key => $update_branch) { + if (isset($this->deletedBranches[$update_branch])) { + unset($update_branches[$key]); + } + } + + // Now, remove any branches which don't actually exist. + foreach ($update_branches as $key => $update_branch) { + list($err) = $api->execManualLocal( + 'rev-parse --verify %s', + $update_branch); + if ($err) { + unset($update_branches[$key]); + } + } + + $is_perforce = $this->getIsGitPerforce(); + if ($is_perforce) { + // If we're in Perforce mode, we don't expect to have a meaningful + // path to the remote: the "p4" remote is not a real remote, and + // "git p4" commands do not configure branch upstreams to provide + // a path. + + // Additionally, we've already set the remote to the right state with an + // implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a + // meaningful operation. + + // We're going to skip everything here and just switch to the most + // desirable branch (if we can find one), then reset the state (if that + // operation is safe). + + if (!$update_branches) { + $log->writeStatus( + pht('DETACHED HEAD'), + pht( + 'Unable to find any local branches to update, staying on '. + 'detached head.')); + $state->discardLocalState(); + return; + } + + $dst_branch = head($update_branches); + if (!$this->isAncestorOf($dst_branch, $into_commit)) { + $log->writeStatus( + pht('CHECKOUT'), + pht( + 'Local branch "%s" has unpublished changes, checking it out '. + 'but leaving them in place.', + $dst_branch)); + $do_reset = false; + } else { + $log->writeStatus( + pht('UPDATE'), + pht( + 'Switching to local branch "%s".', + $dst_branch)); + $do_reset = true; + } + + $api->execxLocal('checkout %s --', $dst_branch); + + if ($do_reset) { + $api->execxLocal('reset --hard %s --', $into_commit); + } + + $state->discardLocalState(); + return; + } + + $onto_refs = array_fuse($this->getOntoRefs()); + + $pull_branches = array(); + foreach ($update_branches as $update_branch) { + $update_path = $api->getPathToUpstream($update_branch); + + // Remove any branches which contain upstream cycles. + if ($update_path->getCycle()) { + $log->writeWarning( + pht('LOCAL CYCLE'), + pht( + 'Local branch "%s" tracks an upstream but following it leads to '. + 'a local cycle, ignoring branch.', + $update_branch)); + continue; + } + + // Remove any branches not connected to a remote. + if (!$update_path->isConnectedToRemote()) { + continue; + } + + // Remove any branches connected to a remote other than the remote + // we actually pushed to. + $remote_name = $update_path->getRemoteRemoteName(); + if ($remote_name !== $this->getOntoRemote()) { + continue; + } + + // Remove any branches not connected to a branch we pushed to. + $remote_branch = $update_path->getRemoteBranchName(); + if (!isset($onto_refs[$remote_branch])) { + continue; + } + + // This is the most-desirable path between some local branch and + // an impacted upstream. Select it and continue. + $pull_branches = $update_path->getLocalBranches(); + break; + } + + // When we update these branches later, we want to start with the branch + // closest to the upstream and work our way down. + $pull_branches = array_reverse($pull_branches); + $pull_branches = array_fuse($pull_branches); + + // If we started on a branch and it still exists but is not impacted + // by the changes we made to the remote (i.e., we aren't actually going + // to pull or update it if we continue), just switch back to it now. It's + // okay if this branch is completely unrelated to the changes we just + // landed. + + if ($local_ref !== null) { + if (isset($update_branches[$local_ref])) { + if (!isset($pull_branches[$local_ref])) { + + $log->writeStatus( + pht('RETURN'), + pht( + 'Returning to original branch "%s" in original state.', + $local_ref)); + + $state->restoreLocalState(); + return; + } + } + } + + // Otherwise, if we don't have any path from the upstream to any local + // branch, we don't want to switch to some unrelated branch which happens + // to have the same name as a branch we interacted with. Just stay where + // we ended up. + + $dst_branch = null; + if ($pull_branches) { + $dst_branch = null; + foreach ($pull_branches as $pull_branch) { + if (!$this->isAncestorOf($pull_branch, $into_commit)) { + + $log->writeStatus( + pht('LOCAL CHANGES'), + pht( + 'Local branch "%s" has unpublished changes, ending updates.', + $pull_branch)); + + break; + } + + $log->writeStatus( + pht('UPDATE'), + pht( + 'Updating local branch "%s"...', + $pull_branch)); + + $api->execxLocal( + 'branch -f %s %s --', + $pull_branch, + $into_commit); + + $dst_branch = $pull_branch; + } + } + + if ($dst_branch) { + $log->writeStatus( + pht('CHECKOUT'), + pht( + 'Checking out "%s".', + $dst_branch)); + + $api->execxLocal('checkout %s --', $dst_branch); + } else { + $log->writeStatus( + pht('DETACHED HEAD'), + pht( + 'Unable to find any local branches to update, staying on '. + 'detached head.')); + } + + $state->discardLocalState(); + } + + private function isAncestorOf($branch, $commit) { + $api = $this->getRepositoryAPI(); + + list($stdout) = $api->execxLocal( + 'merge-base %s %s', + $branch, + $commit); + $merge_base = trim($stdout); + + list($stdout) = $api->execxLocal( + 'rev-parse --verify %s', + $branch); + $branch_hash = trim($stdout); + + return ($merge_base === $branch_hash); + } + + private function getAuthorAndDate($commit) { + $api = $this->getRepositoryAPI(); + + list($info) = $api->execxLocal( + 'log -n1 --format=%s %s --', + '%aD%n%an%n%ae', + $commit); + + $info = trim($info); + list($date, $author, $email) = explode("\n", $info, 3); + + return array( + "$author <{$email}>", + $date, + ); + } + + private function didHoldChanges() { + $log = $this->getLogEngine(); + + if ($this->getIsGitPerforce()) { + $this->writeInfo( + pht('HOLD'), + pht( + 'Holding change locally, it has not been submitted.')); + + $push_command = csprintf( + '$ git p4 submit -M --commit %R --', + $this->mergedRef); + } else { + $log->writeStatus( + pht('HOLD'), + pht( + 'Holding change locally, it has not been pushed.')); + + $push_command = csprintf( + '$ git push -- %R %R:%R', + $this->getTargetRemote(), + $this->mergedRef, + $this->getTargetOnto()); + } + + $restore_command = csprintf( + '$ git checkout %R --', + $this->localRef); + + echo tsprintf( + "\n%s\n\n". + "%s\n\n". + " **%s**\n\n". + "%s\n\n". + " **%s**\n\n". + "%s\n", + pht( + 'This local working copy now contains the merged changes in a '. + 'detached state.'), + pht('You can push the changes manually with this command:'), + $push_command, + pht( + 'You can go back to how things were before you ran "arc land" with '. + 'this command:'), + $restore_command, + pht( + 'Local branches have not been changed, and are still in exactly the '. + 'same state as before.')); + } + + protected function resolveSymbols(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $raw_symbol); + + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Branch "%s" does not exist in the local working copy.', + $raw_symbol)); + } + + $commit = trim($stdout); + $symbol->setCommit($commit); + } + } + + protected function confirmOntoRefs(array $onto_refs) { + foreach ($onto_refs as $onto_ref) { + if (!strlen($onto_ref)) { + throw new PhutilArgumentUsageException( + pht( + 'Selected "onto" ref "%s" is invalid: the empty string is not '. + 'a valid ref.', + $onto_ref)); + } + } + } + + protected function selectOntoRefs(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $onto = $this->getOntoArguments(); + if ($onto) { + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected with the "--onto" flag: %s.', + implode(', ', $onto))); + + return $onto; + } + + $onto = $this->getOntoFromConfiguration(); + if ($onto) { + $onto_key = $this->getOntoConfigurationKey(); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected by reading "%s" configuration: %s.', + $onto_key, + implode(', ', $onto))); + + return $onto; + } + + $api = $this->getRepositoryAPI(); + + $remote_onto = array(); + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + $path = $api->getPathToUpstream($raw_symbol); + + if (!$path->getLength()) { + continue; + } + + $cycle = $path->getCycle(); + if ($cycle) { + $log->writeWarning( + pht('LOCAL CYCLE'), + pht( + 'Local branch "%s" tracks an upstream, but following it leads '. + 'to a local cycle; ignoring branch upstream.', + $raw_symbol)); + + $log->writeWarning( + pht('LOCAL CYCLE'), + implode(' -> ', $cycle)); + + continue; + } + + if (!$path->isConnectedToRemote()) { + $log->writeWarning( + pht('NO PATH TO REMOTE'), + pht( + 'Local branch "%s" tracks an upstream, but there is no path '. + 'to a remote; ignoring branch upstream.', + $raw_symbol)); + + continue; + } + + $onto = $path->getRemoteBranchName(); + + $remote_onto[$onto] = $onto; + } + + if (count($remote_onto) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'The branches you are landing are connected to multiple different '. + 'remote branches via Git branch upstreams. Use "--onto" to select '. + 'the refs you want to push to.')); + } + + if ($remote_onto) { + $remote_onto = array_values($remote_onto); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", selected by following tracking branches '. + 'upstream to the closest remote branch.', + head($remote_onto))); + + return $remote_onto; + } + + $default_onto = 'master'; + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", the default target under Git.', + $default_onto)); + + return array($default_onto); + } + + protected function selectOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $remote = $this->newOntoRemote($symbols); + + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + $is_pushable = $api->isPushableRemote($remote); + $is_perforce = $api->isPerforceRemote($remote); + + if (!$is_pushable && !$is_perforce) { + throw new PhutilArgumentUsageException( + pht( + 'No pushable remote "%s" exists. Use the "--onto-remote" flag to '. + 'choose a valid, pushable remote to land changes onto.', + $remote)); + } + + if ($is_perforce) { + $this->setIsGitPerforce(true); + + $log->writeWarning( + pht('P4 MODE'), + pht( + 'Operating in Git/Perforce mode after selecting a Perforce '. + 'remote.')); + + if (!$this->isSquashStrategy()) { + throw new PhutilArgumentUsageException( + pht( + 'Perforce mode does not support the "merge" land strategy. '. + 'Use the "squash" land strategy when landing to a Perforce '. + 'remote (you can use "--squash" to select this strategy).')); + } + } + + return $remote; + } + + private function newOntoRemote(array $onto_symbols) { + assert_instances_of($onto_symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $remote = $this->getOntoRemoteArgument(); + if ($remote !== null) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected with the "--onto-remote" flag.', + $remote)); + + return $remote; + } + + $remote = $this->getOntoRemoteFromConfiguration(); + if ($remote !== null) { + $remote_key = $this->getOntoRemoteConfigurationKey(); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by reading "%s" configuration.', + $remote, + $remote_key)); + + return $remote; + } + + $api = $this->getRepositoryAPI(); + + $upstream_remotes = array(); + foreach ($onto_symbols as $onto_symbol) { + $path = $api->getPathToUpstream($onto_symbol->getSymbol()); + + $remote = $path->getRemoteRemoteName(); + if ($remote !== null) { + $upstream_remotes[$remote][] = $onto_symbol; + } + } + + if (count($upstream_remotes) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'The "onto" refs you have selected are connected to multiple '. + 'different remotes via Git branch upstreams. Use "--onto-remote" '. + 'to select a single remote.')); + } + + if ($upstream_remotes) { + $upstream_remote = head_key($upstream_remotes); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by following tracking branches '. + 'upstream to the closest remote.', + $remote)); + + return $upstream_remote; + } + + $perforce_remote = 'p4'; + if ($api->isPerforceRemote($remote)) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Peforce remote "%s" was selected because the existence of '. + 'this remote implies this working copy was synchronized '. + 'from a Perforce repository.', + $remote)); + + return $remote; + } + + $default_remote = 'origin'; + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Landing onto remote "%s", the default remote under Git.', + $default_remote)); + + return $default_remote; + } + + protected function selectIntoRemote() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + if ($this->getIntoLocalArgument()) { + $this->setIntoLocal(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into local state, selected with the "--into-local" '. + 'flag.')); + + return; + } + + $into = $this->getIntoRemoteArgument(); + if ($into !== null) { + + // TODO: We could allow users to pass a URI argument instead, but + // this also requires some updates to the fetch logic elsewhere. + + if (!$api->isFetchableRemote($into)) { + throw new PhutilArgumentUsageException( + pht( + 'Remote "%s", specified with "--into", is not a valid fetchable '. + 'remote.', + $into)); + } + + $this->setIntoRemote($into); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $onto = $this->getOntoRemote(); + $this->setIntoRemote($onto); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s" by default, because this is the remote '. + 'the change is landing onto.', + $onto)); + } + + protected function selectIntoRef() { + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + $into = $this->getIntoArgument(); + if ($into !== null) { + $this->setIntoRef($into); + + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $ontos = $this->getOntoRefs(); + $onto = head($ontos); + + $this->setIntoRef($onto); + if (count($ontos) > 1) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the first '. + '"onto" target.', + $onto)); + } else { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the "onto" '. + 'target.', + $onto)); + } + } + + protected function selectIntoCommit() { + // Make sure that our "into" target is valid. + $log = $this->getLogEngine(); + + if ($this->getIntoEmpty()) { + // If we're running under "--into-empty", we don't have to do anything. + + $log->writeStatus( + pht('INTO COMMIT'), + pht('Preparing merge into the empty state.')); + + return null; + } + + if ($this->getIntoLocal()) { + // If we're running under "--into-local", just make sure that the + // target identifies some actual commit. + $api = $this->getRepositoryAPI(); + $local_ref = $this->getIntoRef(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $local_ref); + + if ($err) { + throw new PhutilArgumentUsageException( + pht( + 'Local ref "%s" does not exist.', + $local_ref)); + } + + $into_commit = trim($stdout); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into local target "%s", at commit "%s".', + $local_ref, + $this->getDisplayHash($into_commit))); + + return $into_commit; + } + + $target = id(new ArcanistLandTarget()) + ->setRemote($this->getIntoRemote()) + ->setRef($this->getIntoRef()); + + $commit = $this->fetchTarget($target); + if ($commit !== null) { + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into "%s" from remote "%s", at commit "%s".', + $target->getRef(), + $target->getRemote(), + $this->getDisplayHash($commit))); + return $commit; + } + + // If we have no valid target and the user passed "--into" explicitly, + // treat this as an error. For example, "arc land --into Q --onto Q", + // where "Q" does not exist, is an error. + if ($this->getIntoArgument()) { + throw new PhutilArgumentUsageException( + pht( + 'Ref "%s" does not exist in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + // Otherwise, treat this as implying "--into-empty". For example, + // "arc land --onto Q", where "Q" does not exist, is equivalent to + // "arc land --into-empty --onto Q". + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into the empty state to create target "%s" '. + 'in remote "%s".', + $target->getRef(), + $target->getRemote())); + + return null; + } + + private function getLandTargetLocalCommit(ArcanistLandTarget $target) { + $commit = $this->resolveLandTargetLocalCommit($target); + + if ($commit === null) { + throw new Exception( + pht( + 'No ref "%s" exists in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + return $commit; + } + + private function getLandTargetLocalExists(ArcanistLandTarget $target) { + $commit = $this->resolveLandTargetLocalCommit($target); + return ($commit !== null); + } + + private function resolveLandTargetLocalCommit(ArcanistLandTarget $target) { + $target_key = $target->getLandTargetKey(); + + if (!array_key_exists($target_key, $this->landTargetCommitMap)) { + $full_ref = sprintf( + 'refs/remotes/%s/%s', + $target->getRemote(), + $target->getRef()); + + $api = $this->getRepositoryAPI(); + + list($err, $stdout) = $api->execManualLocal( + 'rev-parse --verify %s', + $full_ref); + + if ($err) { + $result = null; + } else { + $result = trim($stdout); + } + + $this->landTargetCommitMap[$target_key] = $result; + } + + return $this->landTargetCommitMap[$target_key]; + } + + private function fetchLandTarget( + ArcanistLandTarget $target, + $ignore_failure = false) { + $api = $this->getRepositoryAPI(); + + // TODO: Format this fetch nicely as a workflow command. + + $err = $api->execPassthru( + 'fetch --no-tags --quiet -- %s %s', + $target->getRemote(), + $target->getRef()); + if ($err && !$ignore_failure) { + throw new ArcanistUsageException( + pht( + 'Fetch of "%s" from remote "%s" failed! Fix the error and '. + 'run "arc land" again.', + $target->getRef(), + $target->getRemote())); + } + + // TODO: If the remote is a bare URI, we could read ".git/FETCH_HEAD" + // here and write the commit into the map. For now, settle for clearing + // the cache. + + // We could also fetch into some named "refs/arc-land-temporary" named + // ref, then read that. + + if (!$err) { + $target_key = $target->getLandTargetKey(); + unset($this->landTargetCommitMap[$target_key]); + } + } + + protected function selectCommits($into_commit, array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + $commit_map = array(); + foreach ($symbols as $symbol) { + $symbol_commit = $symbol->getCommit(); + $format = '%H%x00%P%x00%s%x00'; + + if ($into_commit === null) { + list($commits) = $api->execxLocal( + 'log %s --format=%s', + $symbol_commit, + $format); + } else { + list($commits) = $api->execxLocal( + 'log %s --not %s --format=%s', + $symbol_commit, + $into_commit, + $format); + } + + $commits = phutil_split_lines($commits, false); + foreach ($commits as $line) { + if (!strlen($line)) { + continue; + } + + $parts = explode("\0", $line, 4); + if (count($parts) < 3) { + throw new Exception( + pht( + 'Unexpected output from "git log ...": %s', + $line)); + } + + $hash = $parts[0]; + if (!isset($commit_map[$hash])) { + $parents = $parts[1]; + $parents = trim($parents); + if (strlen($parents)) { + $parents = explode(' ', $parents); + } else { + $parents = array(); + } + + $summary = $parts[2]; + + $commit_map[$hash] = id(new ArcanistLandCommit()) + ->setHash($hash) + ->setParents($parents) + ->setSummary($summary); + } + + $commit = $commit_map[$hash]; + $commit->addSymbol($symbol); + } + } + + return $this->confirmCommits($into_commit, $symbols, $commit_map); + } + + protected function getDefaultSymbols() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $branch = $api->getBranchName(); + if ($branch === null) { + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current branch, "%s".', + $branch)); + + return array($branch); + } + + $commit = $api->getCurrentCommitRef(); + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current HEAD, "%s".', + $commit->getCommitHash())); + + return array($branch); + } + +} diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php new file mode 100644 index 00000000..051a21ae --- /dev/null +++ b/src/land/engine/ArcanistLandEngine.php @@ -0,0 +1,1430 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setOntoRemote($onto_remote) { + $this->ontoRemote = $onto_remote; + return $this; + } + + final public function getOntoRemote() { + return $this->ontoRemote; + } + + final public function setOntoRefs($onto_refs) { + $this->ontoRefs = $onto_refs; + return $this; + } + + final public function getOntoRefs() { + return $this->ontoRefs; + } + + final public function setIntoRemote($into_remote) { + $this->intoRemote = $into_remote; + return $this; + } + + final public function getIntoRemote() { + return $this->intoRemote; + } + + final public function setIntoRef($into_ref) { + $this->intoRef = $into_ref; + return $this; + } + + final public function getIntoRef() { + return $this->intoRef; + } + + final public function setIntoEmpty($into_empty) { + $this->intoEmpty = $into_empty; + return $this; + } + + final public function getIntoEmpty() { + return $this->intoEmpty; + } + + final public function setIntoLocal($into_local) { + $this->intoLocal = $into_local; + return $this; + } + + final public function getIntoLocal() { + return $this->intoLocal; + } + + final public function setWorkflow($workflow) { + $this->workflow = $workflow; + return $this; + } + + final public function getWorkflow() { + return $this->workflow; + } + + final public function setRepositoryAPI( + ArcanistRepositoryAPI $repository_api) { + $this->repositoryAPI = $repository_api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + final public function setLogEngine(ArcanistLogEngine $log_engine) { + $this->logEngine = $log_engine; + return $this; + } + + final public function getLogEngine() { + return $this->logEngine; + } + + final public function setShouldHold($should_hold) { + $this->shouldHold = $should_hold; + return $this; + } + + final public function getShouldHold() { + return $this->shouldHold; + } + + final public function setShouldKeep($should_keep) { + $this->shouldKeep = $should_keep; + return $this; + } + + final public function getShouldKeep() { + return $this->shouldKeep; + } + + final public function setStrategy($strategy) { + $this->strategy = $strategy; + return $this; + } + + final public function getStrategy() { + return $this->strategy; + } + + final public function setRevisionSymbol($revision_symbol) { + $this->revisionSymbol = $revision_symbol; + return $this; + } + + final public function getRevisionSymbol() { + return $this->revisionSymbol; + } + + final public function setRevisionSymbolRef( + ArcanistRevisionSymbolRef $revision_ref) { + $this->revisionSymbolRef = $revision_ref; + return $this; + } + + final public function getRevisionSymbolRef() { + return $this->revisionSymbolRef; + } + + final public function setShouldPreview($should_preview) { + $this->shouldPreview = $should_preview; + return $this; + } + + final public function getShouldPreview() { + return $this->shouldPreview; + } + + final public function setSourceRefs(array $source_refs) { + $this->sourceRefs = $source_refs; + return $this; + } + + final public function getSourceRefs() { + return $this->sourceRefs; + } + + final public function setOntoRemoteArgument($remote_argument) { + $this->ontoRemoteArgument = $remote_argument; + return $this; + } + + final public function getOntoRemoteArgument() { + return $this->ontoRemoteArgument; + } + + final public function setOntoArguments(array $onto_arguments) { + $this->ontoArguments = $onto_arguments; + return $this; + } + + final public function getOntoArguments() { + return $this->ontoArguments; + } + + final public function setIsIncremental($is_incremental) { + $this->isIncremental = $is_incremental; + return $this; + } + + final public function getIsIncremental() { + return $this->isIncremental; + } + + final public function setIntoEmptyArgument($into_empty_argument) { + $this->intoEmptyArgument = $into_empty_argument; + return $this; + } + + final public function getIntoEmptyArgument() { + return $this->intoEmptyArgument; + } + + final public function setIntoLocalArgument($into_local_argument) { + $this->intoLocalArgument = $into_local_argument; + return $this; + } + + final public function getIntoLocalArgument() { + return $this->intoLocalArgument; + } + + final public function setIntoRemoteArgument($into_remote_argument) { + $this->intoRemoteArgument = $into_remote_argument; + return $this; + } + + final public function getIntoRemoteArgument() { + return $this->intoRemoteArgument; + } + + final public function setIntoArgument($into_argument) { + $this->intoArgument = $into_argument; + return $this; + } + + final public function getIntoArgument() { + return $this->intoArgument; + } + + final protected function getOntoFromConfiguration() { + $config_key = $this->getOntoConfigurationKey(); + return $this->getWorkflow()->getConfig($config_key); + } + + final protected function getOntoConfigurationKey() { + return 'arc.land.onto'; + } + + final protected function getOntoRemoteFromConfiguration() { + $config_key = $this->getOntoRemoteConfigurationKey(); + return $this->getWorkflow()->getConfig($config_key); + } + + final protected function getOntoRemoteConfigurationKey() { + return 'arc.land.onto-remote'; + } + + final protected function confirmRevisions(array $sets) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + + $revision_refs = mpull($sets, 'getRevisionRef'); + $viewer = $this->getViewer(); + $viewer_phid = $viewer->getPHID(); + + $unauthored = array(); + foreach ($revision_refs as $revision_ref) { + $author_phid = $revision_ref->getAuthorPHID(); + if ($author_phid !== $viewer_phid) { + $unauthored[] = $revision_ref; + } + } + + if ($unauthored) { + $this->getWorkflow()->loadHardpoints( + $unauthored, + array( + ArcanistRevisionRef::HARDPOINT_AUTHORREF, + )); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('NOT REVISION AUTHOR'), + pht( + 'You are landing revisions which you ("%s") are not the author of:', + $viewer->getMonogram())); + + foreach ($unauthored as $revision_ref) { + $display_ref = $revision_ref->newDisplayRef(); + + $author_ref = $revision_ref->getAuthorRef(); + if ($author_ref) { + $display_ref->appendLine( + pht( + 'Author: %s', + $author_ref->getMonogram())); + } + + echo tsprintf('%s', $display_ref); + } + + echo tsprintf( + "\n%?\n", + pht( + 'Use "Commandeer" in the web interface to become the author of '. + 'a revision.')); + + $query = pht('Land revisions you are not the author of?'); + + $this->getWorkflow() + ->getPrompt('arc.land.unauthored') + ->setQuery($query) + ->execute(); + } + + $planned = array(); + $closed = array(); + $not_accepted = array(); + foreach ($revision_refs as $revision_ref) { + if ($revision_ref->isStatusChangesPlanned()) { + $planned[] = $revision_ref; + } else if ($revision_ref->isStatusClosed()) { + $closed[] = $revision_ref; + } else if (!$revision_ref->isStatusAccepted()) { + $not_accepted[] = $revision_ref; + } + } + + // See T10233. Previously, this prompt was bundled with the generic "not + // accepted" prompt, but users found it confusing and interpreted the + // prompt as a bug. + + if ($planned) { + $example_ref = head($planned); + + echo tsprintf( + "\n%!\n%W\n\n%W\n\n%W\n\n", + pht('%s REVISION(S) HAVE CHANGES PLANNED', phutil_count($planned)), + pht( + 'You are landing %s revision(s) which are currently in the state '. + '"%s", indicating that you expect to revise them before moving '. + 'forward.', + phutil_count($planned), + $example_ref->getStatusDisplayName()), + pht( + 'Normally, you should update these %s revision(s), submit them '. + 'for review, and wait for reviewers to accept them before '. + 'you continue. To resubmit a revision for review, either: '. + 'update the revision with revised changes; or use '. + '"Request Review" from the web interface.', + phutil_count($planned)), + pht( + 'These %s revision(s) have changes planned:', + phutil_count($planned))); + + foreach ($planned as $revision_ref) { + echo tsprintf('%s', $revision_ref->newDisplayRef()); + } + + $query = pht( + 'Land %s revision(s) with changes planned?', + phutil_count($planned)); + + $this->getWorkflow() + ->getPrompt('arc.land.changes-planned') + ->setQuery($query) + ->execute(); + } + + // See PHI1727. Previously, this prompt was bundled with the generic + // "not accepted" prompt, but at least one user found it confusing. + + if ($closed) { + $example_ref = head($closed); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s REVISION(S) ARE ALREADY CLOSED', phutil_count($closed)), + pht( + 'You are landing %s revision(s) which are already in the state '. + '"%s", indicating that they have previously landed:', + phutil_count($closed), + $example_ref->getStatusDisplayName())); + + foreach ($closed as $revision_ref) { + echo tsprintf('%s', $revision_ref->newDisplayRef()); + } + + $query = pht( + 'Land %s revision(s) that are already closed?', + phutil_count($closed)); + + $this->getWorkflow() + ->getPrompt('arc.land.closed') + ->setQuery($query) + ->execute(); + } + + if ($not_accepted) { + $example_ref = head($not_accepted); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s REVISION(S) ARE NOT ACCEPTED', phutil_count($not_accepted)), + pht( + 'You are landing %s revision(s) which are not in state "Accepted", '. + 'indicating that they have not been accepted by reviewers. '. + 'Normally, you should land changes only once they have been '. + 'accepted. These revisions are in the wrong state:', + phutil_count($not_accepted))); + + foreach ($not_accepted as $revision_ref) { + $display_ref = $revision_ref->newDisplayRef(); + $display_ref->appendLine( + pht( + 'Status: %s', + $revision_ref->getStatusDisplayName())); + echo tsprintf('%s', $display_ref); + } + + $query = pht( + 'Land %s revision(s) in the wrong state?', + phutil_count($not_accepted)); + + $this->getWorkflow() + ->getPrompt('arc.land.not-accepted') + ->setQuery($query) + ->execute(); + } + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_PARENTREVISIONREFS, + )); + + $open_parents = array(); + foreach ($revision_refs as $revision_phid => $revision_ref) { + $parent_refs = $revision_ref->getParentRevisionRefs(); + foreach ($parent_refs as $parent_ref) { + $parent_phid = $parent_ref->getPHID(); + + // If we're landing a parent revision in this operation, we don't need + // to complain that it hasn't been closed yet. + if (isset($revision_refs[$parent_phid])) { + continue; + } + + if ($parent_ref->isClosed()) { + continue; + } + + if (!isset($open_parents[$parent_phid])) { + $open_parents[$parent_phid] = array( + 'ref' => $parent_ref, + 'children' => array(), + ); + } + + $open_parents[$parent_phid]['children'][] = $revision_ref; + } + } + + if ($open_parents) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('%s OPEN PARENT REVISION(S) ', phutil_count($open_parents)), + pht( + 'The changes you are landing depend on %s open parent revision(s). '. + 'Usually, you should land parent revisions before landing the '. + 'changes which depend on them. These parent revisions are open:', + phutil_count($open_parents))); + + foreach ($open_parents as $parent_phid => $spec) { + $parent_ref = $spec['ref']; + + $display_ref = $parent_ref->newDisplayRef(); + + $display_ref->appendLine( + pht( + 'Status: %s', + $parent_ref->getStatusDisplayName())); + + foreach ($spec['children'] as $child_ref) { + $display_ref->appendLine( + pht( + 'Parent of: %s %s', + $child_ref->getMonogram(), + $child_ref->getName())); + } + + echo tsprintf('%s', $display_ref); + } + + $query = pht( + 'Land changes that depend on %s open revision(s)?', + phutil_count($open_parents)); + + $this->getWorkflow() + ->getPrompt('arc.land.open-parents') + ->setQuery($query) + ->execute(); + } + + $this->confirmBuilds($revision_refs); + + // This is a reasonable place to bulk-load the commit messages, which + // we'll need soon. + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_COMMITMESSAGE, + )); + } + + private function confirmBuilds(array $revision_refs) { + assert_instances_of($revision_refs, 'ArcanistRevisionRef'); + + $this->getWorkflow()->loadHardpoints( + $revision_refs, + array( + ArcanistRevisionRef::HARDPOINT_BUILDABLEREF, + )); + + $buildable_refs = array(); + foreach ($revision_refs as $revision_ref) { + $ref = $revision_ref->getBuildableRef(); + if ($ref) { + $buildable_refs[] = $ref; + } + } + + $this->getWorkflow()->loadHardpoints( + $buildable_refs, + array( + ArcanistBuildableRef::HARDPOINT_BUILDREFS, + )); + + $build_refs = array(); + foreach ($buildable_refs as $buildable_ref) { + foreach ($buildable_ref->getBuildRefs() as $build_ref) { + $build_refs[] = $build_ref; + } + } + + $this->getWorkflow()->loadHardpoints( + $build_refs, + array( + ArcanistBuildRef::HARDPOINT_BUILDPLANREF, + )); + + $problem_builds = array(); + $has_failures = false; + $has_ongoing = false; + + $build_refs = msortv($build_refs, 'getStatusSortVector'); + foreach ($build_refs as $build_ref) { + $plan_ref = $build_ref->getBuildPlanRef(); + if (!$plan_ref) { + continue; + } + + $plan_behavior = $plan_ref->getBehavior('arc-land', 'always'); + $if_building = ($plan_behavior == 'building'); + $if_complete = ($plan_behavior == 'complete'); + $if_never = ($plan_behavior == 'never'); + + // If the build plan "Never" warns when landing, skip it. + if ($if_never) { + continue; + } + + // If the build plan warns when landing "If Complete" but the build is + // not complete, skip it. + if ($if_complete && !$build_ref->isComplete()) { + continue; + } + + // If the build plan warns when landing "If Building" but the build is + // complete, skip it. + if ($if_building && $build_ref->isComplete()) { + continue; + } + + // Ignore passing builds. + if ($build_ref->isPassed()) { + continue; + } + + if ($build_ref->isComplete()) { + $has_failures = true; + } else { + $has_ongoing = true; + } + + $problem_builds[] = $build_ref; + } + + if (!$problem_builds) { + return; + } + + $build_map = array(); + $failure_map = array(); + $buildable_map = mpull($buildable_refs, null, 'getPHID'); + $revision_map = mpull($revision_refs, null, 'getDiffPHID'); + foreach ($problem_builds as $build_ref) { + $buildable_phid = $build_ref->getBuildablePHID(); + $buildable_ref = $buildable_map[$buildable_phid]; + + $object_phid = $buildable_ref->getObjectPHID(); + $revision_ref = $revision_map[$object_phid]; + + $revision_phid = $revision_ref->getPHID(); + + if (!isset($build_map[$revision_phid])) { + $build_map[$revision_phid] = array( + 'revisionRef' => $revision_phid, + 'buildRefs' => array(), + ); + } + + $build_map[$revision_phid]['buildRefs'][] = $build_ref; + } + + $log = $this->getLogEngine(); + + if ($has_failures) { + if ($has_ongoing) { + $message = pht( + '%s revision(s) have build failures or ongoing builds:', + phutil_count($build_map)); + + $query = pht( + 'Land %s revision(s) anyway, despite ongoing and failed builds?', + phutil_count($build_map)); + + } else { + $message = pht( + '%s revision(s) have build failures:', + phutil_count($build_map)); + + $query = pht( + 'Land %s revision(s) anyway, despite failed builds?', + phutil_count($build_map)); + } + + echo tsprintf( + "%!\n%s\n\n", + pht('BUILD FAILURES'), + $message); + + $prompt_key = 'arc.land.failed-builds'; + } else if ($has_ongoing) { + echo tsprintf( + "%!\n%s\n\n", + pht('ONGOING BUILDS'), + pht( + '%s revision(s) have ongoing builds:', + phutil_count($build_map))); + + $query = pht( + 'Land %s revision(s) anyway, despite ongoing builds?', + phutil_count($build_map)); + + $prompt_key = 'arc.land.ongoing-builds'; + } + + echo tsprintf("\n"); + foreach ($build_map as $build_item) { + $revision_ref = $build_item['revisionRef']; + + echo tsprintf('%s', $revision_ref->newDisplayRef()); + + foreach ($build_item['buildRefs'] as $build_ref) { + echo tsprintf('%s', $build_ref->newDisplayRef()); + } + + echo tsprintf("\n"); + } + + echo tsprintf( + "\n%s\n\n", + pht('You can review build details here:')); + + // TODO: Only show buildables with problem builds. + + foreach ($buildable_refs as $buildable) { + $display_ref = $buildable->newDisplayRef(); + + // TODO: Include URI here. + + echo tsprintf('%s', $display_ref); + } + + $this->getWorkflow() + ->getPrompt($prompt_key) + ->setQuery($query) + ->execute(); + } + + final protected function confirmImplicitCommits(array $sets, array $symbols) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + assert_instances_of($symbols, 'ArcanistLandSymbol'); + + $implicit = array(); + foreach ($sets as $set) { + if ($set->hasImplicitCommits()) { + $implicit[] = $set; + } + } + + if (!$implicit) { + return; + } + + echo tsprintf( + "\n%!\n%W\n", + pht('IMPLICIT COMMITS'), + pht( + 'Some commits reachable from the specified sources (%s) are not '. + 'associated with revisions, and may not have been reviewed. These '. + 'commits will be landed as though they belong to the nearest '. + 'ancestor revision:', + $this->getDisplaySymbols($symbols))); + + foreach ($implicit as $set) { + $this->printCommitSet($set); + } + + $query = pht( + 'Continue with this mapping between commits and revisions?'); + + $this->getWorkflow() + ->getPrompt('arc.land.implicit') + ->setQuery($query) + ->execute(); + } + + final protected function getDisplaySymbols(array $symbols) { + $display = array(); + + foreach ($symbols as $symbol) { + $display[] = sprintf('"%s"', addcslashes($symbol->getSymbol(), '\\"')); + } + + return implode(', ', $display); + } + + final protected function printCommitSet(ArcanistLandCommitSet $set) { + $revision_ref = $set->getRevisionRef(); + + echo tsprintf( + "\n%s", + $revision_ref->newDisplayRef()); + + foreach ($set->getCommits() as $commit) { + $is_implicit = $commit->getIsImplicitCommit(); + + $display_hash = $this->getDisplayHash($commit->getHash()); + $display_summary = $commit->getDisplaySummary(); + + if ($is_implicit) { + echo tsprintf( + " %s %s\n", + $display_hash, + $display_summary); + } else { + echo tsprintf( + " %s %s\n", + $display_hash, + $display_summary); + } + } + } + + final protected function loadRevisionRefs(array $commit_map) { + assert_instances_of($commit_map, 'ArcanistLandCommit'); + $workflow = $this->getWorkflow(); + + $state_refs = array(); + foreach ($commit_map as $commit) { + $hash = $commit->getHash(); + + $commit_ref = id(new ArcanistCommitRef()) + ->setCommitHash($hash); + + $state_ref = id(new ArcanistWorkingCopyStateRef()) + ->setCommitRef($commit_ref); + + $state_refs[$hash] = $state_ref; + } + + $force_symbol_ref = $this->getRevisionSymbolRef(); + $force_ref = null; + if ($force_symbol_ref) { + $workflow->loadHardpoints( + $force_symbol_ref, + ArcanistSymbolRef::HARDPOINT_OBJECT); + + $force_ref = $force_symbol_ref->getObject(); + if (!$force_ref) { + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" does not identify a valid revision.', + $force_symbol_ref->getSymbol())); + } + } + + $workflow->loadHardpoints( + $state_refs, + ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); + + foreach ($commit_map as $commit) { + $hash = $commit->getHash(); + $state_ref = $state_refs[$hash]; + + $revision_refs = $state_ref->getRevisionRefs(); + + // If we have several possible revisions but one of them matches the + // "--revision" argument, just select it. This is relatively safe and + // reasonable and doesn't need a warning. + + if ($force_ref) { + if (count($revision_refs) > 1) { + foreach ($revision_refs as $revision_ref) { + if ($revision_ref->getPHID() === $force_ref->getPHID()) { + $revision_refs = array($revision_ref); + break; + } + } + } + } + + if (count($revision_refs) === 1) { + $revision_ref = head($revision_refs); + $commit->setExplicitRevisionRef($revision_ref); + continue; + } + + if (!$revision_refs) { + continue; + } + + // TODO: If we have several refs but all but one are abandoned or closed + // or authored by someone else, we could guess what you mean. + + $symbols = $commit->getSymbols(); + $raw_symbols = mpull($symbols, 'getSymbol'); + $symbol_list = implode(', ', $raw_symbols); + $display_hash = $this->getDisplayHash($hash); + + // TODO: Include "use 'arc look --type commit abc' to figure out why" + // once that works? + + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS REVISION'), + pht( + 'The revision associated with commit "%s" (an ancestor of: %s) '. + 'is ambiguous. These %s revision(s) are associated with the commit:', + $display_hash, + implode(', ', $raw_symbols), + phutil_count($revision_refs))); + + foreach ($revision_refs as $revision_ref) { + echo tsprintf( + '%s', + $revision_ref->newDisplayRef()); + } + + echo tsprintf("\n"); + + throw new PhutilArgumentUsageException( + pht( + 'Revision for commit "%s" is ambiguous. Use "--revision" to force '. + 'selection of a particular revision.', + $display_hash)); + } + + // TODO: Some of the revisions we've identified may be mapped to an + // outdated set of commits. We should look in local branches for a better + // set of commits, and try to confirm that the state we're about to land + // is the current state in Differential. + + if ($force_ref) { + $phid_map = array(); + foreach ($commit_map as $commit) { + $explicit_ref = $commit->getExplicitRevisionRef(); + if ($explicit_ref) { + $revision_phid = $explicit_ref->getPHID(); + $phid_map[$revision_phid] = $revision_phid; + } + } + + $force_phid = $force_ref->getPHID(); + + // If none of the commits have a revision, forcing the revision is + // reasonable and we don't need to confirm it. + + // If some of the commits have a revision, but it's the same as the + // revision we're forcing, forcing the revision is also reasonable. + + // Discard the revision we're trying to force, then check if there's + // anything left. If some of the commits have a different revision, + // make sure the user is really doing what they expect. + + unset($phid_map[$force_phid]); + + if ($phid_map) { + // TODO: Make this more clear. + + throw new PhutilArgumentUsageException( + pht( + 'TODO: You are forcing a revision, but commits are associated '. + 'with some other revision. Are you REALLY sure you want to land '. + 'ALL these commits wiht a different unrelated revision???')); + } + + foreach ($commit_map as $commit) { + $commit->setExplicitRevisionRef($force_ref); + } + } + } + + final protected function getDisplayHash($hash) { + // TODO: This should be on the API object. + return substr($hash, 0, 12); + } + + final protected function confirmCommits( + $into_commit, + array $symbols, + array $commit_map) { + + $commit_count = count($commit_map); + + if (!$commit_count) { + $message = pht( + 'There are no commits reachable from the specified sources (%s) '. + 'which are not already present in the state you are merging '. + 'into ("%s"), so nothing can land.', + $this->getDisplaySymbols($symbols), + $this->getDisplayHash($into_commit)); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('NOTHING TO LAND'), + $message); + + throw new PhutilArgumentUsageException( + pht('There are no commits to land.')); + } + + // Reverse the commit list so that it's oldest-first, since this is the + // order we'll use to show revisions. + $commit_map = array_reverse($commit_map, true); + + $warn_limit = $this->getWorkflow()->getLargeWorkingSetLimit(); + $show_limit = 5; + if ($commit_count > $warn_limit) { + if ($into_commit === null) { + $message = pht( + 'There are %s commit(s) reachable from the specified sources (%s). '. + 'You are landing into the empty state, so all of these commits '. + 'will land:', + new PhutilNumber($commit_count), + $this->getDisplaySymbols($symbols)); + } else { + $message = pht( + 'There are %s commit(s) reachable from the specified sources (%s) '. + 'that are not present in the repository state you are merging '. + 'into ("%s"). All of these commits will land:', + new PhutilNumber($commit_count), + $this->getDisplaySymbols($symbols), + $this->getDisplayHash($into_commit)); + } + + echo tsprintf( + "\n%!\n%W\n", + pht('LARGE WORKING SET'), + $message); + + $display_commits = array_merge( + array_slice($commit_map, 0, $show_limit), + array(null), + array_slice($commit_map, -$show_limit)); + + echo tsprintf("\n"); + + foreach ($display_commits as $commit) { + if ($commit === null) { + echo tsprintf( + " %s\n", + pht( + '< ... %s more commits ... >', + new PhutilNumber($commit_count - ($show_limit * 2)))); + } else { + echo tsprintf( + " %s %s\n", + $this->getDisplayHash($commit->getHash()), + $commit->getDisplaySummary()); + } + } + + $query = pht( + 'Land %s commit(s)?', + new PhutilNumber($commit_count)); + + $this->getWorkflow() + ->getPrompt('arc.land.large-working-set') + ->setQuery($query) + ->execute(); + } + + // Build the commit objects into a tree. + foreach ($commit_map as $commit_hash => $commit) { + $parent_map = array(); + foreach ($commit->getParents() as $parent) { + if (isset($commit_map[$parent])) { + $parent_map[$parent] = $commit_map[$parent]; + } + } + $commit->setParentCommits($parent_map); + } + + // Identify the commits which are heads (have no children). + $child_map = array(); + foreach ($commit_map as $commit_hash => $commit) { + foreach ($commit->getParents() as $parent) { + $child_map[$parent][$commit_hash] = $commit; + } + } + + foreach ($commit_map as $commit_hash => $commit) { + if (isset($child_map[$commit_hash])) { + continue; + } + $commit->setIsHeadCommit(true); + } + + return $commit_map; + } + + public function execute() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $this->validateArguments(); + + $raw_symbols = $this->getSourceRefs(); + if (!$raw_symbols) { + $raw_symbols = $this->getDefaultSymbols(); + } + + $symbols = array(); + foreach ($raw_symbols as $raw_symbol) { + $symbols[] = id(new ArcanistLandSymbol()) + ->setSymbol($raw_symbol); + } + + $this->resolveSymbols($symbols); + + $onto_remote = $this->selectOntoRemote($symbols); + $this->setOntoRemote($onto_remote); + + $onto_refs = $this->selectOntoRefs($symbols); + $this->confirmOntoRefs($onto_refs); + $this->setOntoRefs($onto_refs); + + $this->selectIntoRemote(); + $this->selectIntoRef(); + + $into_commit = $this->selectIntoCommit(); + $commit_map = $this->selectCommits($into_commit, $symbols); + + $this->loadRevisionRefs($commit_map); + + // TODO: It's possible we have a list of commits which includes disjoint + // groups of commits associated with the same revision, or groups of + // commits which do not form a range. We should test that here, since we + // can't land commit groups which are not a single contiguous range. + + $revision_groups = array(); + foreach ($commit_map as $commit_hash => $commit) { + $revision_ref = $commit->getRevisionRef(); + + if (!$revision_ref) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('UNKNOWN REVISION'), + pht( + 'Unable to determine which revision is associated with commit '. + '"%s". Use "arc diff" to create or update a revision with this '. + 'commit, or "--revision" to force selection of a particular '. + 'revision.', + $this->getDisplayHash($commit_hash))); + + throw new PhutilArgumentUsageException( + pht( + 'Unable to determine revision for commit "%s".', + $this->getDisplayHash($commit_hash))); + } + + $revision_groups[$revision_ref->getPHID()][] = $commit; + } + + $commit_heads = array(); + foreach ($commit_map as $commit) { + if ($commit->getIsHeadCommit()) { + $commit_heads[] = $commit; + } + } + + $revision_order = array(); + foreach ($commit_heads as $head) { + foreach ($head->getAncestorRevisionPHIDs() as $phid) { + $revision_order[$phid] = true; + } + } + + $revision_groups = array_select_keys( + $revision_groups, + array_keys($revision_order)); + + $sets = array(); + foreach ($revision_groups as $revision_phid => $group) { + $revision_ref = head($group)->getRevisionRef(); + + $set = id(new ArcanistLandCommitSet()) + ->setRevisionRef($revision_ref) + ->setCommits($group); + + $sets[$revision_phid] = $set; + } + + if (!$this->getShouldPreview()) { + $this->confirmImplicitCommits($sets, $symbols); + } + + $log->writeStatus( + pht('LANDING'), + pht('These changes will land:')); + + foreach ($sets as $set) { + $this->printCommitSet($set); + } + + if ($this->getShouldPreview()) { + $log->writeStatus( + pht('PREVIEW'), + pht('Completed preview of land operation.')); + return; + } + + $query = pht('Land these changes?'); + $this->getWorkflow() + ->getPrompt('arc.land.confirm') + ->setQuery($query) + ->execute(); + + $this->confirmRevisions($sets); + + $workflow = $this->getWorkflow(); + + $is_incremental = $this->getIsIncremental(); + $is_hold = $this->getShouldHold(); + $is_keep = $this->getShouldKeep(); + + $local_state = $api->newLocalState() + ->setWorkflow($workflow) + ->saveLocalState(); + + $seen_into = array(); + try { + $last_key = last_key($sets); + + $need_cascade = array(); + $need_prune = array(); + + foreach ($sets as $set_key => $set) { + // Add these first, so we don't add them multiple times if we need + // to retry a push. + $need_prune[] = $set; + $need_cascade[] = $set; + + while (true) { + $into_commit = $this->executeMerge($set, $into_commit); + + if ($is_hold) { + $should_push = false; + } else if ($is_incremental) { + $should_push = true; + } else { + $is_last = ($set_key === $last_key); + $should_push = $is_last; + } + + if ($should_push) { + try { + $this->pushChange($into_commit); + } catch (Exception $ex) { + + // TODO: If the push fails, fetch and retry if the remote ref + // has moved ahead of us. + + if ($this->getIntoLocal()) { + $can_retry = false; + } else if ($this->getIntoEmpty()) { + $can_retry = false; + } else if ($this->getIntoRemote() !== $this->getOntoRemote()) { + $can_retry = false; + } else { + $can_retry = false; + } + + if ($can_retry) { + // New commit state here + $into_commit = '..'; + continue; + } + + throw $ex; + } + + if ($need_cascade) { + + // NOTE: We cascade each set we've pushed, but we're going to + // cascade them from most recent to least recent. This way, + // branches which descend from more recent changes only cascade + // once, directly in to the correct state. + + $need_cascade = array_reverse($need_cascade); + foreach ($need_cascade as $cascade_set) { + $this->cascadeState($set, $into_commit); + } + $need_cascade = array(); + } + + if (!$is_keep) { + $this->pruneBranches($need_prune); + $need_prune = array(); + } + } + + break; + } + } + + if ($is_hold) { + $this->didHoldChanges(); + $this->discardLocalState(); + } else { + $this->reconcileLocalState($into_commit, $local_state); + } + + // TODO: Restore this. + // $this->getWorkflow()->askForRepositoryUpdate(); + + $log->writeSuccess( + pht('DONE'), + pht('Landed changes.')); + } catch (Exception $ex) { + $local_state->restoreLocalState(); + throw $ex; + } catch (Throwable $ex) { + $local_state->restoreLocalState(); + throw $ex; + } + } + + + protected function validateArguments() { + $log = $this->getLogEngine(); + + $into_local = $this->getIntoLocalArgument(); + $into_empty = $this->getIntoEmptyArgument(); + $into_remote = $this->getIntoRemoteArgument(); + + $into_count = 0; + if ($into_remote !== null) { + $into_count++; + } + + if ($into_local) { + $into_count++; + } + + if ($into_empty) { + $into_count++; + } + + if ($into_count > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Arguments "--into-local", "--into-remote", and "--into-empty" '. + 'are mutually exclusive.')); + } + + $into = $this->getIntoArgument(); + if ($into && ($into_empty !== null)) { + throw new PhutilArgumentUsageException( + pht( + 'Arguments "--into" and "--into-empty" are mutually exclusive.')); + } + + $strategy = $this->selectMergeStrategy(); + $this->setStrategy($strategy); + + // Build the symbol ref here (which validates the format of the symbol), + // but don't load the object until later on when we're sure we actually + // need it, since loading it requires a relatively expensive Conduit call. + $revision_symbol = $this->getRevisionSymbol(); + if ($revision_symbol) { + $symbol_ref = id(new ArcanistRevisionSymbolRef()) + ->setSymbol($revision_symbol); + $this->setRevisionSymbolRef($symbol_ref); + } + + // NOTE: When a user provides: "--hold" or "--preview"; and "--incremental" + // or various combinations of remote flags, the flags affecting push/remote + // behavior have no effect. + + // These combinations are allowed to support adding "--preview" or "--hold" + // to any command to run the same command with fewer side effects. + } + + abstract protected function getDefaultSymbols(); + abstract protected function resolveSymbols(array $symbols); + abstract protected function selectOntoRemote(array $symbols); + abstract protected function selectOntoRefs(array $symbols); + abstract protected function confirmOntoRefs(array $onto_refs); + abstract protected function selectIntoRemote(); + abstract protected function selectIntoRef(); + abstract protected function selectIntoCommit(); + abstract protected function selectCommits($into_commit, array $symbols); + abstract protected function executeMerge( + ArcanistLandCommitSet $set, + $into_commit); + abstract protected function pushChange($into_commit); + abstract protected function cascadeState( + ArcanistLandCommitSet $set, + $into_commit); + + protected function isSquashStrategy() { + return ($this->getStrategy() === 'squash'); + } + + abstract protected function pruneBranches(array $sets); + + abstract protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state); + + private function selectMergeStrategy() { + $log = $this->getLogEngine(); + + $supported_strategies = array( + 'merge', + 'squash', + ); + $supported_strategies = array_fuse($supported_strategies); + $strategy_list = implode(', ', $supported_strategies); + + $strategy = $this->getStrategyArgument(); + if ($strategy !== null) { + if (!isset($supported_strategies[$strategy])) { + throw new PhutilArgumentUsageException( + pht( + 'Merge strategy "%s" specified with "--strategy" is unknown. '. + 'Supported merge strategies are: %s.', + $strategy, + $strategy_list)); + } + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, selected with "--strategy".', + $strategy)); + + return $strategy; + } + + $strategy_key = 'arc.land.strategy'; + $strategy = $this->getWorkflow()->getConfig($strategy_key); + if ($strategy !== null) { + if (!isset($supported_strategies[$strategy])) { + throw new PhutilArgumentUsageException( + pht( + 'Merge strategy "%s" specified in "%s" configuration is '. + 'unknown. Supported merge strategies are: %s.', + $strategy, + $strategy_list)); + } + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, configured with "%s".', + $strategy, + $strategy_key)); + + return $strategy; + } + + $strategy = 'squash'; + + $log->writeStatus( + pht('STRATEGY'), + pht( + 'Merging with "%s" strategy, the default strategy.', + $strategy)); + + return $strategy; + } + +} diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php new file mode 100644 index 00000000..cc48de66 --- /dev/null +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -0,0 +1,603 @@ +getRepositoryAPI(); + $log = $this->getLogEngine(); + + $bookmark = $api->getActiveBookmark(); + if ($bookmark !== null) { + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the active bookmark, "%s".', + $bookmark)); + + return array($bookmark); + } + + $branch = $api->getBranchName(); + if ($branch !== null) { + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the current branch, "%s".', + $branch)); + + return array($branch); + } + + throw new Exception(pht('TODO: Operate on raw revision.')); + } + + protected function resolveSymbols(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + foreach ($symbols as $symbol) { + $raw_symbol = $symbol->getSymbol(); + + if ($api->isBookmark($raw_symbol)) { + $hash = $api->getBookmarkCommitHash($raw_symbol); + $symbol->setCommit($hash); + + // TODO: Set that this is a bookmark? + + continue; + } + + if ($api->isBranch($raw_symbol)) { + $hash = $api->getBranchCommitHash($raw_symbol); + $symbol->setCommit($hash); + + // TODO: Set that this is a branch? + + continue; + } + + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" is not a bookmark or branch name.', + $raw_symbol)); + } + } + + protected function selectOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $remote = $this->newOntoRemote($symbols); + + // TODO: Verify this remote actually exists. + + return $remote; + } + + private function newOntoRemote(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $remote = $this->getOntoRemoteArgument(); + if ($remote !== null) { + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected with the "--onto-remote" flag.', + $remote)); + + return $remote; + } + + $remote = $this->getOntoRemoteFromConfiguration(); + if ($remote !== null) { + $remote_key = $this->getOntoRemoteConfigurationKey(); + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Remote "%s" was selected by reading "%s" configuration.', + $remote, + $remote_key)); + + return $remote; + } + + $api = $this->getRepositoryAPI(); + + $default_remote = 'default'; + + $log->writeStatus( + pht('ONTO REMOTE'), + pht( + 'Landing onto remote "%s", the default remote under Mercurial.', + $default_remote)); + + return $default_remote; + } + + protected function selectOntoRefs(array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $log = $this->getLogEngine(); + + $onto = $this->getOntoArguments(); + if ($onto) { + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected with the "--onto" flag: %s.', + implode(', ', $onto))); + + return $onto; + } + + $onto = $this->getOntoFromConfiguration(); + if ($onto) { + $onto_key = $this->getOntoConfigurationKey(); + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Refs were selected by reading "%s" configuration: %s.', + $onto_key, + implode(', ', $onto))); + + return $onto; + } + + $api = $this->getRepositoryAPI(); + + $default_onto = 'default'; + + $log->writeStatus( + pht('ONTO TARGET'), + pht( + 'Landing onto target "%s", the default target under Mercurial.', + $default_onto)); + + return array($default_onto); + } + + protected function confirmOntoRefs(array $onto_refs) { + foreach ($onto_refs as $onto_ref) { + if (!strlen($onto_ref)) { + throw new PhutilArgumentUsageException( + pht( + 'Selected "onto" ref "%s" is invalid: the empty string is not '. + 'a valid ref.', + $onto_ref)); + } + } + } + + protected function selectIntoRemote() { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + if ($this->getIntoLocalArgument()) { + $this->setIntoLocal(true); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into local state, selected with the "--into-local" '. + 'flag.')); + + return; + } + + $into = $this->getIntoRemoteArgument(); + if ($into !== null) { + + // TODO: Verify that this is a valid path. + // TODO: Allow a raw URI? + + $this->setIntoRemote($into); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $onto = $this->getOntoRemote(); + $this->setIntoRemote($onto); + + $log->writeStatus( + pht('INTO REMOTE'), + pht( + 'Will merge into remote "%s" by default, because this is the remote '. + 'the change is landing onto.', + $onto)); + } + + protected function selectIntoRef() { + $log = $this->getLogEngine(); + + if ($this->getIntoEmptyArgument()) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into empty state, selected with the "--into-empty" '. + 'flag.')); + + return; + } + + $into = $this->getIntoArgument(); + if ($into !== null) { + $this->setIntoRef($into); + + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s", selected with the "--into" flag.', + $into)); + + return; + } + + $ontos = $this->getOntoRefs(); + $onto = head($ontos); + + $this->setIntoRef($onto); + if (count($ontos) > 1) { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the first '. + '"onto" target.', + $onto)); + } else { + $log->writeStatus( + pht('INTO TARGET'), + pht( + 'Will merge into target "%s" by default, because this is the "onto" '. + 'target.', + $onto)); + } + } + + protected function selectIntoCommit() { + // Make sure that our "into" target is valid. + $log = $this->getLogEngine(); + + if ($this->getIntoEmpty()) { + // If we're running under "--into-empty", we don't have to do anything. + + $log->writeStatus( + pht('INTO COMMIT'), + pht('Preparing merge into the empty state.')); + + return null; + } + + if ($this->getIntoLocal()) { + // If we're running under "--into-local", just make sure that the + // target identifies some actual commit. + $api = $this->getRepositoryAPI(); + $local_ref = $this->getIntoRef(); + + // TODO: This error handling could probably be cleaner. + + $into_commit = $api->getCanonicalRevisionName($local_ref); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into local target "%s", at commit "%s".', + $local_ref, + $this->getDisplayHash($into_commit))); + + return $into_commit; + } + + $target = id(new ArcanistLandTarget()) + ->setRemote($this->getIntoRemote()) + ->setRef($this->getIntoRef()); + + $commit = $this->fetchTarget($target); + if ($commit !== null) { + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into "%s" from remote "%s", at commit "%s".', + $target->getRef(), + $target->getRemote(), + $this->getDisplayHash($commit))); + return $commit; + } + + // If we have no valid target and the user passed "--into" explicitly, + // treat this as an error. For example, "arc land --into Q --onto Q", + // where "Q" does not exist, is an error. + if ($this->getIntoArgument()) { + throw new PhutilArgumentUsageException( + pht( + 'Ref "%s" does not exist in remote "%s".', + $target->getRef(), + $target->getRemote())); + } + + // Otherwise, treat this as implying "--into-empty". For example, + // "arc land --onto Q", where "Q" does not exist, is equivalent to + // "arc land --into-empty --onto Q". + $this->setIntoEmpty(true); + + $log->writeStatus( + pht('INTO COMMIT'), + pht( + 'Preparing merge into the empty state to create target "%s" '. + 'in remote "%s".', + $target->getRef(), + $target->getRemote())); + + return null; + } + + private function fetchTarget(ArcanistLandTarget $target) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // TODO: Support bookmarks. + // TODO: Deal with bookmark save/restore behavior. + // TODO: Format this nicely with passthru. + // TODO: Raise a good error message when the ref does not exist. + + $api->execPassthru( + 'pull -b %s -- %s', + $target->getRef(), + $target->getRemote()); + + // TODO: Deal with multiple branch heads. + + list($stdout) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf( + 'last(ancestors(%s) and !outgoing(%s))', + $target->getRef(), + $target->getRemote()), + '{node}'); + + return trim($stdout); + } + + protected function selectCommits($into_commit, array $symbols) { + assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + + $commit_map = array(); + foreach ($symbols as $symbol) { + $symbol_commit = $symbol->getCommit(); + $template = '{node}-{parents}-'; + + if ($into_commit === null) { + list($commits) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf('reverse(ancestors(%s))', $into_commit), + $template); + } else { + list($commits) = $api->execxLocal( + 'log --rev %s --template %s --', + hgsprintf( + 'reverse(ancestors(%s) - ancestors(%s))', + $symbol_commit, + $into_commit), + $template); + } + + $commits = phutil_split_lines($commits, false); + foreach ($commits as $line) { + if (!strlen($line)) { + continue; + } + + $parts = explode('-', $line, 3); + if (count($parts) < 3) { + throw new Exception( + pht( + 'Unexpected output from "hg log ...": %s', + $line)); + } + + $hash = $parts[0]; + if (!isset($commit_map[$hash])) { + $parents = $parts[1]; + $parents = trim($parents); + if (strlen($parents)) { + $parents = explode(' ', $parents); + } else { + $parents = array(); + } + + $summary = $parts[2]; + + $commit_map[$hash] = id(new ArcanistLandCommit()) + ->setHash($hash) + ->setParents($parents) + ->setSummary($summary); + } + + $commit = $commit_map[$hash]; + $commit->addSymbol($symbol); + } + } + + return $this->confirmCommits($into_commit, $symbols, $commit_map); + } + + protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + + if ($this->getStrategy() !== 'squash') { + throw new Exception(pht('TODO: Support merge strategies')); + } + + // TODO: Add a Mercurial version check requiring 2.1.1 or newer. + + $api->execxLocal( + 'update --rev %s', + hgsprintf('%s', $into_commit)); + + $commits = $set->getCommits(); + + $min_commit = last($commits)->getHash(); + $max_commit = head($commits)->getHash(); + + $revision_ref = $set->getRevisionRef(); + $commit_message = $revision_ref->getCommitMessage(); + + try { + $argv = array(); + $argv[] = '--dest'; + $argv[] = hgsprintf('%s', $into_commit); + + $argv[] = '--rev'; + $argv[] = hgsprintf('%s..%s', $min_commit, $max_commit); + + $argv[] = '--logfile'; + $argv[] = '-'; + + $argv[] = '--keep'; + $argv[] = '--collapse'; + + $future = $api->execFutureLocal('rebase %Ls', $argv); + $future->write($commit_message); + $future->resolvex(); + + } catch (CommandException $ex) { + // TODO + // $api->execManualLocal('rebase --abort'); + throw $ex; + } + + list($stdout) = $api->execxLocal('log --rev tip --template %s', '{node}'); + $new_cursor = trim($stdout); + + return $new_cursor; + } + + protected function pushChange($into_commit) { + $api = $this->getRepositoryAPI(); + + // TODO: This does not respect "--into" or "--onto" properly. + + $api->execxLocal( + 'push --rev %s -- %s', + $into_commit, + $this->getOntoRemote()); + } + + protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $old_commit = last($set->getCommits())->getHash(); + $new_commit = $into_commit; + + list($output) = $api->execxLocal( + 'log --rev %s --template %s', + hgsprintf('children(%s)', $old_commit), + '{node}\n'); + $child_hashes = phutil_split_lines($output, false); + + foreach ($child_hashes as $child_hash) { + if (!strlen($child_hash)) { + continue; + } + + // TODO: If the only heads which are descendants of this child will + // be deleted, we can skip this rebase? + + try { + $api->execxLocal( + 'rebase --source %s --dest %s --keep --keepbranches', + $child_hash, + $new_commit); + } catch (CommandException $ex) { + // TODO: Recover state. + throw $ex; + } + } + } + + + protected function pruneBranches(array $sets) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + // This has no effect when we're executing a merge strategy. + if (!$this->isSquashStrategy()) { + return; + } + + $strip = array(); + + // We've rebased all descendants already, so we can safely delete all + // of these commits. + + $sets = array_reverse($sets); + foreach ($sets as $set) { + $commits = $set->getCommits(); + + $min_commit = head($commits)->getHash(); + $max_commit = last($commits)->getHash(); + + $strip[] = hgsprintf('%s::%s', $min_commit, $max_commit); + } + + $rev_set = '('.implode(') or (', $strip).')'; + + // See PHI45. If we have "hg evolve", get rid of old commits using + // "hg prune" instead of "hg strip". + + // If we "hg strip" a commit which has an obsolete predecessor, it + // removes the obsolescence marker and revives the predecessor. This is + // not desirable: we want to destroy all predecessors of these commits. + + try { + $api->execxLocal( + '--config extensions.evolve= prune --rev %s', + $rev_set); + } catch (CommandException $ex) { + $api->execxLocal( + '--config extensions.strip= strip --rev %s', + $rev_set); + } + } + + protected function reconcileLocalState( + $into_commit, + ArcanistRepositoryLocalState $state) { + + // TODO: For now, just leave users wherever they ended up. + + $state->discardLocalState(); + } + +} diff --git a/src/query/ArcanistMercurialCommitMessageHardpointQuery.php b/src/query/ArcanistMercurialCommitMessageHardpointQuery.php new file mode 100644 index 00000000..45dc8ee3 --- /dev/null +++ b/src/query/ArcanistMercurialCommitMessageHardpointQuery.php @@ -0,0 +1,36 @@ +getRepositoryAPI(); + + $hashes = mpull($refs, 'getCommitHash'); + $unique_hashes = array_fuse($hashes); + + // TODO: Batch this properly and make it future oriented. + + $messages = array(); + foreach ($unique_hashes as $unique_hash) { + $messages[$unique_hash] = $api->getCommitMessage($unique_hash); + } + + foreach ($hashes as $ref_key => $hash) { + $hashes[$ref_key] = $messages[$hash]; + } + + yield $this->yieldMap($hashes); + } + +} diff --git a/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php b/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php new file mode 100644 index 00000000..c6c03cf3 --- /dev/null +++ b/src/query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php @@ -0,0 +1,76 @@ +yieldRequests( + $refs, + array( + ArcanistWorkingCopyStateRef::HARDPOINT_COMMITREF, + )); + + // TODO: This has a lot in common with the Git query in the same role. + + $hashes = array(); + $map = array(); + foreach ($refs as $ref_key => $ref) { + $commit = $ref->getCommitRef(); + + $commit_hashes = array(); + + $commit_hashes[] = array( + 'hgcm', + $commit->getCommitHash(), + ); + + foreach ($commit_hashes as $hash) { + $hashes[] = $hash; + $hash_key = $this->getHashKey($hash); + $map[$hash_key][$ref_key] = $ref; + } + } + + $results = array_fill_keys(array_keys($refs), array()); + if ($hashes) { + $revisions = (yield $this->yieldConduit( + 'differential.query', + array( + 'commitHashes' => $hashes, + ))); + + foreach ($revisions as $dict) { + $revision_hashes = idx($dict, 'hashes'); + if (!$revision_hashes) { + continue; + } + + $revision_ref = ArcanistRevisionRef::newFromConduitQuery($dict); + foreach ($revision_hashes as $revision_hash) { + $hash_key = $this->getHashKey($revision_hash); + $state_refs = idx($map, $hash_key, array()); + foreach ($state_refs as $ref_key => $state_ref) { + $results[$ref_key][] = $revision_ref; + } + } + } + } + + yield $this->yieldMap($results); + } + + private function getHashKey(array $hash) { + return $hash[0].':'.$hash[1]; + } + +} diff --git a/src/query/ArcanistWorkflowMercurialHardpointQuery.php b/src/query/ArcanistWorkflowMercurialHardpointQuery.php new file mode 100644 index 00000000..b1932ae6 --- /dev/null +++ b/src/query/ArcanistWorkflowMercurialHardpointQuery.php @@ -0,0 +1,11 @@ +getRepositoryAPI(); + return ($api instanceof ArcanistMercurialAPI); + } + +} diff --git a/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php index df23f01a..c0ff6e72 100644 --- a/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php +++ b/src/ref/revision/ArcanistRevisionBuildableHardpointQuery.php @@ -29,7 +29,7 @@ final class ArcanistRevisionBuildableHardpointQuery $buildables = (yield $this->yieldConduitSearch( 'harbormaster.buildable.search', array( - 'objectPHIDs' => $diff_map, + 'objectPHIDs' => array_values($diff_map), 'manual' => false, ))); diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index d8299f1f..2a07f9dd 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1564,6 +1564,11 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return ($uri !== null); } + public function isFetchableRemote($remote_name) { + $uri = $this->getGitRemoteFetchURI($remote_name); + return ($uri !== null); + } + private function getGitRemoteFetchURI($remote_name) { return $this->getGitRemoteURI($remote_name, $for_push = false); } @@ -1739,4 +1744,13 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return false; } + protected function newLandEngine() { + return new ArcanistGitLandEngine(); + } + + public function newLocalState() { + return id(new ArcanistGitLocalState()) + ->setRepositoryAPI($this); + } + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 1a2db585..410da8ea 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -534,6 +534,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } public function getAllBranches() { + // TODO: This is wrong, and returns bookmarks. + list($branch_info) = $this->execxLocal('bookmarks'); if (trim($branch_info) == 'no bookmarks set') { return array(); @@ -548,10 +550,14 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $return = array(); foreach ($matches as $match) { - list(, $current, $name) = $match; + list(, $current, $name, $hash) = $match; + + list($id, $hash) = explode(':', $hash); + $return[] = array( 'current' => (bool)$current, 'name' => rtrim($name), + 'hash' => $hash, ); } return $return; @@ -562,9 +568,13 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $refs = array(); foreach ($branches as $branch) { + $commit_ref = $this->newCommitRef() + ->setCommitHash($branch['hash']); + $refs[] = $this->newBranchRef() ->setBranchName($branch['name']) - ->setIsCurrentBranch($branch['current']); + ->setIsCurrentBranch($branch['current']) + ->attachCommitRef($commit_ref); } return $refs; @@ -1064,6 +1074,46 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $bookmarks; } + public function getBookmarkCommitHash($name) { + // TODO: Cache this. + + $bookmarks = $this->getBookmarks($name); + $bookmarks = ipull($bookmarks, null, 'name'); + + foreach ($bookmarks as $bookmark) { + if ($bookmark['name'] === $name) { + return $bookmark['revision']; + } + } + + throw new Exception(pht('No bookmark "%s".', $name)); + } + + public function getBranchCommitHash($name) { + // TODO: Cache this. + // TODO: This won't work when there are multiple branch heads with the + // same name. + + $branches = $this->getBranches($name); + + $heads = array(); + foreach ($branches as $branch) { + if ($branch['name'] === $name) { + $heads[] = $branch; + } + } + + if (count($heads) === 1) { + return idx(head($heads), 'revision'); + } + + if (!$heads) { + throw new Exception(pht('No branch "%s".', $name)); + } + + throw new Exception(pht('Too many branch heads for "%s".', $name)); + } + private function splitBranchOrBookmarkLine($line) { // branches and bookmarks are printed in the format: // default 0:a5ead76cdf85 (inactive) @@ -1108,4 +1158,14 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $env; } + protected function newLandEngine() { + return new ArcanistMercurialLandEngine(); + } + + public function newLocalState() { + return id(new ArcanistMercurialLocalState()) + ->setRepositoryAPI($this); + } + + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 5d2c748f..43026c60 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -734,4 +734,19 @@ abstract class ArcanistRepositoryAPI extends Phobject { final public function newBranchRef() { return new ArcanistBranchRef(); } + + final public function getLandEngine() { + $engine = $this->newLandEngine(); + + if ($engine) { + $engine->setRepositoryAPI($this); + } + + return $engine; + } + + protected function newLandEngine() { + return null; + } + } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php new file mode 100644 index 00000000..d348e4b1 --- /dev/null +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -0,0 +1,60 @@ +localRef; + } + + public function getLocalPath() { + return $this->localPath; + } + + protected function executeSaveLocalState() { + $api = $this->getRepositoryAPI(); + // TODO: Fix this. + } + + protected function executeRestoreLocalState() { + $api = $this->getRepositoryAPI(); + // TODO: Fix this. + + // TODO: In Mercurial, we may want to discard commits we've created. + // $repository_api->execxLocal( + // '--config extensions.mq= strip %s', + // $this->onto); + + } + + protected function executeDiscardLocalState() { + // TODO: Fix this. + } + + protected function canStashChanges() { + // Depends on stash extension. + return false; + } + + protected function getIgnoreHints() { + // TODO: Provide this. + return array(); + } + + protected function saveStash() { + return null; + } + + protected function restoreStash($stash_ref) { + return null; + } + + protected function discardStash($stash_ref) { + return null; + } + +} diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index eb5883fe..d27d4a16 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -151,6 +151,9 @@ abstract class ArcanistRepositoryLocalState $this->executeSaveLocalState(); $this->shouldRestore = true; + // TODO: Detect when we're in the middle of a rebase. + // TODO: Detect when we're in the middle of a cherry-pick. + return $this; } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 2fa7b022..5fd8c993 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -3,1608 +3,281 @@ /** * Lands a branch by rebasing, merging and amending it. */ -final class ArcanistLandWorkflow extends ArcanistWorkflow { - - private $isGit; - private $isGitSvn; - private $isHg; - private $isHgSvn; - - private $oldBranch; - private $branch; - private $onto; - private $ontoRemoteBranch; - private $remote; - private $useSquash; - private $keepBranch; - private $branchType; - private $ontoType; - private $preview; - - private $revision; - private $messageFile; - - const REFTYPE_BRANCH = 'branch'; - const REFTYPE_BOOKMARK = 'bookmark'; - - public function getRevisionDict() { - return $this->revision; - } +final class ArcanistLandWorkflow + extends ArcanistArcWorkflow { public function getWorkflowName() { return 'land'; } - public function getCommandSynopses() { - return phutil_console_format(<<newWorkflowInformation() + ->setHelp($help); } - public function getCommandHelp() { - return phutil_console_format(<< array( - 'param' => 'master', - 'help' => pht( - "Land feature branch onto a branch other than the default ". - "('master' in git, 'default' in hg). You can change the default ". - "by setting '%s' with `%s` or for the entire project in %s.", - 'arc.land.onto.default', - 'arc set-config', - '.arcconfig'), - ), - 'hold' => array( - 'help' => pht( - 'Prepare the change to be pushed, but do not actually push it.'), - ), - 'keep-branch' => array( - 'help' => pht( - 'Keep the feature branch after pushing changes to the '. - 'remote (by default, it is deleted).'), - ), - 'remote' => array( - 'param' => 'origin', - 'help' => pht( - 'Push to a remote other than the default.'), - ), - 'merge' => array( - 'help' => pht( - 'Perform a %s merge, not a %s merge. If the project '. - 'is marked as having an immutable history, this is the default '. - 'behavior.', - '--no-ff', - '--squash'), - 'supports' => array( - 'git', - ), - 'nosupport' => array( - 'hg' => pht( - 'Use the %s strategy when landing in mercurial.', - '--squash'), - ), - ), - 'squash' => array( - 'help' => pht( - 'Perform a %s merge, not a %s merge. If the project is '. - 'marked as having a mutable history, this is the default behavior.', - '--squash', - '--no-ff'), - 'conflicts' => array( - 'merge' => pht( - '%s and %s are conflicting merge strategies.', - '--merge', - '--squash'), - ), - ), - 'delete-remote' => array( - 'help' => pht( - 'Delete the feature branch in the remote after landing it.'), - 'conflicts' => array( - 'keep-branch' => true, - ), - 'supports' => array( - 'hg', - ), - ), - 'revision' => array( - 'param' => 'id', - 'help' => pht( - 'Use the message from a specific revision, rather than '. - 'inferring the revision based on branch content.'), - ), - 'preview' => array( - 'help' => pht( - 'Prints the commits that would be landed. Does not '. - 'actually modify or land the commits.'), - ), - '*' => 'branch', + $this->newWorkflowArgument('hold') + ->setHelp( + pht( + 'Prepare the change to be pushed, but do not actually push it.')), + $this->newWorkflowArgument('keep-branches') + ->setHelp( + pht( + 'Keep local branches around after changes are pushed. By '. + 'default, local branches are deleted after they land.')), + $this->newWorkflowArgument('onto-remote') + ->setParameter('remote-name') + ->setHelp(pht('Push to a remote other than the default.')), + + // TODO: Formally allow flags to be bound to related configuration + // for documentation, e.g. "setRelatedConfiguration('arc.land.onto')". + + $this->newWorkflowArgument('onto') + ->setParameter('branch-name') + ->setRepeatable(true) + ->setHelp( + pht( + 'After merging, push changes onto a specified branch. '. + 'Specifying this flag multiple times will push multiple '. + 'branches.')), + $this->newWorkflowArgument('strategy') + ->setParameter('strategy-name') + ->setHelp( + pht( + // TODO: Improve this. + 'Merge using a particular strategy.')), + $this->newWorkflowArgument('revision') + ->setParameter('revision-identifier') + ->setHelp( + pht( + 'Land a specific revision, rather than determining the revisions '. + 'from the commits that are landing.')), + $this->newWorkflowArgument('preview') + ->setHelp( + pht( + 'Shows the changes that will land. Does not modify the working '. + 'copy or the remote.')), + $this->newWorkflowArgument('into') + ->setParameter('commit-ref') + ->setHelp( + pht( + 'Specifies the state to merge into. By default, this is the same '. + 'as the "onto" ref.')), + $this->newWorkflowArgument('into-remote') + ->setParameter('remote-name') + ->setHelp( + pht( + 'Specifies the remote to fetch the "into" ref from. By '. + 'default, this is the same as the "onto" remote.')), + $this->newWorkflowArgument('into-local') + ->setHelp( + pht( + 'Use the local "into" ref state instead of fetching it from '. + 'a remote.')), + $this->newWorkflowArgument('into-empty') + ->setHelp( + pht( + 'Merge into the empty state instead of an existing state. This '. + 'mode is primarily useful when creating a new repository, and '. + 'selected automatically if the "onto" ref does not exist and the '. + '"into" state is not specified.')), + $this->newWorkflowArgument('incremental') + ->setHelp( + pht( + 'When landing multiple revisions at once, push and rebase '. + 'after each operation instead of waiting until all merges '. + 'are completed. This is slower than the default behavior and '. + 'not atomic, but may make it easier to resolve conflicts and '. + 'land complicated changes by letting you make progress one '. + 'step at a time.')), + $this->newWorkflowArgument('ref') + ->setWildcard(true), ); } - public function run() { - $this->readArguments(); - - $engine = null; - if ($this->isGit && !$this->isGitSvn) { - $engine = new ArcanistGitLandEngine(); - } - - if ($engine) { - $should_hold = $this->getArgument('hold'); - $remote_arg = $this->getArgument('remote'); - $onto_arg = $this->getArgument('onto'); - - $engine - ->setWorkflow($this) - ->setRepositoryAPI($this->getRepositoryAPI()) - ->setSourceRef($this->branch) - ->setShouldHold($should_hold) - ->setShouldKeep($this->keepBranch) - ->setShouldSquash($this->useSquash) - ->setShouldPreview($this->preview) - ->setRemoteArgument($remote_arg) - ->setOntoArgument($onto_arg) - ->setBuildMessageCallback(array($this, 'buildEngineMessage')); - - // The goal here is to raise errors with flags early (which is cheap), - // before we test if the working copy is clean (which can be slow). This - // could probably be structured more cleanly. - - $engine->parseArguments(); - - // This must be configured or we fail later inside "buildEngineMessage()". - // This is less than ideal. - $this->ontoRemoteBranch = sprintf( - '%s/%s', - $engine->getTargetRemote(), - $engine->getTargetOnto()); - - $this->requireCleanWorkingCopy(); - $engine->execute(); - - if (!$should_hold && !$this->preview) { - $this->didPush(); - } - - return 0; - } - - $this->validate(); - - try { - $this->pullFromRemote(); - } catch (Exception $ex) { - $this->restoreBranch(); - throw $ex; - } - - $this->printPendingCommits(); - if ($this->preview) { - $this->restoreBranch(); - return 0; - } - - $this->checkoutBranch(); - $this->findRevision(); - - if ($this->useSquash) { - $this->rebase(); - $this->squash(); - } else { - $this->merge(); - } - - $this->push(); - - if (!$this->keepBranch) { - $this->cleanupBranch(); - } - - if ($this->oldBranch != $this->onto) { - // If we were on some branch A and the user ran "arc land B", - // switch back to A. - if ($this->keepBranch || $this->oldBranch != $this->branch) { - $this->restoreBranch(); - } - } - - echo pht('Done.'), "\n"; - - return 0; - } - - private function getUpstreamMatching($branch, $pattern) { - if ($this->isGit) { - $repository_api = $this->getRepositoryAPI(); - list($err, $fullname) = $repository_api->execManualLocal( - 'rev-parse --symbolic-full-name %s@{upstream}', - $branch); - if (!$err) { - $matches = null; - if (preg_match($pattern, $fullname, $matches)) { - return last($matches); - } - } - } - return null; - } - - private function getGitSvnTrunk() { - if (!$this->isGitSvn) { - return null; - } - - // See T13293, this depends on the options passed when cloning. - // On any error we return `trunk`, which was the previous default. - - $repository_api = $this->getRepositoryAPI(); - list($err, $refspec) = $repository_api->execManualLocal( - 'config svn-remote.svn.fetch'); - - if ($err) { - return 'trunk'; - } - - $refspec = rtrim(substr($refspec, strrpos($refspec, ':') + 1)); - - $prefix = 'refs/remotes/'; - if (substr($refspec, 0, strlen($prefix)) !== $prefix) { - return 'trunk'; - } - - $refspec = substr($refspec, strlen($prefix)); - return $refspec; - } - - private function readArguments() { - $repository_api = $this->getRepositoryAPI(); - $this->isGit = $repository_api instanceof ArcanistGitAPI; - $this->isHg = $repository_api instanceof ArcanistMercurialAPI; - - if ($this->isGit) { - $repository = $this->loadProjectRepository(); - $this->isGitSvn = (idx($repository, 'vcs') == 'svn'); - } - - if ($this->isHg) { - $this->isHgSvn = $repository_api->isHgSubversionRepo(); - } - - $branch = $this->getArgument('branch'); - if (empty($branch)) { - $branch = $this->getBranchOrBookmark(); - if ($branch !== null) { - $this->branchType = $this->getBranchType($branch); - - // TODO: This message is misleading when landing a detached head or - // a tag in Git. - - echo pht("Landing current %s '%s'.", $this->branchType, $branch), "\n"; - $branch = array($branch); - } - } - - if (count($branch) !== 1) { - throw new ArcanistUsageException( - pht('Specify exactly one branch or bookmark to land changes from.')); - } - $this->branch = head($branch); - $this->keepBranch = $this->getArgument('keep-branch'); - - $this->preview = $this->getArgument('preview'); - - if (!$this->branchType) { - $this->branchType = $this->getBranchType($this->branch); - } - - $onto_default = $this->isGit ? 'master' : 'default'; - $onto_default = nonempty( - $this->getConfigFromAnySource('arc.land.onto.default'), - $onto_default); - $onto_default = coalesce( - $this->getUpstreamMatching($this->branch, '/^refs\/heads\/(.+)$/'), - $onto_default); - $this->onto = $this->getArgument('onto', $onto_default); - $this->ontoType = $this->getBranchType($this->onto); - - $remote_default = $this->isGit ? 'origin' : ''; - $remote_default = coalesce( - $this->getUpstreamMatching($this->onto, '/^refs\/remotes\/(.+?)\//'), - $remote_default); - $this->remote = $this->getArgument('remote', $remote_default); - - if ($this->getArgument('merge')) { - $this->useSquash = false; - } else if ($this->getArgument('squash')) { - $this->useSquash = true; - } else { - $this->useSquash = !$this->isHistoryImmutable(); - } - - $this->ontoRemoteBranch = $this->onto; - if ($this->isGitSvn) { - $this->ontoRemoteBranch = $this->getGitSvnTrunk(); - } else if ($this->isGit) { - $this->ontoRemoteBranch = $this->remote.'/'.$this->onto; - } - - $this->oldBranch = $this->getBranchOrBookmark(); - } - - private function validate() { - $repository_api = $this->getRepositoryAPI(); - - if ($this->onto == $this->branch) { - $message = pht( - "You can not land a %s onto itself -- you are trying ". - "to land '%s' onto '%s'. For more information on how to push ". - "changes, see 'Pushing and Closing Revisions' in 'Arcanist User ". - "Guide: arc diff' in the documentation.", - $this->branchType, - $this->branch, - $this->onto); - if (!$this->isHistoryImmutable()) { - $message .= ' '.pht("You may be able to '%s' instead.", 'arc amend'); - } - throw new ArcanistUsageException($message); - } - - if ($this->isHg) { - if ($this->useSquash) { - if (!$repository_api->supportsRebase()) { - throw new ArcanistUsageException( - pht( - 'You must enable the rebase extension to use the %s strategy.', - '--squash')); - } - } - - if ($this->branchType != $this->ontoType) { - throw new ArcanistUsageException(pht( - 'Source %s is a %s but destination %s is a %s. When landing a '. - '%s, the destination must also be a %s. Use %s to specify a %s, '. - 'or set %s in %s.', - $this->branch, - $this->branchType, - $this->onto, - $this->ontoType, - $this->branchType, - $this->branchType, - '--onto', - $this->branchType, - 'arc.land.onto.default', - '.arcconfig')); - } - } - - if ($this->isGit) { - list($err) = $repository_api->execManualLocal( - 'rev-parse --verify %s', - $this->branch); - - if ($err) { - throw new ArcanistUsageException( - pht("Branch '%s' does not exist.", $this->branch)); - } - } - - $this->requireCleanWorkingCopy(); - } - - private function checkoutBranch() { - $repository_api = $this->getRepositoryAPI(); - if ($this->getBranchOrBookmark() != $this->branch) { - $repository_api->execxLocal('checkout %s', $this->branch); - } - - switch ($this->branchType) { - case self::REFTYPE_BOOKMARK: - $message = pht( - 'Switched to bookmark **%s**. Identifying and merging...', - $this->branch); - break; - case self::REFTYPE_BRANCH: - default: - $message = pht( - 'Switched to branch **%s**. Identifying and merging...', - $this->branch); - break; - } - - echo phutil_console_format($message."\n"); - } - - private function printPendingCommits() { - $repository_api = $this->getRepositoryAPI(); - - if ($repository_api instanceof ArcanistGitAPI) { - list($out) = $repository_api->execxLocal( - 'log --oneline %s %s --', - $this->branch, - '^'.$this->onto); - } else if ($repository_api instanceof ArcanistMercurialAPI) { - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s,%s)', - $this->onto, - $this->branch)); - - $branch_range = hgsprintf( - 'reverse((%s::%s) - %s)', - $common_ancestor, - $this->branch, - $common_ancestor); - - list($out) = $repository_api->execxLocal( - 'log -r %s --template %s', - $branch_range, - '{node|short} {desc|firstline}\n'); - } - - if (!trim($out)) { - $this->restoreBranch(); - throw new ArcanistUsageException( - pht('No commits to land from %s.', $this->branch)); - } - - echo pht("The following commit(s) will be landed:\n\n%s", $out), "\n"; - } - - private function findRevision() { - $repository_api = $this->getRepositoryAPI(); - - $this->parseBaseCommitArgument(array($this->ontoRemoteBranch)); - - $revision_id = $this->getArgument('revision'); - if ($revision_id) { - $revision_id = $this->normalizeRevisionID($revision_id); - $revisions = $this->getConduit()->callMethodSynchronous( - 'differential.query', - array( - 'ids' => array($revision_id), - )); - if (!$revisions) { - throw new ArcanistUsageException(pht( - "No such revision '%s'!", - "D{$revision_id}")); - } - } else { - $revisions = $repository_api->loadWorkingCopyDifferentialRevisions( - $this->getConduit(), - array()); - } - - if (!count($revisions)) { - throw new ArcanistUsageException(pht( - "arc can not identify which revision exists on %s '%s'. Update the ". - "revision with recent changes to synchronize the %s name and hashes, ". - "or use '%s' to amend the commit message at HEAD, or use ". - "'%s' to select a revision explicitly.", - $this->branchType, - $this->branch, - $this->branchType, - 'arc amend', - '--revision ')); - } else if (count($revisions) > 1) { - switch ($this->branchType) { - case self::REFTYPE_BOOKMARK: - $message = pht( - "There are multiple revisions on feature bookmark '%s' which are ". - "not present on '%s':\n\n". - "%s\n". - 'Separate these revisions onto different bookmarks, or use '. - '--revision to use the commit message from '. - 'and land them all.', - $this->branch, - $this->onto, - $this->renderRevisionList($revisions)); - break; - case self::REFTYPE_BRANCH: - default: - $message = pht( - "There are multiple revisions on feature branch '%s' which are ". - "not present on '%s':\n\n". - "%s\n". - 'Separate these revisions onto different branches, or use '. - '--revision to use the commit message from '. - 'and land them all.', - $this->branch, - $this->onto, - $this->renderRevisionList($revisions)); - break; - } - - throw new ArcanistUsageException($message); - } - - $this->revision = head($revisions); - - $rev_status = $this->revision['status']; - $rev_id = $this->revision['id']; - $rev_title = $this->revision['title']; - $rev_auxiliary = idx($this->revision, 'auxiliary', array()); - - $full_name = pht('D%d: %s', $rev_id, $rev_title); - - if ($this->revision['authorPHID'] != $this->getUserPHID()) { - $other_author = $this->getConduit()->callMethodSynchronous( - 'user.query', - array( - 'phids' => array($this->revision['authorPHID']), - )); - $other_author = ipull($other_author, 'userName', 'phid'); - $other_author = $other_author[$this->revision['authorPHID']]; - $ok = phutil_console_confirm(pht( - "This %s has revision '%s' but you are not the author. Land this ". - "revision by %s?", - $this->branchType, - $full_name, - $other_author)); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - - $state_warning = null; - $state_header = null; - if ($rev_status == ArcanistDifferentialRevisionStatus::CHANGES_PLANNED) { - $state_header = pht('REVISION HAS CHANGES PLANNED'); - $state_warning = pht( - 'The revision you are landing ("%s") is currently in the "%s" state, '. - 'indicating that you expect to revise it before moving forward.'. - "\n\n". - 'Normally, you should resubmit it for review and wait until it is '. - '"%s" by reviewers before you continue.'. - "\n\n". - 'To resubmit the revision for review, either: update the revision '. - 'with revised changes; or use "Request Review" from the web interface.', - $full_name, - pht('Changes Planned'), - pht('Accepted')); - } else if ($rev_status != ArcanistDifferentialRevisionStatus::ACCEPTED) { - $state_header = pht('REVISION HAS NOT BEEN ACCEPTED'); - $state_warning = pht( - 'The revision you are landing ("%s") has not been "%s" by reviewers.', - $full_name, - pht('Accepted')); - } - - if ($state_warning !== null) { - $prompt = pht('Land revision in the wrong state?'); - - id(new PhutilConsoleBlock()) - ->addParagraph(tsprintf('** %s **', $state_header)) - ->addParagraph(tsprintf('%B', $state_warning)) - ->draw(); - - $ok = phutil_console_confirm($prompt); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - - if ($rev_auxiliary) { - $phids = idx($rev_auxiliary, 'phabricator:depends-on', array()); - if ($phids) { - $dep_on_revs = $this->getConduit()->callMethodSynchronous( - 'differential.query', - array( - 'phids' => $phids, - 'status' => 'status-open', - )); - - $open_dep_revs = array(); - foreach ($dep_on_revs as $dep_on_rev) { - $dep_on_rev_id = $dep_on_rev['id']; - $dep_on_rev_title = $dep_on_rev['title']; - $dep_on_rev_status = $dep_on_rev['status']; - $open_dep_revs[$dep_on_rev_id] = $dep_on_rev_title; - } - - if (!empty($open_dep_revs)) { - $open_revs = array(); - foreach ($open_dep_revs as $id => $title) { - $open_revs[] = ' - D'.$id.': '.$title; - } - $open_revs = implode("\n", $open_revs); - - echo pht( - "Revision '%s' depends on open revisions:\n\n%s", - "D{$rev_id}: {$rev_title}", - $open_revs); - - $ok = phutil_console_confirm(pht('Continue anyway?')); - if (!$ok) { - throw new ArcanistUserAbortException(); - } - } - } - } - - $message = $this->getConduit()->callMethodSynchronous( - 'differential.getcommitmessage', - array( - 'revision_id' => $rev_id, - )); - - $this->messageFile = new TempFile(); - Filesystem::writeFile($this->messageFile, $message); - - echo pht( - "Landing revision '%s'...", - "D{$rev_id}: {$rev_title}")."\n"; - - $diff_phid = idx($this->revision, 'activeDiffPHID'); - if ($diff_phid) { - $this->checkForBuildables($diff_phid); - } - } - - private function pullFromRemote() { - $repository_api = $this->getRepositoryAPI(); - - $local_ahead_of_remote = false; - if ($this->isGit) { - $repository_api->execxLocal('checkout %s', $this->onto); - - echo phutil_console_format(pht( - "Switched to branch **%s**. Updating branch...\n", - $this->onto)); - - try { - $repository_api->execxLocal('pull --ff-only --no-stat'); - } catch (CommandException $ex) { - if (!$this->isGitSvn) { - throw $ex; - } - } - list($out) = $repository_api->execxLocal( - 'log %s..%s', - $this->ontoRemoteBranch, - $this->onto); - if (strlen(trim($out))) { - $local_ahead_of_remote = true; - } else if ($this->isGitSvn) { - $repository_api->execxLocal('svn rebase'); - } - - } else if ($this->isHg) { - echo phutil_console_format(pht('Updating **%s**...', $this->onto)."\n"); - - try { - list($out, $err) = $repository_api->execxLocal('pull'); - - $divergedbookmark = $this->onto.'@'.$repository_api->getBranchName(); - if (strpos($err, $divergedbookmark) !== false) { - throw new ArcanistUsageException(phutil_console_format(pht( - "Local bookmark **%s** has diverged from the server's **%s** ". - "(now labeled **%s**). Please resolve this divergence and run ". - "'%s' again.", - $this->onto, - $this->onto, - $divergedbookmark, - 'arc land'))); - } - } catch (CommandException $ex) { - $err = $ex->getError(); - $stdout = $ex->getStdout(); - - // Copied from: PhabricatorRepositoryPullLocalDaemon.php - // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the - // behavior of "hg pull" to return 1 in case of a successful pull - // with no changes. This behavior has been reverted, but users who - // updated between Feb 1, 2012 and Mar 1, 2012 will have the - // erroring version. Do a dumb test against stdout to check for this - // possibility. - // See: https://github.com/phacility/phabricator/issues/101/ - - // NOTE: Mercurial has translated versions, which translate this error - // string. In a translated version, the string will be something else, - // like "aucun changement trouve". There didn't seem to be an easy way - // to handle this (there are hard ways but this is not a common - // problem and only creates log spam, not application failures). - // Assume English. - - // TODO: Remove this once we're far enough in the future that - // deployment of 2.1 is exceedingly rare? - if ($err != 1 || !preg_match('/no changes found/', $stdout)) { - throw $ex; - } - } - - // Pull succeeded. Now make sure master is not on an outgoing change - if ($repository_api->supportsPhases()) { - list($out) = $repository_api->execxLocal( - 'log -r %s --template %s', $this->onto, '{phase}'); - if ($out != 'public') { - $local_ahead_of_remote = true; - } - } else { - // execManual instead of execx because outgoing returns - // code 1 when there is nothing outgoing - list($err, $out) = $repository_api->execManualLocal( - 'outgoing -r %s', - $this->onto); - - // $err === 0 means something is outgoing - if ($err === 0) { - $local_ahead_of_remote = true; - } - } - } - - if ($local_ahead_of_remote) { - throw new ArcanistUsageException(pht( - "Local %s '%s' is ahead of remote %s '%s', so landing a feature ". - "%s would push additional changes. Push or reset the changes in '%s' ". - "before running '%s'.", - $this->ontoType, - $this->onto, - $this->ontoType, - $this->ontoRemoteBranch, - $this->ontoType, - $this->onto, - 'arc land')); - } - } - - private function rebase() { - $repository_api = $this->getRepositoryAPI(); - - chdir($repository_api->getPath()); - if ($this->isHg) { - $onto_tip = $repository_api->getCanonicalRevisionName($this->onto); - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s, %s)', $this->onto, $this->branch)); - - // Only rebase if the local branch is not at the tip of the onto branch. - if ($onto_tip != $common_ancestor) { - // keep branch here so later we can decide whether to remove it - $err = $repository_api->execPassthru( - 'rebase -d %s --keepbranches', - $this->onto); - if ($err) { - echo phutil_console_format("%s\n", pht('Aborting rebase')); - $repository_api->execManualLocal('rebase --abort'); - $this->restoreBranch(); - throw new ArcanistUsageException(pht( - "'%s' failed and the rebase was aborted. This is most ". - "likely due to conflicts. Manually rebase %s onto %s, resolve ". - "the conflicts, then run '%s' again.", - sprintf('hg rebase %s', $this->onto), - $this->branch, - $this->onto, - 'arc land')); - } - } - } - - $repository_api->reloadWorkingCopy(); - } - - private function squash() { - $repository_api = $this->getRepositoryAPI(); - - if ($this->isGit) { - $repository_api->execxLocal('checkout %s', $this->onto); - $repository_api->execxLocal( - 'merge --no-stat --squash --ff-only %s', - $this->branch); - } else if ($this->isHg) { - // The hg code is a little more complex than git's because we - // need to handle the case where the landing branch has child branches: - // -a--------b master - // \ - // w--x mybranch - // \--y subbranch1 - // \--z subbranch2 - // - // arc land --branch mybranch --onto master : - // -a--b--wx master - // \--y subbranch1 - // \--z subbranch2 - - $branch_rev_id = $repository_api->getCanonicalRevisionName($this->branch); - - // At this point $this->onto has been pulled from remote and - // $this->branch has been rebased on top of onto(by the rebase() - // function). So we're guaranteed to have onto as an ancestor of branch - // when we use first((onto::branch)-onto) below. - $branch_root = $repository_api->getCanonicalRevisionName( - hgsprintf('first((%s::%s)-%s)', - $this->onto, - $this->branch, - $this->onto)); - - $branch_range = hgsprintf( - '(%s::%s)', - $branch_root, - $this->branch); - - if (!$this->keepBranch) { - $this->handleAlternateBranches($branch_root, $branch_range); - } - - // Collapse just the landing branch onto master. - // Leave its children on the original branch. - $err = $repository_api->execPassthru( - 'rebase --collapse --keep --logfile %s -r %s -d %s', - $this->messageFile, - $branch_range, - $this->onto); - - if ($err) { - $repository_api->execManualLocal('rebase --abort'); - $this->restoreBranch(); - throw new ArcanistUsageException( + protected function newPrompts() { + return array( + $this->newPrompt('arc.land.large-working-set') + ->setDescription( pht( - "Squashing the commits under %s failed. ". - "Manually squash your commits and run '%s' again.", - $this->branch, - 'arc land')); - } - - if ($repository_api->isBookmark($this->branch)) { - // a bug in mercurial means bookmarks end up on the revision prior - // to the collapse when using --collapse with --keep, - // so we manually move them to the correct spots - // see: http://bz.selenic.com/show_bug.cgi?id=3716 - $repository_api->execxLocal( - 'bookmark -f %s', - $this->onto); - - $repository_api->execxLocal( - 'bookmark -f %s -r %s', - $this->branch, - $branch_rev_id); - } - - // check if the branch had children - list($output) = $repository_api->execxLocal( - 'log -r %s --template %s', - hgsprintf('children(%s)', $this->branch), - '{node}\n'); - - $child_branch_roots = phutil_split_lines($output, false); - $child_branch_roots = array_filter($child_branch_roots); - if ($child_branch_roots) { - // move the branch's children onto the collapsed commit - foreach ($child_branch_roots as $child_root) { - $repository_api->execxLocal( - 'rebase -d %s -s %s --keep --keepbranches', - $this->onto, - $child_root); - } - } - - // All the rebases may have moved us to another branch - // so we move back. - $repository_api->execxLocal('checkout %s', $this->onto); - } - } - - /** - * Detect alternate branches and prompt the user for how to handle - * them. An alternate branch is a branch that forks from the landing - * branch prior to the landing branch tip. - * - * In a situation like this: - * -a--------b master - * \ - * w--x landingbranch - * \ \-- g subbranch - * \--y altbranch1 - * \--z altbranch2 - * - * y and z are alternate branches and will get deleted by the squash, - * so we need to detect them and ask the user what they want to do. - * - * @param string The revision id of the landing branch's root commit. - * @param string The revset specifying all the commits in the landing branch. - * @return void - */ - private function handleAlternateBranches($branch_root, $branch_range) { - $repository_api = $this->getRepositoryAPI(); - - // Using the tree in the doccomment, the revset below resolves as follows: - // 1. roots(descendants(w) - descendants(x) - (w::x)) - // 2. roots({x,g,y,z} - {g} - {w,x}) - // 3. roots({y,z}) - // 4. {y,z} - $alt_branch_revset = hgsprintf( - 'roots(descendants(%s)-descendants(%s)-%R)', - $branch_root, - $this->branch, - $branch_range); - list($alt_branches) = $repository_api->execxLocal( - 'log --template %s -r %s', - '{node}\n', - $alt_branch_revset); - - $alt_branches = phutil_split_lines($alt_branches, false); - $alt_branches = array_filter($alt_branches); - - $alt_count = count($alt_branches); - if ($alt_count > 0) { - $input = phutil_console_prompt(pht( - "%s '%s' has %s %s(s) forking off of it that would be deleted ". - "during a squash. Would you like to keep a non-squashed copy, rebase ". - "them on top of '%s', or abort and deal with them yourself? ". - "(k)eep, (r)ebase, (a)bort:", - ucfirst($this->branchType), - $this->branch, - $alt_count, - $this->branchType, - $this->branch)); - - if ($input == 'k' || $input == 'keep') { - $this->keepBranch = true; - } else if ($input == 'r' || $input == 'rebase') { - foreach ($alt_branches as $alt_branch) { - $repository_api->execxLocal( - 'rebase --keep --keepbranches -d %s -s %s', - $this->branch, - $alt_branch); - } - } else if ($input == 'a' || $input == 'abort') { - $branch_string = implode("\n", $alt_branches); - echo - "\n", + 'Confirms landing more than %s commit(s) in a single operation.', + new PhutilNumber($this->getLargeWorkingSetLimit()))), + $this->newPrompt('arc.land.confirm') + ->setDescription( pht( - "Remove the %s starting at these revisions and run %s again:\n%s", - $this->branchType.'s', - $branch_string, - 'arc land'), - "\n\n"; - throw new ArcanistUserAbortException(); - } else { - throw new ArcanistUsageException( - pht('Invalid choice. Aborting arc land.')); - } - } + 'Confirms that the correct changes have been selected.')), + $this->newPrompt('arc.land.implicit') + ->setDescription( + pht( + 'Confirms that local commits which are not associated with '. + 'a revision should land.')), + $this->newPrompt('arc.land.unauthored') + ->setDescription( + pht( + 'Confirms that revisions you did not author should land.')), + $this->newPrompt('arc.land.changes-planned') + ->setDescription( + pht( + 'Confirms that revisions with changes planned should land.')), + $this->newPrompt('arc.land.closed') + ->setDescription( + pht( + 'Confirms that revisions that are already closed should land.')), + $this->newPrompt('arc.land.not-accepted') + ->setDescription( + pht( + 'Confirms that revisions that are not accepted should land.')), + $this->newPrompt('arc.land.open-parents') + ->setDescription( + pht( + 'Confirms that revisions with open parent revisions should '. + 'land.')), + $this->newPrompt('arc.land.failed-builds') + ->setDescription( + pht( + 'Confirms that revisions with failed builds.')), + $this->newPrompt('arc.land.ongoing-builds') + ->setDescription( + pht( + 'Confirms that revisions with ongoing builds.')), + ); } - private function merge() { - $repository_api = $this->getRepositoryAPI(); - - // In immutable histories, do a --no-ff merge to force a merge commit with - // the right message. - $repository_api->execxLocal('checkout %s', $this->onto); - - chdir($repository_api->getPath()); - if ($this->isGit) { - $err = phutil_passthru( - 'git merge --no-stat --no-ff --no-commit %s', - $this->branch); - - if ($err) { - throw new ArcanistUsageException(pht( - "'%s' failed. Your working copy has been left in a partially ". - "merged state. You can: abort with '%s'; or follow the ". - "instructions to complete the merge.", - 'git merge', - 'git merge --abort')); - } - } else if ($this->isHg) { - // HG arc land currently doesn't support --merge. - // When merging a bookmark branch to a master branch that - // hasn't changed since the fork, mercurial fails to merge. - // Instead of only working in some cases, we just disable --merge - // until there is a demand for it. - // The user should never reach this line, since --merge is - // forbidden at the command line argument level. - throw new ArcanistUsageException( - pht('%s is not currently supported for hg repos.', '--merge')); - } + public function getLargeWorkingSetLimit() { + return 50; } - private function push() { - $repository_api = $this->getRepositoryAPI(); + public function runWorkflow() { + $working_copy = $this->getWorkingCopy(); + $repository_api = $working_copy->getRepositoryAPI(); - // These commands can fail legitimately (e.g. commit hooks) - try { - if ($this->isGit) { - $repository_api->execxLocal('commit -F %s', $this->messageFile); - if (phutil_is_windows()) { - // Occasionally on large repositories on Windows, Git can exit with - // an unclean working copy here. This prevents reverts from being - // pushed to the remote when this occurs. - $this->requireCleanWorkingCopy(); - } - } else if ($this->isHg) { - // hg rebase produces a commit earlier as part of rebase - if (!$this->useSquash) { - $repository_api->execxLocal( - 'commit --logfile %s', - $this->messageFile); - } - } - // We dispatch this event so we can run checks on the merged revision, - // right before it gets pushed out. It's easier to do this in arc land - // than to try to hook into git/hg. - $this->didCommitMerge(); - } catch (Exception $ex) { - $this->executeCleanupAfterFailedPush(); - throw $ex; - } - - if ($this->getArgument('hold')) { - echo phutil_console_format(pht( - 'Holding change in **%s**: it has NOT been pushed yet.', - $this->onto)."\n"); - } else { - echo pht('Pushing change...'), "\n\n"; - - chdir($repository_api->getPath()); - - if ($this->isGitSvn) { - $err = phutil_passthru('git svn dcommit'); - $cmd = 'git svn dcommit'; - } else if ($this->isGit) { - $err = phutil_passthru('git push %s %s', $this->remote, $this->onto); - $cmd = 'git push'; - } else if ($this->isHgSvn) { - // hg-svn doesn't support 'push -r', so we do a normal push - // which hg-svn modifies to only push the current branch and - // ancestors. - $err = $repository_api->execPassthru('push %s', $this->remote); - $cmd = 'hg push'; - } else if ($this->isHg) { - if (strlen($this->remote)) { - $err = $repository_api->execPassthru( - 'push -r %s %s', - $this->onto, - $this->remote); - } else { - $err = $repository_api->execPassthru( - 'push -r %s', - $this->onto); - } - $cmd = 'hg push'; - } - - if ($err) { - echo phutil_console_format( - "** %s **\n", - pht('PUSH FAILED!')); - $this->executeCleanupAfterFailedPush(); - if ($this->isGit) { - throw new ArcanistUsageException(pht( - "'%s' failed! Fix the error and run '%s' again.", - $cmd, - 'arc land')); - } - throw new ArcanistUsageException(pht( - "'%s' failed! Fix the error and push this change manually.", - $cmd)); - } - - $this->didPush(); - - echo "\n"; - } - } - - private function executeCleanupAfterFailedPush() { - $repository_api = $this->getRepositoryAPI(); - if ($this->isGit) { - $repository_api->execxLocal('reset --hard HEAD^'); - $this->restoreBranch(); - } else if ($this->isHg) { - $repository_api->execxLocal( - '--config extensions.mq= strip %s', - $this->onto); - $this->restoreBranch(); - } - } - - private function cleanupBranch() { - $repository_api = $this->getRepositoryAPI(); - - echo pht('Cleaning up feature %s...', $this->branchType), "\n"; - if ($this->isGit) { - list($ref) = $repository_api->execxLocal( - 'rev-parse --verify %s', - $this->branch); - $ref = trim($ref); - $recovery_command = csprintf( - 'git checkout -b %s %s', - $this->branch, - $ref); - echo pht('(Use `%s` if you want it back.)', $recovery_command), "\n"; - $repository_api->execxLocal('branch -D %s', $this->branch); - } else if ($this->isHg) { - $common_ancestor = $repository_api->getCanonicalRevisionName( - hgsprintf('ancestor(%s,%s)', $this->onto, $this->branch)); - - $branch_root = $repository_api->getCanonicalRevisionName( - hgsprintf('first((%s::%s)-%s)', - $common_ancestor, - $this->branch, - $common_ancestor)); - - $repository_api->execxLocal( - '--config extensions.mq= strip -r %s', - $branch_root); - - if ($repository_api->isBookmark($this->branch)) { - $repository_api->execxLocal('bookmark -d %s', $this->branch); - } - } - - if ($this->getArgument('delete-remote')) { - if ($this->isHg) { - // named branches were closed as part of the earlier commit - // so only worry about bookmarks - if ($repository_api->isBookmark($this->branch)) { - $repository_api->execxLocal( - 'push -B %s %s', - $this->branch, - $this->remote); - } - } - } - } - - public function getSupportedRevisionControlSystems() { - return array('git', 'hg'); - } - - private function getBranchOrBookmark() { - $repository_api = $this->getRepositoryAPI(); - if ($this->isGit) { - $branch = $repository_api->getBranchName(); - - // If we don't have a branch name, just use whatever's at HEAD. - if (!strlen($branch) && !$this->isGitSvn) { - $branch = $repository_api->getWorkingCopyRevision(); - } - } else if ($this->isHg) { - $branch = $repository_api->getActiveBookmark(); - if (!$branch) { - $branch = $repository_api->getBranchName(); - } - } - - return $branch; - } - - private function getBranchType($branch) { - $repository_api = $this->getRepositoryAPI(); - if ($this->isHg && $repository_api->isBookmark($branch)) { - return 'bookmark'; - } - return 'branch'; - } - - /** - * Restore the original branch, e.g. after a successful land or a failed - * pull. - */ - private function restoreBranch() { - $repository_api = $this->getRepositoryAPI(); - $repository_api->execxLocal('checkout %s', $this->oldBranch); - if ($this->isGit) { - $repository_api->execxLocal('submodule update --init --recursive'); - } - echo pht( - "Switched back to %s %s.\n", - $this->branchType, - phutil_console_format('**%s**', $this->oldBranch)); - } - - - /** - * Check if a diff has a running or failed buildable, and prompt the user - * before landing if it does. - */ - private function checkForBuildables($diff_phid) { - // Try to use the more modern check which respects the "Warn on Land" - // behavioral flag on build plans if we can. This newer check won't work - // unless the server is running code from March 2019 or newer since the - // API methods we need won't exist yet. We'll fall back to the older check - // if this one doesn't work out. - try { - $this->checkForBuildablesWithPlanBehaviors($diff_phid); - return; - } catch (ArcanistUserAbortException $abort_ex) { - throw $abort_ex; - } catch (Exception $ex) { - // Continue with the older approach, below. - } - - // NOTE: Since Harbormaster is still beta and this stuff all got added - // recently, just bail if we can't find a buildable. This is just an - // advisory check intended to prevent human error. - - try { - $buildables = $this->getConduit()->callMethodSynchronous( - 'harbormaster.querybuildables', - array( - 'buildablePHIDs' => array($diff_phid), - 'manualBuildables' => false, - )); - } catch (ConduitClientException $ex) { - return; - } - - if (!$buildables['data']) { - // If there's no corresponding buildable, we're done. - return; - } - - $console = PhutilConsole::getConsole(); - - $buildable = head($buildables['data']); - - if ($buildable['buildableStatus'] == 'passed') { - $console->writeOut( - "** %s ** %s\n", - pht('BUILDS PASSED'), - pht('Harbormaster builds for the active diff completed successfully.')); - return; - } - - switch ($buildable['buildableStatus']) { - case 'building': - $message = pht( - 'Harbormaster is still building the active diff for this revision.'); - $prompt = pht('Land revision anyway, despite ongoing build?'); - break; - case 'failed': - $message = pht( - 'Harbormaster failed to build the active diff for this revision.'); - $prompt = pht('Land revision anyway, despite build failures?'); - break; - default: - // If we don't recognize the status, just bail. - return; - } - - $builds = $this->queryBuilds( - array( - 'buildablePHIDs' => array($buildable['phid']), - )); - - $console->writeOut($message."\n\n"); - - $builds = msortv($builds, 'getStatusSortVector'); - foreach ($builds as $build) { - $ansi_color = $build->getStatusANSIColor(); - $status_name = $build->getStatusName(); - $object_name = $build->getObjectName(); - $build_name = $build->getName(); - - echo tsprintf( - " ** %s ** %s: %s\n", - $status_name, - $object_name, - $build_name); - } - - $console->writeOut( - "\n%s\n\n **%s**: __%s__", - pht('You can review build details here:'), - pht('Harbormaster URI'), - $buildable['uri']); - - if (!phutil_console_confirm($prompt)) { - throw new ArcanistUserAbortException(); - } - } - - private function checkForBuildablesWithPlanBehaviors($diff_phid) { - // TODO: These queries should page through all results instead of fetching - // only the first page, but we don't have good primitives to support that - // in "master" yet. - - $this->writeInfo( - pht('BUILDS'), - pht('Checking build status...')); - - $raw_buildables = $this->getConduit()->callMethodSynchronous( - 'harbormaster.buildable.search', - array( - 'constraints' => array( - 'objectPHIDs' => array( - $diff_phid, - ), - 'manual' => false, - ), - )); - - if (!$raw_buildables['data']) { - return; - } - - $buildables = $raw_buildables['data']; - $buildable_phids = ipull($buildables, 'phid'); - - $raw_builds = $this->getConduit()->callMethodSynchronous( - 'harbormaster.build.search', - array( - 'constraints' => array( - 'buildables' => $buildable_phids, - ), - )); - - if (!$raw_builds['data']) { - return; - } - - $builds = array(); - foreach ($raw_builds['data'] as $raw_build) { - $build_ref = ArcanistBuildRef::newFromConduit($raw_build); - $build_phid = $build_ref->getPHID(); - $builds[$build_phid] = $build_ref; - } - - $plan_phids = mpull($builds, 'getBuildPlanPHID'); - $plan_phids = array_values($plan_phids); - - $raw_plans = $this->getConduit()->callMethodSynchronous( - 'harbormaster.buildplan.search', - array( - 'constraints' => array( - 'phids' => $plan_phids, - ), - )); - - $plans = array(); - foreach ($raw_plans['data'] as $raw_plan) { - $plan_ref = ArcanistBuildPlanRef::newFromConduit($raw_plan); - $plan_phid = $plan_ref->getPHID(); - $plans[$plan_phid] = $plan_ref; - } - - $ongoing_builds = array(); - $failed_builds = array(); - - $builds = msortv($builds, 'getStatusSortVector'); - foreach ($builds as $build_ref) { - $plan = idx($plans, $build_ref->getBuildPlanPHID()); - if (!$plan) { - continue; - } - - $plan_behavior = $plan->getBehavior('arc-land', 'always'); - $if_building = ($plan_behavior == 'building'); - $if_complete = ($plan_behavior == 'complete'); - $if_never = ($plan_behavior == 'never'); - - // If the build plan "Never" warns when landing, skip it. - if ($if_never) { - continue; - } - - // If the build plan warns when landing "If Complete" but the build is - // not complete, skip it. - if ($if_complete && !$build_ref->isComplete()) { - continue; - } - - // If the build plan warns when landing "If Building" but the build is - // complete, skip it. - if ($if_building && $build_ref->isComplete()) { - continue; - } - - // Ignore passing builds. - if ($build_ref->isPassed()) { - continue; - } - - if (!$build_ref->isComplete()) { - $ongoing_builds[] = $build_ref; - } else { - $failed_builds[] = $build_ref; - } - } - - if (!$ongoing_builds && !$failed_builds) { - return; - } - - if ($failed_builds) { - $this->writeWarn( - pht('BUILD FAILURES'), + $land_engine = $repository_api->getLandEngine(); + if (!$land_engine) { + throw new PhutilArgumentUsageException( pht( - 'Harbormaster failed to build the active diff for this revision:')); - $prompt = pht('Land revision anyway, despite build failures?'); - } else if ($ongoing_builds) { - $this->writeWarn( - pht('ONGOING BUILDS'), - pht( - 'Harbormaster is still building the active diff for this revision:')); - $prompt = pht('Land revision anyway, despite ongoing build?'); + '"arc land" must be run in a Git or Mercurial working copy.')); } - $show_builds = array_merge($failed_builds, $ongoing_builds); - echo "\n"; - foreach ($show_builds as $build_ref) { - $ansi_color = $build_ref->getStatusANSIColor(); - $status_name = $build_ref->getStatusName(); - $object_name = $build_ref->getObjectName(); - $build_name = $build_ref->getName(); + $is_incremental = $this->getArgument('incremental'); + $source_refs = $this->getArgument('ref'); - echo tsprintf( - " ** %s ** %s: %s\n", - $status_name, - $object_name, - $build_name); - } + $onto_remote_arg = $this->getArgument('onto-remote'); + $onto_args = $this->getArgument('onto'); - echo tsprintf( - "\n%s\n\n", - pht('You can review build details here:')); + $into_remote = $this->getArgument('into-remote'); + $into_empty = $this->getArgument('into-empty'); + $into_local = $this->getArgument('into-local'); + $into = $this->getArgument('into'); - foreach ($buildables as $buildable) { - $buildable_uri = id(new PhutilURI($this->getConduitURI())) - ->setPath(sprintf('/B%d', $buildable['id'])); + $is_preview = $this->getArgument('preview'); + $should_hold = $this->getArgument('hold'); + $should_keep = $this->getArgument('keep-branches'); - echo tsprintf( - " **%s**: __%s__\n", - pht('Buildable %d', $buildable['id']), - $buildable_uri); - } + $revision = $this->getArgument('revision'); + $strategy = $this->getArgument('strategy'); - if (!phutil_console_confirm($prompt)) { - throw new ArcanistUserAbortException(); - } + $land_engine + ->setViewer($this->getViewer()) + ->setWorkflow($this) + ->setLogEngine($this->getLogEngine()) + ->setSourceRefs($source_refs) + ->setShouldHold($should_hold) + ->setShouldKeep($should_keep) + ->setStrategyArgument($strategy) + ->setShouldPreview($is_preview) + ->setOntoRemoteArgument($onto_remote_arg) + ->setOntoArguments($onto_args) + ->setIntoRemoteArgument($into_remote) + ->setIntoEmptyArgument($into_empty) + ->setIntoLocalArgument($into_local) + ->setIntoArgument($into) + ->setIsIncremental($is_incremental) + ->setRevisionSymbol($revision); + + $land_engine->execute(); } - public function buildEngineMessage(ArcanistLandEngine $engine) { - // TODO: This is oh-so-gross. - $this->findRevision(); - $engine->setCommitMessageFile($this->messageFile); - } - - public function didCommitMerge() { - $this->dispatchEvent( - ArcanistEventType::TYPE_LAND_WILLPUSHREVISION, - array()); - } - - public function didPush() { - $this->askForRepositoryUpdate(); - - $mark_workflow = $this->buildChildWorkflow( - 'close-revision', - array( - '--finalize', - '--quiet', - $this->revision['id'], - )); - $mark_workflow->run(); - } - - private function queryBuilds(array $constraints) { - $conduit = $this->getConduit(); - - // NOTE: This method only loads the 100 most recent builds. It's rare for - // a revision to have more builds than that and there's currently no paging - // wrapper for "*.search" Conduit API calls available in Arcanist. - - try { - $raw_result = $conduit->callMethodSynchronous( - 'harbormaster.build.search', - array( - 'constraints' => $constraints, - )); - } catch (Exception $ex) { - // If the server doesn't have "harbormaster.build.search" yet (Aug 2016), - // try the older "harbormaster.querybuilds" instead. - $raw_result = $conduit->callMethodSynchronous( - 'harbormaster.querybuilds', - $constraints); - } - - $refs = array(); - foreach ($raw_result['data'] as $raw_data) { - $refs[] = ArcanistBuildRef::newFromConduit($raw_data); - } - - return $refs; - } - - } diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index f4514698..1e0a1093 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -244,7 +244,7 @@ abstract class ArcanistWorkflow extends Phobject { return $err; } - final protected function getLogEngine() { + final public function getLogEngine() { return $this->getRuntime()->getLogEngine(); } @@ -2357,6 +2357,15 @@ abstract class ArcanistWorkflow extends Phobject { $prompts = $this->newPrompts(); assert_instances_of($prompts, 'ArcanistPrompt'); + $prompts[] = $this->newPrompt('arc.state.stash') + ->setDescription( + pht( + 'Prompts the user to stash changes and continue when the '. + 'working copy has untracked, uncommitted. or unstaged '. + 'changes.')); + + // TODO: Swap to ArrayCheck? + $map = array(); foreach ($prompts as $prompt) { $key = $prompt->getKey(); @@ -2380,7 +2389,7 @@ abstract class ArcanistWorkflow extends Phobject { return $this->promptMap; } - protected function getPrompt($key) { + final public function getPrompt($key) { $map = $this->getPromptMap(); $prompt = idx($map, $key); From 25afb93f7ad49cc1d39685d5283165651351d278 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 12:15:25 -0700 Subject: [PATCH 21/82] In "arc land", rebase branches in natural order Summary: Ref T13546. When "arc land" performs cascading rebases, do them in "feature1", "feature2", etc., order so they're easier to follow. The outcome is not dependent on execution order. Test Plan: Landed a change which cascaded many other branches, saw more comprehensible update order. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21320 --- src/land/engine/ArcanistGitLandEngine.php | 5 +++++ src/land/engine/ArcanistLandEngine.php | 9 +++++++++ src/workflow/ArcanistWorkflow.php | 4 +++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 48b2d95a..9e039d44 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -103,6 +103,11 @@ final class ArcanistGitLandEngine } } + // Sort the result so that branches are processed in natural order. + $names = array_keys($result); + natcasesort($names); + $result = array_select_keys($result, $names); + return $result; } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 051a21ae..7343c660 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -140,6 +140,15 @@ abstract class ArcanistLandEngine extends Phobject { return $this->shouldKeep; } + final public function setStrategyArgument($strategy_argument) { + $this->strategyArgument = $strategy_argument; + return $this; + } + + final public function getStrategyArgument() { + return $this->strategyArgument; + } + final public function setStrategy($strategy) { $this->strategy = $strategy; return $this; diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index 1e0a1093..5922c8a2 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2357,11 +2357,13 @@ abstract class ArcanistWorkflow extends Phobject { $prompts = $this->newPrompts(); assert_instances_of($prompts, 'ArcanistPrompt'); + // TODO: Move this somewhere modular. + $prompts[] = $this->newPrompt('arc.state.stash') ->setDescription( pht( 'Prompts the user to stash changes and continue when the '. - 'working copy has untracked, uncommitted. or unstaged '. + 'working copy has untracked, uncommitted, or unstaged '. 'changes.')); // TODO: Swap to ArrayCheck? From 6fb84e5164ccdda12eea98d616f4a297093d44b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 12:20:44 -0700 Subject: [PATCH 22/82] Add a synopsis and example for "arc help land" Summary: Ref T13546. Small documentation fix. Mostly so I can have more things to land. Test Plan: Ran "arc help land", saw help. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21321 --- src/workflow/ArcanistLandWorkflow.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 5fd8c993..104a19b4 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -91,9 +91,9 @@ branch. EOTEXT ); - // TODO: Add command synopses. - return $this->newWorkflowInformation() + ->setSynopsis(pht('Publish reviewed changes.')) + ->addExample(pht('**land** [__options__] [__ref__ ...]')) ->setHelp($help); } From 1552397c8695d036022bcc55054ed24d4c3300f7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 12:38:26 -0700 Subject: [PATCH 23/82] Sometimes discard already-closed revisions in "arc land" Summary: Ref T13546. When we find commits in history which are associated with already-closed revisions, and they weren't named explicitly on the command line, and we're using a squash strategy, discard them. This generally happens when "feature2" is on top of "feature1", but "feature1" gets amended or branched elsewhere and lands independently. Test Plan: Ran "arc land feature3" where prior revisions had already landed, got discards on the duplicated changes. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21322 --- src/land/ArcanistLandCommit.php | 21 +++++-- src/land/engine/ArcanistGitLandEngine.php | 8 ++- src/land/engine/ArcanistLandEngine.php | 67 +++++++++++++++++++++-- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/land/ArcanistLandCommit.php b/src/land/ArcanistLandCommit.php index 527611ce..b00c988a 100644 --- a/src/land/ArcanistLandCommit.php +++ b/src/land/ArcanistLandCommit.php @@ -7,13 +7,15 @@ final class ArcanistLandCommit private $summary; private $displaySummary; private $parents; - private $symbols = array(); private $explicitRevisionRef; private $revisionRef = false; private $parentCommits; private $isHeadCommit; private $isImplicitCommit; + private $directSymbols = array(); + private $indirectSymbols = array(); + public function setHash($hash) { $this->hash = $hash; return $this; @@ -50,13 +52,22 @@ final class ArcanistLandCommit return $this->parents; } - public function addSymbol(ArcanistLandSymbol $symbol) { - $this->symbols[] = $symbol; + public function addDirectSymbol(ArcanistLandSymbol $symbol) { + $this->directSymbols[] = $symbol; return $this; } - public function getSymbols() { - return $this->symbols; + public function getDirectSymbols() { + return $this->directSymbols; + } + + public function addIndirectSymbol(ArcanistLandSymbol $symbol) { + $this->indirectSymbols[] = $symbol; + return $this; + } + + public function getIndirectSymbols() { + return $this->indirectSymbols; } public function setExplicitRevisionref(ArcanistRevisionRef $ref) { diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 9e039d44..74a17ad4 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -1299,6 +1299,7 @@ final class ArcanistGitLandEngine } $commits = phutil_split_lines($commits, false); + $is_first = false; foreach ($commits as $line) { if (!strlen($line)) { continue; @@ -1331,7 +1332,12 @@ final class ArcanistGitLandEngine } $commit = $commit_map[$hash]; - $commit->addSymbol($symbol); + if ($is_first) { + $commit->addDirectSymbol($symbol); + $is_first = false; + } + + $commit->addIndirectSymbol($symbol); } } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 7343c660..fa613dec 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -866,7 +866,7 @@ abstract class ArcanistLandEngine extends Phobject { // TODO: If we have several refs but all but one are abandoned or closed // or authored by someone else, we could guess what you mean. - $symbols = $commit->getSymbols(); + $symbols = $commit->getIndirectSymbols(); $raw_symbols = mpull($symbols, 'getSymbol'); $symbol_list = implode(', ', $raw_symbols); $display_hash = $this->getDisplayHash($hash); @@ -899,11 +899,6 @@ abstract class ArcanistLandEngine extends Phobject { $display_hash)); } - // TODO: Some of the revisions we've identified may be mapped to an - // outdated set of commits. We should look in local branches for a better - // set of commits, and try to confirm that the state we're about to land - // is the current state in Differential. - if ($force_ref) { $phid_map = array(); foreach ($commit_map as $commit) { @@ -1155,6 +1150,8 @@ abstract class ArcanistLandEngine extends Phobject { $sets[$revision_phid] = $set; } + $sets = $this->filterCommitSets($sets); + if (!$this->getShouldPreview()) { $this->confirmImplicitCommits($sets, $symbols); } @@ -1436,4 +1433,62 @@ abstract class ArcanistLandEngine extends Phobject { return $strategy; } + private function filterCommitSets(array $sets) { + assert_instances_of($sets, 'ArcanistLandCommitSet'); + $log = $this->getLogEngine(); + + // If some of the ancestor revisions are already closed, and the user did + // not specifically indicate that we should land them, and we are using + // a "squash" strategy, discard those sets. + + if ($this->isSquashStrategy()) { + $discard = array(); + foreach ($sets as $key => $set) { + $revision_ref = $set->getRevisionRef(); + + if (!$revision_ref->isClosed()) { + continue; + } + + $symbols = null; + foreach ($set->getCommits() as $commit) { + $commit_symbols = $commit->getDirectSymbols(); + if ($commit_symbols) { + $symbols = $commit_symbols; + break; + } + } + + if ($symbols) { + continue; + } + + $discard[] = $set; + unset($sets[$key]); + } + + if ($discard) { + echo tsprintf( + "\n%!\n%W\n", + pht('DISCARDING ANCESTORS'), + pht( + 'Some ancestor commits are associated with revisions that have '. + 'already been closed. These changes will be skipped:')); + + foreach ($discard as $set) { + $this->printCommitSet($set); + } + + echo tsprintf("\n"); + } + } + + // TODO: Some of the revisions we've identified may be mapped to an + // outdated set of commits. We should look in local branches for a better + // set of commits, and try to confirm that the state we're about to land + // is the current state in Differential. + + return $sets; + } + } From 94f78cf87c78b2e70651137b59a76294d828a623 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 13:15:43 -0700 Subject: [PATCH 24/82] Provide more information about merge progress in "arc land" under Git Summary: Ref T13546. Communicate more progress information and provide additional details when merge conflicts occur. Test Plan: Hit a merge conflict, saw more helpful output. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21318 --- src/land/engine/ArcanistGitLandEngine.php | 52 +++++++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 74a17ad4..76cf4771 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -235,11 +235,13 @@ final class ArcanistGitLandEngine protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); $this->updateWorkingCopy($into_commit); $commits = $set->getCommits(); - $source_commit = last($commits)->getHash(); + $max_commit = last($commits); + $source_commit = $max_commit->getHash(); // NOTE: See T11435 for some history. See PHI1727 for a case where a user // modified their working copy while running "arc land". This attempts to @@ -263,10 +265,15 @@ final class ArcanistGitLandEngine $this->getDisplayHash($into_commit))); } - list($original_author, $original_date) = $this->getAuthorAndDate( - $source_commit); + $log->writeStatus( + pht('MERGING'), + pht( + '%s %s', + $this->getDisplayHash($source_commit), + $max_commit->getDisplaySummary())); try { + if ($this->isSquashStrategy()) { // NOTE: We're explicitly specifying "--ff" to override the presence // of "merge.ff" options in user configuration. @@ -280,17 +287,44 @@ final class ArcanistGitLandEngine $source_commit); } } catch (CommandException $ex) { + + // TODO: If we previously succeeded with at least one merge, we could + // provide a hint that "--incremental" can do some of the work. + $api->execManualLocal('merge --abort'); $api->execManualLocal('reset --hard HEAD --'); - throw new PhutilArgumentUsageException( - pht( - 'Local "%s" does not merge cleanly into "%s". Merge or rebase '. - 'local changes so they can merge cleanly.', - $source_commit, - $into_commit)); + $direct_symbols = $max_commit->getDirectSymbols(); + $indirect_symbols = $max_commit->getIndirectSymbols(); + if ($direct_symbols) { + $message = pht( + 'Local commit "%s" (%s) does not merge cleanly into "%s". '. + 'Merge or rebase local changes so they can merge cleanly.', + $this->getDisplayHash($source_commit), + $this->getDisplaySymbols($direct_symbols), + $this->getDisplayHash($into_commit)); + } else if ($indirect_symbols) { + $message = pht( + 'Local commit "%s" (reachable from: %s) does not merge cleanly '. + 'into "%s". Merge or rebase local changes so they can merge '. + 'cleanly.', + $this->getDisplayHash($source_commit), + $this->getDisplaySymbols($indirect_symbols), + $this->getDisplayHash($into_commit)); + } else { + $message = pht( + 'Local commit "%s" does not merge cleanly into "%s". Merge or '. + 'rebase local changes so they can merge cleanly.', + $this->getDisplayHash($source_commit), + $this->getDisplayHash($into_commit)); + } + + throw new PhutilArgumentUsageException($message); } + list($original_author, $original_date) = $this->getAuthorAndDate( + $source_commit); + $revision_ref = $set->getRevisionRef(); $commit_message = $revision_ref->getCommitMessage(); From 57d0d690cc76de7bb04dc9614a7ec9f84cbf15f5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 5 Jun 2020 13:24:37 -0700 Subject: [PATCH 25/82] Modernize output when pruning branches in Git during "arc land" Summary: Ref T13546. Make this output look more similar to other modern output. Test Plan: Ran "arc land", saw consistent-looking output. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21319 --- src/land/engine/ArcanistGitLandEngine.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 76cf4771..21915ad4 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -17,6 +17,9 @@ final class ArcanistGitLandEngine } protected function pruneBranches(array $sets) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + $old_commits = array(); foreach ($sets as $set) { $hash = last($set->getCommits())->getHash(); @@ -27,20 +30,19 @@ final class ArcanistGitLandEngine $old_commits, $is_contains = false); - $api = $this->getRepositoryAPI(); foreach ($branch_map as $branch_name => $branch_hash) { $recovery_command = csprintf( 'git checkout -b %s %s', $branch_name, $this->getDisplayHash($branch_hash)); - echo tsprintf( - "%s\n", - pht('Cleaning up branch "%s"...', $branch_name)); + $log->writeStatus( + pht('CLEANUP'), + pht('Destroying branch "%s". To recover, run:', $branch_name)); echo tsprintf( - "%s\n", - pht('(Use `%s` if you want it back.)', $recovery_command)); + "\n **$** %s\n\n", + $recovery_command); $api->execxLocal('branch -D -- %s', $branch_name); $this->deletedBranches[$branch_name] = true; From 8a53b5a4517de44c09869d8786e3fd7b5f88fd0c Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 07:49:30 -0700 Subject: [PATCH 26/82] When landing changes in an empty repository, merge cleanly in Git Summary: Fixes T12876. Ref T13546. When you make the first change in a new Git repository, "arc land" currently can not merge it because there's nothing to merge into. Support merging into the empty state formally, reachable by using "--into-empty" (which should be uncommon) or "arc land" in an empty repository. Test Plan: - Used "arc land --into-empty --hold ..." to generate merges against the empty state under "squash" and "merge" strategies in Git. - Got sensible result commits with appropriate parents and content. Maniphest Tasks: T13546, T12876 Differential Revision: https://secure.phabricator.com/D21324 --- src/__phutil_library_map__.php | 4 + src/land/engine/ArcanistGitLandEngine.php | 98 ++++++---- src/land/engine/ArcanistLandEngine.php | 6 +- src/repository/api/ArcanistGitAPI.php | 18 ++ src/repository/raw/ArcanistGitRawCommit.php | 183 ++++++++++++++++++ .../ArcanistGitRawCommitTestCase.php | 91 +++++++++ 6 files changed, 359 insertions(+), 41 deletions(-) create mode 100644 src/repository/raw/ArcanistGitRawCommit.php create mode 100644 src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 31e4bd10..260ff380 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -220,6 +220,8 @@ phutil_register_library_map(array( 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php', 'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php', 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php', + 'ArcanistGitRawCommit' => 'repository/raw/ArcanistGitRawCommit.php', + 'ArcanistGitRawCommitTestCase' => 'repository/raw/__tests__/ArcanistGitRawCommitTestCase.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php', @@ -1239,6 +1241,8 @@ phutil_register_library_map(array( 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitLandEngine' => 'ArcanistLandEngine', 'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState', + 'ArcanistGitRawCommit' => 'Phobject', + 'ArcanistGitRawCommitTestCase' => 'PhutilTestCase', 'ArcanistGitUpstreamPath' => 'Phobject', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 21915ad4..2e6a8587 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -226,20 +226,18 @@ final class ArcanistGitLandEngine return $this->getLandTargetLocalCommit($target); } - private function updateWorkingCopy($into_commit) { - $api = $this->getRepositoryAPI(); - if ($into_commit === null) { - throw new Exception('TODO: Author a new empty state.'); - } else { - $api->execxLocal('checkout %s --', $into_commit); - } - } - protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) { $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); - $this->updateWorkingCopy($into_commit); + $is_empty = ($into_commit === null); + + if ($is_empty) { + $empty_commit = ArcanistGitRawCommit::newEmptyCommit(); + $into_commit = $api->writeRawCommit($empty_commit); + } + + $api->execxLocal('checkout %s --', $into_commit); $commits = $set->getCommits(); $max_commit = last($commits); @@ -251,7 +249,8 @@ final class ArcanistGitLandEngine // as changes. list($changes) = $api->execxLocal( - 'diff --no-ext-diff HEAD..%s --', + 'diff --no-ext-diff %s..%s --', + $into_commit, $source_commit); $changes = trim($changes); if (!strlen($changes)) { @@ -274,20 +273,30 @@ final class ArcanistGitLandEngine $this->getDisplayHash($source_commit), $max_commit->getDisplaySummary())); + $argv = array(); + $argv[] = '--no-stat'; + $argv[] = '--no-commit'; + + // When we're merging into the empty state, Git refuses to perform the + // merge until we tell it explicitly that we're doing something unusual. + if ($is_empty) { + $argv[] = '--allow-unrelated-histories'; + } + + if ($this->isSquashStrategy()) { + // NOTE: We're explicitly specifying "--ff" to override the presence + // of "merge.ff" options in user configuration. + $argv[] = '--ff'; + $argv[] = '--squash'; + } else { + $argv[] = '--no-ff'; + } + + $argv[] = '--'; + $argv[] = $source_commit; + try { - - if ($this->isSquashStrategy()) { - // NOTE: We're explicitly specifying "--ff" to override the presence - // of "merge.ff" options in user configuration. - - $api->execxLocal( - 'merge --no-stat --no-commit --ff --squash -- %s', - $source_commit); - } else { - $api->execxLocal( - 'merge --no-stat --no-commit --no-ff -- %s', - $source_commit); - } + $api->execxLocal('merge %Ls', $argv); } catch (CommandException $ex) { // TODO: If we previously succeeded with at least one merge, we could @@ -340,14 +349,23 @@ final class ArcanistGitLandEngine list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD'); $new_cursor = trim($stdout); - if ($into_commit === null) { + if ($is_empty) { + // See T12876. If we're landing into the empty state, we just did a fake + // merge on top of an empty commit. We're now on a commit with all of the + // right details except that it has an extra empty commit as a parent. + + // Create a new commit which is the same as the current HEAD, except that + // it doesn't have the extra parent. + + $raw_commit = $api->readRawCommit($new_cursor); if ($this->isSquashStrategy()) { - throw new Exception( - pht('TODO: Rewrite HEAD to have no parents.')); + $raw_commit->setParents(array()); } else { - throw new Exception( - pht('TODO: Rewrite HEAD to have only source as a parent.')); + $raw_commit->setParents(array($source_commit)); } + $new_cursor = $api->writeRawCommit($raw_commit); + + $api->execxLocal('checkout %s --', $new_cursor); } return $new_cursor; @@ -720,9 +738,14 @@ final class ArcanistGitLandEngine ); } - private function didHoldChanges() { + protected function didHoldChanges( + ArcanistRepositoryLocalState $state) { $log = $this->getLogEngine(); + // TODO: This probably needs updates. + + // TODO: We should refuse "--hold" if we stash. + if ($this->getIsGitPerforce()) { $this->writeInfo( pht('HOLD'), @@ -738,16 +761,15 @@ final class ArcanistGitLandEngine pht( 'Holding change locally, it has not been pushed.')); - $push_command = csprintf( - '$ git push -- %R %R:%R', - $this->getTargetRemote(), - $this->mergedRef, - $this->getTargetOnto()); + $push_command = 'TODO: ...'; + // csprintf( + // '$ git push -- %R %R:%R', + // $this->getOntoRemote(), + // $this->mergedRef, + // $this->getOnto()); } - $restore_command = csprintf( - '$ git checkout %R --', - $this->localRef); + $restore_command = 'TODO: ...'; echo tsprintf( "\n%s\n\n". diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index fa613dec..ec2107a1 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1266,8 +1266,8 @@ abstract class ArcanistLandEngine extends Phobject { } if ($is_hold) { - $this->didHoldChanges(); - $this->discardLocalState(); + $this->didHoldChanges($local_state); + $local_state->discardLocalState(); } else { $this->reconcileLocalState($into_commit, $local_state); } @@ -1275,6 +1275,7 @@ abstract class ArcanistLandEngine extends Phobject { // TODO: Restore this. // $this->getWorkflow()->askForRepositoryUpdate(); + // TODO: This is misleading under "--hold". $log->writeSuccess( pht('DONE'), pht('Landed changes.')); @@ -1287,7 +1288,6 @@ abstract class ArcanistLandEngine extends Phobject { } } - protected function validateArguments() { $log = $this->getLogEngine(); diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 2a07f9dd..3610dda0 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1753,4 +1753,22 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { ->setRepositoryAPI($this); } + public function readRawCommit($hash) { + list($stdout) = $this->execxLocal( + 'cat-file commit -- %s', + $hash); + + return ArcanistGitRawCommit::newFromRawBlob($stdout); + } + + public function writeRawCommit(ArcanistGitRawCommit $commit) { + $blob = $commit->getRawBlob(); + + $future = $this->execFutureLocal('hash-object -t commit --stdin -w'); + $future->write($blob); + list($stdout) = $future->resolvex(); + + return trim($stdout); + } + } diff --git a/src/repository/raw/ArcanistGitRawCommit.php b/src/repository/raw/ArcanistGitRawCommit.php new file mode 100644 index 00000000..7fa01de1 --- /dev/null +++ b/src/repository/raw/ArcanistGitRawCommit.php @@ -0,0 +1,183 @@ +setTreeHash(self::GIT_EMPTY_TREE_HASH); + return $raw; + } + + public static function newFromRawBlob($blob) { + $lines = phutil_split_lines($blob); + + $seen = array(); + $raw = new self(); + + $pattern = '(^(\w+) ([^\n]+)\n?\z)'; + foreach ($lines as $key => $line) { + unset($lines[$key]); + + $is_divider = ($line === "\n"); + if ($is_divider) { + break; + } + + $matches = null; + $ok = preg_match($pattern, $line, $matches); + if (!$ok) { + throw new Exception( + pht( + 'Expected to match pattern "%s" against line "%s" in raw commit '. + 'blob: %s', + $pattern, + $line, + $blob)); + } + + $label = $matches[1]; + $value = $matches[2]; + + // Detect unexpected repeated lines. + + if (isset($seen[$label])) { + switch ($label) { + case 'parent': + break; + default: + throw new Exception( + pht( + 'Encountered two "%s" lines ("%s", "%s") while parsing raw '. + 'commit blob, expected at most one: %s', + $label, + $seen[$label], + $line, + $blob)); + } + } else { + $seen[$label] = $line; + } + + switch ($label) { + case 'tree': + $raw->setTreeHash($value); + break; + case 'parent': + $raw->addParent($value); + break; + case 'author': + $raw->setRawAuthor($value); + break; + case 'committer': + $raw->setRawCommitter($value); + break; + default: + throw new Exception( + pht( + 'Unknown attribute label "%s" in line "%s" while parsing raw '. + 'commit blob: %s', + $label, + $line, + $blob)); + } + } + + $message = implode('', $lines); + $raw->setMessage($message); + + return $raw; + } + + public function getRawBlob() { + $out = array(); + + $tree = $this->getTreeHash(); + if ($tree !== null) { + $out[] = sprintf("tree %s\n", $tree); + } + + $parents = $this->getParents(); + foreach ($parents as $parent) { + $out[] = sprintf("parent %s\n", $parent); + } + + $raw_author = $this->getRawAuthor(); + if ($raw_author !== null) { + $out[] = sprintf("author %s\n", $raw_author); + } + + $raw_committer = $this->getRawCommitter(); + if ($raw_committer !== null) { + $out[] = sprintf("committer %s\n", $raw_committer); + } + + $out[] = "\n"; + + $message = $this->getMessage(); + if ($message !== null) { + $out[] = $message; + } + + return implode('', $out); + } + + public function setTreeHash($tree_hash) { + $this->treeHash = $tree_hash; + return $this; + } + + public function getTreeHash() { + return $this->treeHash; + } + + public function setRawAuthor($raw_author) { + $this->rawAuthor = $raw_author; + return $this; + } + + public function getRawAuthor() { + return $this->rawAuthor; + } + + public function setRawCommitter($raw_committer) { + $this->rawCommitter = $raw_committer; + return $this; + } + + public function getRawCommitter() { + return $this->rawCommitter; + } + + public function setParents(array $parents) { + $this->parents = $parents; + return $this; + } + + public function getParents() { + return $this->parents; + } + + public function addParent($hash) { + $this->parents[] = $hash; + return $this; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + +} diff --git a/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php b/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php new file mode 100644 index 00000000..8b04ba7e --- /dev/null +++ b/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php @@ -0,0 +1,91 @@ + 'empty', + 'blob' => array( + 'tree fcfd0454eac6a28c729aa6bf7d38a5f1efc5cc5d', + '', + '', + ), + 'tree' => 'fcfd0454eac6a28c729aa6bf7d38a5f1efc5cc5d', + ), + array( + 'name' => 'parents', + 'blob' => array( + 'tree 63ece8fd5a8283f1da2c14735d059669a09ba628', + 'parent 4aebaaf60895c3f3dd32a8cadff00db2c8f74899', + 'parent 0da1a2e17d921dc27ce9afa76b123cb4c8b73b17', + 'author alice', + 'committer alice', + '', + 'Quack quack quack.', + '', + ), + 'tree' => '63ece8fd5a8283f1da2c14735d059669a09ba628', + 'parents' => array( + '4aebaaf60895c3f3dd32a8cadff00db2c8f74899', + '0da1a2e17d921dc27ce9afa76b123cb4c8b73b17', + ), + 'author' => 'alice', + 'committer' => 'alice', + 'message' => "Quack quack quack.\n", + ), + ); + + foreach ($cases as $case) { + $name = $case['name']; + $blob = $case['blob']; + + if (is_array($blob)) { + $blob = implode("\n", $blob); + } + + $raw = ArcanistGitRawCommit::newFromRawBlob($blob); + $out = $raw->getRawBlob(); + + $this->assertEqual( + $blob, + $out, + pht( + 'Expected read + write to produce the original raw Git commit '. + 'blob in case "%s".', + $name)); + + $tree = idx($case, 'tree'); + $this->assertEqual( + $tree, + $raw->getTreeHash(), + pht('Tree hashes in case "%s".', $name)); + + $parents = idx($case, 'parents', array()); + $this->assertEqual( + $parents, + $raw->getParents(), + pht('Parents in case "%s".', $name)); + + $author = idx($case, 'author'); + $this->assertEqual( + $author, + $raw->getRawAuthor(), + pht('Authors in case "%s".', $name)); + + $committer = idx($case, 'committer'); + $this->assertEqual( + $committer, + $raw->getRawCommitter(), + pht('Committer in case "%s".', $name)); + + $message = idx($case, 'message', ''); + $this->assertEqual( + $message, + $raw->getMessage(), + pht('Message in case "%s".', $name)); + } + } + +} From 709c9cb6fbe8672f637b9e876f28a6952f7da0e2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 08:04:22 -0700 Subject: [PATCH 27/82] Improve the logic for identifying ambiguous commits and applying "--revision" to them Summary: Ref T13546. This is mostly minor cleanup that improves behavior under "--revision". Test Plan: Ran `arc land --into-empty` and `arc land --into-empty --revision 123` with ambiguous revisions in history to hit both the force and non-force outcomes. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21325 --- src/land/ArcanistLandCommit.php | 10 ++ src/land/engine/ArcanistLandEngine.php | 201 +++++++++++++++---------- 2 files changed, 132 insertions(+), 79 deletions(-) diff --git a/src/land/ArcanistLandCommit.php b/src/land/ArcanistLandCommit.php index b00c988a..3d0d2125 100644 --- a/src/land/ArcanistLandCommit.php +++ b/src/land/ArcanistLandCommit.php @@ -12,6 +12,7 @@ final class ArcanistLandCommit private $parentCommits; private $isHeadCommit; private $isImplicitCommit; + private $relatedRevisionRefs = array(); private $directSymbols = array(); private $indirectSymbols = array(); @@ -156,5 +157,14 @@ final class ArcanistLandCommit return null; } + public function setRelatedRevisionRefs(array $refs) { + assert_instances_of($refs, 'ArcanistRevisionRef'); + $this->relatedRevisionRefs = $refs; + return $this; + } + + public function getRelatedRevisionRefs() { + return $this->relatedRevisionRefs; + } } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index ec2107a1..1b295177 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -835,96 +835,76 @@ abstract class ArcanistLandEngine extends Phobject { foreach ($commit_map as $commit) { $hash = $commit->getHash(); $state_ref = $state_refs[$hash]; - $revision_refs = $state_ref->getRevisionRefs(); - - // If we have several possible revisions but one of them matches the - // "--revision" argument, just select it. This is relatively safe and - // reasonable and doesn't need a warning. - - if ($force_ref) { - if (count($revision_refs) > 1) { - foreach ($revision_refs as $revision_ref) { - if ($revision_ref->getPHID() === $force_ref->getPHID()) { - $revision_refs = array($revision_ref); - break; - } - } - } - } - - if (count($revision_refs) === 1) { - $revision_ref = head($revision_refs); - $commit->setExplicitRevisionRef($revision_ref); - continue; - } - - if (!$revision_refs) { - continue; - } - - // TODO: If we have several refs but all but one are abandoned or closed - // or authored by someone else, we could guess what you mean. - - $symbols = $commit->getIndirectSymbols(); - $raw_symbols = mpull($symbols, 'getSymbol'); - $symbol_list = implode(', ', $raw_symbols); - $display_hash = $this->getDisplayHash($hash); - - // TODO: Include "use 'arc look --type commit abc' to figure out why" - // once that works? - - echo tsprintf( - "\n%!\n%W\n\n", - pht('AMBIGUOUS REVISION'), - pht( - 'The revision associated with commit "%s" (an ancestor of: %s) '. - 'is ambiguous. These %s revision(s) are associated with the commit:', - $display_hash, - implode(', ', $raw_symbols), - phutil_count($revision_refs))); - - foreach ($revision_refs as $revision_ref) { - echo tsprintf( - '%s', - $revision_ref->newDisplayRef()); - } - - echo tsprintf("\n"); - - throw new PhutilArgumentUsageException( - pht( - 'Revision for commit "%s" is ambiguous. Use "--revision" to force '. - 'selection of a particular revision.', - $display_hash)); + $commit->setRelatedRevisionRefs($revision_refs); } - if ($force_ref) { - $phid_map = array(); - foreach ($commit_map as $commit) { - $explicit_ref = $commit->getExplicitRevisionRef(); - if ($explicit_ref) { - $revision_phid = $explicit_ref->getPHID(); - $phid_map[$revision_phid] = $revision_phid; - } + // For commits which have exactly one related revision, select it now. + + foreach ($commit_map as $commit) { + $revision_refs = $commit->getRelatedRevisionRefs(); + + if (count($revision_refs) !== 1) { + continue; } + $revision_ref = head($revision_refs); + $commit->setExplicitRevisionRef($revision_ref); + } + + // If we have a "--revision", select that revision for any commits with + // no known related revisions. + + // Also select that revision for any commits which have several possible + // revisions including that revision. This is relatively safe and + // reasonable and doesn't require a warning. + + if ($force_ref) { + $force_phid = $force_ref->getPHID(); + foreach ($commit_map as $commit) { + if ($commit->getExplicitRevisionRef()) { + continue; + } + + $revision_refs = $commit->getRelatedRevisionRefs(); + + if ($revision_refs) { + $revision_refs = mpull($revision_refs, null, 'getPHID'); + if (!isset($revision_refs[$force_phid])) { + continue; + } + } + + $commit->setExplicitRevisionRef($force_ref); + } + } + + // If we have a "--revision", identify any commits which it is not yet + // selected for. These are commits which are not associated with the + // identified revision but are associated with one or more other revisions. + + if ($force_ref) { $force_phid = $force_ref->getPHID(); - // If none of the commits have a revision, forcing the revision is - // reasonable and we don't need to confirm it. + $confirm_force = array(); + foreach ($commit_map as $key => $commit) { + $revision_ref = $commit->getExplicitRevisionRef(); - // If some of the commits have a revision, but it's the same as the - // revision we're forcing, forcing the revision is also reasonable. + if (!$revision_ref) { + continue; + } - // Discard the revision we're trying to force, then check if there's - // anything left. If some of the commits have a different revision, - // make sure the user is really doing what they expect. + if ($revision_ref->getPHID() === $force_phid) { + continue; + } - unset($phid_map[$force_phid]); + $confirm_force[] = $commit; + } + + if ($confirm_force) { - if ($phid_map) { // TODO: Make this more clear. + // TODO: Show all the commits. throw new PhutilArgumentUsageException( pht( @@ -933,10 +913,73 @@ abstract class ArcanistLandEngine extends Phobject { 'ALL these commits wiht a different unrelated revision???')); } - foreach ($commit_map as $commit) { + foreach ($confirm_force as $commit) { $commit->setExplicitRevisionRef($force_ref); } } + + // Finally, raise an error if we're left with ambiguous revisions. This + // happens when we have no "--revision" and some commits in the range + // that are associated with more than one revision. + + $ambiguous = array(); + foreach ($commit_map as $commit) { + if ($commit->getExplicitRevisionRef()) { + continue; + } + + if (!$commit->getRelatedRevisionRefs()) { + continue; + } + + $ambiguous[] = $commit; + } + + if ($ambiguous) { + foreach ($ambiguous as $commit) { + $symbols = $commit->getIndirectSymbols(); + $raw_symbols = mpull($symbols, 'getSymbol'); + $symbol_list = implode(', ', $raw_symbols); + $display_hash = $this->getDisplayHash($hash); + + $revision_refs = $commit->getRelatedRevisionRefs(); + + // TODO: Include "use 'arc look --type commit abc' to figure out why" + // once that works? + + // TODO: We could print all the ambiguous commits. + + // TODO: Suggest "--pick" as a remedy once it exists? + + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS REVISION'), + pht( + 'The revision associated with commit "%s" (an ancestor of: %s) '. + 'is ambiguous. These %s revision(s) are associated with the '. + 'commit:', + $display_hash, + implode(', ', $raw_symbols), + phutil_count($revision_refs))); + + foreach ($revision_refs as $revision_ref) { + echo tsprintf( + '%s', + $revision_ref->newDisplayRef()); + } + + echo tsprintf("\n"); + + throw new PhutilArgumentUsageException( + pht( + 'Revision for commit "%s" is ambiguous. Use "--revision" to force '. + 'selection of a particular revision.', + $display_hash)); + } + } + + // NOTE: We may exit this method with commits that are still unassociated. + // These will be handled later by the "implicit commits" mechanism. } final protected function getDisplayHash($hash) { From a30378a34ab1affacdca518731286e1e4ba928b7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 09:09:45 -0700 Subject: [PATCH 28/82] Update "arc help land" Summary: Ref T13546. Provide more up-to-date help about the "land" workflow, including modern flags and behavior. Test Plan: Read "arc help land". Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21326 --- src/toolset/ArcanistWorkflowArgument.php | 28 ++++ src/workflow/ArcanistLandWorkflow.php | 203 ++++++++++++++--------- 2 files changed, 154 insertions(+), 77 deletions(-) diff --git a/src/toolset/ArcanistWorkflowArgument.php b/src/toolset/ArcanistWorkflowArgument.php index d56b10fb..26d71ece 100644 --- a/src/toolset/ArcanistWorkflowArgument.php +++ b/src/toolset/ArcanistWorkflowArgument.php @@ -10,6 +10,7 @@ final class ArcanistWorkflowArgument private $isPathArgument; private $shortFlag; private $repeatable; + private $relatedConfig = array(); public function setKey($key) { $this->key = $key; @@ -47,6 +48,15 @@ final class ArcanistWorkflowArgument return $this->repeatable; } + public function addRelatedConfig($related_config) { + $this->relatedConfig[] = $related_config; + return $this; + } + + public function getRelatedConfig() { + return $this->relatedConfig; + } + public function getPhutilSpecification() { $spec = array( 'name' => $this->getKey(), @@ -63,6 +73,20 @@ final class ArcanistWorkflowArgument $help = $this->getHelp(); if ($help !== null) { + $config = $this->getRelatedConfig(); + + if ($config) { + $more = array(); + foreach ($this->getRelatedConfig() as $config) { + $more[] = tsprintf( + '%s **%s**', + pht('Related configuration:'), + $config); + } + $more = phutil_glue($more, "\n"); + $help = tsprintf("%B\n\n%B", $help, $more); + } + $spec['help'] = $help; } @@ -80,6 +104,10 @@ final class ArcanistWorkflowArgument } public function setHelp($help) { + if (is_array($help)) { + $help = implode("\n\n", $help); + } + $this->help = $help; return $this; } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 104a19b4..f53cfefe 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -12,82 +12,111 @@ final class ArcanistLandWorkflow public function getWorkflowInformation() { $help = pht(<<newWorkflowArgument('hold') ->setHelp( pht( - 'Prepare the change to be pushed, but do not actually push it.')), + 'Prepare the changes to be pushed, but do not actually push '. + 'them.')), $this->newWorkflowArgument('keep-branches') ->setHelp( pht( 'Keep local branches around after changes are pushed. By '. - 'default, local branches are deleted after they land.')), + 'default, local branches are deleted after the changes they '. + 'contain are published.')), $this->newWorkflowArgument('onto-remote') ->setParameter('remote-name') - ->setHelp(pht('Push to a remote other than the default.')), - - // TODO: Formally allow flags to be bound to related configuration - // for documentation, e.g. "setRelatedConfiguration('arc.land.onto')". - + ->setHelp(pht('Push to a remote other than the default.')) + ->addRelatedConfig('arc.land.onto-remote'), $this->newWorkflowArgument('onto') ->setParameter('branch-name') ->setRepeatable(true) + ->addRelatedConfig('arc.land.onto') ->setHelp( - pht( - 'After merging, push changes onto a specified branch. '. - 'Specifying this flag multiple times will push multiple '. - 'branches.')), + array( + pht( + 'After merging, push changes onto a specified branch.'), + pht( + 'Specifying this flag multiple times will push to multiple '. + 'branches.'), + )), $this->newWorkflowArgument('strategy') ->setParameter('strategy-name') + ->addRelatedConfig('arc.land.strategy') ->setHelp( - pht( - // TODO: Improve this. - 'Merge using a particular strategy.')), + array( + pht( + 'Merge using a particular strategy. Supported strategies are '. + '"squash" and "merge".'), + pht( + 'The "squash" strategy collapses multiple local commits into '. + 'a single commit when publishing. It produces a linear '. + 'published history (but discards local checkpoint commits). '. + 'This is the default strategy.'), + pht( + 'The "merge" strategy generates a merge commit when publishing '. + 'that retains local checkpoint commits (but produces a '. + 'nonlinear published history). Select this strategy if you do '. + 'not want "arc land" to discard checkpoint commits.'), + )), $this->newWorkflowArgument('revision') ->setParameter('revision-identifier') ->setHelp( pht( - 'Land a specific revision, rather than determining the revisions '. - 'from the commits that are landing.')), + 'Land a specific revision, rather than determining revisions '. + 'automatically from the commits that are landing.')), $this->newWorkflowArgument('preview') ->setHelp( pht( - 'Shows the changes that will land. Does not modify the working '. + 'Show the changes that will land. Does not modify the working '. 'copy or the remote.')), $this->newWorkflowArgument('into') ->setParameter('commit-ref') ->setHelp( pht( - 'Specifies the state to merge into. By default, this is the same '. + 'Specify the state to merge into. By default, this is the same '. 'as the "onto" ref.')), $this->newWorkflowArgument('into-remote') ->setParameter('remote-name') @@ -166,13 +211,17 @@ EOTEXT '"into" state is not specified.')), $this->newWorkflowArgument('incremental') ->setHelp( - pht( - 'When landing multiple revisions at once, push and rebase '. - 'after each operation instead of waiting until all merges '. - 'are completed. This is slower than the default behavior and '. - 'not atomic, but may make it easier to resolve conflicts and '. - 'land complicated changes by letting you make progress one '. - 'step at a time.')), + array( + pht( + 'When landing multiple revisions at once, push and rebase '. + 'after each merge completes instead of waiting until all '. + 'merges are completed to push.'), + pht( + 'This is slower than the default behavior and not atomic, '. + 'but may make it easier to resolve conflicts and land '. + 'complicated changes by allowing you to make progress one '. + 'step at a time.'), + )), $this->newWorkflowArgument('ref') ->setWildcard(true), ); From b62919f7e48a1bc1fa5e258693a4d0b2b3d30cc1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 10:40:17 -0700 Subject: [PATCH 29/82] Show some "arc" help pages through a configurable pager, like "less" Summary: Fixes T5420. Some "arc help ..." is long and most similar commands send this kind of output through a pager. Use a pager in at least some cases. Test Plan: Ran "arc help land", got pager output. Ran "arc help land | cat", got raw output. Maniphest Tasks: T5420 Differential Revision: https://secure.phabricator.com/D21327 --- ...rcanistArcConfigurationEngineExtension.php | 12 ++++++++ src/future/exec/PhutilExecPassthru.php | 21 +++++++++++++- .../workflow/PhutilHelpArgumentWorkflow.php | 29 ++++++++++++++++--- src/toolset/workflow/ArcanistHelpWorkflow.php | 3 +- src/workflow/ArcanistLandWorkflow.php | 2 +- src/workflow/ArcanistWorkflow.php | 26 +++++++++++++++++ 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index 62e88c12..62b2fa5a 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -125,6 +125,18 @@ final class ArcanistArcConfigurationEngineExtension array( '["master"]', )), + id(new ArcanistStringListConfigOption()) + ->setKey('pager') + ->setDefaultValue(array()) + ->setSummary(pht('Default pager command.')) + ->setHelp( + pht( + 'Specify the pager command to use when displaying '. + 'documentation.')) + ->setExamples( + array( + '["less", "-R", "--"]', + )), id(new ArcanistStringConfigOption()) ->setKey('arc.land.onto-remote') ->setSummary(pht('Default list of "onto" remote for "arc land".')) diff --git a/src/future/exec/PhutilExecPassthru.php b/src/future/exec/PhutilExecPassthru.php index 6dec0f2a..6501bb8c 100644 --- a/src/future/exec/PhutilExecPassthru.php +++ b/src/future/exec/PhutilExecPassthru.php @@ -20,6 +20,12 @@ */ final class PhutilExecPassthru extends PhutilExecutableFuture { + private $stdinData; + + public function write($data) { + $this->stdinData = $data; + return $this; + } /* -( Executing Passthru Commands )---------------------------------------- */ @@ -34,7 +40,15 @@ final class PhutilExecPassthru extends PhutilExecutableFuture { public function execute() { $command = $this->getCommand(); - $spec = array(STDIN, STDOUT, STDERR); + $is_write = ($this->stdinData !== null); + + if ($is_write) { + $stdin_spec = array('pipe', 'r'); + } else { + $stdin_spec = STDIN; + } + + $spec = array($stdin_spec, STDOUT, STDERR); $pipes = array(); $unmasked_command = $command->getUnmaskedString(); @@ -81,6 +95,11 @@ final class PhutilExecPassthru extends PhutilExecutableFuture { $errors)); } } else { + if ($is_write) { + fwrite($pipes[0], $this->stdinData); + fclose($pipes[0]); + } + $err = proc_close($proc); } diff --git a/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php b/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php index d464e1cf..c1e82c20 100644 --- a/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php +++ b/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php @@ -2,6 +2,17 @@ final class PhutilHelpArgumentWorkflow extends PhutilArgumentWorkflow { + private $workflow; + + public function setWorkflow($workflow) { + $this->workflow = $workflow; + return $this; + } + + public function getWorkflow() { + return $this->workflow; + } + protected function didConstruct() { $this->setName('help'); $this->setExamples(<<getArg('help-with-what'); if (!$with) { + // TODO: Update this to use a pager, too. + $args->printHelpAndExit(); } else { + $out = array(); foreach ($with as $thing) { - echo phutil_console_format( + $out[] = phutil_console_format( "**%s**\n\n", pht('%s WORKFLOW', strtoupper($thing))); - echo $args->renderWorkflowHelp($thing, $show_flags = true); - echo "\n"; + $out[] = $args->renderWorkflowHelp($thing, $show_flags = true); + $out[] = "\n"; + } + $out = implode('', $out); + + $workflow = $this->getWorkflow(); + if ($workflow) { + $workflow->writeToPager($out); + } else { + echo $out; } - exit(PhutilArgumentParser::PARSE_ERROR_CODE); } } diff --git a/src/toolset/workflow/ArcanistHelpWorkflow.php b/src/toolset/workflow/ArcanistHelpWorkflow.php index e7ac38fc..69b1df2e 100644 --- a/src/toolset/workflow/ArcanistHelpWorkflow.php +++ b/src/toolset/workflow/ArcanistHelpWorkflow.php @@ -8,7 +8,8 @@ final class ArcanistHelpWorkflow } public function newPhutilWorkflow() { - return new PhutilHelpArgumentWorkflow(); + return id(new PhutilHelpArgumentWorkflow()) + ->setWorkflow($this); } public function supportsToolset(ArcanistToolset $toolset) { diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index f53cfefe..b4467838 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -122,7 +122,7 @@ EOTEXT return $this->newWorkflowInformation() ->setSynopsis(pht('Publish reviewed changes.')) - ->addExample(pht('**land** [__options__] [__ref__ ...]')) + ->addExample(pht('**land** [__options__] -- [__ref__ ...]')) ->setHelp($help); } diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index 5922c8a2..1a3de034 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2451,4 +2451,30 @@ abstract class ArcanistWorkflow extends Phobject { return $raw_uri; } + final public function writeToPager($corpus) { + $is_tty = (function_exists('posix_isatty') && posix_isatty(STDOUT)); + + if (!$is_tty) { + echo $corpus; + } else { + $pager = $this->getConfig('pager'); + + if (!$pager) { + $pager = array('less', '-R', '--'); + } + + // Try to show the content through a pager. + $err = id(new PhutilExecPassthru('%Ls', $pager)) + ->write($corpus) + ->resolve(); + + // If the pager exits with an error, print the content normally. + if ($err) { + echo $corpus; + } + } + + return $this; + } + } From 4d61c005310eb2bdb856f1e1fa5462953f4c7f7f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 11:49:51 -0700 Subject: [PATCH 30/82] Improve final messages under "arc land --hold" Summary: Ref T13546. Update some of the "arc land --hold" behavior to be more functional/consistent with the updated workflow. Test Plan: Ran "arc land --hold" under various conditions, got sensible forward/restore instructions. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21328 --- src/land/engine/ArcanistGitLandEngine.php | 103 +++++++++--------- src/land/engine/ArcanistLandEngine.php | 32 ++++-- .../engine/ArcanistMercurialLandEngine.php | 50 ++++++++- .../state/ArcanistGitLocalState.php | 23 ++++ .../state/ArcanistMercurialLocalState.php | 5 + .../state/ArcanistRepositoryLocalState.php | 5 + src/xsprintf/tsprintf.php | 5 + 7 files changed, 164 insertions(+), 59 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 2e6a8587..d8d8cd9a 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -435,18 +435,10 @@ final class ArcanistGitLandEngine pht('PUSHING'), pht('Pushing changes to "%s".', $this->getOntoRemote())); - $refspecs = array(); - foreach ($this->getOntoRefs() as $onto_ref) { - $refspecs[] = sprintf( - '%s:%s', - $into_commit, - $onto_ref); - } - $err = $api->execPassthru( 'push -- %s %Ls', $this->getOntoRemote(), - $refspecs); + $this->newOntoRefArguments($into_commit)); if ($err) { throw new ArcanistUsageException( @@ -738,57 +730,57 @@ final class ArcanistGitLandEngine ); } - protected function didHoldChanges( - ArcanistRepositoryLocalState $state) { + protected function didHoldChanges($into_commit) { $log = $this->getLogEngine(); - - // TODO: This probably needs updates. - - // TODO: We should refuse "--hold" if we stash. + $local_state = $this->getLocalState(); if ($this->getIsGitPerforce()) { - $this->writeInfo( - pht('HOLD'), - pht( - 'Holding change locally, it has not been submitted.')); + $message = pht( + 'Holding changes locally, they have not been submitted.'); $push_command = csprintf( - '$ git p4 submit -M --commit %R --', - $this->mergedRef); + 'git p4 submit -M --commit %s --', + $into_commit); } else { - $log->writeStatus( - pht('HOLD'), - pht( - 'Holding change locally, it has not been pushed.')); + $message = pht( + 'Holding changes locally, they have not been pushed.'); - $push_command = 'TODO: ...'; - // csprintf( - // '$ git push -- %R %R:%R', - // $this->getOntoRemote(), - // $this->mergedRef, - // $this->getOnto()); + $push_command = csprintf( + 'git push -- %s %Ls', + $this->getOntoRemote(), + $this->newOntoRefArguments($into_commit)); } - $restore_command = 'TODO: ...'; + echo tsprintf( + "\n%!\n%s\n\n", + pht('HOLD CHANGES'), + $message); + + echo tsprintf( + "%s\n\n%>\n", + pht('To push changes manually, run this command:'), + $push_command); + + $restore_commands = $local_state->getRestoreCommandsForDisplay(); + if ($restore_commands) { + echo tsprintf( + "%s\n\n", + pht( + 'To go back to how things were before you ran "arc land", run '. + 'these %s command(s):', + phutil_count($restore_commands))); + + foreach ($restore_commands as $restore_command) { + echo tsprintf('%>', $restore_command); + } + + echo tsprintf("\n"); + } echo tsprintf( - "\n%s\n\n". - "%s\n\n". - " **%s**\n\n". - "%s\n\n". - " **%s**\n\n". "%s\n", pht( - 'This local working copy now contains the merged changes in a '. - 'detached state.'), - pht('You can push the changes manually with this command:'), - $push_command, - pht( - 'You can go back to how things were before you ran "arc land" with '. - 'this command:'), - $restore_command, - pht( - 'Local branches have not been changed, and are still in exactly the '. + 'Local branches have not been changed, and are still in the '. 'same state as before.')); } @@ -1407,7 +1399,7 @@ final class ArcanistGitLandEngine $log = $this->getLogEngine(); $branch = $api->getBranchName(); - if ($branch === null) { + if ($branch !== null) { $log->writeStatus( pht('SOURCE'), pht( @@ -1425,7 +1417,20 @@ final class ArcanistGitLandEngine 'Landing the current HEAD, "%s".', $commit->getCommitHash())); - return array($branch); + return array($commit->getCommitHash()); + } + + private function newOntoRefArguments($into_commit) { + $refspecs = array(); + + foreach ($this->getOntoRefs() as $onto_ref) { + $refspecs[] = sprintf( + '%s:%s', + $this->getDisplayHash($into_commit), + $onto_ref); + } + + return $refspecs; } } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 1b295177..2c920374 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -31,6 +31,8 @@ abstract class ArcanistLandEngine extends Phobject { private $intoEmpty; private $intoLocal; + private $localState; + final public function setViewer($viewer) { $this->viewer = $viewer; return $this; @@ -258,6 +260,15 @@ abstract class ArcanistLandEngine extends Phobject { return $this->intoArgument; } + private function setLocalState(ArcanistRepositoryLocalState $local_state) { + $this->localState = $local_state; + return $this; + } + + final protected function getLocalState() { + return $this->localState; + } + final protected function getOntoFromConfiguration() { $config_key = $this->getOntoConfigurationKey(); return $this->getWorkflow()->getConfig($config_key); @@ -1232,6 +1243,8 @@ abstract class ArcanistLandEngine extends Phobject { ->setWorkflow($workflow) ->saveLocalState(); + $this->setLocalState($local_state); + $seen_into = array(); try { $last_key = last_key($sets); @@ -1309,19 +1322,18 @@ abstract class ArcanistLandEngine extends Phobject { } if ($is_hold) { - $this->didHoldChanges($local_state); + $this->didHoldChanges($into_commit); $local_state->discardLocalState(); } else { + // TODO: Restore this. + // $this->getWorkflow()->askForRepositoryUpdate(); + $this->reconcileLocalState($into_commit, $local_state); + + $log->writeSuccess( + pht('DONE'), + pht('Landed changes.')); } - - // TODO: Restore this. - // $this->getWorkflow()->askForRepositoryUpdate(); - - // TODO: This is misleading under "--hold". - $log->writeSuccess( - pht('DONE'), - pht('Landed changes.')); } catch (Exception $ex) { $local_state->restoreLocalState(); throw $ex; @@ -1413,6 +1425,8 @@ abstract class ArcanistLandEngine extends Phobject { $into_commit, ArcanistRepositoryLocalState $state); + abstract protected function didHoldChanges($into_commit); + private function selectMergeStrategy() { $log = $this->getLogEngine(); diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index cc48de66..f5b13afd 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -503,7 +503,7 @@ final class ArcanistMercurialLandEngine $api->execxLocal( 'push --rev %s -- %s', - $into_commit, + hgsprintf('%s', $into_commit), $this->getOntoRemote()); } @@ -600,4 +600,52 @@ final class ArcanistMercurialLandEngine $state->discardLocalState(); } + protected function didHoldChanges($into_commit) { + $log = $this->getLogEngine(); + $local_state = $this->getLocalState(); + + $message = pht( + 'Holding changes locally, they have not been pushed.'); + + $push_command = csprintf( + '$ hg push -- %s %Ls', + + // TODO: When a parameter contains only "safe" characters, we could + // relax the behavior of hgsprintf(). + + hgsprintf('%s', $this->getDisplayHash($into_commit)), + $this->newOntoRefArguments($into_commit)); + + echo tsprintf( + "\n%!\n%s\n\n", + pht('HOLD CHANGES'), + $message); + + echo tsprintf( + "%s\n\n **%s**\n\n", + pht('To push changes manually, run this command:'), + $push_command); + + $restore_commands = $local_state->getRestoreCommandsForDisplay(); + if ($restore_commands) { + echo tsprintf( + "%s\n\n", + pht( + 'To go back to how things were before you ran "arc land", run '. + 'these %s command(s):', + phutil_count($restore_commands))); + + foreach ($restore_commands as $restore_command) { + echo tsprintf(" **%s**\n", $restore_command); + } + + echo tsprintf("\n"); + } + + echo tsprintf( + "%s\n". + pht( + 'Local branches and bookmarks have not been changed, and are still '. + 'in the same state as before.')); + } } diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php index d9487c1d..d7b9833c 100644 --- a/src/repository/state/ArcanistGitLocalState.php +++ b/src/repository/state/ArcanistGitLocalState.php @@ -84,6 +84,29 @@ final class ArcanistGitLocalState return; } + protected function newRestoreCommandsForDisplay() { + $ref = $this->localRef; + $commit = $this->localCommit; + + $commands = array(); + + if ($ref !== null) { + $commands[] = csprintf( + 'git checkout -B %s %s --', + $ref, + $this->getDisplayHash($commit)); + } else { + $commands[] = csprintf( + 'git checkout %s --', + $this->getDisplayHash($commit)); + } + + // NOTE: We run "submodule update" in the real restore workflow, but + // assume users can reasonably figure that out on their own. + + return $commands; + } + protected function canStashChanges() { return true; } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index d348e4b1..95aa0624 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -45,6 +45,11 @@ final class ArcanistMercurialLocalState return array(); } + protected function newRestoreCommandsForDisplay() { + // TODO: Provide this. + return array(); + } + protected function saveStash() { return null; } diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index d27d4a16..675e8927 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -189,6 +189,10 @@ abstract class ArcanistRepositoryLocalState $this->discardLocalState(); } + final public function getRestoreCommandsForDisplay() { + return $this->newRestoreCommandsForDisplay(); + } + protected function canStashChanges() { return false; } @@ -208,6 +212,7 @@ abstract class ArcanistRepositoryLocalState abstract protected function executeSaveLocalState(); abstract protected function executeRestoreLocalState(); abstract protected function executeDiscardLocalState(); + abstract protected function newRestoreCommandsForDisplay(); protected function getIgnoreHints() { return array(); diff --git a/src/xsprintf/tsprintf.php b/src/xsprintf/tsprintf.php index 7b8e8934..d2e34508 100644 --- a/src/xsprintf/tsprintf.php +++ b/src/xsprintf/tsprintf.php @@ -53,6 +53,11 @@ function xsprintf_terminal($userdata, &$pattern, &$pos, &$value, &$length) { $value = PhutilTerminalString::escapeStringValue($value, false); $type = 's'; break; + case '>': + $value = tsprintf(" **$ %s**\n", $value); + $value = PhutilTerminalString::escapeStringValue($value, false); + $type = 's'; + break; case 'd': $type = 'd'; break; From 0bf4da60f6d62e5d21ea333ac40b3697336819fc Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 12:51:47 -0700 Subject: [PATCH 31/82] Make Mercurial use "hg shelve" and "hg unshelve" in dirty working copies in "arc land" Summary: Ref T13546. Implement the equivalents of "git stash" in Mercurial. Test Plan: Dirtied a working copy in Mercurial, ran "arc land", saw dirty changes survive the process. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21329 --- src/land/engine/ArcanistGitLandEngine.php | 2 +- .../engine/ArcanistMercurialLandEngine.php | 20 ++++--- src/repository/api/ArcanistMercurialAPI.php | 55 +++++++++++++++++++ .../state/ArcanistMercurialLocalState.php | 47 +++++++++++++--- .../state/ArcanistRepositoryLocalState.php | 26 +++++---- src/xsprintf/hgsprintf.php | 9 ++- 6 files changed, 131 insertions(+), 28 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index d8d8cd9a..c8eda270 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -1349,7 +1349,7 @@ final class ArcanistGitLandEngine } $commits = phutil_split_lines($commits, false); - $is_first = false; + $is_first = true; foreach ($commits as $line) { if (!strlen($line)) { continue; diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index f5b13afd..b0ae9dc0 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -406,6 +406,7 @@ final class ArcanistMercurialLandEngine } $commits = phutil_split_lines($commits, false); + $is_first = true; foreach ($commits as $line) { if (!strlen($line)) { continue; @@ -438,7 +439,12 @@ final class ArcanistMercurialLandEngine } $commit = $commit_map[$hash]; - $commit->addSymbol($symbol); + if ($is_first) { + $commit->addDirectSymbol($symbol); + $is_first = false; + } + + $commit->addIndirectSymbol($symbol); } } @@ -607,14 +613,12 @@ final class ArcanistMercurialLandEngine $message = pht( 'Holding changes locally, they have not been pushed.'); + // TODO: This is only vaguely correct. + $push_command = csprintf( - '$ hg push -- %s %Ls', - - // TODO: When a parameter contains only "safe" characters, we could - // relax the behavior of hgsprintf(). - + '$ hg push --rev %s -- %s', hgsprintf('%s', $this->getDisplayHash($into_commit)), - $this->newOntoRefArguments($into_commit)); + $this->getOntoRemote()); echo tsprintf( "\n%!\n%s\n\n", @@ -643,7 +647,7 @@ final class ArcanistMercurialLandEngine } echo tsprintf( - "%s\n". + "%s\n", pht( 'Local branches and bookmarks have not been changed, and are still '. 'in the same state as before.')); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 410da8ea..a457cbcf 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -12,6 +12,9 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { private $supportsRebase; private $supportsPhases; + private $featureResults = array(); + private $featureFutures = array(); + protected function buildLocalFuture(array $argv) { $env = $this->getMercurialEnvironmentVariables(); @@ -1167,5 +1170,57 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { ->setRepositoryAPI($this); } + public function willTestMercurialFeature($feature) { + $this->executeMercurialFeatureTest($feature, false); + return $this; + } + + public function getMercurialFeature($feature) { + return $this->executeMercurialFeatureTest($feature, true); + } + + private function executeMercurialFeatureTest($feature, $resolve) { + if (array_key_exists($feature, $this->featureResults)) { + return $this->featureResults[$feature]; + } + + if (!array_key_exists($feature, $this->featureFutures)) { + $future = $this->newMercurialFeatureFuture($feature); + $future->start(); + $this->featureFutures[$feature] = $future; + } + + if (!$resolve) { + return; + } + + $future = $this->featureFutures[$feature]; + $result = $this->resolveMercurialFeatureFuture($feature, $future); + $this->featureResults[$feature] = $result; + + return $result; + } + + private function newMercurialFeatureFuture($feature) { + switch ($feature) { + case 'shelve': + return $this->execFutureLocal( + '--config extensions.shelve= shelve --help'); + default: + throw new Exception( + pht( + 'Unknown Mercurial feature "%s".', + $feature)); + } + } + + private function resolveMercurialFeatureFuture($feature, $future) { + // By default, assume the feature is a simple capability test and the + // capability is present if the feature resolves without an error. + + list($err) = $future->resolve(); + return !$err; + } + } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index 95aa0624..338971c8 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -5,7 +5,6 @@ final class ArcanistMercurialLocalState private $localCommit; private $localRef; - private $localPath; public function getLocalRef() { return $this->localRef; @@ -17,6 +16,7 @@ final class ArcanistMercurialLocalState protected function executeSaveLocalState() { $api = $this->getRepositoryAPI(); + // TODO: Fix this. } @@ -36,13 +36,16 @@ final class ArcanistMercurialLocalState } protected function canStashChanges() { - // Depends on stash extension. - return false; + $api = $this->getRepositoryAPI(); + return $api->getMercurialFeature('shelve'); } protected function getIgnoreHints() { - // TODO: Provide this. - return array(); + return array( + pht( + 'To configure Mercurial to ignore certain files in the working '. + 'copy, add them to ".hgignore".'), + ); } protected function newRestoreCommandsForDisplay() { @@ -51,15 +54,43 @@ final class ArcanistMercurialLocalState } protected function saveStash() { - return null; + $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); + + $stash_ref = sprintf( + 'arc-%s', + Filesystem::readRandomCharacters(12)); + + $api->execxLocal( + '--config extensions.shelve= shelve --unknown --name %s --', + $stash_ref); + + $log->writeStatus( + pht('SHELVE'), + pht('Shelving uncommitted changes from working copy.')); + + return $stash_ref; } protected function restoreStash($stash_ref) { - return null; + $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); + + $log->writeStatus( + pht('UNSHELVE'), + pht('Restoring uncommitted changes to working copy.')); + + $api->execxLocal( + '--config extensions.shelve= unshelve --keep --name %s --', + $stash_ref); } protected function discardStash($stash_ref) { - return null; + $api = $this->getRepositoryAPI(); + + $api->execxLocal( + '--config extensions.shelve= shelve --delete %s --', + $stash_ref); } } diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index 675e8927..1526d50d 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -161,9 +161,8 @@ abstract class ArcanistRepositoryLocalState $this->shouldRestore = false; $this->executeRestoreLocalState(); - if ($this->stashRef !== null) { - $this->restoreStash($this->stashRef); - } + $this->applyStash(); + $this->executeDiscardLocalState(); return $this; } @@ -171,12 +170,8 @@ abstract class ArcanistRepositoryLocalState final public function discardLocalState() { $this->shouldRestore = false; + $this->applyStash(); $this->executeDiscardLocalState(); - if ($this->stashRef !== null) { - $this->restoreStash($this->stashRef); - $this->discardStash($this->stashRef); - $this->stashRef = null; - } return $this; } @@ -184,9 +179,9 @@ abstract class ArcanistRepositoryLocalState final public function __destruct() { if ($this->shouldRestore) { $this->restoreLocalState(); + } else { + $this->discardLocalState(); } - - $this->discardLocalState(); } final public function getRestoreCommandsForDisplay() { @@ -209,6 +204,17 @@ abstract class ArcanistRepositoryLocalState throw new PhutilMethodNotImplementedException(); } + private function applyStash() { + if ($this->stashRef === null) { + return; + } + $stash_ref = $this->stashRef; + $this->stashRef = null; + + $this->restoreStash($stash_ref); + $this->discardStash($stash_ref); + } + abstract protected function executeSaveLocalState(); abstract protected function executeRestoreLocalState(); abstract protected function executeDiscardLocalState(); diff --git a/src/xsprintf/hgsprintf.php b/src/xsprintf/hgsprintf.php index 326d0e14..c1006593 100644 --- a/src/xsprintf/hgsprintf.php +++ b/src/xsprintf/hgsprintf.php @@ -22,7 +22,14 @@ function xsprintf_mercurial($userdata, &$pattern, &$pos, &$value, &$length) { switch ($type) { case 's': - $value = "'".addcslashes($value, "'\\")."'"; + // If this is symbol only has "safe" alphanumeric latin characters, + // and is at least one character long, we can let it through without + // escaping it. This tends to produce more readable commands. + if (preg_match('(^[a-zA-Z0-9]+\z)', $value)) { + $value = $value; + } else { + $value = "'".addcslashes($value, "'\\")."'"; + } break; case 'R': $type = 's'; From f3f31155b76118c0a8002079ecae6e9509f3f1bd Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 7 Jun 2020 13:08:34 -0700 Subject: [PATCH 32/82] Format "arc land" passthru commands more nicely, and execute them from CWD Summary: Fixes T13380. Ref T13546. Use slightly nicer command formatting for passthru commands, to make it more obvious what's going on and which pieces of output are from "arc" vs various subcommands. Also, execute repository API passthru commands from the working copy root. All other commands already did this, the older API just didn't support it. Test Plan: Ran "arc land" in Git and Mercurial repositories, saw nicer output formatting. Maniphest Tasks: T13546, T13380 Differential Revision: https://secure.phabricator.com/D21330 --- src/land/engine/ArcanistGitLandEngine.php | 12 ++++-------- src/land/engine/ArcanistLandEngine.php | 16 ++++++++++++++++ src/land/engine/ArcanistMercurialLandEngine.php | 6 +++--- src/repository/api/ArcanistGitAPI.php | 11 +++++------ src/repository/api/ArcanistMercurialAPI.php | 6 ++---- src/repository/api/ArcanistRepositoryAPI.php | 14 ++++++++++++++ src/toolset/command/ArcanistCommand.php | 14 +++++++++++++- 7 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index c8eda270..1e237991 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -174,10 +174,9 @@ final class ArcanistGitLandEngine 'Synchronizing "%s" from Perforce...', $target->getRef())); - $err = $api->execPassthru( + $err = $this->newPassthru( 'p4 sync --silent --branch %s --', $target->getRemote().'/'.$target->getRef()); - if ($err) { throw new ArcanistUsageException( pht( @@ -416,12 +415,11 @@ final class ArcanistGitLandEngine // fix conflicts and run "arc land" again. $flags_argv[] = '--conflict=quit'; - $err = $api->execPassthru( + $err = $this->newPassthru( '%LR p4 submit %LR --commit %R --', $config_argv, $flags_argv, $into_commit); - if ($err) { throw new ArcanistUsageException( pht( @@ -435,7 +433,7 @@ final class ArcanistGitLandEngine pht('PUSHING'), pht('Pushing changes to "%s".', $this->getOntoRemote())); - $err = $api->execPassthru( + $err = $this->newPassthru( 'push -- %s %Ls', $this->getOntoRemote(), $this->newOntoRefArguments($into_commit)); @@ -1298,9 +1296,7 @@ final class ArcanistGitLandEngine $ignore_failure = false) { $api = $this->getRepositoryAPI(); - // TODO: Format this fetch nicely as a workflow command. - - $err = $api->execPassthru( + $err = $this->newPassthru( 'fetch --no-tags --quiet -- %s %s', $target->getRemote(), $target->getRef()); diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 2c920374..bb3d5f95 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1548,4 +1548,20 @@ abstract class ArcanistLandEngine extends Phobject { return $sets; } + final protected function newPassthru($pattern /* , ... */) { + $workflow = $this->getWorkflow(); + $argv = func_get_args(); + + $api = $this->getRepositoryAPI(); + + $passthru = call_user_func_array( + array($api, 'newPassthru'), + $argv); + + $command = $workflow->newCommand($passthru) + ->setResolveOnError(true); + + return $command->execute(); + } + } diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index b0ae9dc0..c6b6df9d 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -360,14 +360,14 @@ final class ArcanistMercurialLandEngine // TODO: Support bookmarks. // TODO: Deal with bookmark save/restore behavior. - // TODO: Format this nicely with passthru. // TODO: Raise a good error message when the ref does not exist. - $api->execPassthru( + $err = $this->newPassthru( 'pull -b %s -- %s', $target->getRef(), $target->getRemote()); + // TODO: Deal with errors. // TODO: Deal with multiple branch heads. list($stdout) = $api->execxLocal( @@ -507,7 +507,7 @@ final class ArcanistMercurialLandEngine // TODO: This does not respect "--into" or "--onto" properly. - $api->execxLocal( + $this->newPassthru( 'push --rev %s -- %s', hgsprintf('%s', $into_commit), $this->getOntoRemote()); diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 3610dda0..0cf1713e 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -20,12 +20,11 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { protected function buildLocalFuture(array $argv) { $argv[0] = 'git '.$argv[0]; - $future = newv('ExecFuture', $argv); - $future->setCWD($this->getPath()); - return $future; + return newv('ExecFuture', $argv) + ->setCWD($this->getPath()); } - public function execPassthru($pattern /* , ... */) { + public function newPassthru($pattern /* , ... */) { $args = func_get_args(); static $git = null; @@ -43,10 +42,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $args[0] = $git.' '.$args[0]; - return call_user_func_array('phutil_passthru', $args); + return newv('PhutilExecPassthru', $args) + ->setCWD($this->getPath()); } - public function getSourceControlSystemName() { return 'git'; } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index a457cbcf..182a52b2 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -27,18 +27,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $future; } - public function execPassthru($pattern /* , ... */) { + public function newPassthru($pattern /* , ... */) { $args = func_get_args(); $env = $this->getMercurialEnvironmentVariables(); $args[0] = 'hg '.$args[0]; - $passthru = newv('PhutilExecPassthru', $args) + return newv('PhutilExecPassthru', $args) ->setEnv($env) ->setCWD($this->getPath()); - - return $passthru->resolve(); } public function getSourceControlSystemName() { diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 43026c60..d5e18ada 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -675,6 +675,20 @@ abstract class ArcanistRepositoryAPI extends Phobject { ->setResolveOnError(false); } + public function newPassthru($pattern /* , ... */) { + throw new PhutilMethodNotImplementedException(); + } + + final public function execPassthru($pattern /* , ... */) { + $args = func_get_args(); + + $future = call_user_func_array( + array($this, 'newPassthru'), + $args); + + return $future->resolve(); + } + final public function setRuntime(ArcanistRuntime $runtime) { $this->runtime = $runtime; return $this; diff --git a/src/toolset/command/ArcanistCommand.php b/src/toolset/command/ArcanistCommand.php index 0eff69f9..1c5390ef 100644 --- a/src/toolset/command/ArcanistCommand.php +++ b/src/toolset/command/ArcanistCommand.php @@ -5,6 +5,7 @@ final class ArcanistCommand private $logEngine; private $executableFuture; + private $resolveOnError = false; public function setExecutableFuture(PhutilExecutableFuture $future) { $this->executableFuture = $future; @@ -24,6 +25,15 @@ final class ArcanistCommand return $this->logEngine; } + public function setResolveOnError($resolve_on_error) { + $this->resolveOnError = $resolve_on_error; + return $this; + } + + public function getResolveOnError() { + return $this->resolveOnError; + } + public function execute() { $log = $this->getLogEngine(); $future = $this->getExecutableFuture(); @@ -41,7 +51,7 @@ final class ArcanistCommand $log->writeNewline(); - if ($err) { + if ($err && !$this->getResolveOnError()) { $log->writeError( pht('ERROR'), pht( @@ -55,5 +65,7 @@ final class ArcanistCommand '', ''); } + + return $err; } } From c5192bde3445bf686ef72561555ecf9d54e53a83 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 05:03:18 -0700 Subject: [PATCH 33/82] 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 --- src/__phutil_library_map__.php | 4 + ...rcanistArcConfigurationEngineExtension.php | 9 + .../option/ArcanistPromptsConfigOption.php | 51 +++++ .../source/ArcanistConfigurationSource.php | 1 + .../ArcanistLocalConfigurationSource.php | 8 + src/runtime/ArcanistRuntime.php | 1 - src/toolset/ArcanistPrompt.php | 213 +++++++++++++++--- src/toolset/ArcanistPromptResponse.php | 59 +++++ .../workflow/ArcanistPromptsWorkflow.php | 70 +++++- src/workflow/ArcanistLandWorkflow.php | 9 +- 10 files changed, 377 insertions(+), 48 deletions(-) create mode 100644 src/config/option/ArcanistPromptsConfigOption.php create mode 100644 src/toolset/ArcanistPromptResponse.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 260ff380..8d45be14 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index 62b2fa5a..ca6c215b 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -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()) diff --git a/src/config/option/ArcanistPromptsConfigOption.php b/src/config/option/ArcanistPromptsConfigOption.php new file mode 100644 index 00000000..99a0935a --- /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 index 0ede49e1..ed3f6666 100644 --- a/src/config/source/ArcanistConfigurationSource.php +++ b/src/config/source/ArcanistConfigurationSource.php @@ -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(); diff --git a/src/config/source/ArcanistLocalConfigurationSource.php b/src/config/source/ArcanistLocalConfigurationSource.php index f5c94944..3994cc23 100644 --- a/src/config/source/ArcanistLocalConfigurationSource.php +++ b/src/config/source/ArcanistLocalConfigurationSource.php @@ -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; + } + } diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php index 07a6ce1b..485857f3 100644 --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -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(); diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php index eab366d4..24523596 100644 --- a/src/toolset/ArcanistPrompt.php +++ b/src/toolset/ArcanistPrompt.php @@ -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( - '** %s ** %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( + '** %s ** %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** %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 index 00000000..b2ab9007 --- /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 index 104bbf8a..1ec50d23 100644 --- 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 @@ EOTEXT 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 index b4467838..04e5ed0c 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -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.')), ); } From 78e9cc9c0129053b95b9d0cc405640bb0edaa927 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 06:37:18 -0700 Subject: [PATCH 34/82] Add a check for ambiguous merge strategies after the "history.immutable" behavioral change Summary: Ref T13546. When users hit a situation where we would select "squash" but would previously select "merge", prevent them from continuing under ambiguous conditions. Test Plan: Ran "arc land" in Git with "history.immutable" true, false, and not configured. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21332 --- src/land/engine/ArcanistGitLandEngine.php | 48 +++++++++++++++++++++++ src/land/engine/ArcanistLandEngine.php | 22 +++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 1e237991..57e8b61f 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -229,6 +229,8 @@ final class ArcanistGitLandEngine $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); + $this->confirmLegacyStrategyConfiguration(); + $is_empty = ($into_commit === null); if ($is_empty) { @@ -1429,4 +1431,50 @@ final class ArcanistGitLandEngine return $refspecs; } + private function confirmLegacyStrategyConfiguration() { + // TODO: See T13547. Remove this check in the future. This prevents users + // from accidentally executing a "squash" workflow under a configuration + // which would previously have executed a "merge" workflow. + + // We're fine if we have an explicit "--strategy". + if ($this->getStrategyArgument() !== null) { + return; + } + + // We're fine if we have an explicit "arc.land.strategy". + if ($this->getStrategyFromConfiguration() !== null) { + return; + } + + // We're fine if "history.immutable" is not set to "true". + $source_list = $this->getWorkflow()->getConfigurationSourceList(); + $config_list = $source_list->getStorageValueList('history.immutable'); + if (!$config_list) { + return; + } + + $config_value = (bool)last($config_list)->getValue(); + if (!$config_value) { + return; + } + + // We're in trouble: we would previously have selected "merge" and will + // now select "squash". Make sure the user knows what they're in for. + + echo tsprintf( + "\n%!\n%W\n\n", + pht('MERGE STRATEGY IS AMBIGUOUS'), + pht( + 'See <%s>. The default merge strategy under Git with '. + '"history.immutable" has changed from "merge" to "squash". Your '. + 'configuration is ambiguous under this behavioral change. '. + '(Use "--strategy" or configure "arc.land.strategy" to bypass '. + 'this check.)', + 'https://secure.phabricator.com/T13547')); + + throw new PhutilArgumentUsageException( + pht( + 'Desired merge strategy is ambiguous, choose an explicit strategy.')); + } + } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index bb3d5f95..da354e27 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -269,13 +269,17 @@ abstract class ArcanistLandEngine extends Phobject { return $this->localState; } + final protected function getOntoConfigurationKey() { + return 'arc.land.onto'; + } + final protected function getOntoFromConfiguration() { $config_key = $this->getOntoConfigurationKey(); return $this->getWorkflow()->getConfig($config_key); } - final protected function getOntoConfigurationKey() { - return 'arc.land.onto'; + final protected function getOntoRemoteConfigurationKey() { + return 'arc.land.onto-remote'; } final protected function getOntoRemoteFromConfiguration() { @@ -283,8 +287,13 @@ abstract class ArcanistLandEngine extends Phobject { return $this->getWorkflow()->getConfig($config_key); } - final protected function getOntoRemoteConfigurationKey() { - return 'arc.land.onto-remote'; + final protected function getStrategyConfigurationKey() { + return 'arc.land.strategy'; + } + + final protected function getStrategyFromConfiguration() { + $config_key = $this->getStrategyConfigurationKey(); + return $this->getWorkflow()->getConfig($config_key); } final protected function confirmRevisions(array $sets) { @@ -1457,8 +1466,7 @@ abstract class ArcanistLandEngine extends Phobject { return $strategy; } - $strategy_key = 'arc.land.strategy'; - $strategy = $this->getWorkflow()->getConfig($strategy_key); + $strategy = $this->getStrategyFromConfiguration(); if ($strategy !== null) { if (!isset($supported_strategies[$strategy])) { throw new PhutilArgumentUsageException( @@ -1474,7 +1482,7 @@ abstract class ArcanistLandEngine extends Phobject { pht( 'Merging with "%s" strategy, configured with "%s".', $strategy, - $strategy_key)); + $this->getStrategyConfigurationKey())); return $strategy; } From 31d08f9a8faf858eab465cb415ce9f35df518b06 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 06:55:15 -0700 Subject: [PATCH 35/82] Remove old Mercurial code testing for rebase and phase support Summary: Ref T13546. The minimum required Mercurial version should now always have these features; if not, they should move to more modern feature tests. Test Plan: Grepped for affected symbols. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21334 --- src/repository/api/ArcanistMercurialAPI.php | 38 +++------------------ 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 182a52b2..dd968b29 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -9,9 +9,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { private $localCommitInfo; private $rawDiffCache = array(); - private $supportsRebase; - private $supportsPhases; - private $featureResults = array(); private $featureFutures = array(); @@ -145,19 +142,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $base; } - // Mercurial 2.1 and up have phases which indicate if something is - // published or not. To find which revs are outgoing, it's much - // faster to check the phase instead of actually checking the server. - if ($this->supportsPhases()) { - list($err, $stdout) = $this->execManualLocal( - 'log --branch %s -r %s --style default', - $this->getBranchName(), - 'draft()'); - } else { - list($err, $stdout) = $this->execManualLocal( - 'outgoing --branch %s --style default', - $this->getBranchName()); - } + list($err, $stdout) = $this->execManualLocal( + 'log --branch %s -r %s --style default', + $this->getBranchName(), + 'draft()'); if (!$err) { $logs = ArcanistMercurialParser::parseMercurialLog($stdout); @@ -508,24 +496,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } } - public function supportsRebase() { - if ($this->supportsRebase === null) { - list($err) = $this->execManualLocal('help rebase'); - $this->supportsRebase = $err === 0; - } - - return $this->supportsRebase; - } - - public function supportsPhases() { - if ($this->supportsPhases === null) { - list($err) = $this->execManualLocal('help phase'); - $this->supportsPhases = $err === 0; - } - - return $this->supportsPhases; - } - public function supportsCommitRanges() { return true; } From e8c3cc32897e30d5b4aee92f8ce3ea6ed9d0377e Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 14:50:35 -0700 Subject: [PATCH 36/82] Allow "arc" to accept any prefix of a command as that command Summary: Ref T13546. Practically, this allows "arc branch" to run "arc branches". (This risks overcorrection to some degree, but command correction only occurs if stdout is a TTY so the risk seems limited.) Test Plan: Ran "arc branch", got "arc branches" as a correction. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21338 --- .../argument/PhutilArgumentSpellingCorrector.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/parser/argument/PhutilArgumentSpellingCorrector.php b/src/parser/argument/PhutilArgumentSpellingCorrector.php index dd999123..47218a5c 100644 --- a/src/parser/argument/PhutilArgumentSpellingCorrector.php +++ b/src/parser/argument/PhutilArgumentSpellingCorrector.php @@ -115,6 +115,21 @@ final class PhutilArgumentSpellingCorrector extends Phobject { $options[$key] = $this->normalizeString($option); } + // In command mode, accept any unique prefix of a command as a shorthand + // for that command. + if ($this->getMode() === self::MODE_COMMANDS) { + $prefixes = array(); + foreach ($options as $option) { + if (!strncmp($input, $option, strlen($input))) { + $prefixes[] = $option; + } + } + + if (count($prefixes) === 1) { + return $prefixes; + } + } + $distances = array(); $inputv = phutil_utf8v($input); foreach ($options as $option) { From 599ba0f999fd5a1dfef700dc30f2ac7e19f89f36 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 08:32:03 -0700 Subject: [PATCH 37/82] Provide a more powerful query mechanism for "markers" (branches/bookmarks) Summary: Ref T13546. Various Arcanist workflows, and particularly the MercurialAPI, currently repeat quite a lot of code around parsing branches and bookmarks. In modern Mercurial, we can generally use the "head()" and "bookmark()" revsets to do this fairly sensibly. This change mostly adds //more// code (and introduces "arc bookmarks" and "arc branches" as replacements for "arc bookmark" and "arc branch") but followups should be able to mostly delete code. Test Plan: Ran "arc branches" and "arc bookmarks" in Git and Mercurial. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21333 --- src/__phutil_library_map__.php | 18 ++- src/console/view/PhutilConsoleTable.php | 14 +- ...rcanistWorkingCopyCommitHardpointQuery.php | 39 ----- src/ref/ArcanistBranchRef.php | 57 ------- src/ref/revision/ArcanistRevisionRef.php | 4 + src/repository/api/ArcanistGitAPI.php | 18 ++- src/repository/api/ArcanistMercurialAPI.php | 12 +- src/repository/api/ArcanistRepositoryAPI.php | 21 ++- .../ArcanistGitRepositoryMarkerQuery.php | 125 +++++++++++++++ src/repository/marker/ArcanistMarkerRef.php | 120 ++++++++++++++ ...ArcanistMercurialRepositoryMarkerQuery.php | 147 ++++++++++++++++++ .../marker/ArcanistRepositoryMarkerQuery.php | 63 ++++++++ src/workflow/ArcanistBookmarksWorkflow.php | 43 +++++ src/workflow/ArcanistBranchesWorkflow.php | 43 +++++ src/workflow/ArcanistFeatureBaseWorkflow.php | 6 +- src/workflow/ArcanistMarkersWorkflow.php | 118 ++++++++++++++ 16 files changed, 736 insertions(+), 112 deletions(-) delete mode 100644 src/query/ArcanistWorkingCopyCommitHardpointQuery.php delete mode 100644 src/ref/ArcanistBranchRef.php create mode 100644 src/repository/marker/ArcanistGitRepositoryMarkerQuery.php create mode 100644 src/repository/marker/ArcanistMarkerRef.php create mode 100644 src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php create mode 100644 src/repository/marker/ArcanistRepositoryMarkerQuery.php create mode 100644 src/workflow/ArcanistBookmarksWorkflow.php create mode 100644 src/workflow/ArcanistBranchesWorkflow.php create mode 100644 src/workflow/ArcanistMarkersWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8d45be14..9cac118f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -50,11 +50,12 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase.php', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'configuration/ArcanistBlindlyTrustHTTPEngineExtension.php', 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', + 'ArcanistBookmarksWorkflow' => 'workflow/ArcanistBookmarksWorkflow.php', 'ArcanistBoolConfigOption' => 'config/option/ArcanistBoolConfigOption.php', 'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php', - 'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php', 'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php', + 'ArcanistBranchesWorkflow' => 'workflow/ArcanistBranchesWorkflow.php', 'ArcanistBrowseCommitHardpointQuery' => 'browse/query/ArcanistBrowseCommitHardpointQuery.php', 'ArcanistBrowseCommitURIHardpointQuery' => 'browse/query/ArcanistBrowseCommitURIHardpointQuery.php', 'ArcanistBrowseObjectNameURIHardpointQuery' => 'browse/query/ArcanistBrowseObjectNameURIHardpointQuery.php', @@ -222,6 +223,7 @@ phutil_register_library_map(array( 'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php', 'ArcanistGitRawCommit' => 'repository/raw/ArcanistGitRawCommit.php', 'ArcanistGitRawCommitTestCase' => 'repository/raw/__tests__/ArcanistGitRawCommitTestCase.php', + 'ArcanistGitRepositoryMarkerQuery' => 'repository/marker/ArcanistGitRepositoryMarkerQuery.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php', @@ -325,12 +327,15 @@ phutil_register_library_map(array( 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLogicalOperatorsXHPASTLinterRuleTestCase.php', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php', + 'ArcanistMarkerRef' => 'repository/marker/ArcanistMarkerRef.php', + 'ArcanistMarkersWorkflow' => 'workflow/ArcanistMarkersWorkflow.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', 'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php', 'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php', 'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php', 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', + 'ArcanistMercurialRepositoryMarkerQuery' => 'repository/marker/ArcanistMercurialRepositoryMarkerQuery.php', 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', @@ -415,6 +420,7 @@ phutil_register_library_map(array( 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', 'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php', + 'ArcanistRepositoryMarkerQuery' => 'repository/marker/ArcanistRepositoryMarkerQuery.php', 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php', @@ -538,7 +544,6 @@ phutil_register_library_map(array( 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', 'ArcanistWorkflowMercurialHardpointQuery' => 'query/ArcanistWorkflowMercurialHardpointQuery.php', 'ArcanistWorkingCopy' => 'workingcopy/ArcanistWorkingCopy.php', - 'ArcanistWorkingCopyCommitHardpointQuery' => 'query/ArcanistWorkingCopyCommitHardpointQuery.php', 'ArcanistWorkingCopyConfigurationSource' => 'config/source/ArcanistWorkingCopyConfigurationSource.php', 'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php', 'ArcanistWorkingCopyPath' => 'workingcopy/ArcanistWorkingCopyPath.php', @@ -1059,11 +1064,12 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'PhutilHTTPEngineExtension', 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureBaseWorkflow', + 'ArcanistBookmarksWorkflow' => 'ArcanistMarkersWorkflow', 'ArcanistBoolConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistBranchRef' => 'ArcanistRef', 'ArcanistBranchWorkflow' => 'ArcanistFeatureBaseWorkflow', + 'ArcanistBranchesWorkflow' => 'ArcanistMarkersWorkflow', 'ArcanistBrowseCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBrowseCommitURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', 'ArcanistBrowseObjectNameURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', @@ -1245,6 +1251,7 @@ phutil_register_library_map(array( 'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistGitRawCommit' => 'Phobject', 'ArcanistGitRawCommitTestCase' => 'PhutilTestCase', + 'ArcanistGitRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', 'ArcanistGitUpstreamPath' => 'Phobject', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', @@ -1348,12 +1355,15 @@ phutil_register_library_map(array( 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistMarkerRef' => 'ArcanistRef', + 'ArcanistMarkersWorkflow' => 'ArcanistArcWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMercurialLandEngine' => 'ArcanistLandEngine', 'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState', 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', + 'ArcanistMercurialRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', @@ -1441,6 +1451,7 @@ phutil_register_library_map(array( 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', 'ArcanistRepositoryLocalState' => 'Phobject', + 'ArcanistRepositoryMarkerQuery' => 'Phobject', 'ArcanistRepositoryRef' => 'ArcanistRef', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', @@ -1571,7 +1582,6 @@ phutil_register_library_map(array( 'ArcanistWorkflowInformation' => 'Phobject', 'ArcanistWorkflowMercurialHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopy' => 'Phobject', - 'ArcanistWorkingCopyCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkingCopyConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistWorkingCopyIdentity' => 'Phobject', 'ArcanistWorkingCopyPath' => 'Phobject', diff --git a/src/console/view/PhutilConsoleTable.php b/src/console/view/PhutilConsoleTable.php index 7c29f991..392ff813 100644 --- a/src/console/view/PhutilConsoleTable.php +++ b/src/console/view/PhutilConsoleTable.php @@ -57,9 +57,9 @@ final class PhutilConsoleTable extends PhutilConsoleView { /* -( Data )--------------------------------------------------------------- */ - public function addColumn($key, array $column) { + public function addColumn($key, array $column = array()) { PhutilTypeSpec::checkMap($column, array( - 'title' => 'string', + 'title' => 'optional string', 'align' => 'optional string', )); $this->columns[$key] = $column; @@ -85,6 +85,16 @@ final class PhutilConsoleTable extends PhutilConsoleView { return $this; } + public function drawRows(array $rows) { + $this->data = array(); + $this->widths = array(); + + foreach ($rows as $row) { + $this->addRow($row); + } + + return $this->draw(); + } /* -( Drawing )------------------------------------------------------------ */ diff --git a/src/query/ArcanistWorkingCopyCommitHardpointQuery.php b/src/query/ArcanistWorkingCopyCommitHardpointQuery.php deleted file mode 100644 index 9f2b4672..00000000 --- a/src/query/ArcanistWorkingCopyCommitHardpointQuery.php +++ /dev/null @@ -1,39 +0,0 @@ -yieldRequests( - $refs, - array( - ArcanistWorkingCopyStateRef::HARDPOINT_BRANCHREF, - )); - - $branch_refs = mpull($refs, 'getBranchRef'); - - yield $this->yieldRequests( - $branch_refs, - array( - ArcanistBranchRef::HARDPOINT_COMMITREF, - )); - - $results = array(); - foreach ($refs as $key => $ref) { - $results[$key] = $ref->getBranchRef()->getCommitRef(); - } - - yield $this->yieldMap($results); - } - -} diff --git a/src/ref/ArcanistBranchRef.php b/src/ref/ArcanistBranchRef.php deleted file mode 100644 index 067e5a2e..00000000 --- a/src/ref/ArcanistBranchRef.php +++ /dev/null @@ -1,57 +0,0 @@ -getBranchName()); - } - - protected function newHardpoints() { - return array( - $this->newHardpoint(self::HARDPOINT_COMMITREF), - ); - } - - public function setBranchName($branch_name) { - $this->branchName = $branch_name; - return $this; - } - - public function getBranchName() { - return $this->branchName; - } - - public function setRefName($ref_name) { - $this->refName = $ref_name; - return $this; - } - - public function getRefName() { - return $this->refName; - } - - public function setIsCurrentBranch($is_current_branch) { - $this->isCurrentBranch = $is_current_branch; - return $this; - } - - public function getIsCurrentBranch() { - return $this->isCurrentBranch; - } - - public function attachCommitRef(ArcanistCommitRef $ref) { - return $this->attachHardpoint(self::HARDPOINT_COMMITREF, $ref); - } - - public function getCommitRef() { - return $this->getHardpoint(self::HARDPOINT_COMMITREF); - } - -} diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index a6cd3c01..9ec2fb87 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -72,6 +72,10 @@ final class ArcanistRevisionRef return idxv($this->parameters, array('fields', 'status', 'name')); } + public function getStatusANSIColor() { + return idxv($this->parameters, array('fields', 'status', 'color.ansi')); + } + public function isStatusChangesPlanned() { $status = $this->getStatus(); return ($status === 'changes-planned'); diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 0cf1713e..2c16a570 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1113,10 +1113,9 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { ->setCommitEpoch($branch['epoch']) ->attachMessage($branch['text']); - $refs[] = $this->newBranchRef() - ->setBranchName($branch['name']) - ->setRefName($branch['ref']) - ->setIsCurrentBranch($branch['current']) + $refs[] = $this->newMarkerRef() + ->setName($branch['name']) + ->setIsActive($branch['current']) ->attachCommitRef($commit_ref); } @@ -1770,4 +1769,15 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return trim($stdout); } + protected function newSupportedMarkerTypes() { + return array( + ArcanistMarkerRef::TYPE_BRANCH, + ); + } + + protected function newMarkerRefQueryTemplate() { + return new ArcanistGitRepositoryMarkerQuery(); + } + + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index dd968b29..25cbd3b3 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -542,7 +542,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $commit_ref = $this->newCommitRef() ->setCommitHash($branch['hash']); - $refs[] = $this->newBranchRef() + $refs[] = $this->newMarkerRef() ->setBranchName($branch['name']) ->setIsCurrentBranch($branch['current']) ->attachCommitRef($commit_ref); @@ -1190,5 +1190,15 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return !$err; } + protected function newSupportedMarkerTypes() { + return array( + ArcanistMarkerRef::TYPE_BRANCH, + ArcanistMarkerRef::TYPE_BOOKMARK, + ); + } + + protected function newMarkerRefQueryTemplate() { + return new ArcanistMercurialRepositoryMarkerQuery(); + } } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index d5e18ada..d1dcf4dc 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -745,8 +745,8 @@ abstract class ArcanistRepositoryAPI extends Phobject { return new ArcanistCommitRef(); } - final public function newBranchRef() { - return new ArcanistBranchRef(); + final public function newMarkerRef() { + return new ArcanistMarkerRef(); } final public function getLandEngine() { @@ -763,4 +763,21 @@ abstract class ArcanistRepositoryAPI extends Phobject { return null; } + final public function getSupportedMarkerTypes() { + return $this->newSupportedMarkerTypes(); + } + + protected function newSupportedMarkerTypes() { + return array(); + } + + final public function newMarkerRefQuery() { + return id($this->newMarkerRefQueryTemplate()) + ->setRepositoryAPI($this); + } + + protected function newMarkerRefQueryTemplate() { + throw new PhutilMethodNotImplementedException(); + } + } diff --git a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php new file mode 100644 index 00000000..0c0ede61 --- /dev/null +++ b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php @@ -0,0 +1,125 @@ +getRepositoryAPI(); + + $future = $this->newCurrentBranchNameFuture()->start(); + + $field_list = array( + '%(refname)', + '%(objectname)', + '%(committerdate:raw)', + '%(tree)', + '%(*objectname)', + '%(subject)', + '%(subject)%0a%0a%(body)', + '%02', + ); + $expect_count = count($field_list); + + $branch_prefix = 'refs/heads/'; + $branch_length = strlen($branch_prefix); + + // NOTE: Since we only return branches today, we restrict this operation + // to branches. + + list($stdout) = $api->newFuture( + 'for-each-ref --format %s -- refs/heads/', + implode('%01', $field_list))->resolve(); + + $markers = array(); + + $lines = explode("\2", $stdout); + foreach ($lines as $line) { + $line = trim($line); + if (!strlen($line)) { + continue; + } + + $fields = explode("\1", $line, $expect_count); + $actual_count = count($fields); + if ($actual_count !== $expect_count) { + throw new Exception( + pht( + 'Unexpected field count when parsing line "%s", got %s but '. + 'expected %s.', + $line, + new PhutilNumber($actual_count), + new PhutilNumber($expect_count))); + } + + list($ref, $hash, $epoch, $tree, $dst_hash, $summary, $text) = $fields; + + if (!strncmp($ref, $branch_prefix, $branch_length)) { + $type = ArcanistMarkerRef::TYPE_BRANCH; + $name = substr($ref, $branch_length); + } else { + // For now, discard other refs. + continue; + } + + $marker = id(new ArcanistMarkerRef()) + ->setName($name) + ->setMarkerType($type) + ->setEpoch((int)$epoch) + ->setMarkerHash($hash) + ->setTreeHash($tree) + ->setSummary($summary) + ->setMessage($text); + + if (strlen($dst_hash)) { + $commit_hash = $dst_hash; + } else { + $commit_hash = $hash; + } + + $marker->setCommitHash($commit_hash); + + $commit_ref = $api->newCommitRef() + ->setCommitHash($commit_hash) + ->attachMessage($text); + + $marker->attachCommitRef($commit_ref); + + $markers[] = $marker; + } + + $current = $this->resolveCurrentBranchNameFuture($future); + + if ($current !== null) { + foreach ($markers as $marker) { + if ($marker->getName() === $current) { + $marker->setIsActive(true); + } + } + } + + return $markers; + } + + private function newCurrentBranchNameFuture() { + $api = $this->getRepositoryAPI(); + return $api->newFuture('symbolic-ref --quiet HEAD --') + ->setResolveOnError(true); + } + + private function resolveCurrentBranchNameFuture($future) { + list($err, $stdout) = $future->resolve(); + + if ($err) { + return null; + } + + $matches = null; + if (!preg_match('(^refs/heads/(.*)\z)', trim($stdout), $matches)) { + return null; + } + + return $matches[1]; + } + +} diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php new file mode 100644 index 00000000..42aeb0e0 --- /dev/null +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -0,0 +1,120 @@ +getName()); + } + + protected function newHardpoints() { + return array( + $this->newHardpoint(self::HARDPOINT_COMMITREF), + ); + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setMarkerType($marker_type) { + $this->markerType = $marker_type; + return $this; + } + + public function getMarkerType() { + return $this->markerType; + } + + public function setEpoch($epoch) { + $this->epoch = $epoch; + return $this; + } + + public function getEpoch() { + return $this->epoch; + } + + public function setMarkerHash($marker_hash) { + $this->markerHash = $marker_hash; + return $this; + } + + public function getMarkerHash() { + return $this->markerHash; + } + + public function setCommitHash($commit_hash) { + $this->commitHash = $commit_hash; + return $this; + } + + public function getCommitHash() { + return $this->commitHash; + } + + public function setTreeHash($tree_hash) { + $this->treeHash = $tree_hash; + return $this; + } + + public function getTreeHash() { + return $this->treeHash; + } + + public function setSummary($summary) { + $this->summary = $summary; + return $this; + } + + public function getSummary() { + return $this->summary; + } + + public function setMessage($message) { + $this->message = $message; + return $this; + } + + public function getMessage() { + return $this->message; + } + + public function setIsActive($is_active) { + $this->isActive = $is_active; + return $this; + } + + public function getIsActive() { + return $this->isActive; + } + + public function attachCommitRef(ArcanistCommitRef $ref) { + return $this->attachHardpoint(self::HARDPOINT_COMMITREF, $ref); + } + + public function getCommitRef() { + return $this->getHardpoint(self::HARDPOINT_COMMITREF); + } + +} diff --git a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php new file mode 100644 index 00000000..9e7a627c --- /dev/null +++ b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php @@ -0,0 +1,147 @@ +shouldQueryMarkerType(ArcanistMarkerRef::TYPE_BRANCH)) { + $markers[] = $this->newBranchOrBookmarkMarkers(false); + } + + if ($this->shouldQueryMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK)) { + $markers[] = $this->newBranchOrBookmarkMarkers(true); + } + + return array_mergev($markers); + } + + private function newBranchOrBookmarkMarkers($is_bookmarks) { + $api = $this->getRepositoryAPI(); + + $is_branches = !$is_bookmarks; + + // NOTE: This is a bit clumsy, but it allows us to get most bookmark and + // branch information in a single command, including full hashes, without + // using "--debug" or matching any human readable strings in the output. + + // NOTE: We can't get branches and bookmarks together in a single command + // because if we query for "heads() + bookmark()", we can't tell if a + // bookmarked result is a branch head or not. + + $template_fields = array( + '{node}', + '{branch}', + '{join(bookmarks, "\3")}', + '{activebookmark}', + '{desc}', + ); + $expect_fields = count($template_fields); + + $template = implode('\2', $template_fields).'\1'; + + if ($is_bookmarks) { + $query = hgsprintf('bookmark()'); + } else { + $query = hgsprintf('head()'); + } + + $future = $api->newFuture( + 'log --rev %s --template %s --', + $query, + $template); + + list($lines) = $future->resolve(); + + $markers = array(); + + $lines = explode("\1", $lines); + foreach ($lines as $line) { + if (!strlen(trim($line))) { + continue; + } + + $fields = explode("\2", $line, $expect_fields); + $actual_fields = count($fields); + if ($actual_fields !== $expect_fields) { + throw new Exception( + pht( + 'Unexpected number of fields in line "%s", expected %s but '. + 'found %s.', + $line, + new PhutilNumber($expect_fields), + new PhutilNumber($actual_fields))); + } + + $node = $fields[0]; + + $branch = $fields[1]; + if (!strlen($branch)) { + $branch = 'default'; + } + + if ($is_bookmarks) { + $bookmarks = $fields[2]; + if (strlen($bookmarks)) { + $bookmarks = explode("\3", $fields[2]); + } else { + $bookmarks = array(); + } + + if (strlen($fields[3])) { + $active_bookmark = $fields[3]; + } else { + $active_bookmark = null; + } + } else { + $bookmarks = array(); + $active_bookmark = null; + } + + $message = $fields[4]; + + $commit_ref = $api->newCommitRef() + ->setCommitHash($node) + ->attachMessage($message); + + $template = id(new ArcanistMarkerRef()) + ->setCommitHash($node) + ->attachCommitRef($commit_ref); + + if ($is_bookmarks) { + foreach ($bookmarks as $bookmark) { + $is_active = ($bookmark === $active_bookmark); + + $markers[] = id(clone $template) + ->setMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK) + ->setName($bookmark) + ->setIsActive($is_active); + } + } + + if ($is_branches) { + $markers[] = id(clone $template) + ->setMarkerType(ArcanistMarkerRef::TYPE_BRANCH) + ->setName($branch); + } + } + + if ($is_branches) { + $current_hash = $api->getCanonicalRevisionName('.'); + + foreach ($markers as $marker) { + if ($marker->getMarkerType() !== ArcanistMarkerRef::TYPE_BRANCH) { + continue; + } + + if ($marker->getCommitHash() === $current_hash) { + $marker->setIsActive(true); + } + } + } + + return $markers; + } + +} diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php new file mode 100644 index 00000000..91d1c3fc --- /dev/null +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -0,0 +1,63 @@ +repositoryAPI = $api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + final public function withTypes(array $types) { + $this->types = array_fuse($types); + return $this; + } + + final public function execute() { + $markers = $this->newRefMarkers(); + + $types = $this->types; + if ($types !== null) { + foreach ($markers as $key => $marker) { + if (!isset($types[$marker->getMarkerType()])) { + unset($markers[$key]); + } + } + } + + return $this->sortMarkers($markers); + } + + private function sortMarkers(array $markers) { + // Sort the list in natural order. If we apply a stable sort later, + // markers will sort in "feature1", "feature2", etc., order if they + // don't otherwise have a unique position. + + // This can improve behavior if two branches were updated at the same + // time, as is common when cascading rebases after changes land. + + $map = mpull($markers, 'getName'); + natcasesort($map); + $markers = array_select_keys($markers, array_keys($map)); + + return $markers; + } + + final protected function shouldQueryMarkerType($marker_type) { + if ($this->types === null) { + return true; + } + + return isset($this->types[$marker_type]); + } + +} diff --git a/src/workflow/ArcanistBookmarksWorkflow.php b/src/workflow/ArcanistBookmarksWorkflow.php new file mode 100644 index 00000000..0deda2e8 --- /dev/null +++ b/src/workflow/ArcanistBookmarksWorkflow.php @@ -0,0 +1,43 @@ +newWorkflowInformation() + ->setSynopsis( + pht('Show an enhanced view of bookmarks in the working copy.')) + ->addExample(pht('**bookmarks**')) + ->setHelp($help); + } + + protected function getWorkflowMarkerType() { + $api = $this->getRepositoryAPI(); + $marker_type = ArcanistMarkerRef::TYPE_BOOKMARK; + + if (!$this->hasMarkerTypeSupport($marker_type)) { + throw new PhutilArgumentUsageException( + pht( + 'The version control system ("%s") in the current working copy '. + 'does not support bookmarks.', + $api->getSourceControlSystemName())); + } + + return $marker_type; + } + +} diff --git a/src/workflow/ArcanistBranchesWorkflow.php b/src/workflow/ArcanistBranchesWorkflow.php new file mode 100644 index 00000000..1adb2afb --- /dev/null +++ b/src/workflow/ArcanistBranchesWorkflow.php @@ -0,0 +1,43 @@ +newWorkflowInformation() + ->setSynopsis( + pht('Show an enhanced view of branches in the working copy.')) + ->addExample(pht('**branches**')) + ->setHelp($help); + } + + protected function getWorkflowMarkerType() { + $api = $this->getRepositoryAPI(); + $marker_type = ArcanistMarkerRef::TYPE_BRANCH; + + if (!$this->hasMarkerTypeSupport($marker_type)) { + throw new PhutilArgumentUsageException( + pht( + 'The version control system ("%s") in the current working copy '. + 'does not support branches.', + $api->getSourceControlSystemName())); + } + + return $marker_type; + } + +} diff --git a/src/workflow/ArcanistFeatureBaseWorkflow.php b/src/workflow/ArcanistFeatureBaseWorkflow.php index b6224d34..2b15c803 100644 --- a/src/workflow/ArcanistFeatureBaseWorkflow.php +++ b/src/workflow/ArcanistFeatureBaseWorkflow.php @@ -191,7 +191,7 @@ EOHELP } } - if (!$this->getArgument('view-all') && !$branch->getIsCurrentBranch()) { + if (!$this->getArgument('view-all') && !$branch->getIsActive()) { if ($status == 'Closed' || $status == 'Abandoned') { continue; } @@ -216,8 +216,8 @@ EOHELP } $out[] = array( - 'name' => $branch->getBranchName(), - 'current' => $branch->getIsCurrentBranch(), + 'name' => $branch->getName(), + 'current' => $branch->getIsActive(), 'status' => $status, 'desc' => $desc, 'revision' => $revision ? $revision->getID() : null, diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php new file mode 100644 index 00000000..c92808cc --- /dev/null +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -0,0 +1,118 @@ +getRepositoryAPI(); + + $marker_type = $this->getWorkflowMarkerType(); + + $markers = $api->newMarkerRefQuery() + ->withTypes(array($marker_type)) + ->execute(); + + $states = array(); + foreach ($markers as $marker) { + $state_ref = id(new ArcanistWorkingCopyStateRef()) + ->setCommitRef($marker->getCommitRef()); + + $states[] = array( + 'marker' => $marker, + 'state' => $state_ref, + ); + } + + $this->loadHardpoints( + ipull($states, 'state'), + ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); + + $vectors = array(); + foreach ($states as $key => $state) { + $marker_ref = $state['marker']; + $state_ref = $state['state']; + + $vector = id(new PhutilSortVector()) + ->addInt($marker_ref->getIsActive() ? 1 : 0) + ->addInt($marker_ref->getEpoch()); + + $vectors[$key] = $vector; + } + + $vectors = msortv($vectors, 'getSelf'); + $states = array_select_keys($states, array_keys($vectors)); + + $table = id(new PhutilConsoleTable()) + ->setShowHeader(false) + ->addColumn('active') + ->addColumn('name') + ->addColumn('status') + ->addColumn('description'); + + $rows = array(); + foreach ($states as $state) { + $marker_ref = $state['marker']; + $state_ref = $state['state']; + $revision_ref = null; + $commit_ref = $marker_ref->getCommitRef(); + + $marker_name = tsprintf('**%s**', $marker_ref->getName()); + + if ($state_ref->hasAmbiguousRevisionRefs()) { + $status = pht('Ambiguous'); + } else { + $revision_ref = $state_ref->getRevisionRef(); + if (!$revision_ref) { + $status = tsprintf( + '%s', + pht('No Revision')); + } else { + $status = $revision_ref->getStatusDisplayName(); + + $ansi_color = $revision_ref->getStatusANSIColor(); + if ($ansi_color) { + $status = tsprintf( + sprintf('%%s', $ansi_color), + $status); + } + } + } + + if ($revision_ref) { + $description = $revision_ref->getFullName(); + } else { + $description = $commit_ref->getSummary(); + } + + if ($marker_ref->getIsActive()) { + $active_mark = '*'; + } else { + $active_mark = ' '; + } + $is_active = tsprintf('** %s **', $active_mark); + + $rows[] = array( + 'active' => $is_active, + 'name' => $marker_name, + 'status' => $status, + 'description' => $description, + ); + } + + $table->drawRows($rows); + + return 0; + } + + final protected function hasMarkerTypeSupport($marker_type) { + $api = $this->getRepositoryAPI(); + + $types = $api->getSupportedMarkerTypes(); + $types = array_fuse($types); + + return isset($types[$marker_type]); + } + +} From 5abf0b96c8d9ef97ecbccc2cdcf68c18e8d74075 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 11:30:28 -0700 Subject: [PATCH 38/82] Use MarkerRefs to resolve landing symbols in Mercurial Summary: Ref T13546. Update the Mercurial code which finds default targets and maps symbols to targets under "arc land" to use the new MarkerRef workflow. Test Plan: Ran "arc land" with (and without) various arguments in Mercurial, saw them resolve in a seemingly sensible way. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21335 --- .../engine/ArcanistMercurialLandEngine.php | 106 +++++++++++++----- src/repository/api/ArcanistMercurialAPI.php | 1 + src/repository/marker/ArcanistMarkerRef.php | 8 ++ .../marker/ArcanistRepositoryMarkerQuery.php | 26 ++++- src/workflow/ArcanistMarkersWorkflow.php | 2 +- 5 files changed, 111 insertions(+), 32 deletions(-) diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index c6b6df9d..43fd3bd9 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -7,9 +7,19 @@ final class ArcanistMercurialLandEngine $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); - $bookmark = $api->getActiveBookmark(); - if ($bookmark !== null) { + $markers = $api->newMarkerRefQuery() + ->withIsActive(true) + ->execute(); + $bookmark = null; + foreach ($markers as $marker) { + if ($marker->isBookmark()) { + $bookmark = $marker->getName(); + break; + } + } + + if ($bookmark !== null) { $log->writeStatus( pht('SOURCE'), pht( @@ -19,50 +29,96 @@ final class ArcanistMercurialLandEngine return array($bookmark); } - $branch = $api->getBranchName(); - if ($branch !== null) { + $branch = null; + foreach ($markers as $marker) { + if ($marker->isBranch()) { + $branch = $marker->getName(); + break; + } + } + if ($branch !== null) { $log->writeStatus( pht('SOURCE'), pht( - 'Landing the current branch, "%s".', + 'Landing the active branch, "%s".', $branch)); return array($branch); } - throw new Exception(pht('TODO: Operate on raw revision.')); + $commit = $api->getCanonicalRevisionName('.'); + + $log->writeStatus( + pht('SOURCE'), + pht( + 'Landing the active commit, "%s".', + $this->getDisplayHash($commit))); + + return array($commit); } protected function resolveSymbols(array $symbols) { assert_instances_of($symbols, 'ArcanistLandSymbol'); $api = $this->getRepositoryAPI(); - foreach ($symbols as $symbol) { + $marker_types = array( + ArcanistMarkerRef::TYPE_BOOKMARK, + ArcanistMarkerRef::TYPE_BRANCH, + ); + + $unresolved = $symbols; + foreach ($marker_types as $marker_type) { + $markers = $api->newMarkerRefQuery() + ->withMarkerTypes(array($marker_type)) + ->execute(); + + $markers = mgroup($markers, 'getName'); + + foreach ($unresolved as $key => $symbol) { + $raw_symbol = $symbol->getSymbol(); + + $named_markers = idx($markers, $raw_symbol); + if (!$named_markers) { + continue; + } + + if (count($named_markers) > 1) { + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" is ambiguous: it matches multiple markers '. + '(of type "%s"). Use an unambiguous identifier.', + $raw_symbol, + $marker_type)); + } + + $marker = head($named_markers); + + $symbol->setCommit($marker->getCommitHash()); + + unset($unresolved[$key]); + } + } + + foreach ($unresolved as $symbol) { $raw_symbol = $symbol->getSymbol(); - if ($api->isBookmark($raw_symbol)) { - $hash = $api->getBookmarkCommitHash($raw_symbol); - $symbol->setCommit($hash); - - // TODO: Set that this is a bookmark? - - continue; + // TODO: This doesn't have accurate error behavior if the user provides + // a revset like "x::y". + try { + $commit = $api->getCanonicalRevisionName($raw_symbol); + } catch (CommandException $ex) { + $commit = null; } - if ($api->isBranch($raw_symbol)) { - $hash = $api->getBranchCommitHash($raw_symbol); - $symbol->setCommit($hash); - - // TODO: Set that this is a branch? - - continue; + if ($commit === null) { + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" does not identify a bookmark, branch, or commit.', + $raw_symbol)); } - throw new PhutilArgumentUsageException( - pht( - 'Symbol "%s" is not a bookmark or branch name.', - $raw_symbol)); + $symbol->setCommit($commit); } } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 25cbd3b3..e73578b8 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -59,6 +59,7 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { 'log -l 1 --template %s -r %s --', '{node}', $string); + return $stdout; } diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index 42aeb0e0..acf53390 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -109,6 +109,14 @@ final class ArcanistMarkerRef return $this->isActive; } + public function isBookmark() { + return ($this->getMarkerType() === self::TYPE_BOOKMARK); + } + + public function isBranch() { + return ($this->getMarkerType() === self::TYPE_BRANCH); + } + public function attachCommitRef(ArcanistCommitRef $ref) { return $this->attachHardpoint(self::HARDPOINT_COMMITREF, $ref); } diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index 91d1c3fc..1260752d 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -4,7 +4,8 @@ abstract class ArcanistRepositoryMarkerQuery extends Phobject { private $repositoryAPI; - private $types; + private $isActive; + private $markerTypes; private $commitHashes; private $ancestorCommitHashes; @@ -17,15 +18,20 @@ abstract class ArcanistRepositoryMarkerQuery return $this->repositoryAPI; } - final public function withTypes(array $types) { - $this->types = array_fuse($types); + final public function withMarkerTypes(array $types) { + $this->markerTypes = array_fuse($types); + return $this; + } + + final public function withIsActive($active) { + $this->isActive = $active; return $this; } final public function execute() { $markers = $this->newRefMarkers(); - $types = $this->types; + $types = $this->markerTypes; if ($types !== null) { foreach ($markers as $key => $marker) { if (!isset($types[$marker->getMarkerType()])) { @@ -34,6 +40,14 @@ abstract class ArcanistRepositoryMarkerQuery } } + if ($this->isActive !== null) { + foreach ($markers as $key => $marker) { + if ($marker->getIsActive() !== $this->isActive) { + unset($markers[$key]); + } + } + } + return $this->sortMarkers($markers); } @@ -53,11 +67,11 @@ abstract class ArcanistRepositoryMarkerQuery } final protected function shouldQueryMarkerType($marker_type) { - if ($this->types === null) { + if ($this->markerTypes === null) { return true; } - return isset($this->types[$marker_type]); + return isset($this->markerTypes[$marker_type]); } } diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index c92808cc..2e01707b 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -11,7 +11,7 @@ abstract class ArcanistMarkersWorkflow $marker_type = $this->getWorkflowMarkerType(); $markers = $api->newMarkerRefQuery() - ->withTypes(array($marker_type)) + ->withMarkerTypes(array($marker_type)) ->execute(); $states = array(); From 3d64140ff31c502c8589ed2e4d41fc465ce5d498 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 12:13:37 -0700 Subject: [PATCH 39/82] Implement "arc work", to replace "arc feature" Summary: Ref T13546. Fixes T2928. Adds a new "arc work" workflow which functions like the older "arc feature" workflow, but with modern infrastructure. Test Plan: Used "arc work" to begin work on branches, bookmarks, and revisions in Git and Mercurial. Maniphest Tasks: T13546, T2928 Differential Revision: https://secure.phabricator.com/D21336 --- src/__phutil_library_map__.php | 17 +- src/engine/ArcanistWorkflowEngine.php | 48 ++++ src/hardpoint/ArcanistHardpointObject.php | 6 + src/land/engine/ArcanistLandEngine.php | 45 +--- src/repository/api/ArcanistGitAPI.php | 4 + src/repository/api/ArcanistMercurialAPI.php | 4 + src/repository/api/ArcanistRepositoryAPI.php | 18 ++ src/repository/marker/ArcanistMarkerRef.php | 48 +++- ...ArcanistMercurialRepositoryMarkerQuery.php | 2 + .../marker/ArcanistRepositoryMarkerQuery.php | 44 ++++ .../state/ArcanistGitLocalState.php | 2 +- src/work/ArcanistGitWorkEngine.php | 57 +++++ src/work/ArcanistMercurialWorkEngine.php | 56 +++++ src/work/ArcanistWorkEngine.php | 215 ++++++++++++++++++ src/workflow/ArcanistWorkWorkflow.php | 95 ++++++++ 15 files changed, 612 insertions(+), 49 deletions(-) create mode 100644 src/engine/ArcanistWorkflowEngine.php create mode 100644 src/work/ArcanistGitWorkEngine.php create mode 100644 src/work/ArcanistMercurialWorkEngine.php create mode 100644 src/work/ArcanistWorkEngine.php create mode 100644 src/workflow/ArcanistWorkWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9cac118f..b44d0d44 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -225,6 +225,7 @@ phutil_register_library_map(array( 'ArcanistGitRawCommitTestCase' => 'repository/raw/__tests__/ArcanistGitRawCommitTestCase.php', 'ArcanistGitRepositoryMarkerQuery' => 'repository/marker/ArcanistGitRepositoryMarkerQuery.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', + 'ArcanistGitWorkEngine' => 'work/ArcanistGitWorkEngine.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php', 'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php', @@ -336,6 +337,7 @@ phutil_register_library_map(array( 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMercurialRepositoryMarkerQuery' => 'repository/marker/ArcanistMercurialRepositoryMarkerQuery.php', + 'ArcanistMercurialWorkEngine' => 'work/ArcanistMercurialWorkEngine.php', 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php', 'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php', @@ -538,8 +540,11 @@ phutil_register_library_map(array( 'ArcanistWeldWorkflow' => 'workflow/ArcanistWeldWorkflow.php', 'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php', 'ArcanistWildConfigOption' => 'config/option/ArcanistWildConfigOption.php', + 'ArcanistWorkEngine' => 'work/ArcanistWorkEngine.php', + 'ArcanistWorkWorkflow' => 'workflow/ArcanistWorkWorkflow.php', 'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php', 'ArcanistWorkflowArgument' => 'toolset/ArcanistWorkflowArgument.php', + 'ArcanistWorkflowEngine' => 'engine/ArcanistWorkflowEngine.php', 'ArcanistWorkflowGitHardpointQuery' => 'query/ArcanistWorkflowGitHardpointQuery.php', 'ArcanistWorkflowInformation' => 'toolset/ArcanistWorkflowInformation.php', 'ArcanistWorkflowMercurialHardpointQuery' => 'query/ArcanistWorkflowMercurialHardpointQuery.php', @@ -1253,6 +1258,7 @@ phutil_register_library_map(array( 'ArcanistGitRawCommitTestCase' => 'PhutilTestCase', 'ArcanistGitRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', 'ArcanistGitUpstreamPath' => 'Phobject', + 'ArcanistGitWorkEngine' => 'ArcanistWorkEngine', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1322,7 +1328,7 @@ phutil_register_library_map(array( 'ArcanistLambdaFuncFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLandCommit' => 'Phobject', 'ArcanistLandCommitSet' => 'Phobject', - 'ArcanistLandEngine' => 'Phobject', + 'ArcanistLandEngine' => 'ArcanistWorkflowEngine', 'ArcanistLandSymbol' => 'Phobject', 'ArcanistLandTarget' => 'Phobject', 'ArcanistLandWorkflow' => 'ArcanistArcWorkflow', @@ -1355,7 +1361,10 @@ phutil_register_library_map(array( 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistMarkerRef' => 'ArcanistRef', + 'ArcanistMarkerRef' => array( + 'ArcanistRef', + 'ArcanistDisplayRefInterface', + ), 'ArcanistMarkersWorkflow' => 'ArcanistArcWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', @@ -1364,6 +1373,7 @@ phutil_register_library_map(array( 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', 'ArcanistMercurialRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', + 'ArcanistMercurialWorkEngine' => 'ArcanistWorkEngine', 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMergeConflictLinter' => 'ArcanistLinter', @@ -1576,8 +1586,11 @@ phutil_register_library_map(array( 'ArcanistWeldWorkflow' => 'ArcanistArcWorkflow', 'ArcanistWhichWorkflow' => 'ArcanistWorkflow', 'ArcanistWildConfigOption' => 'ArcanistConfigOption', + 'ArcanistWorkEngine' => 'ArcanistWorkflowEngine', + 'ArcanistWorkWorkflow' => 'ArcanistArcWorkflow', 'ArcanistWorkflow' => 'Phobject', 'ArcanistWorkflowArgument' => 'Phobject', + 'ArcanistWorkflowEngine' => 'Phobject', 'ArcanistWorkflowGitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistWorkflowInformation' => 'Phobject', 'ArcanistWorkflowMercurialHardpointQuery' => 'ArcanistRuntimeHardpointQuery', diff --git a/src/engine/ArcanistWorkflowEngine.php b/src/engine/ArcanistWorkflowEngine.php new file mode 100644 index 00000000..726ba65b --- /dev/null +++ b/src/engine/ArcanistWorkflowEngine.php @@ -0,0 +1,48 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setWorkflow(ArcanistWorkflow $workflow) { + $this->workflow = $workflow; + return $this; + } + + final public function getWorkflow() { + return $this->workflow; + } + + final public function setRepositoryAPI( + ArcanistRepositoryAPI $repository_api) { + $this->repositoryAPI = $repository_api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + final public function setLogEngine(ArcanistLogEngine $log_engine) { + $this->logEngine = $log_engine; + return $this; + } + + final public function getLogEngine() { + return $this->logEngine; + } + +} diff --git a/src/hardpoint/ArcanistHardpointObject.php b/src/hardpoint/ArcanistHardpointObject.php index 93a3bac9..b2afa8cd 100644 --- a/src/hardpoint/ArcanistHardpointObject.php +++ b/src/hardpoint/ArcanistHardpointObject.php @@ -5,6 +5,12 @@ abstract class ArcanistHardpointObject private $hardpointList; + public function __clone() { + if ($this->hardpointList) { + $this->hardpointList = clone $this->hardpointList; + } + } + final public function getHardpoint($hardpoint) { return $this->getHardpointList()->getHardpoint( $this, diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index da354e27..895cea5c 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1,11 +1,7 @@ viewer = $viewer; - return $this; - } - - final public function getViewer() { - return $this->viewer; - } - final public function setOntoRemote($onto_remote) { $this->ontoRemote = $onto_remote; return $this; @@ -96,34 +83,6 @@ abstract class ArcanistLandEngine extends Phobject { return $this->intoLocal; } - final public function setWorkflow($workflow) { - $this->workflow = $workflow; - return $this; - } - - final public function getWorkflow() { - return $this->workflow; - } - - final public function setRepositoryAPI( - ArcanistRepositoryAPI $repository_api) { - $this->repositoryAPI = $repository_api; - return $this; - } - - final public function getRepositoryAPI() { - return $this->repositoryAPI; - } - - final public function setLogEngine(ArcanistLogEngine $log_engine) { - $this->logEngine = $log_engine; - return $this; - } - - final public function getLogEngine() { - return $this->logEngine; - } - final public function setShouldHold($should_hold) { $this->shouldHold = $should_hold; return $this; diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 2c16a570..153655f2 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1746,6 +1746,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return new ArcanistGitLandEngine(); } + protected function newWorkEngine() { + return new ArcanistGitWorkEngine(); + } + public function newLocalState() { return id(new ArcanistGitLocalState()) ->setRepositoryAPI($this); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index e73578b8..2d72805b 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1134,6 +1134,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return new ArcanistMercurialLandEngine(); } + protected function newWorkEngine() { + return new ArcanistMercurialWorkEngine(); + } + public function newLocalState() { return id(new ArcanistMercurialLocalState()) ->setRepositoryAPI($this); diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index d1dcf4dc..0649fdb5 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -763,6 +763,20 @@ abstract class ArcanistRepositoryAPI extends Phobject { return null; } + final public function getWorkEngine() { + $engine = $this->newWorkEngine(); + + if ($engine) { + $engine->setRepositoryAPI($this); + } + + return $engine; + } + + protected function newWorkEngine() { + return null; + } + final public function getSupportedMarkerTypes() { return $this->newSupportedMarkerTypes(); } @@ -780,4 +794,8 @@ abstract class ArcanistRepositoryAPI extends Phobject { throw new PhutilMethodNotImplementedException(); } + final public function getDisplayHash($hash) { + return substr($hash, 0, 12); + } + } diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index acf53390..e3b27a96 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -1,9 +1,12 @@ getName()); + return $this->getDisplayRefObjectName(); + } + + public function getDisplayRefObjectName() { + switch ($this->getMarkerType()) { + case self::TYPE_BRANCH: + return pht('Branch "%s"', $this->getName()); + case self::TYPE_BOOKMARK: + return pht('Bookmark "%s"', $this->getName()); + default: + return pht('Marker "%s"', $this->getName()); + } + } + + public function getDisplayRefTitle() { + return pht( + '%s %s', + $this->getDisplayHash(), + $this->getSummary()); } protected function newHardpoints() { return array( $this->newHardpoint(self::HARDPOINT_COMMITREF), + $this->newHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF), ); } @@ -64,6 +87,17 @@ final class ArcanistMarkerRef return $this->markerHash; } + public function setDisplayHash($display_hash) { + $this->displayHash = $display_hash; + return $this; + } + + public function getDisplayHash() { + return $this->displayHash; + } + + + public function setCommitHash($commit_hash) { $this->commitHash = $commit_hash; return $this; @@ -125,4 +159,12 @@ final class ArcanistMarkerRef return $this->getHardpoint(self::HARDPOINT_COMMITREF); } + public function attachWorkingCopyStateRef(ArcanistWorkingCopyStateRef $ref) { + return $this->attachHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF, $ref); + } + + public function getWorkingCopyStateRef() { + return $this->getHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF); + } + } diff --git a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php index 9e7a627c..7d4b98a2 100644 --- a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php @@ -100,6 +100,7 @@ final class ArcanistMercurialRepositoryMarkerQuery } $message = $fields[4]; + $message_lines = phutil_split_lines($message, false); $commit_ref = $api->newCommitRef() ->setCommitHash($node) @@ -107,6 +108,7 @@ final class ArcanistMercurialRepositoryMarkerQuery $template = id(new ArcanistMarkerRef()) ->setCommitHash($node) + ->setSummary(head($message_lines)) ->attachCommitRef($commit_ref); if ($is_bookmarks) { diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index 1260752d..9b31ad3f 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -6,6 +6,7 @@ abstract class ArcanistRepositoryMarkerQuery private $repositoryAPI; private $isActive; private $markerTypes; + private $names; private $commitHashes; private $ancestorCommitHashes; @@ -23,14 +24,47 @@ abstract class ArcanistRepositoryMarkerQuery return $this; } + final public function withNames(array $names) { + $this->names = array_fuse($names); + return $this; + } + final public function withIsActive($active) { $this->isActive = $active; return $this; } + final public function executeOne() { + $markers = $this->execute(); + + if (!$markers) { + return null; + } + + if (count($markers) > 1) { + throw new Exception( + pht( + 'Query matched multiple markers, expected zero or one.')); + } + + return head($markers); + } + final public function execute() { $markers = $this->newRefMarkers(); + $api = $this->getRepositoryAPI(); + foreach ($markers as $marker) { + $state_ref = id(new ArcanistWorkingCopyStateRef()) + ->setCommitRef($marker->getCommitRef()); + + $marker->attachWorkingCopyStateRef($state_ref); + + $hash = $marker->getCommitHash(); + $hash = $api->getDisplayHash($hash); + $marker->setDisplayHash($hash); + } + $types = $this->markerTypes; if ($types !== null) { foreach ($markers as $key => $marker) { @@ -40,6 +74,15 @@ abstract class ArcanistRepositoryMarkerQuery } } + $names = $this->names; + if ($names !== null) { + foreach ($markers as $key => $marker) { + if (!isset($names[$marker->getName()])) { + unset($markers[$key]); + } + } + } + if ($this->isActive !== null) { foreach ($markers as $key => $marker) { if ($marker->getIsActive() !== $this->isActive) { @@ -48,6 +91,7 @@ abstract class ArcanistRepositoryMarkerQuery } } + return $this->sortMarkers($markers); } diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php index d7b9833c..30d29d3a 100644 --- a/src/repository/state/ArcanistGitLocalState.php +++ b/src/repository/state/ArcanistGitLocalState.php @@ -42,7 +42,7 @@ final class ArcanistGitLocalState } $log = $this->getWorkflow()->getLogEngine(); - $log->writeStatus(pht('SAVE STATE'), $where); + $log->writeTrace(pht('SAVE STATE'), $where); } protected function executeRestoreLocalState() { diff --git a/src/work/ArcanistGitWorkEngine.php b/src/work/ArcanistGitWorkEngine.php new file mode 100644 index 00000000..ae5238bd --- /dev/null +++ b/src/work/ArcanistGitWorkEngine.php @@ -0,0 +1,57 @@ +getRepositoryAPI(); + + // NOTE: In Git, we're trying to find the current branch name because the + // behavior of "--track" depends on the symbol we pass. + + $marker = $api->newMarkerRefQuery() + ->withIsActive(true) + ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BRANCH)) + ->executeOne(); + if ($marker) { + return $marker->getName(); + } + + return $api->getWorkingCopyRevision(); + } + + protected function newMarker($symbol, $start) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $log->writeStatus( + pht('NEW BRANCH'), + pht( + 'Creating new branch "%s" from "%s".', + $symbol, + $start)); + + $future = $api->newFuture( + 'checkout --track -b %s %s --', + $symbol, + $start); + $future->resolve(); + } + + protected function moveToMarker(ArcanistMarkerRef $marker) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $log->writeStatus( + pht('BRANCH'), + pht( + 'Checking out branch "%s".', + $marker->getName())); + + $future = $api->newFuture( + 'checkout %s --', + $marker->getName()); + $future->resolve(); + } + +} diff --git a/src/work/ArcanistMercurialWorkEngine.php b/src/work/ArcanistMercurialWorkEngine.php new file mode 100644 index 00000000..83aa8b71 --- /dev/null +++ b/src/work/ArcanistMercurialWorkEngine.php @@ -0,0 +1,56 @@ +getRepositoryAPI(); + return $api->getWorkingCopyRevision(); + } + + protected function newMarker($symbol, $start) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + $log->writeStatus( + pht('NEW BOOKMARK'), + pht( + 'Creating new bookmark "%s" from "%s".', + $symbol, + $start)); + + if ($start !== $this->getDefaultStartSymbol()) { + $future = $api->newFuture('update -- %s', $start); + $future->resolve(); + } + + $future = $api->newFuture('bookmark %s --', $symbol); + $future->resolve(); + } + + protected function moveToMarker(ArcanistMarkerRef $marker) { + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + if ($marker->isBookmark()) { + $log->writeStatus( + pht('BOOKMARK'), + pht( + 'Checking out bookmark "%s".', + $marker->getName())); + } else { + $log->writeStatus( + pht('BRANCH'), + pht( + 'Checking out branch "%s".', + $marker->getName())); + } + + $future = $api->newFuture( + 'checkout %s --', + $marker->getName()); + + $future->resolve(); + } + +} diff --git a/src/work/ArcanistWorkEngine.php b/src/work/ArcanistWorkEngine.php new file mode 100644 index 00000000..0d8a01ed --- /dev/null +++ b/src/work/ArcanistWorkEngine.php @@ -0,0 +1,215 @@ +symbolArgument = $symbol_argument; + return $this; + } + + final public function getSymbolArgument() { + return $this->symbolArgument; + } + + final public function setStartArgument($start_argument) { + $this->startArgument = $start_argument; + return $this; + } + + final public function getStartArgument() { + return $this->startArgument; + } + + final public function execute() { + $workflow = $this->getWorkflow(); + $api = $this->getRepositoryAPI(); + + $local_state = $api->newLocalState() + ->setWorkflow($workflow) + ->saveLocalState(); + + $symbol = $this->getSymbolArgument(); + + $markers = $api->newMarkerRefQuery() + ->withNames(array($symbol)) + ->execute(); + + if ($markers) { + if (count($markers) > 1) { + + // TODO: This almost certainly means the symbol is a Mercurial branch + // with multiple heads. We can pick some head. + + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" is ambiguous.', + $symbol)); + } + + $target = head($markers); + $this->moveToMarker($target); + $local_state->discardLocalState(); + return; + } + + $revision_marker = $this->workOnRevision($symbol); + if ($revision_marker) { + $this->moveToMarker($revision_marker); + $local_state->discardLocalState(); + return; + } + + $task_marker = $this->workOnTask($symbol); + if ($task_marker) { + $this->moveToMarker($task_marker); + $local_state->discardLocalState(); + return; + } + + // NOTE: We're resolving this symbol so we can raise an error message if + // it's bogus, but we're using the symbol (not the resolved version) to + // actually create the new marker. This matters in Git because it impacts + // the behavior of "--track" when we pass a branch name. + + $start = $this->getStartArgument(); + if ($start !== null) { + $start_commit = $api->getCanonicalRevisionName($start); + if (!$start_commit) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to resolve startpoint "%s".', + $start)); + } + } else { + $start = $this->getDefaultStartSymbol(); + } + + $this->newMarker($symbol, $start); + $local_state->discardLocalState(); + } + + abstract protected function newMarker($symbol, $start); + abstract protected function moveToMarker(ArcanistMarkerRef $marker); + abstract protected function getDefaultStartSymbol(); + + private function workOnRevision($symbol) { + $workflow = $this->getWorkflow(); + $api = $this->getRepositoryAPI(); + $log = $this->getLogEngine(); + + try { + $revision_symbol = id(new ArcanistRevisionSymbolRef()) + ->setSymbol($symbol); + } catch (Exception $ex) { + return; + } + + $workflow->loadHardpoints( + $revision_symbol, + ArcanistSymbolRef::HARDPOINT_OBJECT); + + $revision_ref = $revision_symbol->getObject(); + if (!$revision_ref) { + throw new PhutilArgumentUsageException( + pht( + 'No revision "%s" exists, or you do not have permission to '. + 'view it.', + $symbol)); + } + + $markers = $api->newMarkerRefQuery() + ->execute(); + + $state_refs = mpull($markers, 'getWorkingCopyStateRef'); + + $workflow->loadHardpoints( + $state_refs, + ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); + + $selected = array(); + foreach ($markers as $marker) { + $state_ref = $marker->getWorkingCopyStateRef(); + $revision_refs = $state_ref->getRevisionRefs(); + $revision_refs = mpull($revision_refs, null, 'getPHID'); + + if (isset($revision_refs[$revision_ref->getPHID()])) { + $selected[] = $marker; + } + } + + if (!$selected) { + + // TODO: We could patch/load here. + + throw new PhutilArgumentUsageException( + pht( + 'Revision "%s" was not found anywhere in this working copy.', + $revision_ref->getMonogram())); + } + + if (count($selected) > 1) { + $selected = msort($selected, 'getEpoch'); + + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS MARKER'), + pht( + 'More than one marker in the local working copy is associated '. + 'with the revision "%s", using the most recent one.', + $revision_ref->getMonogram())); + + foreach ($selected as $marker) { + echo tsprintf('%s', $marker->newDisplayRef()); + } + + echo tsprintf("\n"); + + $target = last($selected); + } else { + $target = head($selected); + } + + $log->writeStatus( + pht('REVISION'), + pht('Resuming work on revision:')); + + echo tsprintf('%s', $revision_ref->newDisplayRef()); + echo tsprintf("\n"); + + return $target; + } + + private function workOnTask($symbol) { + $workflow = $this->getWorkflow(); + + try { + $task_symbol = id(new ArcanistTaskSymbolRef()) + ->setSymbol($symbol); + } catch (Exception $ex) { + return; + } + + $workflow->loadHardpoints( + $task_symbol, + ArcanistSymbolRef::HARDPOINT_OBJECT); + + $task_ref = $task_symbol->getObject(); + if (!$task_ref) { + throw new PhutilArgumentUsageException( + pht( + 'No task "%s" exists, or you do not have permission to view it.', + $symbol)); + } + + throw new Exception(pht('TODO: Implement this workflow.')); + + $this->loadHardpoints( + $task_ref, + ArcanistTaskRef::HARDPOINT_REVISIONREFS); + } + +} diff --git a/src/workflow/ArcanistWorkWorkflow.php b/src/workflow/ArcanistWorkWorkflow.php new file mode 100644 index 00000000..5be3c94c --- /dev/null +++ b/src/workflow/ArcanistWorkWorkflow.php @@ -0,0 +1,95 @@ +newWorkflowArgument('start') + ->setParameter('symbol') + ->setHelp( + pht( + 'When creating a new branch or bookmark, use this as the '. + 'branch point.')), + $this->newWorkflowArgument('symbol') + ->setWildcard(true), + ); + } + + public function getWorkflowInformation() { + $help = pht(<<newWorkflowInformation() + ->setSynopsis(pht('Begin or resume work.')) + ->addExample(pht('**work** [--start __start__] __symbol__')) + ->setHelp($help); + } + + public function runWorkflow() { + $api = $this->getRepositoryAPI(); + + $work_engine = $api->getWorkEngine(); + if (!$work_engine) { + throw new PhutilArgumentUsageException( + pht( + '"arc work" must be run in a Git or Mercurial working copy.')); + } + + $argv = $this->getArgument('symbol'); + if (count($argv) === 0) { + throw new PhutilArgumentUsageException( + pht( + 'Provide a branch, bookmark, task, or revision name to begin '. + 'or resume work on.')); + } else if (count($argv) === 1) { + $symbol_argument = $argv[0]; + if (!strlen($symbol_argument)) { + throw new PhutilArgumentUsageException( + pht( + 'Provide a nonempty symbol to begin or resume work on.')); + } + } else { + throw new PhutilArgumentUsageException( + pht( + 'Too many arguments: provide exactly one argument.')); + } + + $start_argument = $this->getArgument('start'); + + $work_engine + ->setViewer($this->getViewer()) + ->setWorkflow($this) + ->setLogEngine($this->getLogEngine()) + ->setSymbolArgument($symbol_argument) + ->setStartArgument($start_argument) + ->execute(); + + return 0; + } + +} From b003cf93102c290d9410768c0aa54016a0683e19 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 11:05:08 -0700 Subject: [PATCH 40/82] Remove "arc feature", "arc branch", "arc bookmark", and significant chunks of obsolete marker code Summary: Ref T13546. Moves away from the older workflows in favor of "arc branches", "arc bookmarks", and "arc work". Test Plan: Grepped for affected symbols, didn't find any callers. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21337 --- src/__phutil_library_map__.php | 8 - ...rcanistArcConfigurationEngineExtension.php | 8 - src/configuration/ArcanistSettings.php | 7 - src/repository/api/ArcanistGitAPI.php | 29 +- src/repository/api/ArcanistMercurialAPI.php | 216 +------------ src/repository/api/ArcanistRepositoryAPI.php | 9 - src/workflow/ArcanistBookmarkWorkflow.php | 10 - src/workflow/ArcanistBranchWorkflow.php | 10 - src/workflow/ArcanistFeatureBaseWorkflow.php | 286 ------------------ src/workflow/ArcanistFeatureWorkflow.php | 10 - 10 files changed, 12 insertions(+), 581 deletions(-) delete mode 100644 src/workflow/ArcanistBookmarkWorkflow.php delete mode 100644 src/workflow/ArcanistBranchWorkflow.php delete mode 100644 src/workflow/ArcanistFeatureBaseWorkflow.php delete mode 100644 src/workflow/ArcanistFeatureWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b44d0d44..f10c52b3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -49,12 +49,10 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBlacklistedFunctionXHPASTLinterRule.php', 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase.php', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'configuration/ArcanistBlindlyTrustHTTPEngineExtension.php', - 'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php', 'ArcanistBookmarksWorkflow' => 'workflow/ArcanistBookmarksWorkflow.php', 'ArcanistBoolConfigOption' => 'config/option/ArcanistBoolConfigOption.php', 'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php', - 'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php', 'ArcanistBranchesWorkflow' => 'workflow/ArcanistBranchesWorkflow.php', 'ArcanistBrowseCommitHardpointQuery' => 'browse/query/ArcanistBrowseCommitHardpointQuery.php', 'ArcanistBrowseCommitURIHardpointQuery' => 'browse/query/ArcanistBrowseCommitURIHardpointQuery.php', @@ -194,8 +192,6 @@ phutil_register_library_map(array( 'ArcanistExternalLinterTestCase' => 'lint/linter/__tests__/ArcanistExternalLinterTestCase.php', 'ArcanistExtractUseXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistExtractUseXHPASTLinterRule.php', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistExtractUseXHPASTLinterRuleTestCase.php', - 'ArcanistFeatureBaseWorkflow' => 'workflow/ArcanistFeatureBaseWorkflow.php', - 'ArcanistFeatureWorkflow' => 'workflow/ArcanistFeatureWorkflow.php', 'ArcanistFileConfigurationSource' => 'config/source/ArcanistFileConfigurationSource.php', 'ArcanistFileDataRef' => 'upload/ArcanistFileDataRef.php', 'ArcanistFileRef' => 'ref/file/ArcanistFileRef.php', @@ -1068,12 +1064,10 @@ phutil_register_library_map(array( 'ArcanistBlacklistedFunctionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBlacklistedFunctionXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistBlindlyTrustHTTPEngineExtension' => 'PhutilHTTPEngineExtension', - 'ArcanistBookmarkWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistBookmarksWorkflow' => 'ArcanistMarkersWorkflow', 'ArcanistBoolConfigOption' => 'ArcanistSingleSourceConfigOption', 'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistBranchWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistBranchesWorkflow' => 'ArcanistMarkersWorkflow', 'ArcanistBrowseCommitHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistBrowseCommitURIHardpointQuery' => 'ArcanistBrowseURIHardpointQuery', @@ -1224,8 +1218,6 @@ phutil_register_library_map(array( 'ArcanistExternalLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistExtractUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistFeatureBaseWorkflow' => 'ArcanistArcWorkflow', - 'ArcanistFeatureWorkflow' => 'ArcanistFeatureBaseWorkflow', 'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistFileDataRef' => 'Phobject', 'ArcanistFileRef' => array( diff --git a/src/config/arc/ArcanistArcConfigurationEngineExtension.php b/src/config/arc/ArcanistArcConfigurationEngineExtension.php index ca6c215b..d40159d5 100644 --- a/src/config/arc/ArcanistArcConfigurationEngineExtension.php +++ b/src/config/arc/ArcanistArcConfigurationEngineExtension.php @@ -22,14 +22,6 @@ final class ArcanistArcConfigurationEngineExtension 'default' => array(), 'example' => '["/var/arc/customlib/src"]', ), - - 'arc.feature.start.default' => array( - 'type' => 'string', - 'help' => pht( - 'The name of the default branch to create the new feature branch '. - 'off of.'), - 'example' => '"develop"', - ), 'editor' => array( 'type' => 'string', 'help' => pht( diff --git a/src/configuration/ArcanistSettings.php b/src/configuration/ArcanistSettings.php index 496d3771..67081703 100644 --- a/src/configuration/ArcanistSettings.php +++ b/src/configuration/ArcanistSettings.php @@ -65,13 +65,6 @@ final class ArcanistSettings extends Phobject { 'engine is specified by the current project.'), 'example' => '"ExampleUnitTestEngine"', ), - 'arc.feature.start.default' => array( - 'type' => 'string', - 'help' => pht( - 'The name of the default branch to create the new feature branch '. - 'off of.'), - 'example' => '"develop"', - ), 'arc.land.onto.default' => array( 'type' => 'string', 'help' => pht( diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 153655f2..c65257f4 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -597,16 +597,16 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { public function getCanonicalRevisionName($string) { $match = null; + if (preg_match('/@([0-9]+)$/', $string, $match)) { $stdout = $this->getHashFromFromSVNRevisionNumber($match[1]); } else { list($stdout) = $this->execxLocal( - phutil_is_windows() - ? 'show -s --format=%C %s --' - : 'show -s --format=%s %s --', + 'show -s --format=%s %s --', '%H', $string); } + return rtrim($stdout); } @@ -1056,7 +1056,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { * * @return list> Dictionary of branch information. */ - public function getAllBranches() { + private function getAllBranches() { $field_list = array( '%(refname)', '%(objectname)', @@ -1102,26 +1102,6 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return $result; } - public function getAllBranchRefs() { - $branches = $this->getAllBranches(); - - $refs = array(); - foreach ($branches as $branch) { - $commit_ref = $this->newCommitRef() - ->setCommitHash($branch['hash']) - ->setTreeHash($branch['tree']) - ->setCommitEpoch($branch['epoch']) - ->attachMessage($branch['text']); - - $refs[] = $this->newMarkerRef() - ->setName($branch['name']) - ->setIsActive($branch['current']) - ->attachCommitRef($commit_ref); - } - - return $refs; - } - public function getBaseCommitRef() { $base_commit = $this->getBaseCommit(); @@ -1783,5 +1763,4 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return new ArcanistGitRepositoryMarkerQuery(); } - } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 2d72805b..bdc98db0 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -49,12 +49,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } public function getCanonicalRevisionName($string) { - $match = null; - if ($this->isHgSubversionRepo() && - preg_match('/@([0-9]+)$/', $string, $match)) { - $string = hgsprintf('svnrev(%s)', $match[1]); - } - list($stdout) = $this->execxLocal( 'log -l 1 --template %s -r %s --', '{node}', @@ -63,32 +57,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return $stdout; } - public function getHashFromFromSVNRevisionNumber($revision_id) { - $matches = array(); - $string = hgsprintf('svnrev(%s)', $revision_id); - list($stdout) = $this->execxLocal( - 'log -l 1 --template %s -r %s --', - '{node}', - $string); - if (!$stdout) { - throw new ArcanistUsageException( - pht('Cannot find the HG equivalent of %s given.', $revision_id)); - } - return $stdout; - } - - - public function getSVNRevisionNumberFromHash($hash) { - $matches = array(); - list($stdout) = $this->execxLocal( - 'log -r %s --template {svnrev}', $hash); - if (!$stdout) { - throw new ArcanistUsageException( - pht('Cannot find the SVN equivalent of %s given.', $hash)); - } - return $stdout; - } - public function getSourceControlPath() { return '/'; } @@ -505,53 +473,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return true; } - public function getAllBranches() { - // TODO: This is wrong, and returns bookmarks. - - list($branch_info) = $this->execxLocal('bookmarks'); - if (trim($branch_info) == 'no bookmarks set') { - return array(); - } - - $matches = null; - preg_match_all( - '/^\s*(\*?)\s*(.+)\s(\S+)$/m', - $branch_info, - $matches, - PREG_SET_ORDER); - - $return = array(); - foreach ($matches as $match) { - list(, $current, $name, $hash) = $match; - - list($id, $hash) = explode(':', $hash); - - $return[] = array( - 'current' => (bool)$current, - 'name' => rtrim($name), - 'hash' => $hash, - ); - } - return $return; - } - - public function getAllBranchRefs() { - $branches = $this->getAllBranches(); - - $refs = array(); - foreach ($branches as $branch) { - $commit_ref = $this->newCommitRef() - ->setCommitHash($branch['hash']); - - $refs[] = $this->newMarkerRef() - ->setBranchName($branch['name']) - ->setIsCurrentBranch($branch['current']) - ->attachCommitRef($commit_ref); - } - - return $refs; - } - public function getBaseCommitRef() { $base_commit = $this->getBaseCommit(); @@ -937,10 +858,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } - public function isHgSubversionRepo() { - return file_exists($this->getPath('.hg/svn/rev_map')); - } - public function getSubversionInfo() { $info = array(); $base_path = null; @@ -972,133 +889,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } public function getActiveBookmark() { - $bookmarks = $this->getBookmarks(); - foreach ($bookmarks as $bookmark) { - if ($bookmark['is_active']) { - return $bookmark['name']; - } + $bookmark = $this->newMarkerRefQuery() + ->withMarkerTypes(ArcanistMarkerRef::TYPE_BOOKMARK) + ->withIsActive(true) + ->executeOne(); + + if (!$bookmark) { + return null; } - return null; - } - - public function isBookmark($name) { - $bookmarks = $this->getBookmarks(); - foreach ($bookmarks as $bookmark) { - if ($bookmark['name'] === $name) { - return true; - } - } - - return false; - } - - public function isBranch($name) { - $branches = $this->getBranches(); - foreach ($branches as $branch) { - if ($branch['name'] === $name) { - return true; - } - } - - return false; - } - - public function getBranches() { - list($stdout) = $this->execxLocal('--debug branches'); - $lines = ArcanistMercurialParser::parseMercurialBranches($stdout); - - $branches = array(); - foreach ($lines as $name => $spec) { - $branches[] = array( - 'name' => $name, - 'revision' => $spec['rev'], - ); - } - - return $branches; - } - - public function getBookmarks() { - $bookmarks = array(); - - list($raw_output) = $this->execxLocal('bookmarks'); - $raw_output = trim($raw_output); - if ($raw_output !== 'no bookmarks set') { - foreach (explode("\n", $raw_output) as $line) { - // example line: * mybook 2:6b274d49be97 - list($name, $revision) = $this->splitBranchOrBookmarkLine($line); - - $is_active = false; - if ('*' === $name[0]) { - $is_active = true; - $name = substr($name, 2); - } - - $bookmarks[] = array( - 'is_active' => $is_active, - 'name' => $name, - 'revision' => $revision, - ); - } - } - - return $bookmarks; - } - - public function getBookmarkCommitHash($name) { - // TODO: Cache this. - - $bookmarks = $this->getBookmarks($name); - $bookmarks = ipull($bookmarks, null, 'name'); - - foreach ($bookmarks as $bookmark) { - if ($bookmark['name'] === $name) { - return $bookmark['revision']; - } - } - - throw new Exception(pht('No bookmark "%s".', $name)); - } - - public function getBranchCommitHash($name) { - // TODO: Cache this. - // TODO: This won't work when there are multiple branch heads with the - // same name. - - $branches = $this->getBranches($name); - - $heads = array(); - foreach ($branches as $branch) { - if ($branch['name'] === $name) { - $heads[] = $branch; - } - } - - if (count($heads) === 1) { - return idx(head($heads), 'revision'); - } - - if (!$heads) { - throw new Exception(pht('No branch "%s".', $name)); - } - - throw new Exception(pht('Too many branch heads for "%s".', $name)); - } - - private function splitBranchOrBookmarkLine($line) { - // branches and bookmarks are printed in the format: - // default 0:a5ead76cdf85 (inactive) - // * mybook 2:6b274d49be97 - // this code divides the name half from the revision half - // it does not parse the * and (inactive) bits - $colon_index = strrpos($line, ':'); - $before_colon = substr($line, 0, $colon_index); - $start_rev_index = strrpos($before_colon, ' '); - $name = substr($line, 0, $start_rev_index); - $rev = substr($line, $start_rev_index); - - return array(trim($name), trim($rev)); + return $bookmark->getName(); } public function getRemoteURI() { diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 0649fdb5..44a1f059 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -369,15 +369,6 @@ abstract class ArcanistRepositoryAPI extends Phobject { throw new ArcanistCapabilityNotSupportedException($this); } - public function getAllBranches() { - // TODO: Implement for Mercurial/SVN and make abstract. - return array(); - } - - public function getAllBranchRefs() { - throw new ArcanistCapabilityNotSupportedException($this); - } - public function getBaseCommitRef() { throw new ArcanistCapabilityNotSupportedException($this); } diff --git a/src/workflow/ArcanistBookmarkWorkflow.php b/src/workflow/ArcanistBookmarkWorkflow.php deleted file mode 100644 index 49f56de6..00000000 --- a/src/workflow/ArcanistBookmarkWorkflow.php +++ /dev/null @@ -1,10 +0,0 @@ -newWorkflowArgument('view-all') - ->setHelp(pht('Include closed and abandoned revisions.')), - $this->newWorkflowArgument('by-status') - ->setParameter('status') - ->setHelp(pht('Sort branches by status instead of time.')), - $this->newWorkflowArgument('output') - ->setParameter('format') - ->setHelp( - pht( - 'With "json", show features in machine-readable JSON format.')), - $this->newWorkflowArgument('branch') - ->setWildcard(true), - ); - } - - public function getWorkflowInformation() { - return $this->newWorkflowInformation() - ->setSynopsis(pht('Wrapper on "git branch" or "hg bookmark".')) - ->addExample(pht('**%s** [__options__]', $this->getWorkflowName())) - ->addExample(pht('**%s** __name__ [__start__]', $this->getWorkflowName())) - ->setHelp( - pht(<<getRepositoryAPI(); - if (!$repository_api) { - throw new PhutilArgumentUsageException( - pht( - 'This command must be run in a Git or Mercurial working copy.')); - } - - $names = $this->getArgument('branch'); - if ($names) { - if (count($names) > 2) { - throw new ArcanistUsageException(pht('Specify only one branch.')); - } - return $this->checkoutBranch($names); - } - - // TODO: Everything in this whole workflow that says "branch" means - // "bookmark" in Mercurial. - - $branches = $repository_api->getAllBranchRefs(); - if (!$branches) { - throw new ArcanistUsageException( - pht('No branches in this working copy.')); - } - - $states = array(); - foreach ($branches as $branch_key => $branch) { - $state_ref = id(new ArcanistWorkingCopyStateRef()) - ->setCommitRef($branch->getCommitRef()); - - $states[] = array( - 'branch' => $branch, - 'state' => $state_ref, - ); - } - - $this->loadHardpoints( - ipull($states, 'state'), - ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); - - $this->printBranches($states); - - return 0; - } - - private function checkoutBranch(array $names) { - $api = $this->getRepositoryAPI(); - - if ($api instanceof ArcanistMercurialAPI) { - $command = 'update %s'; - } else { - $command = 'checkout %s'; - } - - $err = 1; - - $name = $names[0]; - if (isset($names[1])) { - $start = $names[1]; - } else { - $start = $this->getConfigFromAnySource('arc.feature.start.default'); - } - - $branches = $api->getAllBranches(); - if (in_array($name, ipull($branches, 'name'))) { - list($err, $stdout, $stderr) = $api->execManualLocal($command, $name); - } - - if ($err) { - $match = null; - if (preg_match('/^D(\d+)$/', $name, $match)) { - $diff = $this->getConduitEngine()->resolveCall( - 'differential.querydiffs', - array( - 'revisionIDs' => array($match[1]), - )); - $diff = head($diff); - - if ($diff['branch'] != '') { - $name = $diff['branch']; - list($err, $stdout, $stderr) = $api->execManualLocal( - $command, - $name); - } - } - } - - if ($err) { - if ($api instanceof ArcanistMercurialAPI) { - $rev = ''; - if ($start) { - $rev = csprintf('-r %s', $start); - } - - $exec = $api->execManualLocal('bookmark %C %s', $rev, $name); - - if (!$exec[0] && $start) { - $api->execxLocal('update %s', $name); - } - } else { - $startarg = $start ? csprintf('%s', $start) : ''; - $exec = $api->execManualLocal( - 'checkout --track -b %s %C', - $name, - $startarg); - } - - list($err, $stdout, $stderr) = $exec; - } - - echo $stdout; - fprintf(STDERR, '%s', $stderr); - return $err; - } - - private function printBranches(array $states) { - static $color_map = array( - 'Closed' => 'cyan', - 'Needs Review' => 'magenta', - 'Needs Revision' => 'red', - 'Accepted' => 'green', - 'No Revision' => 'blue', - 'Abandoned' => 'default', - ); - - static $ssort_map = array( - 'Closed' => 1, - 'No Revision' => 2, - 'Needs Review' => 3, - 'Needs Revision' => 4, - 'Accepted' => 5, - ); - - $out = array(); - foreach ($states as $objects) { - $state = $objects['state']; - $branch = $objects['branch']; - - $revision = null; - if ($state->hasAmbiguousRevisionRefs()) { - $status = pht('Ambiguous Revision'); - } else { - $revision = $state->getRevisionRef(); - if ($revision) { - $status = $revision->getStatusDisplayName(); - } else { - $status = pht('No Revision'); - } - } - - if (!$this->getArgument('view-all') && !$branch->getIsActive()) { - if ($status == 'Closed' || $status == 'Abandoned') { - continue; - } - } - - $commit = $branch->getCommitRef(); - $epoch = $commit->getCommitEpoch(); - - $color = idx($color_map, $status, 'default'); - - $epoch_vector = id(new PhutilSortVector()) - ->addInt($epoch); - - $status_vector = id(new PhutilSortVector()) - ->addInt(idx($ssort_map, $status, 0)) - ->addInt($epoch); - - if ($revision) { - $desc = $revision->getFullName(); - } else { - $desc = $commit->getSummary(); - } - - $out[] = array( - 'name' => $branch->getName(), - 'current' => $branch->getIsActive(), - 'status' => $status, - 'desc' => $desc, - 'revision' => $revision ? $revision->getID() : null, - 'color' => $color, - 'epoch' => $epoch, - - 'esort' => $epoch_vector, - 'ssort' => $status_vector, - ); - } - - if (!$out) { - // All of the revisions are closed or abandoned. - return; - } - - $len_name = max(array_map('strlen', ipull($out, 'name'))) + 2; - $len_status = max(array_map('strlen', ipull($out, 'status'))) + 2; - - // Sort the list in natural order first. When we apply a stable sort to - // the list below, branches which were last updated at the same time will - // retain this ordering. This allows "feature1", "feature2", etc., to - // display in the correct order if they were touched at the same second, - // which is common when "arc land" performs a cascading rebase. - - $name_map = ipull($out, 'name'); - natcasesort($name_map); - $out = array_select_keys($out, array_keys($name_map)); - - if ($this->getArgument('by-status')) { - $vectors = ipull($out, 'ssort'); - } else { - $vectors = ipull($out, 'esort'); - } - - $vectors = msortv($vectors, 'getSelf'); - $out = array_select_keys($out, array_keys($vectors)); - - if ($this->getArgument('output') == 'json') { - foreach ($out as &$feature) { - unset($feature['color'], $feature['ssort'], $feature['esort']); - } - echo json_encode(ipull($out, null, 'name'))."\n"; - } else { - $table = id(new PhutilConsoleTable()) - ->setShowHeader(false) - ->addColumn('current', array('title' => '')) - ->addColumn('name', array('title' => pht('Name'))) - ->addColumn('status', array('title' => pht('Status'))) - ->addColumn('descr', array('title' => pht('Description'))); - - foreach ($out as $line) { - $table->addRow(array( - 'current' => $line['current'] ? '*' : '', - 'name' => tsprintf('**%s**', $line['name']), - 'status' => tsprintf( - "%s", $line['status']), - 'descr' => $line['desc'], - )); - } - - $table->draw(); - } - } - -} diff --git a/src/workflow/ArcanistFeatureWorkflow.php b/src/workflow/ArcanistFeatureWorkflow.php deleted file mode 100644 index 760d3dc6..00000000 --- a/src/workflow/ArcanistFeatureWorkflow.php +++ /dev/null @@ -1,10 +0,0 @@ - Date: Mon, 8 Jun 2020 14:57:18 -0700 Subject: [PATCH 41/82] Improve "arc land" behavior in the presence of merge conflicts and change sequences Summary: Ref T13546. When we encounter a merge conflict, suggest "--incremental" if it's likely to help. When merging multiple changes, rebase ranges before merging them. This reduces conflicts when landing sequences of changes. Test Plan: Ran "arc land" to land multiple changes. Hit better merge conflict messaging, then survived merge conflicts entirely. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21339 --- src/land/engine/ArcanistGitLandEngine.php | 88 +++++++++++++++++------ src/land/engine/ArcanistLandEngine.php | 12 ++++ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 57e8b61f..db10bd8d 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -38,7 +38,7 @@ final class ArcanistGitLandEngine $log->writeStatus( pht('CLEANUP'), - pht('Destroying branch "%s". To recover, run:', $branch_name)); + pht('Cleaning up branch "%s". To recover, run:', $branch_name)); echo tsprintf( "\n **$** %s\n\n", @@ -238,11 +238,13 @@ final class ArcanistGitLandEngine $into_commit = $api->writeRawCommit($empty_commit); } - $api->execxLocal('checkout %s --', $into_commit); - $commits = $set->getCommits(); + + $min_commit = head($commits); + $min_hash = $min_commit->getHash(); + $max_commit = last($commits); - $source_commit = $max_commit->getHash(); + $max_hash = $max_commit->getHash(); // NOTE: See T11435 for some history. See PHI1727 for a case where a user // modified their working copy while running "arc land". This attempts to @@ -252,7 +254,7 @@ final class ArcanistGitLandEngine list($changes) = $api->execxLocal( 'diff --no-ext-diff %s..%s --', $into_commit, - $source_commit); + $max_hash); $changes = trim($changes); if (!strlen($changes)) { @@ -263,7 +265,7 @@ final class ArcanistGitLandEngine pht( 'Merging local "%s" into "%s" produces an empty diff. '. 'This usually means these changes have already landed.', - $this->getDisplayHash($source_commit), + $this->getDisplayHash($max_hash), $this->getDisplayHash($into_commit))); } @@ -271,7 +273,7 @@ final class ArcanistGitLandEngine pht('MERGING'), pht( '%s %s', - $this->getDisplayHash($source_commit), + $this->getDisplayHash($max_hash), $max_commit->getDisplaySummary())); $argv = array(); @@ -294,25 +296,44 @@ final class ArcanistGitLandEngine } $argv[] = '--'; - $argv[] = $source_commit; + $is_rebasing = false; + $is_merging = false; try { + if ($this->isSquashStrategy() && !$is_empty) { + // If we're performing a squash merge, we're going to rebase the + // commit range first. We only want to merge the specific commits + // in the range, and merging too much can create conflicts. + + $api->execxLocal('checkout %s --', $max_hash); + + $is_rebasing = true; + $api->execxLocal( + 'rebase --onto %s -- %s', + $into_commit, + $min_hash.'^'); + $is_rebasing = false; + + $merge_hash = $api->getCanonicalRevisionName('HEAD'); + } else { + $merge_hash = $max_hash; + } + + $api->execxLocal('checkout %s --', $into_commit); + + $argv[] = $merge_hash; + + $is_merging = true; $api->execxLocal('merge %Ls', $argv); + $is_merging = false; } catch (CommandException $ex) { - - // TODO: If we previously succeeded with at least one merge, we could - // provide a hint that "--incremental" can do some of the work. - - $api->execManualLocal('merge --abort'); - $api->execManualLocal('reset --hard HEAD --'); - $direct_symbols = $max_commit->getDirectSymbols(); $indirect_symbols = $max_commit->getIndirectSymbols(); if ($direct_symbols) { $message = pht( 'Local commit "%s" (%s) does not merge cleanly into "%s". '. 'Merge or rebase local changes so they can merge cleanly.', - $this->getDisplayHash($source_commit), + $this->getDisplayHash($max_hash), $this->getDisplaySymbols($direct_symbols), $this->getDisplayHash($into_commit)); } else if ($indirect_symbols) { @@ -320,22 +341,47 @@ final class ArcanistGitLandEngine 'Local commit "%s" (reachable from: %s) does not merge cleanly '. 'into "%s". Merge or rebase local changes so they can merge '. 'cleanly.', - $this->getDisplayHash($source_commit), + $this->getDisplayHash($max_hash), $this->getDisplaySymbols($indirect_symbols), $this->getDisplayHash($into_commit)); } else { $message = pht( 'Local commit "%s" does not merge cleanly into "%s". Merge or '. 'rebase local changes so they can merge cleanly.', - $this->getDisplayHash($source_commit), + $this->getDisplayHash($max_hash), $this->getDisplayHash($into_commit)); } - throw new PhutilArgumentUsageException($message); + echo tsprintf( + "\n%!\n%W\n\n", + pht('MERGE CONFLICT'), + $message); + + if ($this->getHasUnpushedChanges()) { + echo tsprintf( + "%?\n\n", + pht( + 'Use "--incremental" to merge and push changes one by one.')); + } + + if ($is_rebasing) { + $api->execManualLocal('rebase --abort'); + } + + if ($is_merging) { + $api->execManualLocal('merge --abort'); + } + + if ($is_merging || $is_rebasing) { + $api->execManualLocal('reset --hard HEAD --'); + } + + throw new PhutilArgumentUsageException( + pht('Encountered a merge conflict.')); } list($original_author, $original_date) = $this->getAuthorAndDate( - $source_commit); + $max_hash); $revision_ref = $set->getRevisionRef(); $commit_message = $revision_ref->getCommitMessage(); @@ -362,7 +408,7 @@ final class ArcanistGitLandEngine if ($this->isSquashStrategy()) { $raw_commit->setParents(array()); } else { - $raw_commit->setParents(array($source_commit)); + $raw_commit->setParents(array($merge_hash)); } $new_cursor = $api->writeRawCommit($raw_commit); diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 895cea5c..3f03e499 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -28,6 +28,7 @@ abstract class ArcanistLandEngine private $intoLocal; private $localState; + private $hasUnpushedChanges; final public function setOntoRemote($onto_remote) { $this->ontoRemote = $onto_remote; @@ -228,6 +229,15 @@ abstract class ArcanistLandEngine return $this->localState; } + private function setHasUnpushedChanges($unpushed) { + $this->hasUnpushedChanges = $unpushed; + return $this; + } + + final protected function getHasUnpushedChanges() { + return $this->hasUnpushedChanges; + } + final protected function getOntoConfigurationKey() { return 'arc.land.onto'; } @@ -1228,6 +1238,7 @@ abstract class ArcanistLandEngine while (true) { $into_commit = $this->executeMerge($set, $into_commit); + $this->setHasUnpushedChanges(true); if ($is_hold) { $should_push = false; @@ -1241,6 +1252,7 @@ abstract class ArcanistLandEngine if ($should_push) { try { $this->pushChange($into_commit); + $this->setHasUnpushedChanges(false); } catch (Exception $ex) { // TODO: If the push fails, fetch and retry if the remote ref From ab70626c1226cd88f4d72ad83111def48220fce8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 15:48:36 -0700 Subject: [PATCH 42/82] Support "arc land --pick" to pick specific changes out of a sequence Summary: Ref T13546. If you have "feature1", "feature2", etc., "arc land feature4" will now land the entire sequence. Provide "arc land --pick feature4" to work more like the old "arc land" did. This cherry-picks the commits associated with "feature4", then cascades onto the ancestor of the range. Test Plan: Ran "arc land --pick land14" to pick a change out of a stack. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21340 --- src/land/ArcanistLandCommitSet.php | 20 ++++++++++++ src/land/engine/ArcanistGitLandEngine.php | 32 +++++++++++++++--- src/land/engine/ArcanistLandEngine.php | 40 +++++++++++++++++------ src/workflow/ArcanistLandWorkflow.php | 11 ++++++- src/workflow/ArcanistWorkWorkflow.php | 2 +- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/land/ArcanistLandCommitSet.php b/src/land/ArcanistLandCommitSet.php index bdf34ce0..b58e2687 100644 --- a/src/land/ArcanistLandCommitSet.php +++ b/src/land/ArcanistLandCommitSet.php @@ -5,6 +5,7 @@ final class ArcanistLandCommitSet private $revisionRef; private $commits; + private $isPick; public function setRevisionRef(ArcanistRevisionRef $revision_ref) { $this->revisionRef = $revision_ref; @@ -49,4 +50,23 @@ final class ArcanistLandCommitSet return false; } + public function hasDirectSymbols() { + foreach ($this->commits as $commit) { + if ($commit->getDirectSymbols()) { + return true; + } + } + + return false; + } + + public function setIsPick($is_pick) { + $this->isPick = $is_pick; + return $this; + } + + public function getIsPick() { + return $this->isPick; + } + } diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index db10bd8d..51655dcf 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -122,6 +122,7 @@ final class ArcanistGitLandEngine return; } + $min_commit = head($set->getCommits())->getHash(); $old_commit = last($set->getCommits())->getHash(); $new_commit = $into_commit; @@ -143,17 +144,38 @@ final class ArcanistGitLandEngine 'Rebasing "%s" onto landed state...', $branch_name)); + // If we used "--pick" to select this commit, we want to rebase branches + // that descend from it onto its ancestor, not onto the landed change. + + // For example, if the change sequence was "W", "X", "Y", "Z" and we + // landed "Y" onto "master" using "--pick", we want to rebase "Z" onto + // "X" (so "W" and "X", which it will often depend on, are still + // its ancestors), not onto the new "master". + + if ($set->getIsPick()) { + $rebase_target = $min_commit.'^'; + } else { + $rebase_target = $new_commit; + } + try { $api->execxLocal( 'rebase --onto %s -- %s %s', - $new_commit, + $rebase_target, $old_commit, $branch_name); } catch (CommandException $ex) { - // TODO: If we have a stashed state or are not running in incremental - // mode: abort the rebase, restore the local state, and pop the stash. - // Otherwise, drop the user out here. - throw $ex; + $api->execManualLocal('rebase --abort'); + $api->execManualLocal('reset --hard HEAD --'); + + $log->writeWarning( + pht('REBASE CONFLICT'), + pht( + 'Branch "%s" does not rebase cleanly from "%s" onto '. + '"%s", skipping.', + $branch_name, + $this->getDisplayHash($old_commit), + $this->getDisplayHash($rebase_target))); } } } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 3f03e499..14657d6d 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -29,6 +29,7 @@ abstract class ArcanistLandEngine private $localState; private $hasUnpushedChanges; + private $pickArgument; final public function setOntoRemote($onto_remote) { $this->ontoRemote = $onto_remote; @@ -75,6 +76,15 @@ abstract class ArcanistLandEngine return $this->intoEmpty; } + final public function setPickArgument($pick_argument) { + $this->pickArgument = $pick_argument; + return $this; + } + + final public function getPickArgument() { + return $this->pickArgument; + } + final public function setIntoLocal($into_local) { $this->intoLocal = $into_local; return $this; @@ -1360,6 +1370,13 @@ abstract class ArcanistLandEngine $strategy = $this->selectMergeStrategy(); $this->setStrategy($strategy); + $is_pick = $this->getPickArgument(); + if ($is_pick && !$this->isSquashStrategy()) { + throw new PhutilArgumentUsageException( + pht( + 'You can not "--pick" changes under the "merge" strategy.')); + } + // Build the symbol ref here (which validates the format of the symbol), // but don't load the object until later on when we're sure we actually // need it, since loading it requires a relatively expensive Conduit call. @@ -1486,16 +1503,7 @@ abstract class ArcanistLandEngine continue; } - $symbols = null; - foreach ($set->getCommits() as $commit) { - $commit_symbols = $commit->getDirectSymbols(); - if ($commit_symbols) { - $symbols = $commit_symbols; - break; - } - } - - if ($symbols) { + if ($set->hasDirectSymbols()) { continue; } @@ -1524,6 +1532,18 @@ abstract class ArcanistLandEngine // set of commits, and try to confirm that the state we're about to land // is the current state in Differential. + $is_pick = $this->getPickArgument(); + if ($is_pick) { + foreach ($sets as $key => $set) { + if ($set->hasDirectSymbols()) { + $set->setIsPick(true); + continue; + } + + unset($sets[$key]); + } + } + return $sets; } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 04e5ed0c..d34dc15e 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -32,7 +32,9 @@ name, a bookmark name, a topic name, a raw commit hash, a symbolic reference, etc. When you provide a __ref__, all unpublished changes which are present in -ancestors of that __ref__ will be selected for publishing. +ancestors of that __ref__ will be selected for publishing. (With the +**--pick** flag, only the unpublished changes you directly reference will be +selected.) For example, if you provide local branch "feature3" as a __ref__ argument, that may also select the changes in "feature1" and "feature2" (if they are ancestors @@ -222,6 +224,11 @@ EOTEXT 'complicated changes by allowing you to make progress one '. 'step at a time.'), )), + $this->newWorkflowArgument('pick') + ->setHelp( + pht( + 'Land only the changes directly named by arguments, instead '. + 'of all reachable ancestors.')), $this->newWorkflowArgument('ref') ->setWildcard(true), ); @@ -308,6 +315,7 @@ EOTEXT $revision = $this->getArgument('revision'); $strategy = $this->getArgument('strategy'); + $pick = $this->getArgument('pick'); $land_engine ->setViewer($this->getViewer()) @@ -324,6 +332,7 @@ EOTEXT ->setIntoEmptyArgument($into_empty) ->setIntoLocalArgument($into_local) ->setIntoArgument($into) + ->setPickArgument($pick) ->setIsIncremental($is_incremental) ->setRevisionSymbol($revision); diff --git a/src/workflow/ArcanistWorkWorkflow.php b/src/workflow/ArcanistWorkWorkflow.php index 5be3c94c..3696bb17 100644 --- a/src/workflow/ArcanistWorkWorkflow.php +++ b/src/workflow/ArcanistWorkWorkflow.php @@ -38,7 +38,7 @@ branch or bookmark that exists in the working copy. If it finds one, it will switch to it. If it does not find one, it will attempt to create a new branch or bookmark. -When "arc work" creates a branch or bookmark, it will use "--start" as the +When "arc work" creates a branch or bookmark, it will use **--start** as the branchpoint if it is provided. Otherwise, the current working copy state will serve as the starting point. EOHELP From 091aebe0149a6a905369b9fa33dac5dbc2eaddbb Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 8 Jun 2020 16:52:59 -0700 Subject: [PATCH 43/82] Refine "arc land" behavior when pushing "onto" a new branch Summary: Ref T13546. If the "onto" branch doesn't exist yet and has a "/" in it, we need to preface it with "refs/heads" explicitly. Fix a "false/null" issue with argument validation. Possibly, "arc land" should prompt you before creating branches in the remote. Test Plan: Ran "arc land --onto does-not-exist/1.1" and created a branch. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21341 --- src/land/engine/ArcanistGitLandEngine.php | 7 ++++++- src/land/engine/ArcanistLandEngine.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index 51655dcf..e12bc242 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -885,6 +885,11 @@ final class ArcanistGitLandEngine $onto_ref)); } } + + // TODO: Check that these refs really exist in the remote? Checking the + // remote is expensive and users probably rarely specify "--onto" manually, + // but if "arc land" creates branches without prompting when you make typos + // that also seems questionable. } protected function selectOntoRefs(array $symbols) { @@ -1491,7 +1496,7 @@ final class ArcanistGitLandEngine foreach ($this->getOntoRefs() as $onto_ref) { $refspecs[] = sprintf( - '%s:%s', + '%s:refs/heads/%s', $this->getDisplayHash($into_commit), $onto_ref); } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 14657d6d..741de7b7 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1361,7 +1361,7 @@ abstract class ArcanistLandEngine } $into = $this->getIntoArgument(); - if ($into && ($into_empty !== null)) { + if ($into && $into_empty) { throw new PhutilArgumentUsageException( pht( 'Arguments "--into" and "--into-empty" are mutually exclusive.')); From 1bb054ef47a1412116f833672cc828cc40f324df Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Jun 2020 06:10:35 -0700 Subject: [PATCH 44/82] Verify remotes ("paths") in Mercurial during "arc land" Summary: Ref T13546. Parse "hg paths" and validate that the remotes "arc land" plans to interact with actually exist. Test Plan: Ran "arc land" with good and bad "--into-remote" and "--onto-remote" arguments, got sensible validation behavior. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21342 --- src/__phutil_library_map__.php | 10 ++++- .../engine/ArcanistMercurialLandEngine.php | 28 ++++++++++-- src/repository/api/ArcanistMercurialAPI.php | 6 +++ src/repository/api/ArcanistRepositoryAPI.php | 9 ++++ .../marker/ArcanistRepositoryMarkerQuery.php | 28 +----------- .../query/ArcanistRepositoryQuery.php | 35 +++++++++++++++ ...ArcanistMercurialRepositoryRemoteQuery.php | 45 +++++++++++++++++++ src/repository/remote/ArcanistRemoteRef.php | 41 +++++++++++++++++ .../remote/ArcanistRepositoryRemoteQuery.php | 31 +++++++++++++ src/workflow/ArcanistWorkflow.php | 2 + 10 files changed, 204 insertions(+), 31 deletions(-) create mode 100644 src/repository/query/ArcanistRepositoryQuery.php create mode 100644 src/repository/remote/ArcanistMercurialRepositoryRemoteQuery.php create mode 100644 src/repository/remote/ArcanistRemoteRef.php create mode 100644 src/repository/remote/ArcanistRepositoryRemoteQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f10c52b3..9ddfbbe0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -333,6 +333,7 @@ phutil_register_library_map(array( 'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php', 'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php', 'ArcanistMercurialRepositoryMarkerQuery' => 'repository/marker/ArcanistMercurialRepositoryMarkerQuery.php', + 'ArcanistMercurialRepositoryRemoteQuery' => 'repository/remote/ArcanistMercurialRepositoryRemoteQuery.php', 'ArcanistMercurialWorkEngine' => 'work/ArcanistMercurialWorkEngine.php', 'ArcanistMercurialWorkingCopy' => 'workingcopy/ArcanistMercurialWorkingCopy.php', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'query/ArcanistMercurialWorkingCopyRevisionHardpointQuery.php', @@ -414,12 +415,15 @@ phutil_register_library_map(array( 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php', 'ArcanistRef' => 'ref/ArcanistRef.php', 'ArcanistRefInspector' => 'inspector/ArcanistRefInspector.php', + 'ArcanistRemoteRef' => 'repository/remote/ArcanistRemoteRef.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', 'ArcanistRepositoryLocalState' => 'repository/state/ArcanistRepositoryLocalState.php', 'ArcanistRepositoryMarkerQuery' => 'repository/marker/ArcanistRepositoryMarkerQuery.php', + 'ArcanistRepositoryQuery' => 'repository/query/ArcanistRepositoryQuery.php', 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php', + 'ArcanistRepositoryRemoteQuery' => 'repository/remote/ArcanistRepositoryRemoteQuery.php', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php', @@ -1365,6 +1369,7 @@ phutil_register_library_map(array( 'ArcanistMercurialParser' => 'Phobject', 'ArcanistMercurialParserTestCase' => 'PhutilTestCase', 'ArcanistMercurialRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', + 'ArcanistMercurialRepositoryRemoteQuery' => 'ArcanistRepositoryRemoteQuery', 'ArcanistMercurialWorkEngine' => 'ArcanistWorkEngine', 'ArcanistMercurialWorkingCopy' => 'ArcanistWorkingCopy', 'ArcanistMercurialWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', @@ -1449,12 +1454,15 @@ phutil_register_library_map(array( 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistRef' => 'ArcanistHardpointObject', 'ArcanistRefInspector' => 'Phobject', + 'ArcanistRemoteRef' => 'ArcanistRef', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', 'ArcanistRepositoryLocalState' => 'Phobject', - 'ArcanistRepositoryMarkerQuery' => 'Phobject', + 'ArcanistRepositoryMarkerQuery' => 'ArcanistRepositoryQuery', + 'ArcanistRepositoryQuery' => 'Phobject', 'ArcanistRepositoryRef' => 'ArcanistRef', + 'ArcanistRepositoryRemoteQuery' => 'ArcanistRepositoryQuery', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 43fd3bd9..241015e7 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -48,6 +48,7 @@ final class ArcanistMercurialLandEngine } $commit = $api->getCanonicalRevisionName('.'); + $commit = $this->getDisplayHash($commit); $log->writeStatus( pht('SOURCE'), @@ -124,9 +125,21 @@ final class ArcanistMercurialLandEngine protected function selectOntoRemote(array $symbols) { assert_instances_of($symbols, 'ArcanistLandSymbol'); + $api = $this->getRepositoryAPI(); + $remote = $this->newOntoRemote($symbols); - // TODO: Verify this remote actually exists. + $remote_ref = $api->newRemoteRefQuery() + ->withNames(array($remote)) + ->executeOne(); + if (!$remote_ref) { + throw new PhutilArgumentUsageException( + pht( + 'No remote "%s" exists in this repository.', + $remote)); + } + + // TODO: Allow selection of a bare URI. return $remote; } @@ -261,8 +274,17 @@ final class ArcanistMercurialLandEngine $into = $this->getIntoRemoteArgument(); if ($into !== null) { - // TODO: Verify that this is a valid path. - // TODO: Allow a raw URI? + $remote_ref = $api->newRemoteRefQuery() + ->withNames(array($into)) + ->executeOne(); + if (!$remote_ref) { + throw new PhutilArgumentUsageException( + pht( + 'No remote "%s" exists in this repository.', + $into)); + } + + // TODO: Allow a raw URI. $this->setIntoRemote($into); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index bdc98db0..12b78bf4 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -902,6 +902,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { } public function getRemoteURI() { + // TODO: Remove this method in favor of RemoteRefQuery. + list($stdout) = $this->execxLocal('paths default'); $stdout = trim($stdout); @@ -1006,4 +1008,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return new ArcanistMercurialRepositoryMarkerQuery(); } + protected function newRemoteRefQueryTemplate() { + return new ArcanistMercurialRepositoryRemoteQuery(); + } + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 44a1f059..070fd085 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -785,6 +785,15 @@ abstract class ArcanistRepositoryAPI extends Phobject { throw new PhutilMethodNotImplementedException(); } + final public function newRemoteRefQuery() { + return id($this->newRemoteRefQueryTemplate()) + ->setRepositoryAPI($this); + } + + protected function newRemoteRefQueryTemplate() { + throw new PhutilMethodNotImplementedException(); + } + final public function getDisplayHash($hash) { return substr($hash, 0, 12); } diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index 9b31ad3f..c969923f 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -1,24 +1,14 @@ repositoryAPI = $api; - return $this; - } - - final public function getRepositoryAPI() { - return $this->repositoryAPI; - } - final public function withMarkerTypes(array $types) { $this->markerTypes = array_fuse($types); return $this; @@ -34,22 +24,6 @@ abstract class ArcanistRepositoryMarkerQuery return $this; } - final public function executeOne() { - $markers = $this->execute(); - - if (!$markers) { - return null; - } - - if (count($markers) > 1) { - throw new Exception( - pht( - 'Query matched multiple markers, expected zero or one.')); - } - - return head($markers); - } - final public function execute() { $markers = $this->newRefMarkers(); diff --git a/src/repository/query/ArcanistRepositoryQuery.php b/src/repository/query/ArcanistRepositoryQuery.php new file mode 100644 index 00000000..23b20aef --- /dev/null +++ b/src/repository/query/ArcanistRepositoryQuery.php @@ -0,0 +1,35 @@ +repositoryAPI = $api; + return $this; + } + + final public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + abstract public function execute(); + + final public function executeOne() { + $refs = $this->execute(); + + if (!$refs) { + return null; + } + + if (count($refs) > 1) { + throw new Exception( + pht( + 'Query matched multiple refs, expected zero or one.')); + } + + return head($refs); + } + +} diff --git a/src/repository/remote/ArcanistMercurialRepositoryRemoteQuery.php b/src/repository/remote/ArcanistMercurialRepositoryRemoteQuery.php new file mode 100644 index 00000000..879c3111 --- /dev/null +++ b/src/repository/remote/ArcanistMercurialRepositoryRemoteQuery.php @@ -0,0 +1,45 @@ +getRepositoryAPI(); + + $future = $api->newFuture('paths'); + list($lines) = $future->resolve(); + + $refs = array(); + + $pattern = '(^(?P.*?) = (?P.*)\z)'; + + $lines = phutil_split_lines($lines, false); + foreach ($lines as $line) { + $matches = null; + if (!preg_match($pattern, $line, $matches)) { + throw new Exception( + pht( + 'Failed to match remote pattern against line "%s".', + $line)); + } + + $name = $matches['name']; + $uri = $matches['uri']; + + // NOTE: Mercurial gives some special behavior to "default" and + // "default-push", but these remotes are both fully-formed remotes that + // are fetchable and pushable, they just have rules around selection + // as default targets for operations. + + $ref = id(new ArcanistRemoteRef()) + ->setRemoteName($name) + ->setFetchURI($uri) + ->setPushURI($uri); + + $refs[] = $ref; + } + + return $refs; + } + +} diff --git a/src/repository/remote/ArcanistRemoteRef.php b/src/repository/remote/ArcanistRemoteRef.php new file mode 100644 index 00000000..c901baec --- /dev/null +++ b/src/repository/remote/ArcanistRemoteRef.php @@ -0,0 +1,41 @@ +getRemoteName()); + } + + public function setRemoteName($remote_name) { + $this->remoteName = $remote_name; + return $this; + } + + public function getRemoteName() { + return $this->remoteName; + } + + public function setFetchURI($fetch_uri) { + $this->fetchURI = $fetch_uri; + return $this; + } + + public function getFetchURI() { + return $this->fetchURI; + } + + public function setPushURI($push_uri) { + $this->pushURI = $push_uri; + return $this; + } + + public function getPushURI() { + return $this->pushURI; + } + +} diff --git a/src/repository/remote/ArcanistRepositoryRemoteQuery.php b/src/repository/remote/ArcanistRepositoryRemoteQuery.php new file mode 100644 index 00000000..cff6a185 --- /dev/null +++ b/src/repository/remote/ArcanistRepositoryRemoteQuery.php @@ -0,0 +1,31 @@ +names = $names; + return $this; + } + + final public function execute() { + $refs = $this->newRemoteRefs(); + + $names = $this->names; + if ($names !== null) { + $names = array_fuse($names); + foreach ($refs as $key => $ref) { + if (!isset($names[$ref->getRemoteName()])) { + unset($refs[$key]); + } + } + } + + return $refs; + } + + abstract protected function newRemoteRefs(); + +} diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index 1a3de034..d50e8780 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2036,6 +2036,8 @@ abstract class ArcanistWorkflow extends Phobject { 'This repository has no VCS UUID (this is normal for git/hg).'); } + // TODO: Swap this for a RemoteRefQuery. + $remote_uri = $this->getRepositoryAPI()->getRemoteURI(); if ($remote_uri !== null) { $query = array( From b1f807f7ca93e866d738d355ae31ced464e4be97 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 9 Jun 2020 10:23:25 -0700 Subject: [PATCH 45/82] Disambiguate various types of Mercurial remote markers with "hg arc-ls-remote" Summary: Ref T13546. Ref T9948. It seems challenging to examine a remote in vanilla Mercurial. Provide an "hg arc-ls-remote" command which functions like "git ls-remote" so we can figure out if "--into X" is a bookmark, branch, both, neither, or a branch with multiple heads without mutating the working copy as a side effect. Test Plan: Ran various "arc land --into ..." commands in a Mercurial working copy, saw apparently-sensible resolution of remote marker names. Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21343 --- .gitignore | 3 + .../engine/ArcanistMercurialLandEngine.php | 167 ++++++++++++++++-- src/repository/api/ArcanistMercurialAPI.php | 12 ++ support/hg/arc-hg.py | 90 ++++++++++ 4 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 support/hg/arc-hg.py diff --git a/.gitignore b/.gitignore index 096c30cb..d40646a4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ # Generated shell completion rulesets. /support/shell/rules/ + +# Python extension compiled files. +/support/hg/arc-hg.pyc diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 241015e7..68dd20f7 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -7,6 +7,16 @@ final class ArcanistMercurialLandEngine $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); + // TODO: In Mercurial, you normally can not create a branch and a bookmark + // with the same name. However, you can fetch a branch or bookmark from + // a remote that has the same name as a local branch or bookmark of the + // other type, and end up with a local branch and bookmark with the same + // name. We should detect this and treat it as an error. + + // TODO: In Mercurial, you can create local bookmarks named + // "default@default" and similar which do not surive a round trip through + // a remote. Possibly, we should disallow interacting with these bookmarks. + $markers = $api->newMarkerRefQuery() ->withIsActive(true) ->execute(); @@ -436,27 +446,156 @@ final class ArcanistMercurialLandEngine $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); - // TODO: Support bookmarks. - // TODO: Deal with bookmark save/restore behavior. - // TODO: Raise a good error message when the ref does not exist. + // See T9948. If the user specified "--into X", we don't know if it's a + // branch, a bookmark, or a symbol which doesn't exist yet. + + // In native Mercurial it is difficult to figure this out, so we use + // an extension to provide a command which works like "git ls-remote". + + // NOTE: We're using passthru on this because it's a remote command and + // may prompt the user for credentials. + + // TODO: This is fairly silly/confusing to show to users in the common + // case where it does not require credentials, particularly because the + // actual command line is full of nonsense. + + $tmpfile = new TempFile(); + Filesystem::remove($tmpfile); $err = $this->newPassthru( - 'pull -b %s -- %s', - $target->getRef(), + '%Ls arc-ls-remote --output %s -- %s', + $api->getMercurialExtensionArguments(), + phutil_string_cast($tmpfile), $target->getRemote()); + if ($err) { + throw new Exception( + pht( + 'Call to "hg arc-ls-remote" failed with error "%s".', + $err)); + } - // TODO: Deal with errors. - // TODO: Deal with multiple branch heads. + $raw_data = Filesystem::readFile($tmpfile); + unset($tmpfile); - list($stdout) = $api->execxLocal( - 'log --rev %s --template %s --', - hgsprintf( - 'last(ancestors(%s) and !outgoing(%s))', + $markers = phutil_json_decode($raw_data); + + $target_name = $target->getRef(); + + $bookmarks = array(); + $branches = array(); + foreach ($markers as $marker) { + if ($marker['name'] !== $target_name) { + continue; + } + + if ($marker['type'] === 'bookmark') { + $bookmarks[] = $marker; + } else { + $branches[] = $marker; + } + } + + if (!$bookmarks && !$branches) { + throw new PhutilArgumentUsageException( + pht( + 'Remote "%s" has no bookmark or branch named "%s".', + $target->getRemote(), + $target->getRef())); + } + + if ($bookmarks && $branches) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS MARKER'), + pht( + 'In remote "%s", the name "%s" identifies one or more branch '. + 'heads and one or more bookmarks. Close, rename, or delete all '. + 'but one of these markers, or pull the state you want to merge '. + 'into and use "--into-local --into " to disambiguate the '. + 'desired merge target.', + $target->getRemote(), + $target->getRef())); + + throw new PhutilArgumentUsageException( + pht('Merge target is ambiguous.')); + } + + $is_bookmark = false; + $is_branch = false; + + if ($bookmarks) { + if (count($bookmarks) > 1) { + throw new Exception( + pht( + 'Remote "%s" has multiple bookmarks with name "%s". This '. + 'is unexpected.', + $target->getRemote(), + $target->getRef())); + } + $bookmark = head($bookmarks); + + $target_hash = $bookmark['node']; + $is_bookmark = true; + } + + if ($branches) { + if (count($branches) > 1) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('MULTIPLE BRANCH HEADS'), + pht( + 'Remote "%s" has multiple branch heads named "%s". Close all '. + 'but one, or pull the head you want and use "--into-local '. + '--into " to specify an explicit merge target.', + $target->getRemote(), + $target->getRef())); + + throw new PhutilArgumentUsageException( + pht( + 'Remote branch has multiple heads.')); + } + + $branch = head($branches); + + $target_hash = $branch['node']; + $is_branch = true; + } + + if ($is_branch) { + $err = $this->newPassthru( + 'pull -b %s -- %s', $target->getRef(), - $target->getRemote()), - '{node}'); + $target->getRemote()); + } else { - return trim($stdout); + // NOTE: This may have side effects: + // + // - It can create a "bookmark@remote" bookmark if there is a local + // bookmark with the same name that is not an ancestor. + // - It can create an arbitrary number of other bookmarks. + // + // Since these seem to generally be intentional behaviors in Mercurial, + // and should theoretically be familiar to Mercurial users, just accept + // them as the cost of doing business. + + $err = $this->newPassthru( + 'pull -B %s -- %s', + $target->getRef(), + $target->getRemote()); + } + + // NOTE: It's possible that between the time we ran "ls-remote" and the + // time we ran "pull" that the remote changed. + + // It may even have been rewound or rewritten, in which case we did not + // actually fetch the ref we are about to return as a target. For now, + // assume this didn't happen: it's so unlikely that it's probably not + // worth spending 100ms to check. + + // TODO: If the Mercurial command server is revived, this check becomes + // more reasonable if it's cheap. + + return $target_hash; } protected function selectCommits($into_commit, array $symbols) { diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 12b78bf4..4756d068 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1012,4 +1012,16 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return new ArcanistMercurialRepositoryRemoteQuery(); } + + public function getMercurialExtensionArguments() { + $path = phutil_get_library_root('arcanist'); + $path = dirname($path); + $path = $path.'/support/hg/arc-hg.py'; + + return array( + '--config', + 'extensions.arc-hg='.$path, + ); + } + } diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py new file mode 100644 index 00000000..5a2e52f9 --- /dev/null +++ b/support/hg/arc-hg.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import + +import os +import json + +from mercurial import ( + cmdutil, + bookmarks, + bundlerepo, + error, + hg, + i18n, + node, + registrar, +) + +_ = i18n._ +cmdtable = {} +command = registrar.command(cmdtable) + +@command( + "arc-ls-remote", + [('', 'output', '', + _('file to output refs to'), _('FILE')), + ] + cmdutil.remoteopts, + _('[--output FILENAME] [SOURCE]')) +def lsremote(ui, repo, source="default", **opts): + """list markers in a remote + + Show the current branch heads and bookmarks in a specified path/URL or the + default pull location. + + Markers are printed to stdout in JSON. + + (This is an Arcanist extension to Mercurial.) + + Returns 0 if listing the markers succeeds, 1 otherwise. + """ + + # Disable status output from fetching a remote. + ui.quiet = True + + source, branches = hg.parseurl(ui.expandpath(source)) + remote = hg.peer(repo, opts, source) + + markers = [] + + bundle, remotebranches, cleanup = bundlerepo.getremotechanges( + ui, + repo, + remote) + + try: + for n in remotebranches: + ctx = bundle[n] + markers.append({ + 'type': 'branch', + 'name': ctx.branch(), + 'node': node.hex(ctx.node()), + }) + finally: + cleanup() + + with remote.commandexecutor() as e: + remotemarks = bookmarks.unhexlifybookmarks(e.callcommand('listkeys', { + 'namespace': 'bookmarks', + }).result()) + + for mark in remotemarks: + markers.append({ + 'type': 'bookmark', + 'name': mark, + 'node': node.hex(remotemarks[mark]), + }) + + json_opts = { + 'indent': 2, + 'sort_keys': True, + } + + output_file = opts.get('output') + if output_file: + if os.path.exists(output_file): + raise error.Abort(_('File "%s" already exists.' % output_file)) + with open(output_file, 'w+') as f: + json.dump(markers, f, **json_opts) + else: + print json.dumps(markers, output_file, **json_opts) + + return 0 From 3cad824e38722b4434e57fe0fe12d1dd8d86ba72 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 08:24:35 -0700 Subject: [PATCH 46/82] In "arc land" in Mercurial, show a tidier "ls-remote" command Summary: Ref T9948. Ref T13546. We must passthru "hg ls-remote" because it might prompt the user for credentials. Since "ls-remote" is implemented as an exension and we can't rely on using stdout beacuse of passthru, the actual command we execute is: ``` $ hg --config extensions.arc-hg= arc-ls-remote --output -- remote ``` This is meaningless and distracting; show the intent of the command we're executing instead. Users can see the raw command in "--trace" if they're actually debugging behavior. Test Plan: Ran "arc land" in a Mercurial repository, got a tidier command output. Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21344 --- src/land/engine/ArcanistLandEngine.php | 12 ++++++++++- .../engine/ArcanistMercurialLandEngine.php | 12 ++++++----- src/toolset/command/ArcanistCommand.php | 20 ++++++++++++++++++- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 741de7b7..31e51976 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1547,7 +1547,7 @@ abstract class ArcanistLandEngine return $sets; } - final protected function newPassthru($pattern /* , ... */) { + final protected function newPassthruCommand($pattern /* , ... */) { $workflow = $this->getWorkflow(); $argv = func_get_args(); @@ -1560,6 +1560,16 @@ abstract class ArcanistLandEngine $command = $workflow->newCommand($passthru) ->setResolveOnError(true); + return $command; + } + + final protected function newPassthru($pattern /* , ... */) { + $argv = func_get_args(); + + $command = call_user_func_array( + array($this, 'newPassthruCommand'), + $argv); + return $command->execute(); } diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 68dd20f7..fec60cdd 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -455,18 +455,20 @@ final class ArcanistMercurialLandEngine // NOTE: We're using passthru on this because it's a remote command and // may prompt the user for credentials. - // TODO: This is fairly silly/confusing to show to users in the common - // case where it does not require credentials, particularly because the - // actual command line is full of nonsense. - $tmpfile = new TempFile(); Filesystem::remove($tmpfile); - $err = $this->newPassthru( + $command = $this->newPassthruCommand( '%Ls arc-ls-remote --output %s -- %s', $api->getMercurialExtensionArguments(), phutil_string_cast($tmpfile), $target->getRemote()); + + $command->setDisplayCommand( + 'hg ls-remote -- %s', + $target->getRemote()); + + $err = $command->execute(); if ($err) { throw new Exception( pht( diff --git a/src/toolset/command/ArcanistCommand.php b/src/toolset/command/ArcanistCommand.php index 1c5390ef..3a7c5338 100644 --- a/src/toolset/command/ArcanistCommand.php +++ b/src/toolset/command/ArcanistCommand.php @@ -6,6 +6,7 @@ final class ArcanistCommand private $logEngine; private $executableFuture; private $resolveOnError = false; + private $displayCommand; public function setExecutableFuture(PhutilExecutableFuture $future) { $this->executableFuture = $future; @@ -34,10 +35,27 @@ final class ArcanistCommand return $this->resolveOnError; } + public function setDisplayCommand($pattern /* , ... */) { + $argv = func_get_args(); + $command = call_user_func_array('csprintf', $argv); + $this->displayCommand = $command; + return $this; + } + + public function getDisplayCommand() { + return $this->displayCommand; + } + public function execute() { $log = $this->getLogEngine(); $future = $this->getExecutableFuture(); - $command = $future->getCommand(); + + $display_command = $this->getDisplayCommand(); + if ($display_command !== null) { + $command = $display_command; + } else { + $command = $future->getCommand(); + } $log->writeNewline(); From 705c48effcb521b88c13173b1ac8b10455162b72 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 09:14:59 -0700 Subject: [PATCH 47/82] Realign "arc land" closed/published warning around more modern language Summary: Ref T13546. The modern constant from the modern API method for this state is "published", and this more narrowly covers the desired behavior (notably, excluding "Abandoned" revisions). Test Plan: Ran "arc land ... --revision X" where "X" is a published revision, got an appropriate prompt. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21345 --- src/land/engine/ArcanistLandEngine.php | 22 +++++++++++----------- src/ref/revision/ArcanistRevisionRef.php | 4 ++-- src/workflow/ArcanistLandWorkflow.php | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 31e51976..9dc7eb50 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -333,13 +333,13 @@ abstract class ArcanistLandEngine } $planned = array(); - $closed = array(); + $published = array(); $not_accepted = array(); foreach ($revision_refs as $revision_ref) { if ($revision_ref->isStatusChangesPlanned()) { $planned[] = $revision_ref; - } else if ($revision_ref->isStatusClosed()) { - $closed[] = $revision_ref; + } else if ($revision_ref->isStatusPublished()) { + $published[] = $revision_ref; } else if (!$revision_ref->isStatusAccepted()) { $not_accepted[] = $revision_ref; } @@ -389,28 +389,28 @@ abstract class ArcanistLandEngine // See PHI1727. Previously, this prompt was bundled with the generic // "not accepted" prompt, but at least one user found it confusing. - if ($closed) { - $example_ref = head($closed); + if ($published) { + $example_ref = head($published); echo tsprintf( "\n%!\n%W\n\n", - pht('%s REVISION(S) ARE ALREADY CLOSED', phutil_count($closed)), + pht('%s REVISION(S) ARE ALREADY PUBLISHED', phutil_count($published)), pht( 'You are landing %s revision(s) which are already in the state '. '"%s", indicating that they have previously landed:', - phutil_count($closed), + phutil_count($published), $example_ref->getStatusDisplayName())); - foreach ($closed as $revision_ref) { + foreach ($published as $revision_ref) { echo tsprintf('%s', $revision_ref->newDisplayRef()); } $query = pht( - 'Land %s revision(s) that are already closed?', - phutil_count($closed)); + 'Land %s revision(s) that are already published?', + phutil_count($published)); $this->getWorkflow() - ->getPrompt('arc.land.closed') + ->getPrompt('arc.land.published') ->setQuery($query) ->execute(); } diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index 9ec2fb87..c507baa4 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -86,9 +86,9 @@ final class ArcanistRevisionRef return ($status === 'abandoned'); } - public function isStatusClosed() { + public function isStatusPublished() { $status = $this->getStatus(); - return ($status === 'closed'); + return ($status === 'published'); } public function isStatusAccepted() { diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index d34dc15e..8a6f9f17 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -259,10 +259,10 @@ EOTEXT ->setDescription( pht( 'Confirms that revisions with changes planned should land.')), - $this->newPrompt('arc.land.closed') + $this->newPrompt('arc.land.published') ->setDescription( pht( - 'Confirms that revisions that are already closed should land.')), + 'Confirms that revisions that are already published should land.')), $this->newPrompt('arc.land.not-accepted') ->setDescription( pht( From 727d73fec9370392cfbfe781350cbf683a9ff60f Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 09:21:27 -0700 Subject: [PATCH 48/82] In "arc land", fix some coarse issues with build warnings Summary: Ref T13546. In the new "arc land": actually reach build warnings; and show buildable URIs. Test Plan: Ran "arc land ..." with intentionally broken builds, got more useful build warnings. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21347 --- src/land/engine/ArcanistLandEngine.php | 17 +++++++++++------ .../ArcanistBuildBuildplanHardpointQuery.php | 2 +- src/ref/buildable/ArcanistBuildableRef.php | 16 +++++++++++----- src/workflow/ArcanistWorkflow.php | 2 +- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 9dc7eb50..29365a9d 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -635,7 +635,7 @@ abstract class ArcanistLandEngine if (!isset($build_map[$revision_phid])) { $build_map[$revision_phid] = array( - 'revisionRef' => $revision_phid, + 'revisionRef' => $revision_ref, 'buildRefs' => array(), ); } @@ -666,14 +666,14 @@ abstract class ArcanistLandEngine } echo tsprintf( - "%!\n%s\n\n", + "%!\n%s\n", pht('BUILD FAILURES'), $message); $prompt_key = 'arc.land.failed-builds'; } else if ($has_ongoing) { echo tsprintf( - "%!\n%s\n\n", + "%!\n%s\n", pht('ONGOING BUILDS'), pht( '%s revision(s) have ongoing builds:', @@ -700,15 +700,20 @@ abstract class ArcanistLandEngine } echo tsprintf( - "\n%s\n\n", + "\n%s\n", pht('You can review build details here:')); // TODO: Only show buildables with problem builds. + $workflow = $this->getWorkflow(); + foreach ($buildable_refs as $buildable) { $display_ref = $buildable->newDisplayRef(); - // TODO: Include URI here. + $raw_uri = $buildable->getURI(); + $raw_uri = $workflow->getAbsoluteURI($raw_uri); + + $display_ref->setURI($raw_uri); echo tsprintf('%s', $display_ref); } @@ -909,7 +914,7 @@ abstract class ArcanistLandEngine pht( 'TODO: You are forcing a revision, but commits are associated '. 'with some other revision. Are you REALLY sure you want to land '. - 'ALL these commits wiht a different unrelated revision???')); + 'ALL these commits with a different unrelated revision???')); } foreach ($confirm_force as $commit) { diff --git a/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php b/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php index 3491c210..8e386527 100644 --- a/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php +++ b/src/ref/build/ArcanistBuildBuildplanHardpointQuery.php @@ -29,7 +29,7 @@ final class ArcanistBuildBuildplanHardpointQuery $plan_ref = ArcanistBuildPlanRef::newFromConduit($plan); $plan_refs[] = $plan_ref; } - $plan_refs = mpull($plan_refs, 'getPHID'); + $plan_refs = mpull($plan_refs, null, 'getPHID'); $results = array(); foreach ($refs as $key => $build_ref) { diff --git a/src/ref/buildable/ArcanistBuildableRef.php b/src/ref/buildable/ArcanistBuildableRef.php index e9b376cc..bf19ebd1 100644 --- a/src/ref/buildable/ArcanistBuildableRef.php +++ b/src/ref/buildable/ArcanistBuildableRef.php @@ -36,10 +36,6 @@ final class ArcanistBuildableRef return idx($this->parameters, 'phid'); } - public function getName() { - return idxv($this->parameters, array('fields', 'name')); - } - public function getObjectPHID() { return idxv($this->parameters, array('fields', 'objectPHID')); } @@ -53,11 +49,21 @@ final class ArcanistBuildableRef } public function getDisplayRefTitle() { - return $this->getName(); + return pht('Buildable %d', $this->getID()); } public function getBuildRefs() { return $this->getHardpoint(self::HARDPOINT_BUILDREFS); } + public function getURI() { + $uri = idxv($this->parameters, array('fields', 'uri')); + + if ($uri === null) { + $uri = '/'.$this->getMonogram(); + } + + return $uri; + } + } diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index d50e8780..c6098ddb 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -2434,7 +2434,7 @@ abstract class ArcanistWorkflow extends Phobject { return $stdin->read(); } - protected function getAbsoluteURI($raw_uri) { + final public function getAbsoluteURI($raw_uri) { // TODO: "ArcanistRevisionRef", at least, may return a relative URI. // If we get a relative URI, guess the correct absolute URI based on // the Conduit URI. This might not be correct for Conduit over SSH. From 488a24c40a266f5437940e05339fa0102877c784 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 13:43:08 -0700 Subject: [PATCH 49/82] In "arc land" in Mercurial, inch closer to making complex branch/bookmark workflows function Summary: Ref T9948. Ref T13546. This change moves toward a functional "arc land" in Mercurial. Because of how "bundlerepo.getremotechanges()" works, "hg arc-ls-markers" does not actually list markers in the remote that aren't different from local markers so it's hard to get anywhere with this. Test Plan: Got somewhat-encouraging output from "arc land" and "hg arc-ls-markers", but too many things are still broken for this to really work yet. Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21348 --- .../engine/ArcanistMercurialLandEngine.php | 230 +++++++++++++----- .../ArcanistGitRepositoryMarkerQuery.php | 7 +- src/repository/marker/ArcanistMarkerRef.php | 10 + ...ArcanistMercurialRepositoryMarkerQuery.php | 193 +++++++-------- .../marker/ArcanistRepositoryMarkerQuery.php | 29 ++- .../state/ArcanistMercurialLocalState.php | 6 +- support/hg/arc-hg.py | 144 +++++++++-- 7 files changed, 429 insertions(+), 190 deletions(-) diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index fec60cdd..25a3fe33 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -3,6 +3,9 @@ final class ArcanistMercurialLandEngine extends ArcanistLandEngine { + private $ontoBranchMarker; + private $ontoMarkers; + protected function getDefaultSymbols() { $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); @@ -242,6 +245,8 @@ final class ArcanistMercurialLandEngine } protected function confirmOntoRefs(array $onto_refs) { + $api = $this->getRepositoryAPI(); + foreach ($onto_refs as $onto_ref) { if (!strlen($onto_ref)) { throw new PhutilArgumentUsageException( @@ -251,6 +256,63 @@ final class ArcanistMercurialLandEngine $onto_ref)); } } + + $remote_ref = id(new ArcanistRemoteRef()) + ->setRemoteName($this->getOntoRemote()); + + $markers = $api->newMarkerRefQuery() + ->withRemotes(array($remote_ref)) + ->execute(); + + $onto_markers = array(); + $new_markers = array(); + foreach ($onto_refs as $onto_ref) { + $matches = array(); + foreach ($markers as $marker) { + if ($marker->getName() === $onto_ref) { + $matches[] = $marker; + } + } + + $match_count = count($matches); + if ($match_count > 1) { + throw new PhutilArgumentUsageException( + pht( + 'TODO: Ambiguous ref.')); + } else if (!$match_count) { + $new_bookmark = id(new ArcanistMarkerRef()) + ->setMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK) + ->setName($onto_ref) + ->attachRemoteRef($remote_ref); + + $onto_markers[] = $new_bookmark; + $new_markers[] = $new_bookmark; + } else { + $onto_markers[] = $marker; + } + } + + $branches = array(); + foreach ($onto_markers as $onto_marker) { + if ($onto_marker->isBranch()) { + $branches[] = $onto_marker; + } + + $branch_count = count($branches); + if ($branch_count > 1) { + throw new PhutilArgumentUsageException( + pht( + 'TODO: You can not push onto multiple branches in Mercurial.')); + } else if ($branch_count) { + $this->ontoBranchMarker = head($branches); + } + } + + if ($new_markers) { + // TODO: If we're creating bookmarks, ask the user to confirm. + } + + $this->ontoMarkers = $onto_markers; } protected function selectIntoRemote() { @@ -366,7 +428,6 @@ final class ArcanistMercurialLandEngine } protected function selectIntoCommit() { - // Make sure that our "into" target is valid. $log = $this->getLogEngine(); if ($this->getIntoEmpty()) { @@ -385,7 +446,8 @@ final class ArcanistMercurialLandEngine $api = $this->getRepositoryAPI(); $local_ref = $this->getIntoRef(); - // TODO: This error handling could probably be cleaner. + // TODO: This error handling could probably be cleaner, it will just + // raise an exception without any context. $into_commit = $api->getCanonicalRevisionName($local_ref); @@ -446,51 +508,20 @@ final class ArcanistMercurialLandEngine $api = $this->getRepositoryAPI(); $log = $this->getLogEngine(); - // See T9948. If the user specified "--into X", we don't know if it's a - // branch, a bookmark, or a symbol which doesn't exist yet. - - // In native Mercurial it is difficult to figure this out, so we use - // an extension to provide a command which works like "git ls-remote". - - // NOTE: We're using passthru on this because it's a remote command and - // may prompt the user for credentials. - - $tmpfile = new TempFile(); - Filesystem::remove($tmpfile); - - $command = $this->newPassthruCommand( - '%Ls arc-ls-remote --output %s -- %s', - $api->getMercurialExtensionArguments(), - phutil_string_cast($tmpfile), - $target->getRemote()); - - $command->setDisplayCommand( - 'hg ls-remote -- %s', - $target->getRemote()); - - $err = $command->execute(); - if ($err) { - throw new Exception( - pht( - 'Call to "hg arc-ls-remote" failed with error "%s".', - $err)); - } - - $raw_data = Filesystem::readFile($tmpfile); - unset($tmpfile); - - $markers = phutil_json_decode($raw_data); - $target_name = $target->getRef(); + $remote_ref = id(new ArcanistRemoteRef()) + ->setRemoteName($target->getRemote()); + + $markers = $api->newMarkerRefQuery() + ->withRemotes(array($remote_ref)) + ->withNames(array($target_name)) + ->execute(); + $bookmarks = array(); $branches = array(); foreach ($markers as $marker) { - if ($marker['name'] !== $target_name) { - continue; - } - - if ($marker['type'] === 'bookmark') { + if ($marker->isBookmark()) { $bookmarks[] = $marker; } else { $branches[] = $marker; @@ -536,8 +567,7 @@ final class ArcanistMercurialLandEngine } $bookmark = head($bookmarks); - $target_hash = $bookmark['node']; - $is_bookmark = true; + $target_marker = $bookmark; } if ($branches) { @@ -559,13 +589,12 @@ final class ArcanistMercurialLandEngine $branch = head($branches); - $target_hash = $branch['node']; - $is_branch = true; + $target_marker = $branch; } if ($is_branch) { $err = $this->newPassthru( - 'pull -b %s -- %s', + 'pull --branch %s -- %s', $target->getRef(), $target->getRemote()); } else { @@ -581,12 +610,12 @@ final class ArcanistMercurialLandEngine // them as the cost of doing business. $err = $this->newPassthru( - 'pull -B %s -- %s', + 'pull --bookmark %s -- %s', $target->getRef(), $target->getRemote()); } - // NOTE: It's possible that between the time we ran "ls-remote" and the + // NOTE: It's possible that between the time we ran "ls-markers" and the // time we ran "pull" that the remote changed. // It may even have been rewound or rewritten, in which case we did not @@ -597,7 +626,7 @@ final class ArcanistMercurialLandEngine // TODO: If the Mercurial command server is revived, this check becomes // more reasonable if it's cheap. - return $target_hash; + return $target_marker->getCommitHash(); } protected function selectCommits($into_commit, array $symbols) { @@ -691,6 +720,17 @@ final class ArcanistMercurialLandEngine $revision_ref = $set->getRevisionRef(); $commit_message = $revision_ref->getCommitMessage(); + // If we're landing "--onto" a branch, set that as the branch marker + // before creating the new commit. + + // TODO: We could skip this if we know that the "$into_commit" already + // has the right branch, which it will if we created it. + + $branch_marker = $this->ontoBranchMarker; + if ($branch_marker) { + $api->execxLocal('branch -- %s', $branch_marker); + } + try { $argv = array(); $argv[] = '--dest'; @@ -724,12 +764,83 @@ final class ArcanistMercurialLandEngine protected function pushChange($into_commit) { $api = $this->getRepositoryAPI(); - // TODO: This does not respect "--into" or "--onto" properly. + $bookmarks = array(); + foreach ($this->ontoMarkers as $onto_marker) { + if (!$onto_marker->isBookmark()) { + continue; + } + $bookmarks[] = $onto_marker; + } + + // If we're pushing to bookmarks, move all the bookmarks we want to push + // to the merge commit. (There doesn't seem to be any way to specify + // "push commit X as bookmark Y" in Mercurial.) + + $restore = array(); + if ($bookmarks) { + $markers = $api->newMarkerRefQuery() + ->withNames(array(mpull($bookmarks, 'getName'))) + ->withTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) + ->execute(); + $markers = mpull($markers, 'getCommitHash', 'getName'); + + foreach ($bookmarks as $bookmark) { + $bookmark_name = $bookmark->getName(); + + $old_position = idx($markers, $bookmark_name); + $new_position = $into_commit; + + if ($old_position === $new_position) { + continue; + } + + $api->execxLocal( + 'bookmark --force --rev %s -- %s', + hgsprintf('%s', $new_position), + $bookmark_name); + + $restore[$bookmark_name] = $old_position; + } + } + + // Now, do the actual push. + + $argv = array(); + $argv[] = 'push'; + + if ($bookmarks) { + // If we're pushing at least one bookmark, we can just specify the list + // of bookmarks as things we want to push. + foreach ($bookmarks as $bookmark) { + $argv[] = '--bookmark'; + $argv[] = $bookmark->getName(); + } + } else { + // Otherwise, specify the commit itself. + $argv[] = '--rev'; + $argv[] = hgsprintf('%s', $into_commit); + } + + $argv[] = '--'; + $argv[] = $this->getOntoRemote(); + + try { + $this->newPassthru('%Ls', $argv); + } finally { + foreach ($restore as $bookmark_name => $old_position) { + if ($old_position === null) { + $api->execxLocal( + 'bookmark --delete -- %s', + $bookmark_name); + } else { + $api->execxLocal( + 'bookmark --force --rev %s -- %s', + hgsprintf('%s', $old_position), + $bookmark_name); + } + } + } - $this->newPassthru( - 'push --rev %s -- %s', - hgsprintf('%s', $into_commit), - $this->getOntoRemote()); } protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { @@ -871,4 +982,13 @@ final class ArcanistMercurialLandEngine 'Local branches and bookmarks have not been changed, and are still '. 'in the same state as before.')); } + + + private function newRemoteMarkers($remote) { + // See T9948. If the user specified "--into X" or "--onto X", we don't know + // if it's a branch, a bookmark, or a symbol which doesn't exist yet. + + + } + } diff --git a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php index 0c0ede61..5e185767 100644 --- a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php @@ -3,8 +3,7 @@ final class ArcanistGitRepositoryMarkerQuery extends ArcanistRepositoryMarkerQuery { - - protected function newRefMarkers() { + protected function newLocalRefMarkers() { $api = $this->getRepositoryAPI(); $future = $this->newCurrentBranchNameFuture()->start(); @@ -122,4 +121,8 @@ final class ArcanistGitRepositoryMarkerQuery return $matches[1]; } + protected function newRemoteRefMarkers(ArcanistRemoteRef $remote) { + throw new PhutilMethodNotImplementedException(); + } + } diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index e3b27a96..77112b1e 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -7,6 +7,7 @@ final class ArcanistMarkerRef const HARDPOINT_COMMITREF = 'arc.marker.commitRef'; const HARDPOINT_WORKINGCOPYSTATEREF = 'arc.marker.workingCopyStateRef'; + const HARDPOINT_REMOTEREF = 'arc.marker.remoteRef'; const TYPE_BRANCH = 'branch'; const TYPE_BOOKMARK = 'bookmark'; @@ -48,6 +49,7 @@ final class ArcanistMarkerRef return array( $this->newHardpoint(self::HARDPOINT_COMMITREF), $this->newHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF), + $this->newHardpoint(self::HARDPOINT_REMOTEREF), ); } @@ -167,4 +169,12 @@ final class ArcanistMarkerRef return $this->getHardpoint(self::HARDPOINT_WORKINGCOPYSTATEREF); } + public function attachRemoteRef(ArcanistRemoteRef $ref = null) { + return $this->attachHardpoint(self::HARDPOINT_REMOTEREF, $ref); + } + + public function getRemoteRef() { + return $this->getHardpoint(self::HARDPOINT_REMOTEREF); + } + } diff --git a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php index 7d4b98a2..0cca7d76 100644 --- a/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistMercurialRepositoryMarkerQuery.php @@ -3,144 +3,123 @@ final class ArcanistMercurialRepositoryMarkerQuery extends ArcanistRepositoryMarkerQuery { - protected function newRefMarkers() { - $markers = array(); - - if ($this->shouldQueryMarkerType(ArcanistMarkerRef::TYPE_BRANCH)) { - $markers[] = $this->newBranchOrBookmarkMarkers(false); - } - - if ($this->shouldQueryMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK)) { - $markers[] = $this->newBranchOrBookmarkMarkers(true); - } - - return array_mergev($markers); + protected function newLocalRefMarkers() { + return $this->newMarkers(); } - private function newBranchOrBookmarkMarkers($is_bookmarks) { + protected function newRemoteRefMarkers(ArcanistRemoteRef $remote = null) { + return $this->newMarkers($remote); + } + + private function newMarkers(ArcanistRemoteRef $remote = null) { $api = $this->getRepositoryAPI(); - $is_branches = !$is_bookmarks; + // In native Mercurial it is difficult to identify remote markers, and + // complicated to identify local markers efficiently. We use an extension + // to provide a command which works like "git for-each-ref" locally and + // "git ls-remote" when given a remote. - // NOTE: This is a bit clumsy, but it allows us to get most bookmark and - // branch information in a single command, including full hashes, without - // using "--debug" or matching any human readable strings in the output. + $argv = array(); + foreach ($api->getMercurialExtensionArguments() as $arg) { + $argv[] = $arg; + } + $argv[] = 'arc-ls-markers'; - // NOTE: We can't get branches and bookmarks together in a single command - // because if we query for "heads() + bookmark()", we can't tell if a - // bookmarked result is a branch head or not. + // NOTE: In remote mode, we're using passthru and a tempfile on this + // because it's a remote command and may prompt the user to provide + // credentials interactively. In local mode, we can just read stdout. - $template_fields = array( - '{node}', - '{branch}', - '{join(bookmarks, "\3")}', - '{activebookmark}', - '{desc}', - ); - $expect_fields = count($template_fields); + if ($remote !== null) { + $tmpfile = new TempFile(); + Filesystem::remove($tmpfile); - $template = implode('\2', $template_fields).'\1'; - - if ($is_bookmarks) { - $query = hgsprintf('bookmark()'); - } else { - $query = hgsprintf('head()'); + $argv[] = '--output'; + $argv[] = phutil_string_cast($tmpfile); } - $future = $api->newFuture( - 'log --rev %s --template %s --', - $query, - $template); + $argv[] = '--'; - list($lines) = $future->resolve(); + if ($remote !== null) { + $argv[] = $remote->getRemoteName(); + } + + if ($remote !== null) { + $passthru = $api->newPassthru('%Ls', $argv); + + $err = $passthru->execute(); + if ($err) { + throw new Exception( + pht( + 'Call to "hg arc-ls-markers" failed with error "%s".', + $err)); + } + + $raw_data = Filesystem::readFile($tmpfile); + unset($tmpfile); + } else { + $future = $api->newFuture('%Ls', $argv); + list($raw_data) = $future->resolve(); + } + + $items = phutil_json_decode($raw_data); $markers = array(); - - $lines = explode("\1", $lines); - foreach ($lines as $line) { - if (!strlen(trim($line))) { + foreach ($items as $item) { + if (!empty($item['isClosed'])) { + // NOTE: For now, we ignore closed branch heads. continue; } - $fields = explode("\2", $line, $expect_fields); - $actual_fields = count($fields); - if ($actual_fields !== $expect_fields) { - throw new Exception( - pht( - 'Unexpected number of fields in line "%s", expected %s but '. - 'found %s.', - $line, - new PhutilNumber($expect_fields), - new PhutilNumber($actual_fields))); + $node = $item['node']; + if (!$node) { + // NOTE: For now, we ignore the virtual "current branch" marker. + continue; } - $node = $fields[0]; - - $branch = $fields[1]; - if (!strlen($branch)) { - $branch = 'default'; + switch ($item['type']) { + case 'branch': + $marker_type = ArcanistMarkerRef::TYPE_BRANCH; + break; + case 'bookmark': + $marker_type = ArcanistMarkerRef::TYPE_BOOKMARK; + break; + case 'commit': + $marker_type = null; + break; + default: + throw new Exception( + pht( + 'Call to "hg arc-ls-markers" returned marker of unknown '. + 'type "%s".', + $item['type'])); } - if ($is_bookmarks) { - $bookmarks = $fields[2]; - if (strlen($bookmarks)) { - $bookmarks = explode("\3", $fields[2]); - } else { - $bookmarks = array(); - } - - if (strlen($fields[3])) { - $active_bookmark = $fields[3]; - } else { - $active_bookmark = null; - } - } else { - $bookmarks = array(); - $active_bookmark = null; + if ($marker_type === null) { + // NOTE: For now, we ignore the virtual "head" marker. + continue; } - $message = $fields[4]; - $message_lines = phutil_split_lines($message, false); - $commit_ref = $api->newCommitRef() - ->setCommitHash($node) - ->attachMessage($message); + ->setCommitHash($node); - $template = id(new ArcanistMarkerRef()) + $marker_ref = id(new ArcanistMarkerRef()) + ->setName($item['name']) ->setCommitHash($node) - ->setSummary(head($message_lines)) ->attachCommitRef($commit_ref); - if ($is_bookmarks) { - foreach ($bookmarks as $bookmark) { - $is_active = ($bookmark === $active_bookmark); + if (isset($item['description'])) { + $description = $item['description']; + $commit_ref->attachMessage($description); - $markers[] = id(clone $template) - ->setMarkerType(ArcanistMarkerRef::TYPE_BOOKMARK) - ->setName($bookmark) - ->setIsActive($is_active); - } + $description_lines = phutil_split_lines($description, false); + $marker_ref->setSummary(head($description_lines)); } - if ($is_branches) { - $markers[] = id(clone $template) - ->setMarkerType(ArcanistMarkerRef::TYPE_BRANCH) - ->setName($branch); - } - } + $marker_ref + ->setMarkerType($marker_type) + ->setIsActive(!empty($item['isActive'])); - if ($is_branches) { - $current_hash = $api->getCanonicalRevisionName('.'); - - foreach ($markers as $marker) { - if ($marker->getMarkerType() !== ArcanistMarkerRef::TYPE_BRANCH) { - continue; - } - - if ($marker->getCommitHash() === $current_hash) { - $marker->setIsActive(true); - } - } + $markers[] = $marker_ref; } return $markers; diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index c969923f..b39c232b 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -8,6 +8,7 @@ abstract class ArcanistRepositoryMarkerQuery private $names; private $commitHashes; private $ancestorCommitHashes; + private $remotes; final public function withMarkerTypes(array $types) { $this->markerTypes = array_fuse($types); @@ -19,13 +20,35 @@ abstract class ArcanistRepositoryMarkerQuery return $this; } + final public function withRemotes(array $remotes) { + assert_instances_of($remotes, 'ArcanistRemoteRef'); + $this->remotes = $remotes; + return $this; + } + final public function withIsActive($active) { $this->isActive = $active; return $this; } final public function execute() { - $markers = $this->newRefMarkers(); + $remotes = $this->remotes; + if ($remotes !== null) { + $marker_lists = array(); + foreach ($remotes as $remote) { + $marker_list = $this->newRemoteRefMarkers($remote); + foreach ($marker_list as $marker) { + $marker->attachRemoteRef($remote); + } + $marker_lists[] = $marker_list; + } + $markers = array_mergev($marker_lists); + } else { + $markers = $this->newLocalRefMarkers(); + foreach ($markers as $marker) { + $marker->attachRemoteRef(null); + } + } $api = $this->getRepositoryAPI(); foreach ($markers as $marker) { @@ -65,7 +88,6 @@ abstract class ArcanistRepositoryMarkerQuery } } - return $this->sortMarkers($markers); } @@ -92,4 +114,7 @@ abstract class ArcanistRepositoryMarkerQuery return isset($this->markerTypes[$marker_type]); } + abstract protected function newLocalRefMarkers(); + abstract protected function newRemoteRefMarkers(ArcanistRemoteRef $remote); + } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index 338971c8..0bcbd65d 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -17,12 +17,14 @@ final class ArcanistMercurialLocalState protected function executeSaveLocalState() { $api = $this->getRepositoryAPI(); - // TODO: Fix this. + // TODO: We need to save the position of "." and the current active + // branch, which may be any symbol at all. Both of these can be pulled + // from "hg arc-ls-markers". + } protected function executeRestoreLocalState() { $api = $this->getRepositoryAPI(); - // TODO: Fix this. // TODO: In Mercurial, we may want to discard commits we've created. // $repository_api->execxLocal( diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py index 5a2e52f9..70dad19f 100644 --- a/support/hg/arc-hg.py +++ b/support/hg/arc-hg.py @@ -19,16 +19,16 @@ cmdtable = {} command = registrar.command(cmdtable) @command( - "arc-ls-remote", + "arc-ls-markers", [('', 'output', '', _('file to output refs to'), _('FILE')), ] + cmdutil.remoteopts, _('[--output FILENAME] [SOURCE]')) -def lsremote(ui, repo, source="default", **opts): - """list markers in a remote +def lsmarkers(ui, repo, source=None, **opts): + """list markers - Show the current branch heads and bookmarks in a specified path/URL or the - default pull location. + Show the current branch heads and bookmarks in the local working copy, or + a specified path/URL. Markers are printed to stdout in JSON. @@ -37,14 +37,128 @@ def lsremote(ui, repo, source="default", **opts): Returns 0 if listing the markers succeeds, 1 otherwise. """ + if source is None: + markers = localmarkers(ui, repo) + else: + markers = remotemarkers(ui, repo, source, opts) + + json_opts = { + 'indent': 2, + 'sort_keys': True, + } + + output_file = opts.get('output') + if output_file: + if os.path.exists(output_file): + raise error.Abort(_('File "%s" already exists.' % output_file)) + with open(output_file, 'w+') as f: + json.dump(markers, f, **json_opts) + else: + print json.dumps(markers, output_file, **json_opts) + + return 0 + +def localmarkers(ui, repo): + markers = [] + + active_node = repo['.'].node() + all_heads = set(repo.heads()) + current_name = repo.dirstate.branch() + saw_current = False + saw_active = False + + branch_list = repo.branchmap().iterbranches() + for branch_name, branch_heads, tip_node, is_closed in branch_list: + for head_node in branch_heads: + is_active = (head_node == active_node) + is_tip = (head_node == tip_node) + is_current = (branch_name == current_name) + + if is_current: + saw_current = True + + if is_active: + saw_active = True + + if is_closed: + head_closed = True + else: + head_closed = bool(head_node not in all_heads) + + description = repo[head_node].description() + + markers.append({ + 'type': 'branch', + 'name': branch_name, + 'node': node.hex(head_node), + 'isActive': is_active, + 'isClosed': head_closed, + 'isTip': is_tip, + 'isCurrent': is_current, + 'description': description, + }) + + # If the current branch (selected with "hg branch X") is not reflected in + # the list of heads we selected, add a virtual head for it so callers get + # a complete picture of repository marker state. + + if not saw_current: + markers.append({ + 'type': 'branch', + 'name': current_name, + 'node': None, + 'isActive': False, + 'isClosed': False, + 'isTip': False, + 'isCurrent': True, + 'description': None, + }) + + bookmarks = repo._bookmarks + active_bookmark = repo._activebookmark + + for bookmark_name, bookmark_node in bookmarks.iteritems(): + is_active = (active_bookmark == bookmark_name) + description = repo[bookmark_node].description() + + if is_active: + saw_active = True + + markers.append({ + 'type': 'bookmark', + 'name': bookmark_name, + 'node': node.hex(bookmark_node), + 'isActive': is_active, + 'description': description, + }) + + # If the current working copy state is not the head of a branch and there is + # also no active bookmark, add a virtual marker for it so callers can figure + # out exactly where we are. + + if not saw_active: + markers.append({ + 'type': 'commit', + 'name': None, + 'node': node.hex(active_node), + 'isActive': False, + 'isClosed': False, + 'isTip': False, + 'isCurrent': True, + 'description': repo['.'].description(), + }) + + return markers + +def remotemarkers(ui, repo, source, opts): # Disable status output from fetching a remote. ui.quiet = True + markers = [] + source, branches = hg.parseurl(ui.expandpath(source)) remote = hg.peer(repo, opts, source) - markers = [] - bundle, remotebranches, cleanup = bundlerepo.getremotechanges( ui, repo, @@ -73,18 +187,4 @@ def lsremote(ui, repo, source="default", **opts): 'node': node.hex(remotemarks[mark]), }) - json_opts = { - 'indent': 2, - 'sort_keys': True, - } - - output_file = opts.get('output') - if output_file: - if os.path.exists(output_file): - raise error.Abort(_('File "%s" already exists.' % output_file)) - with open(output_file, 'w+') as f: - json.dump(markers, f, **json_opts) - else: - print json.dumps(markers, output_file, **json_opts) - - return 0 + return markers From 86951ad0678f117ff0fa64199023e60bf5b4f330 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 15:30:51 -0700 Subject: [PATCH 50/82] Use a "branchmap" call to identify remote branches in "arc-hg" Summary: Ref T9948. Ref T13546. To identify remote branch heads -- not just modified heads -- use "branchmap" like "hg outgoing" does. I wasn't able to find any other way to get what we want: for example, with a bundlerepo, "peer.heads()" and "peer.changelog.heads()" include local branches which are not present in the remote. It generally seems difficult (perhaps impossible?) to distinguish between these cases by using "getremotechanges()": - branch X exists at position Y in both the local and remote; - branch X exists at positino Y in the local, but not the remote. In any case, this seems to work properly and //should// do less work than "getremotechanges()" since we don't need to pull as much data over the wire. Test Plan: Ran "hg arc-ls-markers" in various repositories, got what appeared to be a faithful representation of the remote branch and bookmark state. Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21349 --- support/hg/arc-hg.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/support/hg/arc-hg.py b/support/hg/arc-hg.py index 70dad19f..a11cef73 100644 --- a/support/hg/arc-hg.py +++ b/support/hg/arc-hg.py @@ -159,25 +159,20 @@ def remotemarkers(ui, repo, source, opts): source, branches = hg.parseurl(ui.expandpath(source)) remote = hg.peer(repo, opts, source) - bundle, remotebranches, cleanup = bundlerepo.getremotechanges( - ui, - repo, - remote) + with remote.commandexecutor() as e: + branchmap = e.callcommand('branchmap', {}).result() - try: - for n in remotebranches: - ctx = bundle[n] + for branch_name in branchmap: + for branch_node in branchmap[branch_name]: markers.append({ 'type': 'branch', - 'name': ctx.branch(), - 'node': node.hex(ctx.node()), + 'name': branch_name, + 'node': node.hex(branch_node), }) - finally: - cleanup() with remote.commandexecutor() as e: remotemarks = bookmarks.unhexlifybookmarks(e.callcommand('listkeys', { - 'namespace': 'bookmarks', + 'namespace': 'bookmarks', }).result()) for mark in remotemarks: From 50c534b5911a18906ca2b69b8d27d217f750cb4b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 15:51:30 -0700 Subject: [PATCH 51/82] Correct some minor "arc land" workflow issues in Mercurial Summary: Ref T9948. Ref T13546. Clean up some minor behaviors to allow "arc land" to function in the simplest cases again. Also, do a capability test for "prune" rather than just falling back. Test Plan: Ran "arc land " in Mercurial, got changes pushed. Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21350 --- .../engine/ArcanistMercurialLandEngine.php | 21 ++++++++----------- src/repository/api/ArcanistMercurialAPI.php | 4 +++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 25a3fe33..37e79801 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -288,7 +288,7 @@ final class ArcanistMercurialLandEngine $onto_markers[] = $new_bookmark; $new_markers[] = $new_bookmark; } else { - $onto_markers[] = $marker; + $onto_markers[] = head($matches); } } @@ -553,9 +553,6 @@ final class ArcanistMercurialLandEngine pht('Merge target is ambiguous.')); } - $is_bookmark = false; - $is_branch = false; - if ($bookmarks) { if (count($bookmarks) > 1) { throw new Exception( @@ -592,7 +589,7 @@ final class ArcanistMercurialLandEngine $target_marker = $branch; } - if ($is_branch) { + if ($target_marker->isBranch()) { $err = $this->newPassthru( 'pull --branch %s -- %s', $target->getRef(), @@ -728,7 +725,7 @@ final class ArcanistMercurialLandEngine $branch_marker = $this->ontoBranchMarker; if ($branch_marker) { - $api->execxLocal('branch -- %s', $branch_marker); + $api->execxLocal('branch -- %s', $branch_marker->getName()); } try { @@ -892,7 +889,7 @@ final class ArcanistMercurialLandEngine return; } - $strip = array(); + $revs = array(); // We've rebased all descendants already, so we can safely delete all // of these commits. @@ -904,10 +901,10 @@ final class ArcanistMercurialLandEngine $min_commit = head($commits)->getHash(); $max_commit = last($commits)->getHash(); - $strip[] = hgsprintf('%s::%s', $min_commit, $max_commit); + $revs[] = hgsprintf('%s::%s', $min_commit, $max_commit); } - $rev_set = '('.implode(') or (', $strip).')'; + $rev_set = '('.implode(') or (', $revs).')'; // See PHI45. If we have "hg evolve", get rid of old commits using // "hg prune" instead of "hg strip". @@ -916,11 +913,11 @@ final class ArcanistMercurialLandEngine // removes the obsolescence marker and revives the predecessor. This is // not desirable: we want to destroy all predecessors of these commits. - try { + if ($api->getMercurialFeature('evolve')) { $api->execxLocal( - '--config extensions.evolve= prune --rev %s', + 'prune --rev %s', $rev_set); - } catch (CommandException $ex) { + } else { $api->execxLocal( '--config extensions.strip= strip --rev %s', $rev_set); diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 4756d068..e606f6a1 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -980,7 +980,9 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { switch ($feature) { case 'shelve': return $this->execFutureLocal( - '--config extensions.shelve= shelve --help'); + '--config extensions.shelve= shelve --help --'); + case 'evolve': + return $this->execFutureLocal('prune --help --'); default: throw new Exception( pht( From 92f860ae9b2fc0b2c50061f1f6bba6bf04af5f8c Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 16:32:46 -0700 Subject: [PATCH 52/82] Improve "--hold", save/restore state, bookmark creation, and some warnings for "arc land" in Mercurial Summary: Ref T13546. Ref T9948. - Make "--hold" show the same set of commands to manually push that the normal workflow would use. - Make save/restore state work. - Make bookmark creation prompt for confirmation. - Improve / provide some additional warnings and help text. Test Plan: Ran various increasingly complex "arc land" workflows, e.g. "arc land --hold --onto fauxmark1 --onto fauxmark2 --into default . --revision 118 --trace" Maniphest Tasks: T13546, T9948 Differential Revision: https://secure.phabricator.com/D21351 --- .../engine/ArcanistMercurialLandEngine.php | 170 ++++++++++++++---- .../state/ArcanistGitLocalState.php | 5 - .../state/ArcanistMercurialLocalState.php | 55 ++++-- .../state/ArcanistRepositoryLocalState.php | 4 + src/workflow/ArcanistLandWorkflow.php | 5 + 5 files changed, 176 insertions(+), 63 deletions(-) diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 37e79801..eeba0122 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -89,7 +89,7 @@ final class ArcanistMercurialLandEngine $markers = mgroup($markers, 'getName'); - foreach ($unresolved as $key => $symbol) { + foreach ($unresolved as $key => $symbol) { $raw_symbol = $symbol->getSymbol(); $named_markers = idx($markers, $raw_symbol); @@ -98,12 +98,25 @@ final class ArcanistMercurialLandEngine } if (count($named_markers) > 1) { - throw new PhutilArgumentUsageException( + echo tsprintf( + "\n%!\n%W\n\n", + pht('AMBIGUOUS SYMBOL'), pht( 'Symbol "%s" is ambiguous: it matches multiple markers '. '(of type "%s"). Use an unambiguous identifier.', $raw_symbol, $marker_type)); + + foreach ($named_markers as $named_marker) { + echo tsprintf('%s', $named_marker->newDisplayRef()); + } + + echo tsprintf("\n"); + + throw new PhutilArgumentUsageException( + pht( + 'Symbol "%s" is ambiguous.', + $symbol)); } $marker = head($named_markers); @@ -300,16 +313,58 @@ final class ArcanistMercurialLandEngine $branch_count = count($branches); if ($branch_count > 1) { + echo tsprintf( + "\n%!\n%W\n\n%W\n\n%W\n\n", + pht('MULTIPLE "ONTO" BRANCHES'), + pht( + 'You have selected multiple branches to push changes onto. '. + 'Pushing to multiple branches is not supported by "arc land" '. + 'in Mercurial: Mercurial commits may only belong to one '. + 'branch, so this operation can not be executed atomically.'), + pht( + 'You may land one branches and any number of bookmarks in a '. + 'single operation.'), + pht('These branches were selected:')); + + foreach ($branches as $branch) { + echo tsprintf('%s', $branch->newDisplayRef()); + } + + echo tsprintf("\n"); + throw new PhutilArgumentUsageException( pht( - 'TODO: You can not push onto multiple branches in Mercurial.')); + 'Landing onto multiple branches at once is not supported in '. + 'Mercurial.')); } else if ($branch_count) { $this->ontoBranchMarker = head($branches); } } if ($new_markers) { - // TODO: If we're creating bookmarks, ask the user to confirm. + echo tsprintf( + "\n%!\n%W\n\n", + pht('CREATE %s BOOKMARK(S)', phutil_count($new_markers)), + pht( + 'These %s symbol(s) do not exist in the remote. They will be created '. + 'as new bookmarks:', + phutil_count($new_markers))); + + + foreach ($new_markers as $new_marker) { + echo tsprintf('%s', $new_marker->newDisplayRef()); + } + + echo tsprintf("\n"); + + $query = pht( + 'Create %s new remote bookmark(s)?', + phutil_count($new_markers)); + + $this->getWorkflow() + ->getPrompt('arc.land.create') + ->setQuery($query) + ->execute(); } $this->ontoMarkers = $onto_markers; @@ -761,6 +816,30 @@ final class ArcanistMercurialLandEngine protected function pushChange($into_commit) { $api = $this->getRepositoryAPI(); + list($head, $body, $tail) = $this->newPushCommands($into_commit); + + foreach ($head as $command) { + $api->execxLocal('%Ls', $command); + } + + try { + foreach ($body as $command) { + $this->newPasthru('%Ls', $command); + } + } finally { + foreach ($tail as $command) { + $api->execxLocal('%Ls', $command); + } + } + } + + private function newPushCommands($into_commit) { + $api = $this->getRepositoryAPI(); + + $head_commands = array(); + $body_commands = array(); + $tail_commands = array(); + $bookmarks = array(); foreach ($this->ontoMarkers as $onto_marker) { if (!$onto_marker->isBookmark()) { @@ -776,8 +855,8 @@ final class ArcanistMercurialLandEngine $restore = array(); if ($bookmarks) { $markers = $api->newMarkerRefQuery() - ->withNames(array(mpull($bookmarks, 'getName'))) - ->withTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) + ->withNames(mpull($bookmarks, 'getName')) + ->withMarkerTypes(array(ArcanistMarkerRef::TYPE_BOOKMARK)) ->execute(); $markers = mpull($markers, 'getCommitHash', 'getName'); @@ -791,6 +870,15 @@ final class ArcanistMercurialLandEngine continue; } + $head_commands[] = array( + 'bookmark', + '--force', + '--rev', + hgsprintf('%s', $this->getDisplayHash($new_position)), + '--', + $bookmark_name, + ); + $api->execxLocal( 'bookmark --force --rev %s -- %s', hgsprintf('%s', $new_position), @@ -800,7 +888,7 @@ final class ArcanistMercurialLandEngine } } - // Now, do the actual push. + // Now, prepare the actual push. $argv = array(); $argv[] = 'push'; @@ -821,23 +909,33 @@ final class ArcanistMercurialLandEngine $argv[] = '--'; $argv[] = $this->getOntoRemote(); - try { - $this->newPassthru('%Ls', $argv); - } finally { - foreach ($restore as $bookmark_name => $old_position) { - if ($old_position === null) { - $api->execxLocal( - 'bookmark --delete -- %s', - $bookmark_name); - } else { - $api->execxLocal( - 'bookmark --force --rev %s -- %s', - hgsprintf('%s', $old_position), - $bookmark_name); - } + $body_commands[] = $argv; + + // Finally, restore the bookmarks. + + foreach ($restore as $bookmark_name => $old_position) { + $tail = array(); + $tail[] = 'bookmark'; + + if ($old_position === null) { + $tail[] = '--delete'; + } else { + $tail[] = '--force'; + $tail[] = '--rev'; + $tail[] = hgsprintf('%s', $this->getDisplayHash($old_position)); } + + $tail[] = '--'; + $tail[] = $bookmark_name; + + $tail_commands[] = $tail; } + return array( + $head_commands, + $body_commands, + $tail_commands, + ); } protected function cascadeState(ArcanistLandCommitSet $set, $into_commit) { @@ -940,12 +1038,8 @@ final class ArcanistMercurialLandEngine $message = pht( 'Holding changes locally, they have not been pushed.'); - // TODO: This is only vaguely correct. - - $push_command = csprintf( - '$ hg push --rev %s -- %s', - hgsprintf('%s', $this->getDisplayHash($into_commit)), - $this->getOntoRemote()); + list($head, $body, $tail) = $this->newPushCommands($into_commit); + $commands = array_merge($head, $body, $tail); echo tsprintf( "\n%!\n%s\n\n", @@ -953,9 +1047,15 @@ final class ArcanistMercurialLandEngine $message); echo tsprintf( - "%s\n\n **%s**\n\n", - pht('To push changes manually, run this command:'), - $push_command); + "%s\n\n", + pht('To push changes manually, run these %s command(s):', + phutil_count($commands))); + + foreach ($commands as $command) { + echo tsprintf('%>', csprintf('hg %Ls', $command)); + } + + echo tsprintf("\n"); $restore_commands = $local_state->getRestoreCommandsForDisplay(); if ($restore_commands) { @@ -967,7 +1067,7 @@ final class ArcanistMercurialLandEngine phutil_count($restore_commands))); foreach ($restore_commands as $restore_command) { - echo tsprintf(" **%s**\n", $restore_command); + echo tsprintf('%>', $restore_command); } echo tsprintf("\n"); @@ -980,12 +1080,4 @@ final class ArcanistMercurialLandEngine 'in the same state as before.')); } - - private function newRemoteMarkers($remote) { - // See T9948. If the user specified "--into X" or "--onto X", we don't know - // if it's a branch, a bookmark, or a symbol which doesn't exist yet. - - - } - } diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php index 30d29d3a..61a70e0d 100644 --- a/src/repository/state/ArcanistGitLocalState.php +++ b/src/repository/state/ArcanistGitLocalState.php @@ -47,7 +47,6 @@ final class ArcanistGitLocalState protected function executeRestoreLocalState() { $api = $this->getRepositoryAPI(); - $log = $this->getWorkflow()->getLogEngine(); $ref = $this->localRef; @@ -163,8 +162,4 @@ final class ArcanistGitLocalState return substr($stash_ref, 0, 12); } - private function getDisplayHash($hash) { - return substr($hash, 0, 12); - } - } diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index 0bcbd65d..ea24e070 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -4,37 +4,45 @@ final class ArcanistMercurialLocalState extends ArcanistRepositoryLocalState { private $localCommit; - private $localRef; - - public function getLocalRef() { - return $this->localRef; - } - - public function getLocalPath() { - return $this->localPath; - } + private $localBranch; protected function executeSaveLocalState() { $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); - // TODO: We need to save the position of "." and the current active - // branch, which may be any symbol at all. Both of these can be pulled - // from "hg arc-ls-markers". + // TODO: Both of these can be pulled from "hg arc-ls-markers" more + // efficiently. + $this->localCommit = $api->getCanonicalRevisionName('.'); + + list($branch) = $api->execxLocal('branch'); + $this->localBranch = trim($branch); + + $log->writeTrace( + pht('SAVE STATE'), + pht( + 'Saving local state (at "%s" on branch "%s").', + $this->getDisplayHash($this->localCommit), + $this->localBranch)); } protected function executeRestoreLocalState() { $api = $this->getRepositoryAPI(); + $log = $this->getWorkflow()->getLogEngine(); - // TODO: In Mercurial, we may want to discard commits we've created. - // $repository_api->execxLocal( - // '--config extensions.mq= strip %s', - // $this->onto); + $log->writeStatus( + pht('LOAD STATE'), + pht( + 'Restoring local state (at "%s" on branch "%s").', + $this->getDisplayHash($this->localCommit), + $this->localBranch)); + $api->execxLocal('update -- %s', $this->localCommit); + $api->execxLocal('branch --force -- %s', $this->localBranch); } protected function executeDiscardLocalState() { - // TODO: Fix this. + return; } protected function canStashChanges() { @@ -51,8 +59,17 @@ final class ArcanistMercurialLocalState } protected function newRestoreCommandsForDisplay() { - // TODO: Provide this. - return array(); + $commands = array(); + + $commands[] = csprintf( + 'hg update -- %s', + $this->getDisplayHash($this->localCommit)); + + $commands[] = csprintf( + 'hg branch --force -- %s', + $this->localBranch); + + return $commands; } protected function saveStash() { diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index 1526d50d..42138456 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -256,4 +256,8 @@ abstract class ArcanistRepositoryLocalState echo tsprintf("\n"); } + final protected function getDisplayHash($hash) { + return substr($hash, 0, 12); + } + } diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 8a6f9f17..72f1ae99 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -280,6 +280,11 @@ EOTEXT ->setDescription( pht( 'Confirms that revisions with ongoing builds should land.')), + $this->newPrompt('arc.land.create') + ->setDescription( + pht( + 'Confirms that new branches or bookmarks should be created '. + 'in the remote.')), ); } From 1e09a0ee7e91842c18408a85169f8e29946a8f0c Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Jun 2020 12:05:31 -0700 Subject: [PATCH 53/82] When a linter raises a message at a nonexistent line, don't fatal during rendering Summary: See PHI1782. If a linter raises a message at a line which does not exist in the file, render a confused warning rather than fataling. This is a long-existing issue which was exacerbated by D21044. Test Plan: Modified a linter to raise issues at line 99999. Before change: fatal in console rendering. After change: reasonable rendering. Differential Revision: https://secure.phabricator.com/D21357 --- .../renderer/ArcanistConsoleLintRenderer.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lint/renderer/ArcanistConsoleLintRenderer.php b/src/lint/renderer/ArcanistConsoleLintRenderer.php index 6f6daf33..31169084 100644 --- a/src/lint/renderer/ArcanistConsoleLintRenderer.php +++ b/src/lint/renderer/ArcanistConsoleLintRenderer.php @@ -122,6 +122,41 @@ final class ArcanistConsoleLintRenderer extends ArcanistLintRenderer { $old_impact = substr_count($original, "\n") + 1; $start = $line; + // See PHI1782. If a linter raises a message at a line that does not + // exist, just render a warning message. + + // Linters are permitted to raise a warning at the very end of a file. + // For example, if a file is 13 lines long, it is valid to raise a message + // on line 14 as long as the character position is 1 or unspecified and + // there is no "original" text. + + $max_old = count($old_lines); + + $invalid_position = false; + if ($start > ($max_old + 1)) { + $invalid_position = true; + } else if ($start > $max_old) { + if (strlen($original)) { + $invalid_position = true; + } else if ($char !== null && $char !== 1) { + $invalid_position = true; + } + } + + if ($invalid_position) { + $warning = $this->renderLine( + $start, + pht( + '(This message was raised at line %s, but the file only has '. + '%s line(s).)', + new PhutilNumber($start), + new PhutilNumber($max_old)), + false, + '?'); + + return $warning."\n\n"; + } + if ($message->isPatchable()) { $patch_offset = $line_map[$line] + ($char - 1); From 33dfa859d8e68a66559c15c78cea4da19204653b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Jun 2020 14:16:36 -0700 Subject: [PATCH 54/82] On Windows, don't try to set "stdin" nonblocking, as it does not work Summary: See . See also . Note that you can't ^C during a prompt (or at any other time) in Windows currently, see T13549. Test Plan: On Windows, hit a prompt in "arc land", then answered it successfully. Differential Revision: https://secure.phabricator.com/D21358 --- src/toolset/ArcanistPrompt.php | 82 +++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/toolset/ArcanistPrompt.php b/src/toolset/ArcanistPrompt.php index 24523596..caff32c6 100644 --- a/src/toolset/ArcanistPrompt.php +++ b/src/toolset/ArcanistPrompt.php @@ -86,18 +86,22 @@ final class ArcanistPrompt 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.')); + // 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. + + // On Windows, we skip this because stdin can not be made nonblocking. + + if (!phutil_is_windows()) { + $ok = stream_set_blocking($stdin, false); + if (!$ok) { + throw new Exception(pht('Unable to set stdin nonblocking.')); + } } echo "\n"; @@ -117,44 +121,48 @@ final class ArcanistPrompt $query, $options); - while (true) { - $is_saved = false; + $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. - } - - $response = ''; + if (phutil_is_windows()) { + $response = fgets($stdin); + } else { while (true) { - $bytes = fread($stdin, 8192); - if ($bytes === false) { - throw new Exception( - pht('fread() from stdin failed with an error.')); + $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; + } - if (!strlen($response)) { - continue; + break; } - - break; } $response = trim($response); From 63f2e667b9a424a7a273287d100d7b10add12fae Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 10 Jun 2020 17:15:42 -0700 Subject: [PATCH 55/82] Update "arc land" display of build failures, and rename "DisplayRef" to "RefView" Summary: Ref T13546. Show ongoing and failed builds more clearly in "arc land" output. Also rename "DisplayRef" (which is not a "Ref") to "RefView" with the goal of improving clarity, and let callers "build...()" it so they can add more status, etc., information. Get rid of "[DisplayRef|RefView]Interface". In theory, future refs (say, in Phabricator) might not do anything here, but every Ref just ends up implementing it. This could perhaps be subclassed more narrowly in the future if necessary. Test Plan: Ran "arc land", grepped for various symbols. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21352 --- src/__phutil_library_map__.php | 56 ++++----------- src/land/engine/ArcanistLandEngine.php | 49 ++++++------- .../engine/ArcanistMercurialLandEngine.php | 6 +- src/ref/ArcanistDisplayRefInterface.php | 8 --- src/ref/ArcanistRef.php | 13 +++- ...nistDisplayRef.php => ArcanistRefView.php} | 71 +++++++++++++++---- src/ref/build/ArcanistBuildRef.php | 16 ++--- src/ref/buildable/ArcanistBuildableRef.php | 14 ++-- src/ref/buildplan/ArcanistBuildPlanRef.php | 20 +++--- src/ref/file/ArcanistFileRef.php | 14 ++-- src/ref/paste/ArcanistPasteRef.php | 14 ++-- src/ref/revision/ArcanistRevisionRef.php | 14 ++-- src/ref/task/ArcanistTaskRef.php | 14 ++-- src/ref/user/ArcanistUserRef.php | 16 ++--- src/repository/marker/ArcanistMarkerRef.php | 28 ++++---- src/work/ArcanistWorkEngine.php | 4 +- src/workflow/ArcanistAmendWorkflow.php | 6 +- src/workflow/ArcanistPasteWorkflow.php | 2 +- src/workflow/ArcanistUploadWorkflow.php | 2 +- 19 files changed, 172 insertions(+), 195 deletions(-) delete mode 100644 src/ref/ArcanistDisplayRefInterface.php rename src/ref/{ArcanistDisplayRef.php => ArcanistRefView.php} (58%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9ddfbbe0..7bf922b9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -168,8 +168,6 @@ phutil_register_library_map(array( 'ArcanistDifferentialDependencyGraph' => 'differential/ArcanistDifferentialDependencyGraph.php', 'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php', 'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php', - 'ArcanistDisplayRef' => 'ref/ArcanistDisplayRef.php', - 'ArcanistDisplayRefInterface' => 'ref/ArcanistDisplayRefInterface.php', 'ArcanistDoubleQuoteXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistDoubleQuoteXHPASTLinterRule.php', 'ArcanistDoubleQuoteXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistDoubleQuoteXHPASTLinterRuleTestCase.php', 'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php', @@ -415,6 +413,7 @@ phutil_register_library_map(array( 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php', 'ArcanistRef' => 'ref/ArcanistRef.php', 'ArcanistRefInspector' => 'inspector/ArcanistRefInspector.php', + 'ArcanistRefView' => 'ref/ArcanistRefView.php', 'ArcanistRemoteRef' => 'repository/remote/ArcanistRemoteRef.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', @@ -1084,21 +1083,12 @@ phutil_register_library_map(array( 'ArcanistBrowseURIRef' => 'ArcanistRef', 'ArcanistBrowseWorkflow' => 'ArcanistArcWorkflow', 'ArcanistBuildBuildplanHardpointQuery' => 'ArcanistRuntimeHardpointQuery', - 'ArcanistBuildPlanRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistBuildPlanRef' => 'ArcanistRef', 'ArcanistBuildPlanSymbolRef' => 'ArcanistSimpleSymbolRef', - 'ArcanistBuildRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistBuildRef' => 'ArcanistRef', 'ArcanistBuildSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBuildableBuildsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', - 'ArcanistBuildableRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistBuildableRef' => 'ArcanistRef', 'ArcanistBuildableSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistBundle' => 'Phobject', 'ArcanistBundleTestCase' => 'PhutilTestCase', @@ -1196,10 +1186,6 @@ phutil_register_library_map(array( 'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph', 'ArcanistDifferentialRevisionHash' => 'Phobject', 'ArcanistDifferentialRevisionStatus' => 'Phobject', - 'ArcanistDisplayRef' => array( - 'Phobject', - 'ArcanistTerminalStringInterface', - ), 'ArcanistDoubleQuoteXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistDoubleQuoteXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistDownloadWorkflow' => 'ArcanistArcWorkflow', @@ -1224,10 +1210,7 @@ phutil_register_library_map(array( 'ArcanistExtractUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistFileConfigurationSource' => 'ArcanistFilesystemConfigurationSource', 'ArcanistFileDataRef' => 'Phobject', - 'ArcanistFileRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistFileRef' => 'ArcanistRef', 'ArcanistFileSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistFileUploader' => 'Phobject', 'ArcanistFilenameLinter' => 'ArcanistLinter', @@ -1357,10 +1340,7 @@ phutil_register_library_map(array( 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistMarkerRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistMarkerRef' => 'ArcanistRef', 'ArcanistMarkersWorkflow' => 'ArcanistArcWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', @@ -1420,10 +1400,7 @@ phutil_register_library_map(array( 'ArcanistParenthesesSpacingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistParseStrUseXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistParseStrUseXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistPasteRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistPasteRef' => 'ArcanistRef', 'ArcanistPasteSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistPasteWorkflow' => 'ArcanistArcWorkflow', 'ArcanistPatchWorkflow' => 'ArcanistWorkflow', @@ -1454,6 +1431,10 @@ phutil_register_library_map(array( 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistRef' => 'ArcanistHardpointObject', 'ArcanistRefInspector' => 'Phobject', + 'ArcanistRefView' => array( + 'Phobject', + 'ArcanistTerminalStringInterface', + ), 'ArcanistRemoteRef' => 'ArcanistRef', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', @@ -1473,10 +1454,7 @@ phutil_register_library_map(array( 'ArcanistRevisionBuildableHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionCommitMessageHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRevisionParentRevisionsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', - 'ArcanistRevisionRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistRevisionRef' => 'ArcanistRef', 'ArcanistRevisionRefSource' => 'Phobject', 'ArcanistRevisionSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistRuboCopLinter' => 'ArcanistExternalLinter', @@ -1517,10 +1495,7 @@ phutil_register_library_map(array( 'ArcanistSymbolRef' => 'ArcanistRef', 'ArcanistSyntaxErrorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistSystemConfigurationSource' => 'ArcanistFilesystemConfigurationSource', - 'ArcanistTaskRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistTaskRef' => 'ArcanistRef', 'ArcanistTaskSymbolRef' => 'ArcanistSimpleSymbolRef', 'ArcanistTasksWorkflow' => 'ArcanistWorkflow', 'ArcanistTautologicalExpressionXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', @@ -1570,10 +1545,7 @@ phutil_register_library_map(array( 'ArcanistUselessOverridingMethodXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistUserAbortException' => 'ArcanistUsageException', 'ArcanistUserConfigurationSource' => 'ArcanistFilesystemConfigurationSource', - 'ArcanistUserRef' => array( - 'ArcanistRef', - 'ArcanistDisplayRefInterface', - ), + 'ArcanistUserRef' => 'ArcanistRef', 'ArcanistUserSymbolHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistUserSymbolRef' => 'ArcanistSymbolRef', 'ArcanistUserSymbolRefInspector' => 'ArcanistRefInspector', diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index 29365a9d..d02eec5a 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -305,7 +305,7 @@ abstract class ArcanistLandEngine $viewer->getMonogram())); foreach ($unauthored as $revision_ref) { - $display_ref = $revision_ref->newDisplayRef(); + $display_ref = $revision_ref->newRefView(); $author_ref = $revision_ref->getAuthorRef(); if ($author_ref) { @@ -373,7 +373,7 @@ abstract class ArcanistLandEngine phutil_count($planned))); foreach ($planned as $revision_ref) { - echo tsprintf('%s', $revision_ref->newDisplayRef()); + echo tsprintf('%s', $revision_ref->newRefView()); } $query = pht( @@ -402,7 +402,7 @@ abstract class ArcanistLandEngine $example_ref->getStatusDisplayName())); foreach ($published as $revision_ref) { - echo tsprintf('%s', $revision_ref->newDisplayRef()); + echo tsprintf('%s', $revision_ref->newRefView()); } $query = pht( @@ -429,7 +429,7 @@ abstract class ArcanistLandEngine phutil_count($not_accepted))); foreach ($not_accepted as $revision_ref) { - $display_ref = $revision_ref->newDisplayRef(); + $display_ref = $revision_ref->newRefView(); $display_ref->appendLine( pht( 'Status: %s', @@ -493,7 +493,7 @@ abstract class ArcanistLandEngine foreach ($open_parents as $parent_phid => $spec) { $parent_ref = $spec['ref']; - $display_ref = $parent_ref->newDisplayRef(); + $display_ref = $parent_ref->newRefView(); $display_ref->appendLine( pht( @@ -686,38 +686,31 @@ abstract class ArcanistLandEngine $prompt_key = 'arc.land.ongoing-builds'; } + $workflow = $this->getWorkflow(); + echo tsprintf("\n"); foreach ($build_map as $build_item) { $revision_ref = $build_item['revisionRef']; + $revision_view = $revision_ref->newRefView(); - echo tsprintf('%s', $revision_ref->newDisplayRef()); + $buildable_ref = $revision_ref->getBuildableRef(); + $buildable_view = $buildable_ref->newRefView(); + + $raw_uri = $buildable_ref->getURI(); + $raw_uri = $workflow->getAbsoluteURI($raw_uri); + $buildable_view->setURI($raw_uri); + + $revision_view->addChild($buildable_view); foreach ($build_item['buildRefs'] as $build_ref) { - echo tsprintf('%s', $build_ref->newDisplayRef()); + $build_view = $build_ref->newRefView(); + $buildable_view->addChild($build_view); } + echo tsprintf('%s', $revision_view); echo tsprintf("\n"); } - echo tsprintf( - "\n%s\n", - pht('You can review build details here:')); - - // TODO: Only show buildables with problem builds. - - $workflow = $this->getWorkflow(); - - foreach ($buildable_refs as $buildable) { - $display_ref = $buildable->newDisplayRef(); - - $raw_uri = $buildable->getURI(); - $raw_uri = $workflow->getAbsoluteURI($raw_uri); - - $display_ref->setURI($raw_uri); - - echo tsprintf('%s', $display_ref); - } - $this->getWorkflow() ->getPrompt($prompt_key) ->setQuery($query) @@ -777,7 +770,7 @@ abstract class ArcanistLandEngine echo tsprintf( "\n%s", - $revision_ref->newDisplayRef()); + $revision_ref->newRefView()); foreach ($set->getCommits() as $commit) { $is_implicit = $commit->getIsImplicitCommit(); @@ -969,7 +962,7 @@ abstract class ArcanistLandEngine foreach ($revision_refs as $revision_ref) { echo tsprintf( '%s', - $revision_ref->newDisplayRef()); + $revision_ref->newRefView()); } echo tsprintf("\n"); diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index eeba0122..25b49929 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -108,7 +108,7 @@ final class ArcanistMercurialLandEngine $marker_type)); foreach ($named_markers as $named_marker) { - echo tsprintf('%s', $named_marker->newDisplayRef()); + echo tsprintf('%s', $named_marker->newRefView()); } echo tsprintf("\n"); @@ -327,7 +327,7 @@ final class ArcanistMercurialLandEngine pht('These branches were selected:')); foreach ($branches as $branch) { - echo tsprintf('%s', $branch->newDisplayRef()); + echo tsprintf('%s', $branch->newRefView()); } echo tsprintf("\n"); @@ -352,7 +352,7 @@ final class ArcanistMercurialLandEngine foreach ($new_markers as $new_marker) { - echo tsprintf('%s', $new_marker->newDisplayRef()); + echo tsprintf('%s', $new_marker->newRefView()); } echo tsprintf("\n"); diff --git a/src/ref/ArcanistDisplayRefInterface.php b/src/ref/ArcanistDisplayRefInterface.php deleted file mode 100644 index 550763ec..00000000 --- a/src/ref/ArcanistDisplayRefInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -setRef($this); + + $this->buildRefView($ref_view); + + return $ref_view; } + + protected function buildRefView(ArcanistRefView $view) { + return null; + } + } diff --git a/src/ref/ArcanistDisplayRef.php b/src/ref/ArcanistRefView.php similarity index 58% rename from src/ref/ArcanistDisplayRef.php rename to src/ref/ArcanistRefView.php index 631b65b4..bb166aca 100644 --- a/src/ref/ArcanistDisplayRef.php +++ b/src/ref/ArcanistRefView.php @@ -1,13 +1,16 @@ ref = $ref; @@ -18,6 +21,24 @@ final class ArcanistDisplayRef return $this->ref; } + public function setObjectName($object_name) { + $this->objectName = $object_name; + return $this; + } + + public function getObjectName() { + return $this->objectName; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + public function setURI($uri) { $this->uri = $uri; return $this; @@ -27,21 +48,29 @@ final class ArcanistDisplayRef return $this->uri; } + public function addChild(ArcanistRefView $view) { + $this->children[] = $view; + return $this; + } + + private function getChildren() { + return $this->children; + } + public function appendLine($line) { $this->lines[] = $line; return $this; } public function newTerminalString() { + return $this->newLines(0); + } + + private function newLines($indent) { $ref = $this->getRef(); - if ($ref instanceof ArcanistDisplayRefInterface) { - $object_name = $ref->getDisplayRefObjectName(); - $title = $ref->getDisplayRefTitle(); - } else { - $object_name = null; - $title = $ref->getRefDisplayName(); - } + $object_name = $this->getObjectName(); + $title = $this->getTitle(); if ($object_name !== null) { $reserve_width = phutil_utf8_console_strlen($object_name) + 1; @@ -49,10 +78,18 @@ final class ArcanistDisplayRef $reserve_width = 0; } + if ($indent) { + $indent_text = str_repeat(' ', $indent); + } else { + $indent_text = ''; + } + $indent_width = strlen($indent_text); + $marker_width = 6; $display_width = phutil_console_get_terminal_width(); $usable_width = ($display_width - $marker_width - $reserve_width); + $usable_width = ($usable_width - $indent_width); // If the terminal is extremely narrow, don't degrade so much that the // output is completely unusable. @@ -75,22 +112,32 @@ final class ArcanistDisplayRef $display_text = $title; } - $ref = $this->getRef(); $output = array(); $output[] = tsprintf( - "** * ** %s\n", + "** * ** %s%s\n", + $indent_text, $display_text); $uri = $this->getURI(); if ($uri !== null) { $output[] = tsprintf( - "** :// ** __%s__\n", + "** :// ** %s__%s__\n", + $indent_text, $uri); } foreach ($this->lines as $line) { - $output[] = tsprintf(" %s\n", $line); + $output[] = tsprintf( + " %s%s\n", + $indent_text, + $line); + } + + foreach ($this->getChildren() as $child) { + foreach ($child->newLines($indent + 1) as $line) { + $output[] = $line; + } } return $output; diff --git a/src/ref/build/ArcanistBuildRef.php b/src/ref/build/ArcanistBuildRef.php index f2ffcd90..7b1fe227 100644 --- a/src/ref/build/ArcanistBuildRef.php +++ b/src/ref/build/ArcanistBuildRef.php @@ -1,9 +1,7 @@ getDisplayRefObjectName(); + return pht('Build %d', $this->getID()); } public static function newFromConduit(array $parameters) { @@ -37,12 +35,10 @@ final class ArcanistBuildRef return idxv($this->parameters, array('fields', 'name')); } - public function getDisplayRefObjectName() { - return pht('Build %d', $this->getID()); - } - - public function getDisplayRefTitle() { - return $this->getName(); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getRefDisplayName()) + ->setTitle($this->getName()); } public function getBuildPlanRef() { diff --git a/src/ref/buildable/ArcanistBuildableRef.php b/src/ref/buildable/ArcanistBuildableRef.php index bf19ebd1..b91df702 100644 --- a/src/ref/buildable/ArcanistBuildableRef.php +++ b/src/ref/buildable/ArcanistBuildableRef.php @@ -1,9 +1,7 @@ getID(); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { - return pht('Buildable %d', $this->getID()); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getMonogram()) + ->setTitle($this->getRefDisplayName()); } public function getBuildRefs() { diff --git a/src/ref/buildplan/ArcanistBuildPlanRef.php b/src/ref/buildplan/ArcanistBuildPlanRef.php index 37570513..ea0ad609 100644 --- a/src/ref/buildplan/ArcanistBuildPlanRef.php +++ b/src/ref/buildplan/ArcanistBuildPlanRef.php @@ -1,14 +1,12 @@ getDisplayRefObjectName(); + return pht('Build Plan %d', $this->getID()); } public static function newFromConduit(array $parameters) { @@ -29,14 +27,6 @@ final class ArcanistBuildPlanRef return idxv($this->parameters, array('fields', 'name')); } - public function getDisplayRefObjectName() { - return pht('Build Plan %d', $this->getID()); - } - - public function getDisplayRefTitle() { - return $this->getName(); - } - public function getBehavior($behavior_key, $default = null) { return idxv( $this->parameters, @@ -44,4 +34,10 @@ final class ArcanistBuildPlanRef $default); } + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getRefDisplayName()) + ->setTitle($this->getName()); + } + } diff --git a/src/ref/file/ArcanistFileRef.php b/src/ref/file/ArcanistFileRef.php index 37fcf520..615987e9 100644 --- a/src/ref/file/ArcanistFileRef.php +++ b/src/ref/file/ArcanistFileRef.php @@ -1,9 +1,7 @@ getID(); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { - return $this->getName(); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getMonogram()) + ->setTitle($this->getName()); } } diff --git a/src/ref/paste/ArcanistPasteRef.php b/src/ref/paste/ArcanistPasteRef.php index 2c4bb58d..bfeaa77b 100644 --- a/src/ref/paste/ArcanistPasteRef.php +++ b/src/ref/paste/ArcanistPasteRef.php @@ -1,9 +1,7 @@ getID(); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { - return $this->getTitle(); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getMonogram()) + ->setTitle($this->getName()); } } diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index c507baa4..8e73dec2 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -1,9 +1,7 @@ getHardpoint(self::HARDPOINT_BUILDABLEREF); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { - return $this->getName(); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getMonogram()) + ->setTitle($this->getTitle()); } } diff --git a/src/ref/task/ArcanistTaskRef.php b/src/ref/task/ArcanistTaskRef.php index d446b8df..ac918f77 100644 --- a/src/ref/task/ArcanistTaskRef.php +++ b/src/ref/task/ArcanistTaskRef.php @@ -1,9 +1,7 @@ getID(); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { - return $this->getName(); + protected function buildRefView(ArcanistRefView $view) { + $view + ->setObjectName($this->getMonogram()) + ->setTitle($this->getName()); } } diff --git a/src/ref/user/ArcanistUserRef.php b/src/ref/user/ArcanistUserRef.php index 0d27ca6b..d430682d 100644 --- a/src/ref/user/ArcanistUserRef.php +++ b/src/ref/user/ArcanistUserRef.php @@ -1,9 +1,7 @@ parameters, array('fields', 'realName')); } - public function getDisplayRefObjectName() { - return $this->getMonogram(); - } - - public function getDisplayRefTitle() { + protected function buildRefView(ArcanistRefView $view) { $real_name = $this->getRealName(); - if (strlen($real_name)) { $real_name = sprintf('(%s)', $real_name); } - return $real_name; + $view + ->setObjectName($this->getMonogram()) + ->setTitle($real_name); } + } diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index 77112b1e..a945d572 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -1,9 +1,7 @@ getDisplayRefObjectName(); - } - - public function getDisplayRefObjectName() { switch ($this->getMarkerType()) { case self::TYPE_BRANCH: return pht('Branch "%s"', $this->getName()); @@ -38,13 +32,6 @@ final class ArcanistMarkerRef } } - public function getDisplayRefTitle() { - return pht( - '%s %s', - $this->getDisplayHash(), - $this->getSummary()); - } - protected function newHardpoints() { return array( $this->newHardpoint(self::HARDPOINT_COMMITREF), @@ -98,8 +85,6 @@ final class ArcanistMarkerRef return $this->displayHash; } - - public function setCommitHash($commit_hash) { $this->commitHash = $commit_hash; return $this; @@ -177,4 +162,15 @@ final class ArcanistMarkerRef return $this->getHardpoint(self::HARDPOINT_REMOTEREF); } + protected function buildRefView(ArcanistRefView $view) { + $title = pht( + '%s %s', + $this->getDisplayHash(), + $this->getSummary()); + + $view + ->setObjectName($this->getRefDisplayName()) + ->setTitle($title); + } + } diff --git a/src/work/ArcanistWorkEngine.php b/src/work/ArcanistWorkEngine.php index 0d8a01ed..531270b8 100644 --- a/src/work/ArcanistWorkEngine.php +++ b/src/work/ArcanistWorkEngine.php @@ -163,7 +163,7 @@ abstract class ArcanistWorkEngine $revision_ref->getMonogram())); foreach ($selected as $marker) { - echo tsprintf('%s', $marker->newDisplayRef()); + echo tsprintf('%s', $marker->newRefView()); } echo tsprintf("\n"); @@ -177,7 +177,7 @@ abstract class ArcanistWorkEngine pht('REVISION'), pht('Resuming work on revision:')); - echo tsprintf('%s', $revision_ref->newDisplayRef()); + echo tsprintf('%s', $revision_ref->newRefView()); echo tsprintf("\n"); return $target; diff --git a/src/workflow/ArcanistAmendWorkflow.php b/src/workflow/ArcanistAmendWorkflow.php index be1cf25f..33288852 100644 --- a/src/workflow/ArcanistAmendWorkflow.php +++ b/src/workflow/ArcanistAmendWorkflow.php @@ -108,7 +108,7 @@ EOTEXT echo tsprintf( "%s\n\n%s\n", pht('Amending commit message to reflect revision:'), - $revision_ref->newDisplayRef()); + $revision_ref->newRefView()); $this->confirmAmendAuthor($revision_ref); $this->confirmAmendNotFound($revision_ref, $state_ref); @@ -193,7 +193,7 @@ EOTEXT "%!\n%W\n\n%B\n", pht('MULTIPLE REVISIONS IN WORKING COPY'), pht('More than one revision was found in the working copy:'), - mpull($revisions, 'newDisplayRef')); + mpull($revisions, 'newRefView')); throw new PhutilArgumentUsageException( pht( @@ -233,7 +233,7 @@ EOTEXT 'The author of this revision (%s) is:', $revision_ref->getMonogram()), ), - $author_ref->newDisplayRef()); + $author_ref->newRefView()); $prompt = pht( 'Amend working copy using revision owned by %s?', diff --git a/src/workflow/ArcanistPasteWorkflow.php b/src/workflow/ArcanistPasteWorkflow.php index 24468e7a..f9e2a693 100644 --- a/src/workflow/ArcanistPasteWorkflow.php +++ b/src/workflow/ArcanistPasteWorkflow.php @@ -159,7 +159,7 @@ EOTEXT echo tsprintf( '%s', - $paste_ref->newDisplayRef() + $paste_ref->newRefView() ->setURI($uri)); if ($is_browse) { diff --git a/src/workflow/ArcanistUploadWorkflow.php b/src/workflow/ArcanistUploadWorkflow.php index c9c9dd83..1ed34f46 100644 --- a/src/workflow/ArcanistUploadWorkflow.php +++ b/src/workflow/ArcanistUploadWorkflow.php @@ -119,7 +119,7 @@ EOTEXT $uri = $this->getAbsoluteURI($uri); echo tsprintf( '%s', - $ref->newDisplayRef() + $ref->newRefView() ->setURI($uri)); } } From 33bb0acf97b3964b297b56749c895acf278e9602 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jun 2020 06:34:40 -0700 Subject: [PATCH 56/82] Collect scattered implementations of "getDisplayHash()" into RepositoryAPI Summary: Ref T13546. All of LandEngine, LocalState, and RepositoryAPI implement "getDisplayHash()". Always use the RepositoryAPI implementation. Test Plan: Grepped for symbols. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21353 --- src/land/engine/ArcanistGitLandEngine.php | 33 ++++++++++--------- src/land/engine/ArcanistLandEngine.php | 22 ++++++------- .../engine/ArcanistMercurialLandEngine.php | 12 +++---- src/ref/revision/ArcanistRevisionRef.php | 2 +- .../state/ArcanistGitLocalState.php | 13 ++++---- .../state/ArcanistMercurialLocalState.php | 7 ++-- .../state/ArcanistRepositoryLocalState.php | 4 --- 7 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index e12bc242..a47b6bfa 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -34,7 +34,7 @@ final class ArcanistGitLandEngine $recovery_command = csprintf( 'git checkout -b %s %s', $branch_name, - $this->getDisplayHash($branch_hash)); + $api->getDisplayHash($branch_hash)); $log->writeStatus( pht('CLEANUP'), @@ -174,8 +174,8 @@ final class ArcanistGitLandEngine 'Branch "%s" does not rebase cleanly from "%s" onto '. '"%s", skipping.', $branch_name, - $this->getDisplayHash($old_commit), - $this->getDisplayHash($rebase_target))); + $api->getDisplayHash($old_commit), + $api->getDisplayHash($rebase_target))); } } } @@ -287,15 +287,15 @@ final class ArcanistGitLandEngine pht( 'Merging local "%s" into "%s" produces an empty diff. '. 'This usually means these changes have already landed.', - $this->getDisplayHash($max_hash), - $this->getDisplayHash($into_commit))); + $api->getDisplayHash($max_hash), + $api->getDisplayHash($into_commit))); } $log->writeStatus( pht('MERGING'), pht( '%s %s', - $this->getDisplayHash($max_hash), + $api->getDisplayHash($max_hash), $max_commit->getDisplaySummary())); $argv = array(); @@ -355,23 +355,23 @@ final class ArcanistGitLandEngine $message = pht( 'Local commit "%s" (%s) does not merge cleanly into "%s". '. 'Merge or rebase local changes so they can merge cleanly.', - $this->getDisplayHash($max_hash), + $api->getDisplayHash($max_hash), $this->getDisplaySymbols($direct_symbols), - $this->getDisplayHash($into_commit)); + $api->getDisplayHash($into_commit)); } else if ($indirect_symbols) { $message = pht( 'Local commit "%s" (reachable from: %s) does not merge cleanly '. 'into "%s". Merge or rebase local changes so they can merge '. 'cleanly.', - $this->getDisplayHash($max_hash), + $api->getDisplayHash($max_hash), $this->getDisplaySymbols($indirect_symbols), - $this->getDisplayHash($into_commit)); + $api->getDisplayHash($into_commit)); } else { $message = pht( 'Local commit "%s" does not merge cleanly into "%s". Merge or '. 'rebase local changes so they can merge cleanly.', - $this->getDisplayHash($max_hash), - $this->getDisplayHash($into_commit)); + $api->getDisplayHash($max_hash), + $api->getDisplayHash($into_commit)); } echo tsprintf( @@ -1235,6 +1235,7 @@ final class ArcanistGitLandEngine } protected function selectIntoCommit() { + $api = $this->getRepositoryAPI(); // Make sure that our "into" target is valid. $log = $this->getLogEngine(); @@ -1251,7 +1252,6 @@ final class ArcanistGitLandEngine if ($this->getIntoLocal()) { // If we're running under "--into-local", just make sure that the // target identifies some actual commit. - $api = $this->getRepositoryAPI(); $local_ref = $this->getIntoRef(); list($err, $stdout) = $api->execManualLocal( @@ -1272,7 +1272,7 @@ final class ArcanistGitLandEngine pht( 'Preparing merge into local target "%s", at commit "%s".', $local_ref, - $this->getDisplayHash($into_commit))); + $api->getDisplayHash($into_commit))); return $into_commit; } @@ -1289,7 +1289,7 @@ final class ArcanistGitLandEngine 'Preparing merge into "%s" from remote "%s", at commit "%s".', $target->getRef(), $target->getRemote(), - $this->getDisplayHash($commit))); + $api->getDisplayHash($commit))); return $commit; } @@ -1492,12 +1492,13 @@ final class ArcanistGitLandEngine } private function newOntoRefArguments($into_commit) { + $api = $this->getRepositoryAPI(); $refspecs = array(); foreach ($this->getOntoRefs() as $onto_ref) { $refspecs[] = sprintf( '%s:refs/heads/%s', - $this->getDisplayHash($into_commit), + $api->getDisplayHash($into_commit), $onto_ref); } diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index d02eec5a..d16dc5e2 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -766,6 +766,7 @@ abstract class ArcanistLandEngine } final protected function printCommitSet(ArcanistLandCommitSet $set) { + $api = $this->getRepositoryAPI(); $revision_ref = $set->getRevisionRef(); echo tsprintf( @@ -775,7 +776,7 @@ abstract class ArcanistLandEngine foreach ($set->getCommits() as $commit) { $is_implicit = $commit->getIsImplicitCommit(); - $display_hash = $this->getDisplayHash($commit->getHash()); + $display_hash = $api->getDisplayHash($commit->getHash()); $display_summary = $commit->getDisplaySummary(); if ($is_implicit) { @@ -794,6 +795,7 @@ abstract class ArcanistLandEngine final protected function loadRevisionRefs(array $commit_map) { assert_instances_of($commit_map, 'ArcanistLandCommit'); + $api = $this->getRepositoryAPI(); $workflow = $this->getWorkflow(); $state_refs = array(); @@ -937,7 +939,7 @@ abstract class ArcanistLandEngine $symbols = $commit->getIndirectSymbols(); $raw_symbols = mpull($symbols, 'getSymbol'); $symbol_list = implode(', ', $raw_symbols); - $display_hash = $this->getDisplayHash($hash); + $display_hash = $api->getDisplayHash($hash); $revision_refs = $commit->getRelatedRevisionRefs(); @@ -979,15 +981,11 @@ abstract class ArcanistLandEngine // These will be handled later by the "implicit commits" mechanism. } - final protected function getDisplayHash($hash) { - // TODO: This should be on the API object. - return substr($hash, 0, 12); - } - final protected function confirmCommits( $into_commit, array $symbols, array $commit_map) { + $api = $this->getRepositoryAPI(); $commit_count = count($commit_map); @@ -997,7 +995,7 @@ abstract class ArcanistLandEngine 'which are not already present in the state you are merging '. 'into ("%s"), so nothing can land.', $this->getDisplaySymbols($symbols), - $this->getDisplayHash($into_commit)); + $api->getDisplayHash($into_commit)); echo tsprintf( "\n%!\n%W\n\n", @@ -1029,7 +1027,7 @@ abstract class ArcanistLandEngine 'into ("%s"). All of these commits will land:', new PhutilNumber($commit_count), $this->getDisplaySymbols($symbols), - $this->getDisplayHash($into_commit)); + $api->getDisplayHash($into_commit)); } echo tsprintf( @@ -1054,7 +1052,7 @@ abstract class ArcanistLandEngine } else { echo tsprintf( " %s %s\n", - $this->getDisplayHash($commit->getHash()), + $api->getDisplayHash($commit->getHash()), $commit->getDisplaySummary()); } } @@ -1150,12 +1148,12 @@ abstract class ArcanistLandEngine '"%s". Use "arc diff" to create or update a revision with this '. 'commit, or "--revision" to force selection of a particular '. 'revision.', - $this->getDisplayHash($commit_hash))); + $api->getDisplayHash($commit_hash))); throw new PhutilArgumentUsageException( pht( 'Unable to determine revision for commit "%s".', - $this->getDisplayHash($commit_hash))); + $api->getDisplayHash($commit_hash))); } $revision_groups[$revision_ref->getPHID()][] = $commit; diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 25b49929..36e74f25 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -61,13 +61,13 @@ final class ArcanistMercurialLandEngine } $commit = $api->getCanonicalRevisionName('.'); - $commit = $this->getDisplayHash($commit); + $commit = $api->getDisplayHash($commit); $log->writeStatus( pht('SOURCE'), pht( 'Landing the active commit, "%s".', - $this->getDisplayHash($commit))); + $api->getDisplayHash($commit))); return array($commit); } @@ -511,7 +511,7 @@ final class ArcanistMercurialLandEngine pht( 'Preparing merge into local target "%s", at commit "%s".', $local_ref, - $this->getDisplayHash($into_commit))); + $api->getDisplayHash($into_commit))); return $into_commit; } @@ -528,7 +528,7 @@ final class ArcanistMercurialLandEngine 'Preparing merge into "%s" from remote "%s", at commit "%s".', $target->getRef(), $target->getRemote(), - $this->getDisplayHash($commit))); + $api->getDisplayHash($commit))); return $commit; } @@ -874,7 +874,7 @@ final class ArcanistMercurialLandEngine 'bookmark', '--force', '--rev', - hgsprintf('%s', $this->getDisplayHash($new_position)), + hgsprintf('%s', $api->getDisplayHash($new_position)), '--', $bookmark_name, ); @@ -922,7 +922,7 @@ final class ArcanistMercurialLandEngine } else { $tail[] = '--force'; $tail[] = '--rev'; - $tail[] = hgsprintf('%s', $this->getDisplayHash($old_position)); + $tail[] = hgsprintf('%s', $api->getDisplayHash($old_position)); } $tail[] = '--'; diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index 8e73dec2..b7a7f1cc 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -168,7 +168,7 @@ final class ArcanistRevisionRef protected function buildRefView(ArcanistRefView $view) { $view ->setObjectName($this->getMonogram()) - ->setTitle($this->getTitle()); + ->setTitle($this->getName()); } } diff --git a/src/repository/state/ArcanistGitLocalState.php b/src/repository/state/ArcanistGitLocalState.php index 61a70e0d..7b0b168a 100644 --- a/src/repository/state/ArcanistGitLocalState.php +++ b/src/repository/state/ArcanistGitLocalState.php @@ -26,12 +26,12 @@ final class ArcanistGitLocalState $ref = null; $where = pht( 'Saving local state (at detached commit "%s").', - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } else { $where = pht( 'Saving local state (on ref "%s" at commit "%s").', $ref, - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } $this->localRef = $ref; @@ -56,11 +56,11 @@ final class ArcanistGitLocalState $where = pht( 'Restoring local state (to ref "%s" at commit "%s").', $ref, - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } else { $where = pht( 'Restoring local state (to detached commit "%s").', - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } $log->writeStatus(pht('LOAD STATE'), $where); @@ -84,6 +84,7 @@ final class ArcanistGitLocalState } protected function newRestoreCommandsForDisplay() { + $api = $this->getRepositoryAPI(); $ref = $this->localRef; $commit = $this->localCommit; @@ -93,11 +94,11 @@ final class ArcanistGitLocalState $commands[] = csprintf( 'git checkout -B %s %s --', $ref, - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } else { $commands[] = csprintf( 'git checkout %s --', - $this->getDisplayHash($commit)); + $api->getDisplayHash($commit)); } // NOTE: We run "submodule update" in the real restore workflow, but diff --git a/src/repository/state/ArcanistMercurialLocalState.php b/src/repository/state/ArcanistMercurialLocalState.php index ea24e070..a9ad0a31 100644 --- a/src/repository/state/ArcanistMercurialLocalState.php +++ b/src/repository/state/ArcanistMercurialLocalState.php @@ -22,7 +22,7 @@ final class ArcanistMercurialLocalState pht('SAVE STATE'), pht( 'Saving local state (at "%s" on branch "%s").', - $this->getDisplayHash($this->localCommit), + $api->getDisplayHash($this->localCommit), $this->localBranch)); } @@ -34,7 +34,7 @@ final class ArcanistMercurialLocalState pht('LOAD STATE'), pht( 'Restoring local state (at "%s" on branch "%s").', - $this->getDisplayHash($this->localCommit), + $api->getDisplayHash($this->localCommit), $this->localBranch)); $api->execxLocal('update -- %s', $this->localCommit); @@ -59,11 +59,12 @@ final class ArcanistMercurialLocalState } protected function newRestoreCommandsForDisplay() { + $api = $this->getRepositoryAPI(); $commands = array(); $commands[] = csprintf( 'hg update -- %s', - $this->getDisplayHash($this->localCommit)); + $api->getDisplayHash($this->localCommit)); $commands[] = csprintf( 'hg branch --force -- %s', diff --git a/src/repository/state/ArcanistRepositoryLocalState.php b/src/repository/state/ArcanistRepositoryLocalState.php index 42138456..1526d50d 100644 --- a/src/repository/state/ArcanistRepositoryLocalState.php +++ b/src/repository/state/ArcanistRepositoryLocalState.php @@ -256,8 +256,4 @@ abstract class ArcanistRepositoryLocalState echo tsprintf("\n"); } - final protected function getDisplayHash($hash) { - return substr($hash, 0, 12); - } - } From b0a9ef8351f1de94303a86fe2168d3d0444345dc Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jun 2020 06:45:34 -0700 Subject: [PATCH 57/82] In "arc land" under Git, confirm branch creation Summary: Ref T13546. If "arc land" would create a branch, warn the user before it does. Test Plan: Ran "arc land --onto mtarse", a typo of "master". Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21354 --- src/land/engine/ArcanistGitLandEngine.php | 62 +++++++++++++++++-- src/land/engine/ArcanistLandEngine.php | 5 ++ .../engine/ArcanistMercurialLandEngine.php | 18 ++++-- .../ArcanistGitRepositoryMarkerQuery.php | 54 +++++++++++++++- src/xsprintf/tsprintf.php | 1 + 5 files changed, 131 insertions(+), 9 deletions(-) diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php index a47b6bfa..b67d0792 100644 --- a/src/land/engine/ArcanistGitLandEngine.php +++ b/src/land/engine/ArcanistGitLandEngine.php @@ -876,6 +876,8 @@ final class ArcanistGitLandEngine } protected function confirmOntoRefs(array $onto_refs) { + $api = $this->getRepositoryAPI(); + foreach ($onto_refs as $onto_ref) { if (!strlen($onto_ref)) { throw new PhutilArgumentUsageException( @@ -886,10 +888,61 @@ final class ArcanistGitLandEngine } } - // TODO: Check that these refs really exist in the remote? Checking the - // remote is expensive and users probably rarely specify "--onto" manually, - // but if "arc land" creates branches without prompting when you make typos - // that also seems questionable. + $markers = $api->newMarkerRefQuery() + ->withRemotes(array($this->getOntoRemoteRef())) + ->withNames($onto_refs) + ->execute(); + + $markers = mgroup($markers, 'getName'); + + $new_markers = array(); + foreach ($onto_refs as $onto_ref) { + if (isset($markers[$onto_ref])) { + // Remote already has a branch with this name, so we're fine: we + // aren't creatinga new branch. + continue; + } + + $new_markers[] = id(new ArcanistMarkerRef()) + ->setMarkerType(ArcanistMarkerRef::TYPE_BRANCH) + ->setName($onto_ref); + } + + if ($new_markers) { + echo tsprintf( + "\n%!\n%W\n\n", + pht('CREATE %s BRANCHE(S)', phutil_count($new_markers)), + pht( + 'These %s symbol(s) do not exist in the remote. They will be '. + 'created as new branches:', + phutil_count($new_markers))); + + foreach ($new_markers as $new_marker) { + echo tsprintf('%s', $new_marker->newRefView()); + } + + echo tsprintf("\n"); + + $is_hold = $this->getShouldHold(); + if ($is_hold) { + echo tsprintf( + "%?\n", + pht( + 'You are using "--hold", so execution will stop before the '. + '%s branche(s) are actually created. You will be given '. + 'instructions to create the branches.', + phutil_count($new_markers))); + } + + $query = pht( + 'Create %s new branche(s) in the remote?', + phutil_count($new_markers)); + + $this->getWorkflow() + ->getPrompt('arc.land.create') + ->setQuery($query) + ->execute(); + } } protected function selectOntoRefs(array $symbols) { @@ -1238,6 +1291,7 @@ final class ArcanistGitLandEngine $api = $this->getRepositoryAPI(); // Make sure that our "into" target is valid. $log = $this->getLogEngine(); + $api = $this->getRepositoryAPI(); if ($this->getIntoEmpty()) { // If we're running under "--into-empty", we don't have to do anything. diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php index d16dc5e2..a3e36ee7 100644 --- a/src/land/engine/ArcanistLandEngine.php +++ b/src/land/engine/ArcanistLandEngine.php @@ -1569,4 +1569,9 @@ abstract class ArcanistLandEngine return $command->execute(); } + final protected function getOntoRemoteRef() { + return id(new ArcanistRemoteRef()) + ->setRemoteName($this->getOntoRemote()); + } + } diff --git a/src/land/engine/ArcanistMercurialLandEngine.php b/src/land/engine/ArcanistMercurialLandEngine.php index 36e74f25..0fb8c6a9 100644 --- a/src/land/engine/ArcanistMercurialLandEngine.php +++ b/src/land/engine/ArcanistMercurialLandEngine.php @@ -270,8 +270,7 @@ final class ArcanistMercurialLandEngine } } - $remote_ref = id(new ArcanistRemoteRef()) - ->setRemoteName($this->getOntoRemote()); + $remote_ref = $this->getOntoRemoteRef(); $markers = $api->newMarkerRefQuery() ->withRemotes(array($remote_ref)) @@ -346,8 +345,8 @@ final class ArcanistMercurialLandEngine "\n%!\n%W\n\n", pht('CREATE %s BOOKMARK(S)', phutil_count($new_markers)), pht( - 'These %s symbol(s) do not exist in the remote. They will be created '. - 'as new bookmarks:', + 'These %s symbol(s) do not exist in the remote. They will be '. + 'created as new bookmarks:', phutil_count($new_markers))); @@ -357,6 +356,17 @@ final class ArcanistMercurialLandEngine echo tsprintf("\n"); + $is_hold = $this->getShouldHold(); + if ($is_hold) { + echo tsprintf( + "%?\n", + pht( + 'You are using "--hold", so execution will stop before the '. + '%s bookmark(s) are actually created. You will be given '. + 'instructions to create the bookmarks.', + phutil_count($new_markers))); + } + $query = pht( 'Create %s new remote bookmark(s)?', phutil_count($new_markers)); diff --git a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php index 5e185767..a1d747c6 100644 --- a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php @@ -122,7 +122,59 @@ final class ArcanistGitRepositoryMarkerQuery } protected function newRemoteRefMarkers(ArcanistRemoteRef $remote) { - throw new PhutilMethodNotImplementedException(); + $api = $this->getRepositoryAPI(); + + // NOTE: Since we only care about branches today, we only list branches. + + $future = $api->newFuture( + 'ls-remote --refs %s %s', + $remote->getRemoteName(), + 'refs/heads/*'); + list($stdout) = $future->resolve(); + + $branch_prefix = 'refs/heads/'; + $branch_length = strlen($branch_prefix); + + $pattern = '(^(?P\S+)\t(?P\S+)\z)'; + $markers = array(); + + $lines = phutil_split_lines($stdout, false); + foreach ($lines as $line) { + $matches = null; + $ok = preg_match($pattern, $line, $matches); + if (!$ok) { + throw new Exception( + pht( + 'Failed to match "ls-remote" pattern against line "%s".', + $line)); + } + + $hash = $matches['hash']; + $ref = $matches['ref']; + + if (!strncmp($ref, $branch_prefix, $branch_length)) { + $type = ArcanistMarkerRef::TYPE_BRANCH; + $name = substr($ref, $branch_length); + } else { + // For now, discard other refs. + continue; + } + + $marker = id(new ArcanistMarkerRef()) + ->setName($name) + ->setMarkerType($type) + ->setMarkerHash($hash) + ->setCommitHash($hash); + + $commit_ref = $api->newCommitRef() + ->setCommitHash($hash); + + $marker->attachCommitRef($commit_ref); + + $markers[] = $marker; + } + + return $markers; } } diff --git a/src/xsprintf/tsprintf.php b/src/xsprintf/tsprintf.php index d2e34508..a2100bfd 100644 --- a/src/xsprintf/tsprintf.php +++ b/src/xsprintf/tsprintf.php @@ -51,6 +51,7 @@ function xsprintf_terminal($userdata, &$pattern, &$pos, &$value, &$length) { case '?': $value = tsprintf('** ? ** %s', $value); $value = PhutilTerminalString::escapeStringValue($value, false); + $value = phutil_console_wrap($value, 6, false); $type = 's'; break; case '>': From f52222ad19a00a422e1fb422e1bf5c0d08dfc0b2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jun 2020 10:38:27 -0700 Subject: [PATCH 58/82] Add more "RepositoryRef" legacy status mappings Summary: Ref T13546. The old "differential.query" call is still used to fill refs when all we have locally is hashes. Add some mappings to improve the resulting refs. Test Plan: Viewed "arc branches", saw statuses colored more consistently. Reviewers: ptarjan Reviewed By: ptarjan Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21355 --- src/ref/revision/ArcanistRevisionRef.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index b7a7f1cc..5e2ffa0e 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -49,6 +49,27 @@ final class ArcanistRevisionRef break; } + $value_map = array( + '0' => 'needs-review', + '1' => 'needs-revision', + '2' => 'accepted', + '3' => 'published', + '4' => 'abandoned', + '5' => 'changes-planned', + ); + + $color_map = array( + 'needs-review' => 'magenta', + 'needs-revision' => 'red', + 'accepted' => 'green', + 'published' => 'cyan', + 'abandoned' => null, + 'changes-planned' => 'red', + ); + + $status_value = idx($value_map, idx($dict, 'status')); + $ansi_color = idx($color_map, $status_value); + $dict['fields'] = array( 'uri' => idx($dict, 'uri'), 'title' => idx($dict, 'title'), @@ -56,6 +77,8 @@ final class ArcanistRevisionRef 'status' => array( 'name' => $status_name, 'closed' => $is_closed, + 'value' => $status_value, + 'color.ansi' => $ansi_color, ), ); From 98bf58db4a39a0974a5de0b375080141e2f28481 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 11 Jun 2020 11:05:31 -0700 Subject: [PATCH 59/82] Correct a leftover reference to "--keep-branch" Summary: See . This flag is now "--keep-branches". Test Plan: Grepped for "keep-branch". Differential Revision: https://secure.phabricator.com/D21356 --- src/workflow/ArcanistLandWorkflow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workflow/ArcanistLandWorkflow.php b/src/workflow/ArcanistLandWorkflow.php index 72f1ae99..92fc643d 100644 --- a/src/workflow/ArcanistLandWorkflow.php +++ b/src/workflow/ArcanistLandWorkflow.php @@ -118,7 +118,7 @@ power, the workflow makes a guess about what state you wanted to end up in after the process finishes. The working copy is put into that state. Any obsolete refs that point at commits which were published are deleted, -unless the **--keep-branch** flag is passed. +unless the **--keep-branches** flag is passed. EOTEXT ); From 33484b43c9c8fdf1be711f45a25c8967d74dc7cb Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 15 Jun 2020 07:58:08 -0700 Subject: [PATCH 60/82] Introduce "GridView", an updated version of "ConsoleTableView" Summary: Ref T13546. In a future change, I'm providing a fancier version of "arc branches" that requires more sophisticated table rendering. Implement a new view which can do some fancier things, like handle alignment of multi-line table cells. Test Plan: See future changes. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21360 --- src/__phutil_library_map__.php | 8 + src/console/grid/ArcanistGridCell.php | 56 ++++++ src/console/grid/ArcanistGridColumn.php | 41 ++++ src/console/grid/ArcanistGridRow.php | 40 ++++ src/console/grid/ArcanistGridView.php | 247 ++++++++++++++++++++++++ 5 files changed, 392 insertions(+) create mode 100644 src/console/grid/ArcanistGridCell.php create mode 100644 src/console/grid/ArcanistGridColumn.php create mode 100644 src/console/grid/ArcanistGridRow.php create mode 100644 src/console/grid/ArcanistGridView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7bf922b9..493f83d1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -228,6 +228,10 @@ phutil_register_library_map(array( 'ArcanistGoLintLinterTestCase' => 'lint/linter/__tests__/ArcanistGoLintLinterTestCase.php', 'ArcanistGoTestResultParser' => 'unit/parser/ArcanistGoTestResultParser.php', 'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php', + 'ArcanistGridCell' => 'console/grid/ArcanistGridCell.php', + 'ArcanistGridColumn' => 'console/grid/ArcanistGridColumn.php', + 'ArcanistGridRow' => 'console/grid/ArcanistGridRow.php', + 'ArcanistGridView' => 'console/grid/ArcanistGridView.php', 'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php', 'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php', 'ArcanistHardpoint' => 'hardpoint/ArcanistHardpoint.php', @@ -1246,6 +1250,10 @@ phutil_register_library_map(array( 'ArcanistGoLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistGoTestResultParser' => 'ArcanistTestResultParser', 'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase', + 'ArcanistGridCell' => 'Phobject', + 'ArcanistGridColumn' => 'Phobject', + 'ArcanistGridRow' => 'Phobject', + 'ArcanistGridView' => 'Phobject', 'ArcanistHLintLinter' => 'ArcanistExternalLinter', 'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistHardpoint' => 'Phobject', diff --git a/src/console/grid/ArcanistGridCell.php b/src/console/grid/ArcanistGridCell.php new file mode 100644 index 00000000..b18560af --- /dev/null +++ b/src/console/grid/ArcanistGridCell.php @@ -0,0 +1,56 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setContent($content) { + $this->content = $content; + return $this; + } + + public function getContent() { + return $this->content; + } + + public function getContentDisplayWidth() { + $lines = $this->getContentDisplayLines(); + + $width = 0; + foreach ($lines as $line) { + $width = max($width, phutil_utf8_console_strlen($line)); + } + + return $width; + } + + public function getContentDisplayLines() { + $content = $this->getContent(); + $content = tsprintf('%B', $content); + $content = phutil_string_cast($content); + + $lines = phutil_split_lines($content, false); + + $result = array(); + foreach ($lines as $line) { + $result[] = tsprintf('%R', $line); + } + + return $result; + } + + +} diff --git a/src/console/grid/ArcanistGridColumn.php b/src/console/grid/ArcanistGridColumn.php new file mode 100644 index 00000000..15d0d142 --- /dev/null +++ b/src/console/grid/ArcanistGridColumn.php @@ -0,0 +1,41 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setAlignment($alignment) { + $this->alignment = $alignment; + return $this; + } + + public function getAlignment() { + return $this->alignment; + } + + public function setDisplayWidth($display_width) { + $this->displayWidth = $display_width; + return $this; + } + + public function getDisplayWidth() { + return $this->displayWidth; + } + +} diff --git a/src/console/grid/ArcanistGridRow.php b/src/console/grid/ArcanistGridRow.php new file mode 100644 index 00000000..0f24e4ef --- /dev/null +++ b/src/console/grid/ArcanistGridRow.php @@ -0,0 +1,40 @@ +setInstancesOf('ArcanistGridCell') + ->setUniqueMethod('getKey') + ->setContext($this, 'setCells') + ->checkValue($cells); + + $this->cells = $cells; + + return $this; + } + + public function getCells() { + return $this->cells; + } + + public function hasCell($key) { + return isset($this->cells[$key]); + } + + public function getCell($key) { + if (!isset($this->cells[$key])) { + throw new Exception( + pht( + 'Row has no cell "%s".\n', + $key)); + } + + return $this->cells[$key]; + } + + +} diff --git a/src/console/grid/ArcanistGridView.php b/src/console/grid/ArcanistGridView.php new file mode 100644 index 00000000..73e77486 --- /dev/null +++ b/src/console/grid/ArcanistGridView.php @@ -0,0 +1,247 @@ +columns = $columns; + return $this; + } + + public function getColumns() { + return $this->columns; + } + + public function newColumn($key) { + $column = id(new ArcanistGridColumn()) + ->setKey($key); + + $this->columns[$key] = $column; + + return $column; + } + + public function newRow(array $cells) { + assert_instances_of($cells, 'ArcanistGridCell'); + + $row = id(new ArcanistGridRow()) + ->setCells($cells); + + $this->rows[] = $row; + + return $row; + } + + public function drawGrid() { + $columns = $this->getColumns(); + if (!$columns) { + throw new Exception( + pht( + 'Can not draw a grid with no columns!')); + } + + $rows = array(); + foreach ($this->rows as $row) { + $rows[] = $this->drawRow($row); + } + + $rows = phutil_glue($rows, tsprintf("\n")); + + return tsprintf("%s\n", $rows); + } + + private function getDisplayWidth($key) { + if (!isset($this->displayWidths[$key])) { + $column = $this->getColumn($key); + + $width = $column->getDisplayWidth(); + if ($width === null) { + $width = 1; + foreach ($this->getRows() as $row) { + if (!$row->hasCell($key)) { + continue; + } + + $cell = $row->getCell($key); + $width = max($width, $cell->getContentDisplayWidth()); + } + } + + $this->displayWidths[$key] = $width; + } + + return $this->displayWidths[$key]; + } + + public function getColumn($key) { + if (!isset($this->columns[$key])) { + throw new Exception( + pht( + 'Grid has no column "%s".', + $key)); + } + + return $this->columns[$key]; + } + + public function getRows() { + return $this->rows; + } + + private function drawRow(ArcanistGridRow $row) { + $columns = $this->getColumns(); + + $cells = $row->getCells(); + + $out = array(); + $widths = array(); + foreach ($columns as $column_key => $column) { + $display_width = $this->getDisplayWidth($column_key); + + $cell = idx($cells, $column_key); + if ($cell) { + $content = $cell->getContentDisplayLines(); + } else { + $content = array(''); + } + + foreach ($content as $line_key => $line) { + $line_width = phutil_utf8_console_strlen($line); + + if ($line_width === $display_width) { + continue; + } + + if ($line_width < $display_width) { + $line = $this->padContentLineToWidth( + $line, + $line_width, + $display_width, + $column->getAlignment()); + } else if ($line_width > $display_width) { + $line = $this->truncateContentLineToWidth( + $line, + $line_width, + $display_width, + $column->getAlignment()); + } + + $content[$line_key] = $line; + } + + $out[] = $content; + $widths[] = $display_width; + } + + return $this->drawRowLayout($out, $widths); + } + + private function drawRowLayout(array $raw_cells, array $display_widths) { + $line_count = 0; + foreach ($raw_cells as $key => $cells) { + $raw_cells[$key] = array_values($cells); + $line_count = max($line_count, count($cells)); + } + + $line_head = ''; + $cell_separator = ' '; + $line_tail = ''; + + $out = array(); + $cell_count = count($raw_cells); + for ($ii = 0; $ii < $line_count; $ii++) { + $line = array(); + for ($jj = 0; $jj < $cell_count; $jj++) { + if (isset($raw_cells[$jj][$ii])) { + $raw_line = $raw_cells[$jj][$ii]; + } else { + $display_width = $display_widths[$jj]; + $raw_line = str_repeat(' ', $display_width); + } + $line[] = $raw_line; + } + + $line = array( + $line_head, + phutil_glue($line, $cell_separator), + $line_tail, + ); + + $out[] = $line; + } + + $out = phutil_glue($out, tsprintf("\n")); + + return $out; + } + + private function padContentLineToWidth( + $line, + $src_width, + $dst_width, + $alignment) { + + $delta = ($dst_width - $src_width); + + switch ($alignment) { + case ArcanistGridColumn::ALIGNMENT_LEFT: + $head = null; + $tail = str_repeat(' ', $delta); + break; + case ArcanistGridColumn::ALIGNMENT_CENTER: + $head_delta = (int)floor($delta / 2); + $tail_delta = (int)ceil($delta / 2); + + if ($head_delta) { + $head = str_repeat(' ', $head_delta); + } else { + $head = null; + } + + if ($tail_delta) { + $tail = str_repeat(' ', $tail_delta); + } else { + $tail = null; + } + break; + case ArcanistGridColumn::ALIGNMENT_RIGHT: + $head = str_repeat(' ', $delta); + $tail = null; + break; + default: + throw new Exception( + pht( + 'Unknown column alignment "%s".', + $alignment)); + } + + $result = array(); + + if ($head !== null) { + $result[] = $head; + } + + $result[] = $line; + + if ($tail !== null) { + $result[] = $tail; + } + + return $result; + } + + private function truncateContentLineToWidth( + $line, + $src_width, + $dst_width, + $alignment) { + return $line; + } + +} From c53c05e5b2a6f8213fff81a783f648bdbae71f56 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 06:35:27 -0700 Subject: [PATCH 61/82] Introduce "phutil_partition()" and natural case sorting for "msortv(...)" Summary: Ref T13546. Pull some small utility changes out of the deeper stack of "land/markers" changes. "phutil_partition()" makes it easier to write code that loops over a list grouping elements, then acts on each group. This kind of code is not terribly common, but often feels awkward when implemented with raw primitives. "msortv()" can support "natural" sorting, which sorts "feature1", "feature2", ..., "feature10" in a more human-readable order. Test Plan: Ran unit tests, used new behaviors elsewhere in "arc markers" workflows. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21371 --- src/__phutil_library_map__.php | 3 ++ src/utils/__tests__/PhutilUtilsTestCase.php | 34 ++++++++++++++++++ src/utils/utils.php | 39 ++++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 493f83d1..a4d2faed 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -925,6 +925,8 @@ phutil_register_library_map(array( 'mpull' => 'utils/utils.php', 'msort' => 'utils/utils.php', 'msortv' => 'utils/utils.php', + 'msortv_internal' => 'utils/utils.php', + 'msortv_natural' => 'utils/utils.php', 'newv' => 'utils/utils.php', 'nonempty' => 'utils/utils.php', 'phlog' => 'error/phlog.php', @@ -979,6 +981,7 @@ phutil_register_library_map(array( 'phutil_loggable_string' => 'utils/utils.php', 'phutil_microseconds_since' => 'utils/utils.php', 'phutil_parse_bytes' => 'utils/viewutils.php', + 'phutil_partition' => 'utils/utils.php', 'phutil_passthru' => 'future/exec/execx.php', 'phutil_person' => 'internationalization/pht.php', 'phutil_register_library' => 'init/lib/core.php', diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php index e1bade52..75fe4692 100644 --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -966,4 +966,38 @@ final class PhutilUtilsTestCase extends PhutilTestCase { } } + public function testArrayPartition() { + $map = array( + 'empty' => array( + array(), + array(), + ), + 'unique' => array( + array('a' => 'a', 'b' => 'b', 'c' => 'c'), + array(array('a' => 'a'), array('b' => 'b'), array('c' => 'c')), + ), + 'xy' => array( + array('a' => 'x', 'b' => 'x', 'c' => 'y', 'd' => 'y'), + array( + array('a' => 'x', 'b' => 'x'), + array('c' => 'y', 'd' => 'y'), + ), + ), + 'multi' => array( + array('a' => true, 'b' => true, 'c' => false, 'd' => true), + array( + array('a' => true, 'b' => true), + array('c' => false), + array('d' => true), + ), + ), + ); + + foreach ($map as $name => $item) { + list($input, $expect) = $item; + $actual = phutil_partition($input); + $this->assertEqual($expect, $actual, pht('Partition of "%s"', $name)); + } + } + } diff --git a/src/utils/utils.php b/src/utils/utils.php index 0bb1b4f8..4c44d7b7 100644 --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -433,6 +433,14 @@ function msort(array $list, $method) { * @return list Objects ordered by the vectors. */ function msortv(array $list, $method) { + return msortv_internal($list, $method, SORT_STRING); +} + +function msortv_natural(array $list, $method) { + return msortv_internal($list, $method, SORT_NATURAL | SORT_FLAG_CASE); +} + +function msortv_internal(array $list, $method, $flags) { $surrogate = mpull($list, $method); $index = 0; @@ -455,7 +463,7 @@ function msortv(array $list, $method) { $surrogate[$key] = (string)$value; } - asort($surrogate, SORT_STRING); + asort($surrogate, $flags); $result = array(); foreach ($surrogate as $key => $value) { @@ -1966,3 +1974,32 @@ function phutil_glue(array $list, $glue) { return array_select_keys($tmp, $keys); } + +function phutil_partition(array $map) { + $partitions = array(); + + $partition = array(); + $is_first = true; + $partition_value = null; + + foreach ($map as $key => $value) { + if (!$is_first) { + if ($partition_value === $value) { + $partition[$key] = $value; + continue; + } + + $partitions[] = $partition; + } + + $is_first = false; + $partition = array($key => $value); + $partition_value = $value; + } + + if ($partition) { + $partitions[] = $partition; + } + + return $partitions; +} From b19985a4bde67dc5e2d8babf4139f263587c5ee6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 10:43:39 -0700 Subject: [PATCH 62/82] Copy repository URI normalization code from Phabricator to Arcanist Summary: Ref T13546. Move toward smarter remote repository lookup by providing URI normalization code in Arcanist. This diff duplicates code from Phabricator; the next change will collapse it. Test Plan: Ran unit tests. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21372 --- src/__phutil_library_map__.php | 4 + .../ArcanistRepositoryURINormalizer.php | 159 ++++++++++++++++++ ...rcanistRepositoryURINormalizerTestCase.php | 84 +++++++++ 3 files changed, 247 insertions(+) create mode 100644 src/repository/remote/ArcanistRepositoryURINormalizer.php create mode 100644 src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a4d2faed..8d14cb01 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -427,6 +427,8 @@ phutil_register_library_map(array( 'ArcanistRepositoryQuery' => 'repository/query/ArcanistRepositoryQuery.php', 'ArcanistRepositoryRef' => 'ref/ArcanistRepositoryRef.php', 'ArcanistRepositoryRemoteQuery' => 'repository/remote/ArcanistRepositoryRemoteQuery.php', + 'ArcanistRepositoryURINormalizer' => 'repository/remote/ArcanistRepositoryURINormalizer.php', + 'ArcanistRepositoryURINormalizerTestCase' => 'repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedAsIteratorXHPASTLinterRule.php', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedAsIteratorXHPASTLinterRuleTestCase.php', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorReferenceXHPASTLinterRule.php', @@ -1455,6 +1457,8 @@ phutil_register_library_map(array( 'ArcanistRepositoryQuery' => 'Phobject', 'ArcanistRepositoryRef' => 'ArcanistRef', 'ArcanistRepositoryRemoteQuery' => 'ArcanistRepositoryQuery', + 'ArcanistRepositoryURINormalizer' => 'Phobject', + 'ArcanistRepositoryURINormalizerTestCase' => 'PhutilTestCase', 'ArcanistReusedAsIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistReusedAsIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistReusedIteratorReferenceXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', diff --git a/src/repository/remote/ArcanistRepositoryURINormalizer.php b/src/repository/remote/ArcanistRepositoryURINormalizer.php new file mode 100644 index 00000000..33c3f5b9 --- /dev/null +++ b/src/repository/remote/ArcanistRepositoryURINormalizer.php @@ -0,0 +1,159 @@ +getNormalizedPath() === $norm_b->getNormalizedPath()) { + * // URIs appear to point at the same repository. + * } else { + * // URIs are very unlikely to be the same repository. + * } + * + * Because a repository can be hosted at arbitrarily many arbitrary URIs, there + * is no way to completely prevent false negatives by only examining URIs + * (that is, repositories with totally different URIs could really be the same). + * However, normalization is relatively aggressive and false negatives should + * be rare: if normalization says two URIs are different repositories, they + * probably are. + * + * @task normal Normalizing URIs + */ +final class ArcanistRepositoryURINormalizer + extends Phobject { + + const TYPE_GIT = 'git'; + const TYPE_SVN = 'svn'; + const TYPE_MERCURIAL = 'hg'; + + private $type; + private $uri; + private $domainMap = array(); + + public function __construct($type, $uri) { + switch ($type) { + case self::TYPE_GIT: + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + break; + default: + throw new Exception(pht('Unknown URI type "%s"!', $type)); + } + + $this->type = $type; + $this->uri = $uri; + } + + public static function getAllURITypes() { + return array( + self::TYPE_GIT, + self::TYPE_SVN, + self::TYPE_MERCURIAL, + ); + } + + public function setDomainMap(array $domain_map) { + foreach ($domain_map as $key => $domain) { + $domain_map[$key] = phutil_utf8_strtolower($domain); + } + + $this->domainMap = $domain_map; + return $this; + } + + +/* -( Normalizing URIs )--------------------------------------------------- */ + + + /** + * @task normal + */ + public function getPath() { + switch ($this->type) { + case self::TYPE_GIT: + $uri = new PhutilURI($this->uri); + return $uri->getPath(); + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + $uri = new PhutilURI($this->uri); + if ($uri->getProtocol()) { + return $uri->getPath(); + } + + return $this->uri; + } + } + + public function getNormalizedURI() { + return $this->getNormalizedDomain().'/'.$this->getNormalizedPath(); + } + + + /** + * @task normal + */ + public function getNormalizedPath() { + $path = $this->getPath(); + $path = trim($path, '/'); + + switch ($this->type) { + case self::TYPE_GIT: + $path = preg_replace('/\.git$/', '', $path); + break; + case self::TYPE_SVN: + case self::TYPE_MERCURIAL: + break; + } + + // If this is a Phabricator URI, strip it down to the callsign. We mutably + // allow you to clone repositories as "/diffusion/X/anything.git", for + // example. + + $matches = null; + if (preg_match('@^(diffusion/(?:[A-Z]+|\d+))@', $path, $matches)) { + $path = $matches[1]; + } + + return $path; + } + + public function getNormalizedDomain() { + $domain = null; + + $uri = new PhutilURI($this->uri); + $domain = $uri->getDomain(); + + if (!strlen($domain)) { + return ''; + } + + $domain = phutil_utf8_strtolower($domain); + + foreach ($this->domainMap as $domain_key => $domain_value) { + if ($domain === $domain_value) { + $domain = $domain_key; + break; + } + } + + return $domain; + } + +} diff --git a/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php b/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php new file mode 100644 index 00000000..e2e6edb5 --- /dev/null +++ b/src/repository/remote/__tests__/ArcanistRepositoryURINormalizerTestCase.php @@ -0,0 +1,84 @@ + 'path', + 'https://user@domain.com/path.git' => 'path', + 'git@domain.com:path.git' => 'path', + 'ssh://user@gitserv002.com/path.git' => 'path', + 'ssh://htaft@domain.com/path.git' => 'path', + 'ssh://user@domain.com/bananas.git' => 'bananas', + 'git@domain.com:bananas.git' => 'bananas', + 'user@domain.com:path/repo' => 'path/repo', + 'user@domain.com:path/repo/' => 'path/repo', + 'file:///path/to/local/repo.git' => 'path/to/local/repo', + '/path/to/local/repo.git' => 'path/to/local/repo', + 'ssh://something.com/diffusion/X/anything.git' => 'diffusion/X', + 'ssh://something.com/diffusion/X/' => 'diffusion/X', + ); + + $type_git = ArcanistRepositoryURINormalizer::TYPE_GIT; + + foreach ($cases as $input => $expect) { + $normal = new ArcanistRepositoryURINormalizer($type_git, $input); + $this->assertEqual( + $expect, + $normal->getNormalizedPath(), + pht('Normalized Git path for "%s".', $input)); + } + } + + public function testDomainURINormalizer() { + $base_domain = 'base.phabricator.example.com'; + $ssh_domain = 'ssh.phabricator.example.com'; + + $domain_map = array( + '' => $base_domain, + '' => $ssh_domain, + ); + + $cases = array( + '/' => '', + '/path/to/local/repo.git' => '', + 'ssh://user@domain.com/path.git' => 'domain.com', + 'ssh://user@DOMAIN.COM/path.git' => 'domain.com', + 'http://'.$base_domain.'/diffusion/X/' => '', + 'ssh://'.$ssh_domain.'/diffusion/X/' => '', + 'git@'.$ssh_domain.':bananas.git' => '', + ); + + $type_git = ArcanistRepositoryURINormalizer::TYPE_GIT; + + foreach ($cases as $input => $expect) { + $normalizer = new ArcanistRepositoryURINormalizer($type_git, $input); + + $normalizer->setDomainMap($domain_map); + + $this->assertEqual( + $expect, + $normalizer->getNormalizedDomain(), + pht('Normalized domain for "%s".', $input)); + } + } + + public function testSVNURINormalizer() { + $cases = array( + 'file:///path/to/repo' => 'path/to/repo', + 'file:///path/to/repo/' => 'path/to/repo', + ); + + $type_svn = ArcanistRepositoryURINormalizer::TYPE_SVN; + + foreach ($cases as $input => $expect) { + $normal = new ArcanistRepositoryURINormalizer($type_svn, $input); + $this->assertEqual( + $expect, + $normal->getNormalizedPath(), + pht('Normalized SVN path for "%s".', $input)); + } + } + +} From 89f9eb66a74bff16101bef1ec6d2b3359855d2d5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 11:55:55 -0700 Subject: [PATCH 63/82] Support inspection of remote refs with "arc inspect remote(...)" Summary: Ref T13546. Expose remote refs for inspection via "arc inspect". For now, this only works in Mercurial. Test Plan: Ran "arc inspect remote(default)" in Mercurial, got a ref out. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21374 --- src/__phutil_library_map__.php | 2 + src/inspector/ArcanistRefInspector.php | 11 ++++++ .../remote/ArcanistRemoteRefInspector.php | 37 +++++++++++++++++++ src/workflow/ArcanistInspectWorkflow.php | 4 ++ 4 files changed, 54 insertions(+) create mode 100644 src/repository/remote/ArcanistRemoteRefInspector.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8d14cb01..1f785647 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -419,6 +419,7 @@ phutil_register_library_map(array( 'ArcanistRefInspector' => 'inspector/ArcanistRefInspector.php', 'ArcanistRefView' => 'ref/ArcanistRefView.php', 'ArcanistRemoteRef' => 'repository/remote/ArcanistRemoteRef.php', + 'ArcanistRemoteRefInspector' => 'repository/remote/ArcanistRemoteRefInspector.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', @@ -1449,6 +1450,7 @@ phutil_register_library_map(array( 'ArcanistTerminalStringInterface', ), 'ArcanistRemoteRef' => 'ArcanistRef', + 'ArcanistRemoteRefInspector' => 'ArcanistRefInspector', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', diff --git a/src/inspector/ArcanistRefInspector.php b/src/inspector/ArcanistRefInspector.php index 8115b12d..2afb44b5 100644 --- a/src/inspector/ArcanistRefInspector.php +++ b/src/inspector/ArcanistRefInspector.php @@ -3,6 +3,17 @@ abstract class ArcanistRefInspector extends Phobject { + private $workflow; + + final public function setWorkflow(ArcanistWorkflow $workflow) { + $this->workflow = $workflow; + return $this; + } + + final public function getWorkflow() { + return $this->workflow; + } + abstract public function getInspectFunctionName(); abstract public function newInspectRef(array $argv); diff --git a/src/repository/remote/ArcanistRemoteRefInspector.php b/src/repository/remote/ArcanistRemoteRefInspector.php new file mode 100644 index 00000000..d7f7e778 --- /dev/null +++ b/src/repository/remote/ArcanistRemoteRefInspector.php @@ -0,0 +1,37 @@ +getWorkflow(); + $api = $workflow->getRepositoryAPI(); + + $ref = $api->newRemoteRefQuery() + ->withNames(array($remote_name)) + ->executeOne(); + + if (!$ref) { + throw new PhutilArgumentUsageException( + pht( + 'This working copy has no remote named "%s".', + $remote_name)); + } + + return $ref; + } + +} diff --git a/src/workflow/ArcanistInspectWorkflow.php b/src/workflow/ArcanistInspectWorkflow.php index 414820e8..05247f7b 100644 --- a/src/workflow/ArcanistInspectWorkflow.php +++ b/src/workflow/ArcanistInspectWorkflow.php @@ -34,6 +34,10 @@ EOTEXT $inspectors = ArcanistRefInspector::getAllInspectors(); + foreach ($inspectors as $inspector) { + $inspector->setWorkflow($this); + } + if (!$objects) { echo tsprintf( "%s\n\n", From ffb027e85ccfffa37e2eb5bda4282aae994fbad3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 12:02:37 -0700 Subject: [PATCH 64/82] Support generating remote refs in Git Summary: Ref T13546. Allow construction of remote refs in Git; previously they were only supported in Mercurial. Test Plan: Ran "arc inspect remote(origin)" in Git, got a ref. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21375 --- src/__phutil_library_map__.php | 2 + src/repository/api/ArcanistGitAPI.php | 4 ++ src/repository/api/ArcanistMercurialAPI.php | 1 - .../ArcanistGitRepositoryRemoteQuery.php | 63 +++++++++++++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/repository/remote/ArcanistGitRepositoryRemoteQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1f785647..15bb86ce 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -218,6 +218,7 @@ phutil_register_library_map(array( 'ArcanistGitRawCommit' => 'repository/raw/ArcanistGitRawCommit.php', 'ArcanistGitRawCommitTestCase' => 'repository/raw/__tests__/ArcanistGitRawCommitTestCase.php', 'ArcanistGitRepositoryMarkerQuery' => 'repository/marker/ArcanistGitRepositoryMarkerQuery.php', + 'ArcanistGitRepositoryRemoteQuery' => 'repository/remote/ArcanistGitRepositoryRemoteQuery.php', 'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php', 'ArcanistGitWorkEngine' => 'work/ArcanistGitWorkEngine.php', 'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php', @@ -1246,6 +1247,7 @@ phutil_register_library_map(array( 'ArcanistGitRawCommit' => 'Phobject', 'ArcanistGitRawCommitTestCase' => 'PhutilTestCase', 'ArcanistGitRepositoryMarkerQuery' => 'ArcanistRepositoryMarkerQuery', + 'ArcanistGitRepositoryRemoteQuery' => 'ArcanistRepositoryRemoteQuery', 'ArcanistGitUpstreamPath' => 'Phobject', 'ArcanistGitWorkEngine' => 'ArcanistWorkEngine', 'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy', diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index c65257f4..ebd2d1d9 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1763,4 +1763,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return new ArcanistGitRepositoryMarkerQuery(); } + protected function newRemoteRefQueryTemplate() { + return new ArcanistGitRepositoryRemoteQuery(); + } + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index e606f6a1..fdb5e1e0 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1014,7 +1014,6 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return new ArcanistMercurialRepositoryRemoteQuery(); } - public function getMercurialExtensionArguments() { $path = phutil_get_library_root('arcanist'); $path = dirname($path); diff --git a/src/repository/remote/ArcanistGitRepositoryRemoteQuery.php b/src/repository/remote/ArcanistGitRepositoryRemoteQuery.php new file mode 100644 index 00000000..47b914e9 --- /dev/null +++ b/src/repository/remote/ArcanistGitRepositoryRemoteQuery.php @@ -0,0 +1,63 @@ +getRepositoryAPI(); + + $future = $api->newFuture('remote --verbose'); + list($lines) = $future->resolve(); + + $pattern = + '(^'. + '(?P[^\t]+)'. + '\t'. + '(?P[^\s]+)'. + ' '. + '\((?Pfetch|push)\)'. + '\z'. + ')'; + + $map = array(); + + $lines = phutil_split_lines($lines, false); + foreach ($lines as $line) { + $matches = null; + if (!preg_match($pattern, $line, $matches)) { + throw new Exception( + pht( + 'Failed to match remote pattern against line "%s".', + $line)); + } + + $name = $matches['name']; + $uri = $matches['uri']; + $mode = $matches['mode']; + + $map[$name][$mode] = $uri; + } + + $refs = array(); + foreach ($map as $name => $uris) { + $fetch_uri = idx($uris, 'fetch'); + $push_uri = idx($uris, 'push'); + + $ref = id(new ArcanistRemoteRef()) + ->setRemoteName($name); + + if ($fetch_uri !== null) { + $ref->setFetchURI($fetch_uri); + } + + if ($push_uri !== null) { + $ref->setPushURI($push_uri); + } + + $refs[] = $ref; + } + + return $refs; + } + +} From 6bf7a40358f8e8d00927ef09061be9fdb5cce88c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 12:07:23 -0700 Subject: [PATCH 65/82] Provide "arc look", a user-facing inspection command Summary: Ref T13546. Currently, "arc which" provides some amount of inspection but it generally isn't very helpful to users and is too limited and inflexible. "arc inspect" is an internal/debugging workflow. The new "arc look" is much more aggressively unhelpful. Test Plan: I'm not sure if this command should allow you to continue at night, because it's too dark. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21376 --- src/__phutil_library_map__.php | 2 + src/repository/remote/ArcanistRemoteRef.php | 4 + src/workflow/ArcanistLookWorkflow.php | 171 ++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 src/workflow/ArcanistLookWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 15bb86ce..12416504 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -325,6 +325,7 @@ phutil_register_library_map(array( 'ArcanistLogMessage' => 'log/ArcanistLogMessage.php', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLogicalOperatorsXHPASTLinterRule.php', 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLogicalOperatorsXHPASTLinterRuleTestCase.php', + 'ArcanistLookWorkflow' => 'workflow/ArcanistLookWorkflow.php', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLowercaseFunctionsXHPASTLinterRule.php', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase.php', 'ArcanistMarkerRef' => 'repository/marker/ArcanistMarkerRef.php', @@ -1354,6 +1355,7 @@ phutil_register_library_map(array( 'ArcanistLogMessage' => 'Phobject', 'ArcanistLogicalOperatorsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLogicalOperatorsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistLookWorkflow' => 'ArcanistArcWorkflow', 'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistMarkerRef' => 'ArcanistRef', diff --git a/src/repository/remote/ArcanistRemoteRef.php b/src/repository/remote/ArcanistRemoteRef.php index c901baec..40092798 100644 --- a/src/repository/remote/ArcanistRemoteRef.php +++ b/src/repository/remote/ArcanistRemoteRef.php @@ -38,4 +38,8 @@ final class ArcanistRemoteRef return $this->pushURI; } + protected function buildRefView(ArcanistRefView $view) { + $view->setObjectName($this->getRemoteName()); + } + } diff --git a/src/workflow/ArcanistLookWorkflow.php b/src/workflow/ArcanistLookWorkflow.php new file mode 100644 index 00000000..58e23e35 --- /dev/null +++ b/src/workflow/ArcanistLookWorkflow.php @@ -0,0 +1,171 @@ +newWorkflowInformation() + ->setSynopsis( + pht('You stand in the middle of a small clearing.')) + ->addExample('**look**') + ->addExample('**look** [options] -- __thing__') + ->setHelp($help); + } + + public function getWorkflowArguments() { + return array( + $this->newWorkflowArgument('argv') + ->setWildcard(true), + ); + } + + public function runWorkflow() { + echo tsprintf( + "%!\n\n", + pht( + 'Arcventure')); + + $argv = $this->getArgument('argv'); + + if ($argv) { + if ($argv === array('remotes')) { + return $this->lookRemotes(); + } + + echo tsprintf( + "%s\n", + pht( + 'You do not see "%s" anywhere.', + implode(' ', $argv))); + + return 1; + } + + echo tsprintf( + "%W\n\n", + pht( + 'You stand in the middle of a small clearing in the woods.')); + + $now = time(); + $hour = (int)date('h', $now); + + if ($hour >= 5 && $hour <= 7) { + $time = pht( + 'It is early morning. Glimses of sunlight peek through the trees '. + 'and you hear the faint sound of birds overhead.'); + } else if ($hour >= 8 && $hour <= 10) { + $time = pht( + 'It is morning. The sun is high in the sky to the east and you hear '. + 'birds all around you. A gentle breeze rustles the leaves overhead.'); + } else if ($hour >= 11 && $hour <= 13) { + $time = pht( + 'It is midday. The sun is high overhead and the air is still. It is '. + 'very warm. You hear the cry of a hawk high overhead and far in the '. + 'distance.'); + } else if ($hour >= 14 && $hour <= 16) { + $time = pht( + 'It is afternoon. The air has changed and it feels as though it '. + 'may rain. You hear a squirrel chittering high overhead.'); + } else if ($hour >= 17 && $hour <= 19) { + $time = pht( + 'It is nearly dusk. The wind has picked up and the trees around you '. + 'sway and rustle.'); + } else if ($hour >= 21 && $hour <= 23) { + $time = pht( + 'It is late in the evening. The air is cool and still, and filled '. + 'with the sound of crickets.'); + } else { + $phase = new PhutilLunarPhase($now); + if ($phase->isNew()) { + $time = pht( + 'Night has fallen, and the thin sliver of moon overhead offers '. + 'no comfort. It is almost pitch black. The night is bitter '. + 'cold. It will be difficult to look around in these conditions.'); + } else if ($phase->isFull()) { + $time = pht( + 'Night has fallen, but your surroundings are illuminated by the '. + 'silvery glow of a full moon overhead. The night is cool and '. + 'the air is crisp. The trees are calm.'); + } else if ($phase->isWaxing()) { + $time = pht( + 'Night has fallen. The moon overhead is waxing, and provides '. + 'just enough light that you can make out your surroundings. It '. + 'is quite cold.'); + } else if ($phase->isWaning()) { + $time = pht( + 'Night has fallen. The moon overhead is waning. You can barely '. + 'make out your surroundings. It is very cold.'); + } + } + + echo tsprintf( + "%W\n\n", + $time); + + echo tsprintf( + "%W\n\n", + pht( + 'Several small trails and footpaths cross here, twisting away from '. + 'you among the trees.')); + + echo tsprintf( + pht("Just ahead to the north, you can see **remotes**.\n")); + + return 0; + } + + private function lookRemotes() { + echo tsprintf( + "%W\n\n", + pht( + 'You follow a wide, straight path to the north and arrive in a '. + 'grove of fruit trees after a few minutes of walking. The grass '. + 'underfoot is thick and small insects flit through the air.')); + + echo tsprintf( + "%W\n\n", + pht( + 'At the far edge of the grove, you see remotes:')); + + $api = $this->getRepositoryAPI(); + + $remotes = $api->newRemoteRefQuery() + ->execute(); + foreach ($remotes as $remote) { + + $view = $remote->newRefView(); + + $push_uri = $remote->getPushURI(); + if ($push_uri === null) { + $push_uri = '-'; + } + + $view->appendLine( + pht( + 'Push URI: %s', + $push_uri)); + + $fetch_uri = $remote->getFetchURI(); + if ($fetch_uri === null) { + $fetch_uri = '-'; + } + + $view->appendLine( + pht( + 'Fetch URI: %s', + $fetch_uri)); + + echo tsprintf('%s', $view); + } + } + +} From 50f7a853b5cf8f33ef3dfc21693ad5b375af2f80 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 13:03:20 -0700 Subject: [PATCH 66/82] Load and map repository objects for remote URIs Summary: Ref T13546. Query and associate known Phabricator repositories to working copy remotes by normalizing and comparing URIs. This primarily gives us access to "permanentRefRules" so we can tell which branches have published changes. Test Plan: Ran "arc look remotes" in Git and Mercurial working copies, saw repositories map properly. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21377 --- src/__phutil_library_map__.php | 2 + src/ref/ArcanistRepositoryRef.php | 42 ++++++++- src/repository/api/ArcanistGitAPI.php | 6 ++ src/repository/api/ArcanistMercurialAPI.php | 6 ++ src/repository/api/ArcanistRepositoryAPI.php | 9 ++ src/repository/remote/ArcanistRemoteRef.php | 47 ++++++++++ ...nistRemoteRepositoryRefsHardpointQuery.php | 89 +++++++++++++++++++ .../remote/ArcanistRepositoryRemoteQuery.php | 5 ++ src/workflow/ArcanistLookWorkflow.php | 29 ++++++ 9 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/repository/remote/ArcanistRemoteRepositoryRefsHardpointQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 12416504..d43615b1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -422,6 +422,7 @@ phutil_register_library_map(array( 'ArcanistRefView' => 'ref/ArcanistRefView.php', 'ArcanistRemoteRef' => 'repository/remote/ArcanistRemoteRef.php', 'ArcanistRemoteRefInspector' => 'repository/remote/ArcanistRemoteRefInspector.php', + 'ArcanistRemoteRepositoryRefsHardpointQuery' => 'repository/remote/ArcanistRemoteRepositoryRefsHardpointQuery.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', @@ -1455,6 +1456,7 @@ phutil_register_library_map(array( ), 'ArcanistRemoteRef' => 'ArcanistRef', 'ArcanistRemoteRefInspector' => 'ArcanistRefInspector', + 'ArcanistRemoteRepositoryRefsHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', diff --git a/src/ref/ArcanistRepositoryRef.php b/src/ref/ArcanistRepositoryRef.php index e7e7a2ee..ffbe52fb 100644 --- a/src/ref/ArcanistRepositoryRef.php +++ b/src/ref/ArcanistRepositoryRef.php @@ -3,6 +3,7 @@ final class ArcanistRepositoryRef extends ArcanistRef { + private $parameters = array(); private $phid; private $browseURI; @@ -24,6 +25,37 @@ final class ArcanistRepositoryRef return $this; } + public static function newFromConduit(array $map) { + $ref = new self(); + $ref->parameters = $map; + + $ref->phid = $map['phid']; + + return $ref; + } + + public function getURIs() { + $uris = idxv($this->parameters, array('attachments', 'uris', 'uris')); + + if (!$uris) { + return array(); + } + + $results = array(); + foreach ($uris as $uri) { + $effective_uri = idxv($uri, array('fields', 'uri', 'effective')); + if ($effective_uri !== null) { + $results[] = $effective_uri; + } + } + + return $results; + } + + public function getDisplayName() { + return idxv($this->parameters, array('fields', 'name')); + } + public function newBrowseURI(array $params) { PhutilTypeSpec::checkMap( $params, @@ -67,9 +99,13 @@ final class ArcanistRepositoryRef } public function getDefaultBranch() { - // TODO: This should read from the remote, and is not correct for - // Mercurial anyway, as "default" would be a better default branch. - return 'master'; + $branch = idxv($this->parameters, array('fields', 'defaultBranch')); + + if ($branch === null) { + return 'master'; + } + + return $branch; } } diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index ebd2d1d9..5b5c56cc 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1767,4 +1767,10 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return new ArcanistGitRepositoryRemoteQuery(); } + protected function newNormalizedURI($uri) { + return new ArcanistRepositoryURINormalizer( + ArcanistRepositoryURINormalizer::TYPE_GIT, + $uri); + } + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index fdb5e1e0..a24cb13f 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1025,4 +1025,10 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { ); } + protected function newNormalizedURI($uri) { + return new ArcanistRepositoryURINormalizer( + ArcanistRepositoryURINormalizer::TYPE_MERCURIAL, + $uri); + } + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 070fd085..79052eaa 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -798,4 +798,13 @@ abstract class ArcanistRepositoryAPI extends Phobject { return substr($hash, 0, 12); } + final public function getNormalizedURI($uri) { + $normalized_uri = $this->newNormalizedURI($uri); + return $normalized_uri->getNormalizedURI(); + } + + protected function newNormalizedURI($uri) { + return $uri; + } + } diff --git a/src/repository/remote/ArcanistRemoteRef.php b/src/repository/remote/ArcanistRemoteRef.php index 40092798..aea02147 100644 --- a/src/repository/remote/ArcanistRemoteRef.php +++ b/src/repository/remote/ArcanistRemoteRef.php @@ -3,14 +3,26 @@ final class ArcanistRemoteRef extends ArcanistRef { + private $repositoryAPI; private $remoteName; private $fetchURI; private $pushURI; + const HARDPOINT_REPOSITORYREFS = 'arc.remote.repositoryRefs'; + public function getRefDisplayName() { return pht('Remote "%s"', $this->getRemoteName()); } + public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) { + $this->repositoryAPI = $repository_api; + return $this; + } + + public function getRepositoryAPI() { + return $this->repositoryAPI; + } + public function setRemoteName($remote_name) { $this->remoteName = $remote_name; return $this; @@ -42,4 +54,39 @@ final class ArcanistRemoteRef $view->setObjectName($this->getRemoteName()); } + protected function newHardpoints() { + $object_list = new ArcanistObjectListHardpoint(); + return array( + $this->newTemplateHardpoint(self::HARDPOINT_REPOSITORYREFS, $object_list), + ); + } + + private function getRepositoryRefs() { + return $this->getHardpoint(self::HARDPOINT_REPOSITORYREFS); + } + + public function getPushRepositoryRef() { + return $this->getRepositoryRefByURI($this->getPushURI()); + } + + public function getFetchRepositoryRef() { + return $this->getRepositoryRefByURI($this->getFetchURI()); + } + + private function getRepositoryRefByURI($uri) { + $api = $this->getRepositoryAPI(); + + $uri = $api->getNormalizedURI($uri); + foreach ($this->getRepositoryRefs() as $repository_ref) { + foreach ($repository_ref->getURIs() as $repository_uri) { + $repository_uri = $api->getNormalizedURI($repository_uri); + if ($repository_uri === $uri) { + return $repository_ref; + } + } + } + + return null; + } + } diff --git a/src/repository/remote/ArcanistRemoteRepositoryRefsHardpointQuery.php b/src/repository/remote/ArcanistRemoteRepositoryRefsHardpointQuery.php new file mode 100644 index 00000000..9969203f --- /dev/null +++ b/src/repository/remote/ArcanistRemoteRepositoryRefsHardpointQuery.php @@ -0,0 +1,89 @@ +getRepositoryAPI(); + + $uris = array(); + foreach ($refs as $remote) { + $fetch_uri = $remote->getFetchURI(); + if ($fetch_uri !== null) { + $uris[] = $fetch_uri; + } + + $push_uri = $remote->getPushURI(); + if ($push_uri !== null) { + $uris[] = $push_uri; + } + } + + if (!$uris) { + yield $this->yieldValue($refs, array()); + } + + $uris = array_fuse($uris); + $uris = array_values($uris); + + $search_future = $this->newConduitSearch( + 'diffusion.repository.search', + array( + 'uris' => $uris, + ), + array( + 'uris' => true, + )); + + $repository_info = (yield $this->yieldFuture($search_future)); + + $repository_refs = array(); + foreach ($repository_info as $raw_result) { + $repository_refs[] = ArcanistRepositoryRef::newFromConduit($raw_result); + } + + $uri_map = array(); + foreach ($repository_refs as $repository_ref) { + foreach ($repository_ref->getURIs() as $repository_uri) { + $repository_uri = $api->getNormalizedURI($repository_uri); + $uri_map[$repository_uri] = $repository_ref; + } + } + + $results = array(); + foreach ($refs as $key => $remote) { + $result = array(); + + $fetch_uri = $remote->getFetchURI(); + if ($fetch_uri !== null) { + $fetch_uri = $api->getNormalizedURI($fetch_uri); + if (isset($uri_map[$fetch_uri])) { + $result[] = $uri_map[$fetch_uri]; + } + } + + $push_uri = $remote->getPushURI(); + if ($push_uri !== null) { + $push_uri = $api->getNormalizedURI($push_uri); + if (isset($uri_map[$push_uri])) { + $result[] = $uri_map[$push_uri]; + } + } + + $results[$key] = $result; + } + + yield $this->yieldMap($results); + } + +} diff --git a/src/repository/remote/ArcanistRepositoryRemoteQuery.php b/src/repository/remote/ArcanistRepositoryRemoteQuery.php index cff6a185..9918f2ac 100644 --- a/src/repository/remote/ArcanistRepositoryRemoteQuery.php +++ b/src/repository/remote/ArcanistRepositoryRemoteQuery.php @@ -11,8 +11,13 @@ abstract class ArcanistRepositoryRemoteQuery } final public function execute() { + $api = $this->getRepositoryAPI(); $refs = $this->newRemoteRefs(); + foreach ($refs as $ref) { + $ref->setRepositoryAPI($api); + } + $names = $this->names; if ($names !== null) { $names = array_fuse($names); diff --git a/src/workflow/ArcanistLookWorkflow.php b/src/workflow/ArcanistLookWorkflow.php index 58e23e35..d4f25022 100644 --- a/src/workflow/ArcanistLookWorkflow.php +++ b/src/workflow/ArcanistLookWorkflow.php @@ -140,6 +140,11 @@ EOTEXT $remotes = $api->newRemoteRefQuery() ->execute(); + + $this->loadHardpoints( + $remotes, + ArcanistRemoteRef::HARDPOINT_REPOSITORYREFS); + foreach ($remotes as $remote) { $view = $remote->newRefView(); @@ -154,6 +159,18 @@ EOTEXT 'Push URI: %s', $push_uri)); + $push_repository = $remote->getPushRepositoryRef(); + if ($push_repository) { + $push_display = $push_repository->getDisplayName(); + } else { + $push_display = '-'; + } + + $view->appendLine( + pht( + 'Push Repository: %s', + $push_display)); + $fetch_uri = $remote->getFetchURI(); if ($fetch_uri === null) { $fetch_uri = '-'; @@ -164,6 +181,18 @@ EOTEXT 'Fetch URI: %s', $fetch_uri)); + $fetch_repository = $remote->getFetchRepositoryRef(); + if ($fetch_repository) { + $fetch_display = $fetch_repository->getDisplayName(); + } else { + $fetch_display = '-'; + } + + $view->appendLine( + pht( + 'Fetch Repository: %s', + $fetch_display)); + echo tsprintf('%s', $view); } } From 80f5166b701d788fb598c98d60fd707ea85fff6b Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 14:49:06 -0700 Subject: [PATCH 67/82] Identify published commits in working copies by using remote configuration Summary: Ref T13546. When running "arc branches", we want to show all unpublished commits. This is often a different set of commits than "commits not present in any remote". Attempt to identify published commits by using the permanent ref rules in Phabricator. Test Plan: Ran "arc look published", saw sensible published commits in Git. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21378 --- src/ref/ArcanistRepositoryRef.php | 35 ++++++++++++++ src/repository/api/ArcanistGitAPI.php | 43 +++++++++++++++++ src/repository/api/ArcanistRepositoryAPI.php | 8 ++++ .../ArcanistGitRepositoryMarkerQuery.php | 21 +++++++-- src/repository/marker/ArcanistMarkerRef.php | 10 ++++ .../marker/ArcanistRepositoryMarkerQuery.php | 16 +++++++ src/repository/remote/ArcanistRemoteRef.php | 9 ++++ src/workflow/ArcanistLookWorkflow.php | 46 +++++++++++++++++++ 8 files changed, 185 insertions(+), 3 deletions(-) diff --git a/src/ref/ArcanistRepositoryRef.php b/src/ref/ArcanistRepositoryRef.php index ffbe52fb..b0122ab4 100644 --- a/src/ref/ArcanistRepositoryRef.php +++ b/src/ref/ArcanistRepositoryRef.php @@ -108,4 +108,39 @@ final class ArcanistRepositoryRef return $branch; } + public function isPermanentRef(ArcanistMarkerRef $ref) { + $rules = idxv( + $this->parameters, + array('fields', 'refRules', 'permanentRefRules')); + + if ($rules === null) { + return false; + } + + // If the rules exist but there are no specified rules, treat every ref + // as permanent. + if (!$rules) { + return true; + } + + // TODO: It would be nice to unify evaluation of permanent ref rules + // across Arcanist and Phabricator. + + $ref_name = $ref->getName(); + foreach ($rules as $rule) { + $matches = null; + if (preg_match('(^regexp\\((.*)\\)\z)', $rule, $matches)) { + if (preg_match($matches[1], $ref_name)) { + return true; + } + } else { + if ($rule === $ref_name) { + return true; + } + } + } + + return false; + } + } diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index 5b5c56cc..fb2e883a 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1773,4 +1773,47 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { $uri); } + protected function newPublishedCommitHashes() { + $remotes = $this->newRemoteRefQuery() + ->execute(); + if (!$remotes) { + return array(); + } + + $markers = $this->newMarkerRefQuery() + ->withIsRemoteCache(true) + ->execute(); + + if (!$markers) { + return array(); + } + + $runtime = $this->getRuntime(); + $workflow = $runtime->getCurrentWorkflow(); + + $workflow->loadHardpoints( + $remotes, + ArcanistRemoteRef::HARDPOINT_REPOSITORYREFS); + + $remotes = mpull($remotes, null, 'getRemoteName'); + + $hashes = array(); + + foreach ($markers as $marker) { + $remote_name = $marker->getRemoteName(); + $remote = idx($remotes, $remote_name); + if (!$remote) { + continue; + } + + if (!$remote->isPermanentRef($marker)) { + continue; + } + + $hashes[] = $marker->getCommitHash(); + } + + return $hashes; + } + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 79052eaa..2a5e3555 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -807,4 +807,12 @@ abstract class ArcanistRepositoryAPI extends Phobject { return $uri; } + final public function getPublishedCommitHashes() { + return $this->newPublishedCommitHashes(); + } + + protected function newPublishedCommitHashes() { + return array(); + } + } diff --git a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php index a1d747c6..2c634b66 100644 --- a/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistGitRepositoryMarkerQuery.php @@ -23,11 +23,11 @@ final class ArcanistGitRepositoryMarkerQuery $branch_prefix = 'refs/heads/'; $branch_length = strlen($branch_prefix); - // NOTE: Since we only return branches today, we restrict this operation - // to branches. + $remote_prefix = 'refs/remotes/'; + $remote_length = strlen($remote_prefix); list($stdout) = $api->newFuture( - 'for-each-ref --format %s -- refs/heads/', + 'for-each-ref --format %s -- refs/', implode('%01', $field_list))->resolve(); $markers = array(); @@ -53,9 +53,20 @@ final class ArcanistGitRepositoryMarkerQuery list($ref, $hash, $epoch, $tree, $dst_hash, $summary, $text) = $fields; + $remote_name = null; + if (!strncmp($ref, $branch_prefix, $branch_length)) { $type = ArcanistMarkerRef::TYPE_BRANCH; $name = substr($ref, $branch_length); + } else if (!strncmp($ref, $remote_prefix, $remote_length)) { + // This isn't entirely correct: the ref may be a tag, etc. + $type = ArcanistMarkerRef::TYPE_BRANCH; + + $label = substr($ref, $remote_length); + $parts = explode('/', $label, 2); + + $remote_name = $parts[0]; + $name = $parts[1]; } else { // For now, discard other refs. continue; @@ -70,6 +81,10 @@ final class ArcanistGitRepositoryMarkerQuery ->setSummary($summary) ->setMessage($text); + if ($remote_name !== null) { + $marker->setRemoteName($remote_name); + } + if (strlen($dst_hash)) { $commit_hash = $dst_hash; } else { diff --git a/src/repository/marker/ArcanistMarkerRef.php b/src/repository/marker/ArcanistMarkerRef.php index a945d572..c580ab56 100644 --- a/src/repository/marker/ArcanistMarkerRef.php +++ b/src/repository/marker/ArcanistMarkerRef.php @@ -20,6 +20,7 @@ final class ArcanistMarkerRef private $summary; private $message; private $isActive = false; + private $remoteName; public function getRefDisplayName() { switch ($this->getMarkerType()) { @@ -130,6 +131,15 @@ final class ArcanistMarkerRef return $this->isActive; } + public function setRemoteName($remote_name) { + $this->remoteName = $remote_name; + return $this; + } + + public function getRemoteName() { + return $this->remoteName; + } + public function isBookmark() { return ($this->getMarkerType() === self::TYPE_BOOKMARK); } diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index b39c232b..02e12d64 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -9,6 +9,7 @@ abstract class ArcanistRepositoryMarkerQuery private $commitHashes; private $ancestorCommitHashes; private $remotes; + private $isRemoteCache = false; final public function withMarkerTypes(array $types) { $this->markerTypes = array_fuse($types); @@ -26,6 +27,11 @@ abstract class ArcanistRepositoryMarkerQuery return $this; } + final public function withIsRemoteCache($is_cache) { + $this->isRemoteCache = $is_cache; + return $this; + } + final public function withIsActive($active) { $this->isActive = $active; return $this; @@ -88,6 +94,16 @@ abstract class ArcanistRepositoryMarkerQuery } } + if ($this->isRemoteCache !== null) { + $want_cache = $this->isRemoteCache; + foreach ($markers as $key => $marker) { + $is_cache = ($marker->getRemoteName() !== null); + if ($is_cache !== $want_cache) { + unset($markers[$key]); + } + } + } + return $this->sortMarkers($markers); } diff --git a/src/repository/remote/ArcanistRemoteRef.php b/src/repository/remote/ArcanistRemoteRef.php index aea02147..7a34e1bd 100644 --- a/src/repository/remote/ArcanistRemoteRef.php +++ b/src/repository/remote/ArcanistRemoteRef.php @@ -89,4 +89,13 @@ final class ArcanistRemoteRef return null; } + public function isPermanentRef(ArcanistMarkerRef $ref) { + $repository_ref = $this->getPushRepositoryRef(); + if (!$repository_ref) { + return false; + } + + return $repository_ref->isPermanentRef($ref); + } + } diff --git a/src/workflow/ArcanistLookWorkflow.php b/src/workflow/ArcanistLookWorkflow.php index d4f25022..e54fffe9 100644 --- a/src/workflow/ArcanistLookWorkflow.php +++ b/src/workflow/ArcanistLookWorkflow.php @@ -41,6 +41,10 @@ EOTEXT return $this->lookRemotes(); } + if ($argv === array('published')) { + return $this->lookPublished(); + } + echo tsprintf( "%s\n", pht( @@ -195,6 +199,48 @@ EOTEXT echo tsprintf('%s', $view); } + + echo tsprintf("\n"); + echo tsprintf( + pht( + "Across the grove, a stream flows north toward ". + "**published** commits.\n")); + } + + private function lookPublished() { + echo tsprintf( + "%W\n\n", + pht( + 'You walk along the narrow bank of the stream as it winds lazily '. + 'downhill and turns east, gradually widening into a river.')); + + $api = $this->getRepositoryAPI(); + + $published = $api->getPublishedCommitHashes(); + + if ($published) { + echo tsprintf( + "%W\n\n", + pht( + 'Floating on the water, you see published commits:')); + + foreach ($published as $hash) { + echo tsprintf( + "%s\n", + $hash); + } + + echo tsprintf( + "\n%W\n", + pht( + 'They river bubbles peacefully.')); + } else { + echo tsprintf( + "%W\n", + pht( + 'The river bubbles quietly, but you do not see any published '. + 'commits anywhere.')); + } } } From cd19216ea28f1097e6b003f4975e1d1c9f4baff8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 15 Jun 2020 08:12:54 -0700 Subject: [PATCH 68/82] Render "arc markers" workflows as a tree, not a list Summary: Ref T13546. Currently, each "land" workflow executes custom graph queries to find commits: move toward abstracting this logic. The "land" workflow also has a potentially dangerous behavior: if you have "master > A > B > C" and "arc land C", it will land A, B, and C. However, an updated version of A or B may exist elsewhere in the working copy. If it does, "arc land" will incorrectly land an out-of-date set of changes. To find newer versions of "A" and "B", we need to search backwards from all local markers to the nearest outgoing marker, then compare the sets of changes we find to the sets of changes selected by "arc land". This is also roughly the workflow that "arc branches", etc., need to show local markers as a tree, and starting in "arc branches" allows the process to be visualized. As implemented here ,this rendering is still somewhat rough, and the selection of "outgoing markers" isn't good. In Mercurial, we may plausibly be able to use phase markers, but in Git we likely can't guess the right behavior automatically and probably need additional configuration. Test Plan: Ran "arc branches" and "arc bookmarks" in Git and Mercurial. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21363 --- src/__phutil_library_map__.php | 26 ++ src/repository/api/ArcanistGitAPI.php | 4 + src/repository/api/ArcanistMercurialAPI.php | 4 + src/repository/api/ArcanistRepositoryAPI.php | 19 + src/repository/graph/ArcanistCommitGraph.php | 55 +++ .../graph/ArcanistCommitGraphPartition.php | 62 +++ .../ArcanistCommitGraphPartitionQuery.php | 153 +++++++ .../graph/ArcanistCommitGraphSet.php | 97 +++++ .../graph/ArcanistCommitGraphSetQuery.php | 305 +++++++++++++ src/repository/graph/ArcanistCommitNode.php | 78 ++++ .../__tests__/ArcanistCommitGraphTestCase.php | 56 +++ .../graph/query/ArcanistCommitGraphQuery.php | 69 +++ .../query/ArcanistGitCommitGraphQuery.php | 150 +++++++ .../ArcanistMercurialCommitGraphQuery.php | 180 ++++++++ .../query/ArcanistSimpleCommitGraphQuery.php | 50 +++ .../view/ArcanistCommitGraphSetTreeView.php | 147 +++++++ .../graph/view/ArcanistCommitGraphSetView.php | 407 ++++++++++++++++++ src/workflow/ArcanistMarkersWorkflow.php | 320 +++++++++++--- 18 files changed, 2114 insertions(+), 68 deletions(-) create mode 100644 src/repository/graph/ArcanistCommitGraph.php create mode 100644 src/repository/graph/ArcanistCommitGraphPartition.php create mode 100644 src/repository/graph/ArcanistCommitGraphPartitionQuery.php create mode 100644 src/repository/graph/ArcanistCommitGraphSet.php create mode 100644 src/repository/graph/ArcanistCommitGraphSetQuery.php create mode 100644 src/repository/graph/ArcanistCommitNode.php create mode 100644 src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php create mode 100644 src/repository/graph/query/ArcanistCommitGraphQuery.php create mode 100644 src/repository/graph/query/ArcanistGitCommitGraphQuery.php create mode 100644 src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php create mode 100644 src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php create mode 100644 src/repository/graph/view/ArcanistCommitGraphSetTreeView.php create mode 100644 src/repository/graph/view/ArcanistCommitGraphSetView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d43615b1..c988291b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -106,6 +106,16 @@ phutil_register_library_map(array( 'ArcanistCommentSpacingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentSpacingXHPASTLinterRule.php', 'ArcanistCommentStyleXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistCommentStyleXHPASTLinterRule.php', 'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistCommentStyleXHPASTLinterRuleTestCase.php', + 'ArcanistCommitGraph' => 'repository/graph/ArcanistCommitGraph.php', + 'ArcanistCommitGraphPartition' => 'repository/graph/ArcanistCommitGraphPartition.php', + 'ArcanistCommitGraphPartitionQuery' => 'repository/graph/ArcanistCommitGraphPartitionQuery.php', + 'ArcanistCommitGraphQuery' => 'repository/graph/query/ArcanistCommitGraphQuery.php', + 'ArcanistCommitGraphSet' => 'repository/graph/ArcanistCommitGraphSet.php', + 'ArcanistCommitGraphSetQuery' => 'repository/graph/ArcanistCommitGraphSetQuery.php', + 'ArcanistCommitGraphSetTreeView' => 'repository/graph/view/ArcanistCommitGraphSetTreeView.php', + 'ArcanistCommitGraphSetView' => 'repository/graph/view/ArcanistCommitGraphSetView.php', + 'ArcanistCommitGraphTestCase' => 'repository/graph/__tests__/ArcanistCommitGraphTestCase.php', + 'ArcanistCommitNode' => 'repository/graph/ArcanistCommitNode.php', 'ArcanistCommitRef' => 'ref/commit/ArcanistCommitRef.php', 'ArcanistCommitSymbolRef' => 'ref/commit/ArcanistCommitSymbolRef.php', 'ArcanistCommitSymbolRefInspector' => 'ref/commit/ArcanistCommitSymbolRefInspector.php', @@ -211,6 +221,7 @@ phutil_register_library_map(array( 'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php', 'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php', 'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php', + 'ArcanistGitCommitGraphQuery' => 'repository/graph/query/ArcanistGitCommitGraphQuery.php', 'ArcanistGitCommitMessageHardpointQuery' => 'query/ArcanistGitCommitMessageHardpointQuery.php', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php', 'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php', @@ -331,6 +342,7 @@ phutil_register_library_map(array( 'ArcanistMarkerRef' => 'repository/marker/ArcanistMarkerRef.php', 'ArcanistMarkersWorkflow' => 'workflow/ArcanistMarkersWorkflow.php', 'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php', + 'ArcanistMercurialCommitGraphQuery' => 'repository/graph/query/ArcanistMercurialCommitGraphQuery.php', 'ArcanistMercurialCommitMessageHardpointQuery' => 'query/ArcanistMercurialCommitMessageHardpointQuery.php', 'ArcanistMercurialLandEngine' => 'land/engine/ArcanistMercurialLandEngine.php', 'ArcanistMercurialLocalState' => 'repository/state/ArcanistMercurialLocalState.php', @@ -465,6 +477,7 @@ phutil_register_library_map(array( 'ArcanistSetting' => 'configuration/ArcanistSetting.php', 'ArcanistSettings' => 'configuration/ArcanistSettings.php', 'ArcanistShellCompleteWorkflow' => 'toolset/workflow/ArcanistShellCompleteWorkflow.php', + 'ArcanistSimpleCommitGraphQuery' => 'repository/graph/query/ArcanistSimpleCommitGraphQuery.php', 'ArcanistSimpleSymbolHardpointQuery' => 'ref/simple/ArcanistSimpleSymbolHardpointQuery.php', 'ArcanistSimpleSymbolRef' => 'ref/simple/ArcanistSimpleSymbolRef.php', 'ArcanistSimpleSymbolRefInspector' => 'ref/simple/ArcanistSimpleSymbolRefInspector.php', @@ -1137,6 +1150,16 @@ phutil_register_library_map(array( 'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCommentStyleXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistCommitGraph' => 'Phobject', + 'ArcanistCommitGraphPartition' => 'Phobject', + 'ArcanistCommitGraphPartitionQuery' => 'Phobject', + 'ArcanistCommitGraphQuery' => 'Phobject', + 'ArcanistCommitGraphSet' => 'Phobject', + 'ArcanistCommitGraphSetQuery' => 'Phobject', + 'ArcanistCommitGraphSetTreeView' => 'Phobject', + 'ArcanistCommitGraphSetView' => 'Phobject', + 'ArcanistCommitGraphTestCase' => 'PhutilTestCase', + 'ArcanistCommitNode' => 'Phobject', 'ArcanistCommitRef' => 'ArcanistRef', 'ArcanistCommitSymbolRef' => 'ArcanistSymbolRef', 'ArcanistCommitSymbolRefInspector' => 'ArcanistRefInspector', @@ -1242,6 +1265,7 @@ phutil_register_library_map(array( 'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow', 'ArcanistGitAPI' => 'ArcanistRepositoryAPI', + 'ArcanistGitCommitGraphQuery' => 'ArcanistCommitGraphQuery', 'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery', 'ArcanistGitLandEngine' => 'ArcanistLandEngine', @@ -1362,6 +1386,7 @@ phutil_register_library_map(array( 'ArcanistMarkerRef' => 'ArcanistRef', 'ArcanistMarkersWorkflow' => 'ArcanistArcWorkflow', 'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI', + 'ArcanistMercurialCommitGraphQuery' => 'ArcanistCommitGraphQuery', 'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery', 'ArcanistMercurialLandEngine' => 'ArcanistLandEngine', 'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState', @@ -1498,6 +1523,7 @@ phutil_register_library_map(array( 'ArcanistSetting' => 'Phobject', 'ArcanistSettings' => 'Phobject', 'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow', + 'ArcanistSimpleCommitGraphQuery' => 'ArcanistCommitGraphQuery', 'ArcanistSimpleSymbolHardpointQuery' => 'ArcanistRuntimeHardpointQuery', 'ArcanistSimpleSymbolRef' => 'ArcanistSymbolRef', 'ArcanistSimpleSymbolRefInspector' => 'ArcanistRefInspector', diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php index fb2e883a..2ce9e41b 100644 --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -1816,4 +1816,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI { return $hashes; } + protected function newCommitGraphQueryTemplate() { + return new ArcanistGitCommitGraphQuery(); + } + } diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index a24cb13f..9915a879 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1031,4 +1031,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { $uri); } + protected function newCommitGraphQueryTemplate() { + return new ArcanistMercurialCommitGraphQuery(); + } + } diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php index 2a5e3555..48b44f66 100644 --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -42,6 +42,7 @@ abstract class ArcanistRepositoryAPI extends Phobject { private $runtime; private $currentWorkingCopyStateRef = false; private $currentCommitRef = false; + private $graph; abstract public function getSourceControlSystemName(); @@ -794,10 +795,19 @@ abstract class ArcanistRepositoryAPI extends Phobject { throw new PhutilMethodNotImplementedException(); } + final public function newCommitGraphQuery() { + return id($this->newCommitGraphQueryTemplate()); + } + + protected function newCommitGraphQueryTemplate() { + throw new PhutilMethodNotImplementedException(); + } + final public function getDisplayHash($hash) { return substr($hash, 0, 12); } + final public function getNormalizedURI($uri) { $normalized_uri = $this->newNormalizedURI($uri); return $normalized_uri->getNormalizedURI(); @@ -815,4 +825,13 @@ abstract class ArcanistRepositoryAPI extends Phobject { return array(); } + final public function getGraph() { + if (!$this->graph) { + $this->graph = id(new ArcanistCommitGraph()) + ->setRepositoryAPI($this); + } + + return $this->graph; + } + } diff --git a/src/repository/graph/ArcanistCommitGraph.php b/src/repository/graph/ArcanistCommitGraph.php new file mode 100644 index 00000000..67d80458 --- /dev/null +++ b/src/repository/graph/ArcanistCommitGraph.php @@ -0,0 +1,55 @@ +repositoryAPI = $api; + return $this; + } + + public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + public function getNode($hash) { + if (isset($this->nodes[$hash])) { + return $this->nodes[$hash]; + } else { + return null; + } + } + + public function getNodes() { + return $this->nodes; + } + + public function newQuery() { + $api = $this->getRepositoryAPI(); + return $api->newCommitGraphQuery() + ->setGraph($this); + } + + public function newNode($hash) { + if (isset($this->nodes[$hash])) { + throw new Exception( + pht( + 'Graph already has a node "%s"!', + $hash)); + } + + $this->nodes[$hash] = id(new ArcanistCommitNode()) + ->setCommitHash($hash); + + return $this->nodes[$hash]; + } + + public function newPartitionQuery() { + return id(new ArcanistCommitGraphPartitionQuery()) + ->setGraph($this); + } + +} diff --git a/src/repository/graph/ArcanistCommitGraphPartition.php b/src/repository/graph/ArcanistCommitGraphPartition.php new file mode 100644 index 00000000..b072f6bd --- /dev/null +++ b/src/repository/graph/ArcanistCommitGraphPartition.php @@ -0,0 +1,62 @@ +graph = $graph; + return $this; + } + + public function getGraph() { + return $this->graph; + } + + public function setHashes(array $hashes) { + $this->hashes = $hashes; + return $this; + } + + public function getHashes() { + return $this->hashes; + } + + public function setHeads(array $heads) { + $this->heads = $heads; + return $this; + } + + public function getHeads() { + return $this->heads; + } + + public function setTails($tails) { + $this->tails = $tails; + return $this; + } + + public function getTails() { + return $this->tails; + } + + public function setWaypoints($waypoints) { + $this->waypoints = $waypoints; + return $this; + } + + public function getWaypoints() { + return $this->waypoints; + } + + public function newSetQuery() { + return id(new ArcanistCommitGraphSetQuery()) + ->setPartition($this); + } + +} diff --git a/src/repository/graph/ArcanistCommitGraphPartitionQuery.php b/src/repository/graph/ArcanistCommitGraphPartitionQuery.php new file mode 100644 index 00000000..2a20566a --- /dev/null +++ b/src/repository/graph/ArcanistCommitGraphPartitionQuery.php @@ -0,0 +1,153 @@ +graph = $graph; + return $this; + } + + public function getGraph() { + return $this->graph; + } + + public function withHeads(array $heads) { + $this->heads = $heads; + return $this; + } + + public function withHashes(array $hashes) { + $this->hashes = $hashes; + return $this; + } + + public function execute() { + $graph = $this->getGraph(); + + $heads = $this->heads; + $heads = array_fuse($heads); + if (!$heads) { + throw new Exception(pht('Partition query requires heads.')); + } + + $waypoints = $heads; + + $stack = array(); + $partitions = array(); + $partition_identities = array(); + $n = 0; + foreach ($heads as $hash) { + $node = $graph->getNode($hash); + + if (!$node) { + echo "TODO: WARNING: Bad hash {$hash}\n"; + continue; + } + + $partitions[$hash] = $n; + $partition_identities[$n] = array($n => $n); + $n++; + + $stack[] = $node; + } + + $scope = null; + if ($this->hashes) { + $scope = array_fuse($this->hashes); + } + + $leaves = array(); + while ($stack) { + $node = array_pop($stack); + + $node_hash = $node->getCommitHash(); + $node_partition = $partition_identities[$partitions[$node_hash]]; + + $saw_parent = false; + foreach ($node->getParentNodes() as $parent) { + $parent_hash = $parent->getCommitHash(); + + if ($scope !== null) { + if (!isset($scope[$parent_hash])) { + continue; + } + } + + $saw_parent = true; + + if (isset($partitions[$parent_hash])) { + $parent_partition = $partition_identities[$partitions[$parent_hash]]; + + // If we've reached this node from a child, it clearly is not a + // head. + unset($heads[$parent_hash]); + + // If we've reached a node which is already part of another + // partition, we can stop following it and merge the partitions. + + $new_partition = $node_partition + $parent_partition; + ksort($new_partition); + + if ($node_partition !== $new_partition) { + foreach ($node_partition as $partition_id) { + $partition_identities[$partition_id] = $new_partition; + } + } + + if ($parent_partition !== $new_partition) { + foreach ($parent_partition as $partition_id) { + $partition_identities[$partition_id] = $new_partition; + } + } + continue; + } else { + $partitions[$parent_hash] = $partitions[$node_hash]; + } + + $stack[] = $parent; + } + + if (!$saw_parent) { + $leaves[$node_hash] = true; + } + } + + $partition_lists = array(); + $partition_heads = array(); + $partition_waypoints = array(); + $partition_leaves = array(); + foreach ($partitions as $hash => $partition) { + $partition = reset($partition_identities[$partition]); + $partition_lists[$partition][] = $hash; + if (isset($heads[$hash])) { + $partition_heads[$partition][] = $hash; + } + if (isset($waypoints[$hash])) { + $partition_waypoints[$partition][] = $hash; + } + if (isset($leaves[$hash])) { + $partition_leaves[$partition][] = $hash; + } + } + + $results = array(); + foreach ($partition_lists as $partition_id => $partition_list) { + $partition_set = array_fuse($partition_list); + + $results[] = id(new ArcanistCommitGraphPartition()) + ->setGraph($graph) + ->setHashes($partition_set) + ->setHeads($partition_heads[$partition_id]) + ->setWaypoints($partition_waypoints[$partition_id]) + ->setTails($partition_leaves[$partition_id]); + } + + return $results; + } + +} diff --git a/src/repository/graph/ArcanistCommitGraphSet.php b/src/repository/graph/ArcanistCommitGraphSet.php new file mode 100644 index 00000000..f8ce61b1 --- /dev/null +++ b/src/repository/graph/ArcanistCommitGraphSet.php @@ -0,0 +1,97 @@ +color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + + public function setHashes($hashes) { + $this->hashes = $hashes; + return $this; + } + + public function getHashes() { + return $this->hashes; + } + + public function setSetID($set_id) { + $this->setID = $set_id; + return $this; + } + + public function getSetID() { + return $this->setID; + } + + public function setParentHashes($parent_hashes) { + $this->parentHashes = $parent_hashes; + return $this; + } + + public function getParentHashes() { + return $this->parentHashes; + } + + public function setChildHashes($child_hashes) { + $this->childHashes = $child_hashes; + return $this; + } + + public function getChildHashes() { + return $this->childHashes; + } + + public function setParentSets($parent_sets) { + $this->parentSets = $parent_sets; + return $this; + } + + public function getParentSets() { + return $this->parentSets; + } + + public function setChildSets($child_sets) { + $this->childSets = $child_sets; + return $this; + } + + public function getChildSets() { + return $this->childSets; + } + + public function setDisplayDepth($display_depth) { + $this->displayDepth = $display_depth; + return $this; + } + + public function getDisplayDepth() { + return $this->displayDepth; + } + + public function setDisplayChildSets(array $display_child_sets) { + $this->displayChildSets = $display_child_sets; + return $this; + } + + public function getDisplayChildSets() { + return $this->displayChildSets; + } + +} diff --git a/src/repository/graph/ArcanistCommitGraphSetQuery.php b/src/repository/graph/ArcanistCommitGraphSetQuery.php new file mode 100644 index 00000000..2b5df45f --- /dev/null +++ b/src/repository/graph/ArcanistCommitGraphSetQuery.php @@ -0,0 +1,305 @@ +partition = $partition; + return $this; + } + + public function getPartition() { + return $this->partition; + } + + public function setWaypointMap(array $waypoint_map) { + $this->waypointMap = $waypoint_map; + return $this; + } + + public function getWaypointMap() { + return $this->waypointMap; + } + + public function execute() { + $partition = $this->getPartition(); + $graph = $partition->getGraph(); + + $waypoint_color = array(); + $color = array(); + + $waypoints = $this->getWaypointMap(); + foreach ($waypoints as $waypoint => $colors) { + // TODO: Validate that "$waypoint" is in the partition. + // TODO: Validate that "$colors" is a list of scalars. + $waypoint_color[$waypoint] = $this->newColorFromRaw($colors); + } + + $stack = array(); + + $hashes = $partition->getTails(); + foreach ($hashes as $hash) { + $stack[] = $graph->getNode($hash); + + if (isset($waypoint_color[$hash])) { + $color[$hash] = $waypoint_color[$hash]; + } else { + $color[$hash] = true; + } + } + + $partition_map = $partition->getHashes(); + + $wait = array(); + foreach ($partition_map as $hash) { + $node = $graph->getNode($hash); + + $incoming = $node->getParentNodes(); + if (count($incoming) < 2) { + // If the node has one or fewer incoming edges, we can paint it as soon + // as we reach it. + continue; + } + + // Discard incoming edges which aren't in the partition. + $need = array(); + foreach ($incoming as $incoming_node) { + $incoming_hash = $incoming_node->getCommitHash(); + + if (!isset($partition_map[$incoming_hash])) { + continue; + } + + $need[] = $incoming_hash; + } + + $need_count = count($need); + if ($need_count < 2) { + // If we have one or fewer incoming edges in the partition, we can + // paint as soon as we reach the node. + continue; + } + + $wait[$hash] = $need_count; + } + + while ($stack) { + $node = array_pop($stack); + $node_hash = $node->getCommitHash(); + + $node_color = $color[$node_hash]; + + $outgoing_nodes = $node->getChildNodes(); + + foreach ($outgoing_nodes as $outgoing_node) { + $outgoing_hash = $outgoing_node->getCommitHash(); + + if (isset($waypoint_color[$outgoing_hash])) { + $color[$outgoing_hash] = $waypoint_color[$outgoing_hash]; + } else if (isset($color[$outgoing_hash])) { + $color[$outgoing_hash] = $this->newColorFromColors( + $color[$outgoing_hash], + $node_color); + } else { + $color[$outgoing_hash] = $node_color; + } + + if (isset($wait[$outgoing_hash])) { + $wait[$outgoing_hash]--; + if ($wait[$outgoing_hash]) { + continue; + } + unset($wait[$outgoing_hash]); + } + + $stack[] = $outgoing_node; + } + } + + if ($wait) { + throw new Exception( + pht( + 'Did not reach every wait node??')); + } + + // Now, we've colored the entire graph. Collect contiguous pieces of it + // with the same color into sets. + + static $set_n = 1; + + $seen = array(); + $sets = array(); + foreach ($color as $hash => $node_color) { + if (isset($seen[$hash])) { + continue; + } + + $seen[$hash] = true; + + $in_set = array(); + $in_set[$hash] = true; + + $stack = array(); + $stack[] = $graph->getNode($hash); + + while ($stack) { + $node = array_pop($stack); + $node_hash = $node->getCommitHash(); + + $nearby = array(); + foreach ($node->getParentNodes() as $nearby_node) { + $nearby[] = $nearby_node; + } + foreach ($node->getChildNodes() as $nearby_node) { + $nearby[] = $nearby_node; + } + + foreach ($nearby as $nearby_node) { + $nearby_hash = $nearby_node->getCommitHash(); + + if (isset($seen[$nearby_hash])) { + continue; + } + + if (idx($color, $nearby_hash) !== $node_color) { + continue; + } + + $seen[$nearby_hash] = true; + $in_set[$nearby_hash] = true; + $stack[] = $nearby_node; + } + } + + $set = id(new ArcanistCommitGraphSet()) + ->setSetID($set_n++) + ->setColor($node_color) + ->setHashes(array_keys($in_set)); + + $sets[] = $set; + } + + $set_map = array(); + foreach ($sets as $set) { + foreach ($set->getHashes() as $hash) { + $set_map[$hash] = $set; + } + } + + foreach ($sets as $set) { + $parents = array(); + $children = array(); + + foreach ($set->getHashes() as $hash) { + $node = $graph->getNode($hash); + + foreach ($node->getParentNodes() as $edge => $ignored) { + if (isset($set_map[$edge])) { + if ($set_map[$edge] === $set) { + continue; + } + } + + $parents[$edge] = true; + } + + foreach ($node->getChildNodes() as $edge => $ignored) { + if (isset($set_map[$edge])) { + if ($set_map[$edge] === $set) { + continue; + } + } + + $children[$edge] = true; + } + + $parent_sets = array(); + foreach ($parents as $edge => $ignored) { + if (!isset($set_map[$edge])) { + continue; + } + + $adjacent_set = $set_map[$edge]; + $parent_sets[$adjacent_set->getSetID()] = $adjacent_set; + } + + $child_sets = array(); + foreach ($children as $edge => $ignored) { + if (!isset($set_map[$edge])) { + continue; + } + + $adjacent_set = $set_map[$edge]; + $child_sets[$adjacent_set->getSetID()] = $adjacent_set; + } + } + + $set + ->setParentHashes(array_keys($parents)) + ->setChildHashes(array_keys($children)) + ->setParentSets($parent_sets) + ->setChildSets($child_sets); + } + + $this->buildDisplayLayout($sets); + + return $sets; + } + + private function newColorFromRaw($color) { + return array_fuse($color); + } + + private function newColorFromColors($u, $v) { + if ($u === true) { + return $v; + } + + if ($v === true) { + return $u; + } + + return $u + $v; + } + + private function buildDisplayLayout(array $sets) { + $this->visitedDisplaySets = array(); + foreach ($sets as $set) { + if (!$set->getParentSets()) { + $this->visitDisplaySet($set); + } + } + } + + private function visitDisplaySet(ArcanistCommitGraphSet $set) { + // If at least one parent has not been visited yet, don't visit this + // set. We want to put the set at the deepest depth it is reachable + // from. + foreach ($set->getParentSets() as $parent_id => $parent_set) { + if (!isset($this->visitedDisplaySets[$parent_id])) { + return false; + } + } + + $set_id = $set->getSetID(); + $this->visitedDisplaySets[$set_id] = true; + + $display_children = array(); + foreach ($set->getChildSets() as $child_id => $child_set) { + $visited = $this->visitDisplaySet($child_set); + if ($visited) { + $display_children[$child_id] = $child_set; + } + } + + $set->setDisplayChildSets($display_children); + + return true; + } + + +} diff --git a/src/repository/graph/ArcanistCommitNode.php b/src/repository/graph/ArcanistCommitNode.php new file mode 100644 index 00000000..318ca43a --- /dev/null +++ b/src/repository/graph/ArcanistCommitNode.php @@ -0,0 +1,78 @@ +commitHash = $commit_hash; + return $this; + } + + public function getCommitHash() { + return $this->commitHash; + } + + public function addChildNode(ArcanistCommitNode $node) { + $this->childNodes[$node->getCommitHash()] = $node; + return $this; + } + + public function setChildNodes(array $nodes) { + $this->childNodes = $nodes; + return $this; + } + + public function getChildNodes() { + return $this->childNodes; + } + + public function addParentNode(ArcanistCommitNode $node) { + $this->parentNodes[$node->getCommitHash()] = $node; + return $this; + } + + public function setParentNodes(array $nodes) { + $this->parentNodes = $nodes; + return $this; + } + + public function getParentNodes() { + return $this->parentNodes; + } + + public function setCommitMessage($commit_message) { + $this->commitMessage = $commit_message; + return $this; + } + + public function getCommitMessage() { + return $this->commitMessage; + } + + public function getCommitRef() { + if ($this->commitRef === null) { + $this->commitRef = id(new ArcanistCommitRef()) + ->setCommitHash($this->getCommitHash()) + ->attachMessage($this->getCommitMessage()); + } + + return $this->commitRef; + } + + public function setCommitEpoch($commit_epoch) { + $this->commitEpoch = $commit_epoch; + return $this; + } + + public function getCommitEpoch() { + return $this->commitEpoch; + } + +} diff --git a/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php b/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php new file mode 100644 index 00000000..cb50777c --- /dev/null +++ b/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php @@ -0,0 +1,56 @@ +assertPartitionCount( + 1, + pht('Simple Graph'), + array('D'), + 'A>B B>C C>D'); + + $this->assertPartitionCount( + 1, + pht('Multiple Heads'), + array('D', 'E'), + 'A>B B>C C>D C>E'); + + $this->assertPartitionCount( + 1, + pht('Disjoint Graph, One Head'), + array('B'), + 'A>B C>D'); + + $this->assertPartitionCount( + 2, + pht('Disjoint Graph, Two Heads'), + array('B', 'D'), + 'A>B C>D'); + + $this->assertPartitionCount( + 1, + pht('Complex Graph'), + array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'), + 'A>B B>C B>D B>E E>F E>G E>H C>H A>I C>I B>J J>K I>K'); + } + + private function assertPartitionCount($expect, $name, $heads, $corpus) { + $graph = new ArcanistCommitGraph(); + + $query = id(new ArcanistSimpleCommitGraphQuery()) + ->setGraph($graph); + + $query->setCorpus($corpus)->execute(); + + $partitions = $graph->newPartitionQuery() + ->withHeads($heads) + ->execute(); + + $this->assertEqual( + $expect, + count($partitions), + pht('Partition Count for "%s"', $name)); + } + +} diff --git a/src/repository/graph/query/ArcanistCommitGraphQuery.php b/src/repository/graph/query/ArcanistCommitGraphQuery.php new file mode 100644 index 00000000..98cbb35c --- /dev/null +++ b/src/repository/graph/query/ArcanistCommitGraphQuery.php @@ -0,0 +1,69 @@ +graph = $graph; + return $this; + } + + final public function getGraph() { + return $this->graph; + } + + final public function withHeadHashes(array $hashes) { + $this->headHashes = $hashes; + return $this; + } + + final protected function getHeadHashes() { + return $this->headHashes; + } + + final public function withTailHashes(array $hashes) { + $this->tailHashes = $hashes; + return $this; + } + + final protected function getTailHashes() { + return $this->tailHashes; + } + + final public function withExactHashes(array $hashes) { + $this->exactHashes = $hashes; + return $this; + } + + final protected function getExactHashes() { + return $this->exactHashes; + } + + final public function withStopAtGCA($stop_gca) { + $this->stopAtGCA = $stop_gca; + return $this; + } + + final public function setLimit($limit) { + $this->limit = $limit; + return $this; + } + + final protected function getLimit() { + return $this->limit; + } + + final public function getRepositoryAPI() { + return $this->getGraph()->getRepositoryAPI(); + } + + abstract public function execute(); + +} diff --git a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php new file mode 100644 index 00000000..028fbbe3 --- /dev/null +++ b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php @@ -0,0 +1,150 @@ +beginExecute(); + $this->continueExecute(); + + return $this->seen; + } + + protected function beginExecute() { + $head_hashes = $this->getHeadHashes(); + $exact_hashes = $this->getExactHashes(); + + if (!$head_hashes && !$exact_hashes) { + throw new Exception(pht('Need head hashes or exact hashes!')); + } + + $api = $this->getRepositoryAPI(); + + $refs = array(); + if ($head_hashes !== null) { + foreach ($head_hashes as $hash) { + $refs[] = $hash; + } + } + + $tail_hashes = $this->getTailHashes(); + if ($tail_hashes !== null) { + foreach ($tail_hashes as $tail_hash) { + $refs[] = sprintf('^%s^@', $tail_hash); + } + } + + if ($exact_hashes !== null) { + if (count($exact_hashes) > 1) { + // If "A" is a parent of "B" and we search for exact hashes ["A", "B"], + // the exclusion rule generated by "^B^@" is stronger than the inclusion + // rule generated by "A" and we don't get "A" in the result set. + throw new Exception( + pht( + 'TODO: Multiple exact hashes not supported under Git.')); + } + foreach ($exact_hashes as $exact_hash) { + $refs[] = $exact_hash; + $refs[] = sprintf('^%s^@', $exact_hash); + } + } + + $refs[] = '--'; + $refs = implode("\n", $refs)."\n"; + + $fields = array( + '%e', + '%H', + '%P', + '%ct', + '%B', + ); + + $format = implode('%x02', $fields).'%x01'; + + $future = $api->newFuture( + 'log --format=%s --stdin', + $format); + $future->write($refs); + $future->setResolveOnError(true); + $future->start(); + + $lines = id(new LinesOfALargeExecFuture($future)) + ->setDelimiter("\1"); + $lines->rewind(); + + $this->queryFuture = $lines; + } + + protected function continueExecute() { + $graph = $this->getGraph(); + $limit = $this->getLimit(); + + $lines = $this->queryFuture; + + while (true) { + if (!$lines->valid()) { + return false; + } + + $line = $lines->current(); + $lines->next(); + + if ($line === "\n") { + continue; + } + + $fields = explode("\2", $line); + + if (count($fields) !== 5) { + throw new Exception( + pht( + 'Failed to split line "%s" from "git log".', + $line)); + } + + list($encoding, $hash, $parents, $commit_epoch, $message) = $fields; + + // TODO: Handle encoding, see DiffusionLowLevelCommitQuery. + + $node = $graph->getNode($hash); + if (!$node) { + $node = $graph->newNode($hash); + } + + $this->seen[$hash] = $node; + + $node + ->setCommitMessage($message) + ->setCommitEpoch((int)$commit_epoch); + + if (strlen($parents)) { + $parents = explode(' ', $parents); + + $parent_nodes = array(); + foreach ($parents as $parent) { + $parent_node = $graph->getNode($parent); + if (!$parent_node) { + $parent_node = $graph->newNode($parent); + } + + $parent_nodes[$parent] = $parent_node; + $parent_node->addChildNode($node); + } + $node->setParentNodes($parent_nodes); + } else { + $parents = array(); + } + + if ($limit) { + if (count($this->seen) >= $limit) { + break; + } + } + } + } + +} diff --git a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php new file mode 100644 index 00000000..0a3836a9 --- /dev/null +++ b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php @@ -0,0 +1,180 @@ +beginExecute(); + $this->continueExecute(); + + return $this->seen; + } + + protected function beginExecute() { + $head_hashes = $this->getHeadHashes(); + $exact_hashes = $this->getExactHashes(); + + if (!$head_hashes && !$exact_hashes) { + throw new Exception(pht('Need head hashes or exact hashes!')); + } + + $api = $this->getRepositoryAPI(); + + $revsets = array(); + if ($head_hashes !== null) { + $revs = array(); + foreach ($head_hashes as $hash) { + $revs[] = hgsprintf( + 'ancestors(%s)', + $hash); + } + $revsets[] = $this->joinOrRevsets($revs); + } + + $tail_hashes = $this->getTailHashes(); + if ($tail_hashes !== null) { + $revs = array(); + foreach ($tail_hashes as $tail_hash) { + $revs[] = hgsprintf( + 'descendants(%s)', + $tail_hash); + } + $revsets[] = $this->joinOrRevsets($revs); + } + + if ($revsets) { + $revsets = array( + $this->joinAndRevsets($revs), + ); + } + + if ($exact_hashes !== null) { + $revs = array(); + foreach ($exact_hashes as $exact_hash) { + $revs[] = hgsprintf( + '%s', + $exact_hash); + } + $revsets[] = array( + $this->joinOrRevsets($revs), + ); + } + + $revsets = $this->joinOrRevsets($revs); + + $fields = array( + '', // Placeholder for "encoding". + '{node}', + '{parents}', + '{date|rfc822date}', + '{description|utf8}', + ); + + $template = implode("\2", $fields)."\1"; + + $future = $api->newFuture( + 'log --rev %s --template %s --', + $revsets, + $template); + $future->setResolveOnError(true); + $future->start(); + + $lines = id(new LinesOfALargeExecFuture($future)) + ->setDelimiter("\1"); + $lines->rewind(); + + $this->queryFuture = $lines; + } + + protected function continueExecute() { + $graph = $this->getGraph(); + $lines = $this->queryFuture; + $limit = $this->getLimit(); + while (true) { + if (!$lines->valid()) { + return false; + } + + $line = $lines->current(); + $lines->next(); + + if ($line === "\n") { + continue; + } + + $fields = explode("\2", $line); + + if (count($fields) !== 5) { + throw new Exception( + pht( + 'Failed to split line "%s" from "git log".', + $line)); + } + + list($encoding, $hash, $parents, $commit_epoch, $message) = $fields; + + $node = $graph->getNode($hash); + if (!$node) { + $node = $graph->newNode($hash); + } + + $this->seen[$hash] = $node; + + $node + ->setCommitMessage($message) + ->setCommitEpoch((int)strtotime($commit_epoch)); + + if (strlen($parents)) { + $parents = explode(' ', $parents); + + $parent_nodes = array(); + foreach ($parents as $parent) { + $parent_node = $graph->getNode($parent); + if (!$parent_node) { + $parent_node = $graph->newNode($parent); + } + + $parent_nodes[$parent] = $parent_node; + $parent_node->addChildNode($node); + } + $node->setParentNodes($parent_nodes); + } else { + $parents = array(); + } + + if ($limit) { + if (count($this->seen) >= $limit) { + break; + } + } + } + } + + private function joinOrRevsets(array $revsets) { + return $this->joinRevsets($revsets, false); + } + + private function joinAndRevsets(array $revsets) { + return $this->joinRevsets($revsets, true); + } + + private function joinRevsets(array $revsets, $is_and) { + if (!$revsets) { + return array(); + } + + if (count($revsets) === 1) { + return head($revsets); + } + + if ($is_and) { + return '('.implode(' and ', $revsets).')'; + } else { + return '('.implode(' or ', $revsets).')'; + } + } + +} diff --git a/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php b/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php new file mode 100644 index 00000000..1931486f --- /dev/null +++ b/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php @@ -0,0 +1,50 @@ +corpus = $corpus; + return $this; + } + + public function getCorpus() { + return $this->corpus; + } + + public function execute() { + $graph = $this->getGraph(); + $corpus = $this->getCorpus(); + + $edges = preg_split('(\s+)', trim($corpus)); + foreach ($edges as $edge) { + $matches = null; + $ok = preg_match('(^(?P\S+)>(?P\S+)\z)', $edge, $matches); + if (!$ok) { + throw new Exception( + pht( + 'Failed to match SimpleCommitGraph directive "%s".', + $edge)); + } + + $parent = $matches['parent']; + $child = $matches['child']; + + $pnode = $graph->getNode($parent); + if (!$pnode) { + $pnode = $graph->newNode($parent); + } + + $cnode = $graph->getNode($child); + if (!$cnode) { + $cnode = $graph->newNode($child); + } + + $cnode->addParentNode($pnode); + $pnode->addChildNode($cnode); + } + } + +} diff --git a/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php new file mode 100644 index 00000000..a69641fb --- /dev/null +++ b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php @@ -0,0 +1,147 @@ +rootSet = $root_set; + return $this; + } + + public function getRootSet() { + return $this->rootSet; + } + + public function setMarkers($markers) { + $this->markers = $markers; + $this->markerGroups = mgroup($markers, 'getCommitHash'); + return $this; + } + + public function getMarkers() { + return $this->markers; + } + + public function setStateRefs($state_refs) { + $this->stateRefs = $state_refs; + return $this; + } + + public function getStateRefs() { + return $this->stateRefs; + } + + public function setRepositoryAPI($repository_api) { + $this->repositoryAPI = $repository_api; + return $this; + } + + public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + public function draw() { + $set = $this->getRootSet(); + + $this->setViews = array(); + $view_root = $this->newSetViews($set); + $view_list = $this->setViews; + + foreach ($view_list as $view) { + $parent_view = $view->getParentView(); + if ($parent_view) { + $depth = $parent_view->getViewDepth() + 1; + } else { + $depth = 0; + } + $view->setViewDepth($depth); + } + + $api = $this->getRepositoryAPI(); + + foreach ($view_list as $view) { + $view_set = $view->getSet(); + $hashes = $view_set->getHashes(); + + $commit_refs = $this->getCommitRefs($hashes); + $revision_refs = $this->getRevisionRefs(head($hashes)); + $marker_refs = $this->getMarkerRefs($hashes); + + $view + ->setRepositoryAPI($api) + ->setCommitRefs($commit_refs) + ->setRevisionRefs($revision_refs) + ->setMarkerRefs($marker_refs); + } + + $rows = array(); + foreach ($view_list as $view) { + $rows[] = $view->newCellViews(); + } + + return $rows; + } + + private function newSetViews(ArcanistCommitGraphSet $set) { + $set_view = $this->newSetView($set); + + $this->setViews[] = $set_view; + + foreach ($set->getDisplayChildSets() as $child_set) { + $child_view = $this->newSetViews($child_set); + $child_view->setParentView($set_view); + $set_view->addChildView($child_view); + } + + return $set_view; + } + + private function newSetView(ArcanistCommitGraphSet $set) { + return id(new ArcanistCommitGraphSetView()) + ->setSet($set); + } + + private function getStateRef($hash) { + $state_refs = $this->getStateRefs(); + + if (!isset($state_refs[$hash])) { + throw new Exception( + pht( + 'Found no state ref for hash "%s".', + $hash)); + } + + return $state_refs[$hash]; + } + + private function getRevisionRefs($hash) { + $state_ref = $this->getStateRef($hash); + return $state_ref->getRevisionRefs(); + } + + private function getCommitRefs(array $hashes) { + $results = array(); + foreach ($hashes as $hash) { + $state_ref = $this->getStateRef($hash); + $results[$hash] = $state_ref->getCommitRef(); + } + + return $results; + } + + private function getMarkerRefs(array $hashes) { + $results = array(); + foreach ($hashes as $hash) { + $results[$hash] = idx($this->markerGroups, $hash, array()); + } + return $results; + } + +} diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php new file mode 100644 index 00000000..4d271ec3 --- /dev/null +++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php @@ -0,0 +1,407 @@ +repositoryAPI = $repository_api; + return $this; + } + + public function getRepositoryAPI() { + return $this->repositoryAPI; + } + + public function setSet(ArcanistCommitGraphSet $set) { + $this->set = $set; + return $this; + } + + public function getSet() { + return $this->set; + } + + public function setParentView(ArcanistCommitGraphSetView $parent_view) { + $this->parentView = $parent_view; + return $this; + } + + public function getParentView() { + return $this->parentView; + } + + public function addChildView(ArcanistCommitGraphSetView $child_view) { + $this->childViews[] = $child_view; + return $this; + } + + public function setChildViews(array $child_views) { + assert_instances_of($child_views, __CLASS__); + $this->childViews = $child_views; + return $this; + } + + public function getChildViews() { + return $this->childViews; + } + + public function setCommitRefs($commit_refs) { + $this->commitRefs = $commit_refs; + return $this; + } + + public function getCommitRefs() { + return $this->commitRefs; + } + + public function setRevisionRefs($revision_refs) { + $this->revisionRefs = $revision_refs; + return $this; + } + + public function getRevisionRefs() { + return $this->revisionRefs; + } + + public function setMarkerRefs($marker_refs) { + $this->markerRefs = $marker_refs; + return $this; + } + + public function getMarkerRefs() { + return $this->markerRefs; + } + + public function setViewDepth($view_depth) { + $this->viewDepth = $view_depth; + return $this; + } + + public function getViewDepth() { + return $this->viewDepth; + } + + public function newCellViews() { + $set = $this->getSet(); + $api = $this->getRepositoryAPI(); + + $commit_refs = $this->getCommitRefs(); + $revision_refs = $this->getRevisionRefs(); + $marker_refs = $this->getMarkerRefs(); + + $merge_strings = array(); + foreach ($revision_refs as $revision_ref) { + $summary = $revision_ref->getName(); + $merge_key = substr($summary, 0, 32); + $merge_key = phutil_utf8_strtolower($merge_key); + + $merge_strings[$merge_key][] = $revision_ref; + } + + $merge_map = array(); + foreach ($commit_refs as $commit_ref) { + $summary = $commit_ref->getSummary(); + + $merge_with = null; + if (count($revision_refs) === 1) { + $merge_with = head($revision_refs); + } else { + $merge_key = substr($summary, 0, 32); + $merge_key = phutil_utf8_strtolower($merge_key); + if (isset($merge_strings[$merge_key])) { + $merge_refs = $merge_strings[$merge_key]; + if (count($merge_refs) === 1) { + $merge_with = head($merge_refs); + } + } + } + + if ($merge_with) { + $revision_phid = $merge_with->getPHID(); + $merge_map[$revision_phid][] = $commit_ref; + } + } + + $revision_map = mpull($revision_refs, null, 'getPHID'); + + $result_map = array(); + foreach ($merge_map as $merge_phid => $merge_refs) { + if (count($merge_refs) !== 1) { + continue; + } + + $merge_ref = head($merge_refs); + $commit_hash = $merge_ref->getCommitHash(); + + $result_map[$commit_hash] = $revision_map[$merge_phid]; + } + + $object_layout = array(); + + $merged_map = array_flip(mpull($result_map, 'getPHID')); + foreach ($revision_refs as $revision_ref) { + $revision_phid = $revision_ref->getPHID(); + if (isset($merged_map[$revision_phid])) { + continue; + } + + $object_layout[] = array( + 'revision' => $revision_ref, + ); + } + + foreach ($commit_refs as $commit_ref) { + $commit_hash = $commit_ref->getCommitHash(); + $revision_ref = idx($result_map, $commit_hash); + + $object_layout[] = array( + 'commit' => $commit_ref, + 'revision' => $revision_ref, + ); + } + + $marker_layout = array(); + foreach ($object_layout as $layout) { + $commit_ref = idx($layout, 'commit'); + if (!$commit_ref) { + $marker_layout[] = $layout; + continue; + } + + $commit_hash = $commit_ref->getCommitHash(); + $markers = idx($marker_refs, $commit_hash); + if (!$markers) { + $marker_layout[] = $layout; + continue; + } + + $head_marker = array_shift($markers); + $layout['marker'] = $head_marker; + $marker_layout[] = $layout; + + if (!$markers) { + continue; + } + + foreach ($markers as $marker) { + $marker_layout[] = array( + 'marker' => $marker, + ); + } + } + + $marker_view = $this->drawMarkerCell($marker_layout); + $commits_view = $this->drawCommitsCell($marker_layout); + $status_view = $this->drawStatusCell($marker_layout); + $revisions_view = $this->drawRevisionsCell($marker_layout); + $messages_view = $this->drawMessagesCell($marker_layout); + + return array( + id(new ArcanistGridCell()) + ->setKey('marker') + ->setContent($marker_view), + id(new ArcanistGridCell()) + ->setKey('commits') + ->setContent($commits_view), + id(new ArcanistGridCell()) + ->setKey('status') + ->setContent($status_view), + id(new ArcanistGridCell()) + ->setKey('revisions') + ->setContent($revisions_view), + id(new ArcanistGridCell()) + ->setKey('messages') + ->setContent($messages_view), + ); + } + + private function drawMarkerCell(array $items) { + $api = $this->getRepositoryAPI(); + $depth = $this->getViewDepth(); + + $marker_refs = $this->getMarkerRefs(); + $commit_refs = $this->getCommitRefs(); + + if (count($commit_refs) === 1) { + $commit_ref = head($commit_refs); + + $commit_hash = $commit_ref->getCommitHash(); + $commit_hash = tsprintf( + '%s', + substr($commit_hash, 0, 7)); + + $commit_label = $commit_hash; + } else { + $min = head($commit_refs); + $max = last($commit_refs); + $commit_label = tsprintf( + '%s..%s', + substr($min->getCommitHash(), 0, 7), + substr($max->getCommitHash(), 0, 7)); + } + + // TODO: Make this a function of terminal width? + + $max_depth = 25; + if ($depth <= $max_depth) { + $indent = str_repeat(' ', ($depth * 2)); + } else { + $more = ' ... '; + $indent = str_repeat(' ', ($max_depth * 2) - strlen($more)).$more; + } + $indent .= '- '; + + $empty_indent = str_repeat(' ', strlen($indent)); + + $is_first = true; + $cell = array(); + foreach ($items as $item) { + $marker_ref = idx($item, 'marker'); + + if ($marker_ref) { + if ($marker_ref->getIsActive()) { + $label = tsprintf( + '**%s**', + $marker_ref->getName()); + } else { + $label = tsprintf( + '**%s**', + $marker_ref->getName()); + } + } else if ($is_first) { + $label = $commit_label; + } else { + $label = ''; + } + + if ($is_first) { + $indent_text = $indent; + } else { + $indent_text = $empty_indent; + } + + $cell[] = tsprintf( + "%s%s\n", + $indent_text, + $label); + + $is_first = false; + } + + return $cell; + } + + private function drawCommitsCell(array $items) { + $cell = array(); + foreach ($items as $item) { + $commit_ref = idx($item, 'commit'); + if (!$commit_ref) { + $cell[] = tsprintf("\n"); + continue; + } + + $commit_label = $this->drawCommitLabel($commit_ref); + $cell[] = tsprintf("%s\n", $commit_label); + } + + return $cell; + } + + private function drawCommitLabel(ArcanistCommitRef $commit_ref) { + $api = $this->getRepositoryAPI(); + + $hash = $commit_ref->getCommitHash(); + $hash = substr($hash, 0, 7); + + return tsprintf('%s', $hash); + } + + private function drawRevisionsCell(array $items) { + $cell = array(); + + foreach ($items as $item) { + $revision_ref = idx($item, 'revision'); + if (!$revision_ref) { + $cell[] = tsprintf("\n"); + continue; + } + $revision_label = $this->drawRevisionLabel($revision_ref); + $cell[] = tsprintf("%s\n", $revision_label); + } + + return $cell; + } + + private function drawRevisionLabel(ArcanistRevisionRef $revision_ref) { + $api = $this->getRepositoryAPI(); + + $monogram = $revision_ref->getMonogram(); + + return tsprintf('%s', $monogram); + } + + private function drawMessagesCell(array $items) { + $cell = array(); + + foreach ($items as $item) { + $revision_ref = idx($item, 'revision'); + if ($revision_ref) { + $cell[] = tsprintf("%s\n", $revision_ref->getName()); + continue; + } + + $commit_ref = idx($item, 'commit'); + if ($commit_ref) { + $cell[] = tsprintf("%s\n", $commit_ref->getSummary()); + continue; + } + + $cell[] = tsprintf("\n"); + } + + return $cell; + } + + private function drawStatusCell(array $items) { + $cell = array(); + + foreach ($items as $item) { + $revision_ref = idx($item, 'revision'); + + if (!$revision_ref) { + $cell[] = tsprintf("\n"); + continue; + } + + $revision_label = $this->drawRevisionStatus($revision_ref); + $cell[] = tsprintf("%s\n", $revision_label); + } + + return $cell; + } + + + private function drawRevisionStatus(ArcanistRevisionRef $revision_ref) { + $status = $revision_ref->getStatusDisplayName(); + + $ansi_color = $revision_ref->getStatusANSIColor(); + if ($ansi_color) { + $status = tsprintf( + sprintf('%%s', $ansi_color), + $status); + } + + return tsprintf('%s', $status); + } + + +} diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index 2e01707b..e0daef52 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -3,6 +3,8 @@ abstract class ArcanistMarkersWorkflow extends ArcanistArcWorkflow { + private $nodes; + abstract protected function getWorkflowMarkerType(); public function runWorkflow() { @@ -14,96 +16,152 @@ abstract class ArcanistMarkersWorkflow ->withMarkerTypes(array($marker_type)) ->execute(); - $states = array(); - foreach ($markers as $marker) { - $state_ref = id(new ArcanistWorkingCopyStateRef()) - ->setCommitRef($marker->getCommitRef()); + $tail_hashes = $this->getTailHashes(); - $states[] = array( - 'marker' => $marker, - 'state' => $state_ref, - ); + $heads = mpull($markers, 'getCommitHash'); + + $graph = $api->getGraph(); + $limit = 1000; + + $query = $graph->newQuery() + ->withHeadHashes($heads) + ->setLimit($limit + 1); + + if ($tail_hashes) { + $query->withTailHashes($tail_hashes); + } + + $nodes = $query->execute(); + + if (count($nodes) > $limit) { + + // TODO: Show what we can. + + throw new PhutilArgumentUsageException( + pht( + 'Found more than %s unpublished commits which are ancestors of '. + 'heads.', + new PhutilNumber($limit))); + } + + // We may have some markers which point at commits which are already + // published. These markers won't be reached by following heads backwards + // until we reach published commits. + + // Load these markers exactly so they don't vanish in the output. + + // TODO: Mark these sets as published. + + $disjoint_heads = array(); + foreach ($heads as $head) { + if (!isset($nodes[$head])) { + $disjoint_heads[] = $head; + } + } + + if ($disjoint_heads) { + + // TODO: Git currently can not query for more than one exact hash at a + // time. + + foreach ($disjoint_heads as $disjoint_head) { + $disjoint_nodes = $graph->newQuery() + ->withExactHashes(array($disjoint_head)) + ->execute(); + + $nodes += $disjoint_nodes; + } + } + + $state_refs = array(); + foreach ($nodes as $node) { + $commit_ref = $node->getCommitRef(); + + $state_ref = id(new ArcanistWorkingCopyStateRef()) + ->setCommitRef($commit_ref); + + $state_refs[$node->getCommitHash()] = $state_ref; } $this->loadHardpoints( - ipull($states, 'state'), + $state_refs, ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); - $vectors = array(); - foreach ($states as $key => $state) { - $marker_ref = $state['marker']; - $state_ref = $state['state']; + $partitions = $graph->newPartitionQuery() + ->withHeads($heads) + ->withHashes(array_keys($nodes)) + ->execute(); - $vector = id(new PhutilSortVector()) - ->addInt($marker_ref->getIsActive() ? 1 : 0) - ->addInt($marker_ref->getEpoch()); - - $vectors[$key] = $vector; + $revision_refs = array(); + foreach ($state_refs as $hash => $state_ref) { + $revision_ids = mpull($state_ref->getRevisionRefs(), 'getID'); + $revision_refs[$hash] = array_fuse($revision_ids); } - $vectors = msortv($vectors, 'getSelf'); - $states = array_select_keys($states, array_keys($vectors)); + $partition_sets = array(); + $partition_vectors = array(); + foreach ($partitions as $partition_key => $partition) { + $sets = $partition->newSetQuery() + ->setWaypointMap($revision_refs) + ->execute(); - $table = id(new PhutilConsoleTable()) - ->setShowHeader(false) - ->addColumn('active') - ->addColumn('name') - ->addColumn('status') - ->addColumn('description'); + list($sets, $partition_vector) = $this->sortSets( + $graph, + $sets, + $markers); - $rows = array(); - foreach ($states as $state) { - $marker_ref = $state['marker']; - $state_ref = $state['state']; - $revision_ref = null; - $commit_ref = $marker_ref->getCommitRef(); + $partition_sets[$partition_key] = $sets; + $partition_vectors[$partition_key] = $partition_vector; + } - $marker_name = tsprintf('**%s**', $marker_ref->getName()); + $partition_vectors = msortv($partition_vectors, 'getSelf'); + $partitions = array_select_keys( + $partitions, + array_keys($partition_vectors)); - if ($state_ref->hasAmbiguousRevisionRefs()) { - $status = pht('Ambiguous'); - } else { - $revision_ref = $state_ref->getRevisionRef(); - if (!$revision_ref) { - $status = tsprintf( - '%s', - pht('No Revision')); - } else { - $status = $revision_ref->getStatusDisplayName(); + $partition_lists = array(); + foreach ($partitions as $partition_key => $partition) { + $sets = $partition_sets[$partition_key]; - $ansi_color = $revision_ref->getStatusANSIColor(); - if ($ansi_color) { - $status = tsprintf( - sprintf('%%s', $ansi_color), - $status); - } + $roots = array(); + foreach ($sets as $set) { + if (!$set->getParentSets()) { + $roots[] = $set; } } - if ($revision_ref) { - $description = $revision_ref->getFullName(); - } else { - $description = $commit_ref->getSummary(); - } + // TODO: When no parent of a set is in the node list, we should render + // a marker showing that the commit sequence is historic. - if ($marker_ref->getIsActive()) { - $active_mark = '*'; - } else { - $active_mark = ' '; - } - $is_active = tsprintf('** %s **', $active_mark); + $row_lists = array(); + foreach ($roots as $set) { + $view = id(new ArcanistCommitGraphSetTreeView()) + ->setRepositoryAPI($api) + ->setRootSet($set) + ->setMarkers($markers) + ->setStateRefs($state_refs); - $rows[] = array( - 'active' => $is_active, - 'name' => $marker_name, - 'status' => $status, - 'description' => $description, - ); + $row_lists[] = $view->draw(); + } + $partition_lists[] = $row_lists; } - $table->drawRows($rows); + $grid = id(new ArcanistGridView()); + $grid->newColumn('marker'); + $grid->newColumn('commits'); + $grid->newColumn('status'); + $grid->newColumn('revisions'); + $grid->newColumn('messages'); - return 0; + foreach ($partition_lists as $row_lists) { + foreach ($row_lists as $row_list) { + foreach ($row_list as $row) { + $grid->newRow($row); + } + } + } + + echo tsprintf('%s', $grid->drawGrid()); } final protected function hasMarkerTypeSupport($marker_type) { @@ -115,4 +173,130 @@ abstract class ArcanistMarkersWorkflow return isset($types[$marker_type]); } + private function getTailHashes() { + $api = $this->getRepositoryAPI(); + return $api->getPublishedCommitHashes(); + } + + private function sortSets( + ArcanistCommitGraph $graph, + array $sets, + array $markers) { + + $marker_groups = mgroup($markers, 'getCommitHash'); + $sets = mpull($sets, null, 'getSetID'); + + $active_markers = array(); + foreach ($sets as $set_id => $set) { + foreach ($set->getHashes() as $hash) { + $markers = idx($marker_groups, $hash, array()); + + $has_active = false; + foreach ($markers as $marker) { + if ($marker->getIsActive()) { + $has_active = true; + break; + } + } + + if ($has_active) { + $active_markers[$set_id] = $set; + break; + } + } + } + + $stack = array_select_keys($sets, array_keys($active_markers)); + while ($stack) { + $cursor = array_pop($stack); + foreach ($cursor->getParentSets() as $parent_id => $parent) { + if (isset($active_markers[$parent_id])) { + continue; + } + $active_markers[$parent_id] = $parent; + $stack[] = $parent; + } + } + + $partition_epoch = 0; + $partition_names = array(); + + $vectors = array(); + foreach ($sets as $set_id => $set) { + if (isset($active_markers[$set_id])) { + $has_active = 1; + } else { + $has_active = 0; + } + + $max_epoch = 0; + $marker_names = array(); + foreach ($set->getHashes() as $hash) { + $node = $graph->getNode($hash); + $max_epoch = max($max_epoch, $node->getCommitEpoch()); + + $markers = idx($marker_groups, $hash, array()); + foreach ($markers as $marker) { + $marker_names[] = $marker->getName(); + } + } + + $partition_epoch = max($partition_epoch, $max_epoch); + + if ($marker_names) { + $has_markers = 1; + natcasesort($marker_names); + $max_name = last($marker_names); + + $partition_names[] = $max_name; + } else { + $has_markers = 0; + $max_name = ''; + } + + + $vector = id(new PhutilSortVector()) + ->addInt($has_active) + ->addInt($max_epoch) + ->addInt($has_markers) + ->addString($max_name); + + $vectors[$set_id] = $vector; + } + + $vectors = msortv_natural($vectors, 'getSelf'); + $vector_keys = array_keys($vectors); + + foreach ($sets as $set_id => $set) { + $child_sets = $set->getDisplayChildSets(); + $child_sets = array_select_keys($child_sets, $vector_keys); + $set->setDisplayChildSets($child_sets); + } + + $sets = array_select_keys($sets, $vector_keys); + + if ($active_markers) { + $any_active = true; + } else { + $any_active = false; + } + + if ($partition_names) { + $has_markers = 1; + natcasesort($partition_names); + $partition_name = last($partition_names); + } else { + $has_markers = 0; + $partition_name = ''; + } + + $partition_vector = id(new PhutilSortVector()) + ->addInt($any_active) + ->addInt($partition_epoch) + ->addInt($has_markers) + ->addString($partition_name); + + return array($sets, $partition_vector); + } + } From 10c4a551ae9df1ebdf90dd4f89d73b213b1b4581 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jun 2020 07:51:27 -0700 Subject: [PATCH 69/82] Remove implicit sorting from "MarkerRefQuery" Summary: Ref T13546. This is no longer necessary after the introduction of "msortv_natural()", which can handle natural string sorting. Test Plan: Ran "arc branches", saw the same sorting applied. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21364 --- .../marker/ArcanistRepositoryMarkerQuery.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/repository/marker/ArcanistRepositoryMarkerQuery.php b/src/repository/marker/ArcanistRepositoryMarkerQuery.php index 02e12d64..e72ea78f 100644 --- a/src/repository/marker/ArcanistRepositoryMarkerQuery.php +++ b/src/repository/marker/ArcanistRepositoryMarkerQuery.php @@ -104,21 +104,6 @@ abstract class ArcanistRepositoryMarkerQuery } } - return $this->sortMarkers($markers); - } - - private function sortMarkers(array $markers) { - // Sort the list in natural order. If we apply a stable sort later, - // markers will sort in "feature1", "feature2", etc., order if they - // don't otherwise have a unique position. - - // This can improve behavior if two branches were updated at the same - // time, as is common when cascading rebases after changes land. - - $map = mpull($markers, 'getName'); - natcasesort($map); - $markers = array_select_keys($markers, array_keys($map)); - return $markers; } From 0ad3222d5966230f54dd37455c9de30861f5ed06 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 06:44:15 -0700 Subject: [PATCH 70/82] Improve grid layout in "arc branches" at various terminal widths Summary: Ref T13546. Make "arc branches" use a flexible grid width and try to match the content to the display width in a reasonable way. Test Plan: Ran "arc branches" at various terminal widths, got generally sensible output. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21365 --- src/console/grid/ArcanistGridColumn.php | 10 +++ src/console/grid/ArcanistGridView.php | 80 +++++++++++++++---- src/ref/revision/ArcanistRevisionRef.php | 12 +++ .../graph/view/ArcanistCommitGraphSetView.php | 24 ++++-- src/workflow/ArcanistMarkersWorkflow.php | 3 +- 5 files changed, 106 insertions(+), 23 deletions(-) diff --git a/src/console/grid/ArcanistGridColumn.php b/src/console/grid/ArcanistGridColumn.php index 15d0d142..1c63575c 100644 --- a/src/console/grid/ArcanistGridColumn.php +++ b/src/console/grid/ArcanistGridColumn.php @@ -6,6 +6,7 @@ final class ArcanistGridColumn private $key; private $alignment = self::ALIGNMENT_LEFT; private $displayWidth; + private $minimumWidth; const ALIGNMENT_LEFT = 'align.left'; const ALIGNMENT_CENTER = 'align.center'; @@ -38,4 +39,13 @@ final class ArcanistGridColumn return $this->displayWidth; } + public function setMinimumWidth($minimum_width) { + $this->minimumWidth = $minimum_width; + return $this; + } + + public function getMinimumWidth() { + return $this->minimumWidth; + } + } diff --git a/src/console/grid/ArcanistGridView.php b/src/console/grid/ArcanistGridView.php index 73e77486..5391ea9c 100644 --- a/src/console/grid/ArcanistGridView.php +++ b/src/console/grid/ArcanistGridView.php @@ -10,7 +10,7 @@ final class ArcanistGridView public function setColumns(array $columns) { assert_instances_of($columns, 'ArcanistGridColumn'); - $this->columns = $columns; + $this->columns = mpull($columns, null, 'getKey'); return $this; } @@ -56,27 +56,70 @@ final class ArcanistGridView return tsprintf("%s\n", $rows); } - private function getDisplayWidth($key) { - if (!isset($this->displayWidths[$key])) { - $column = $this->getColumn($key); + private function getDisplayWidth($display_key) { + if (!isset($this->displayWidths[$display_key])) { + $flexible_columns = array(); - $width = $column->getDisplayWidth(); - if ($width === null) { - $width = 1; - foreach ($this->getRows() as $row) { - if (!$row->hasCell($key)) { - continue; + $columns = $this->getColumns(); + foreach ($columns as $key => $column) { + $width = $column->getDisplayWidth(); + + if ($width === null) { + $width = 1; + foreach ($this->getRows() as $row) { + if (!$row->hasCell($key)) { + continue; + } + + $cell = $row->getCell($key); + $width = max($width, $cell->getContentDisplayWidth()); } - - $cell = $row->getCell($key); - $width = max($width, $cell->getContentDisplayWidth()); } + + if ($column->getMinimumWidth() !== null) { + $flexible_columns[] = $key; + } + + $this->displayWidths[$key] = $width; } - $this->displayWidths[$key] = $width; + $available_width = phutil_console_get_terminal_width(); + + // Adjust the available width to account for cell spacing. + $available_width -= (2 * (count($columns) - 1)); + + while (true) { + $total_width = array_sum($this->displayWidths); + + if ($total_width <= $available_width) { + break; + } + + if (!$flexible_columns) { + break; + } + + // NOTE: This is very unsophisticated, and just shortcuts us to a + // reasonable result when only one column is flexible. + + foreach ($flexible_columns as $flexible_key) { + $column = $columns[$flexible_key]; + + $need_width = ($total_width - $available_width); + $old_width = $this->displayWidths[$flexible_key]; + $new_width = ($old_width - $need_width); + + $new_width = max($new_width, $column->getMinimumWidth()); + + $this->displayWidths[$flexible_key] = $new_width; + + $flexible_columns = array(); + break; + } + } } - return $this->displayWidths[$key]; + return $this->displayWidths[$display_key]; } public function getColumn($key) { @@ -241,7 +284,12 @@ final class ArcanistGridView $src_width, $dst_width, $alignment) { - return $line; + + $line = phutil_string_cast($line); + + return id(new PhutilUTF8StringTruncator()) + ->setMaximumGlyphs($dst_width) + ->truncateString($line); } } diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index 5e2ffa0e..c36845bd 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -89,6 +89,13 @@ final class ArcanistRevisionRef return 'D'.$this->getID(); } + public function getStatusShortDisplayName() { + if ($this->isStatusNeedsReview()) { + return pht('Review'); + } + return idxv($this->parameters, array('fields', 'status', 'name')); + } + public function getStatusDisplayName() { return idxv($this->parameters, array('fields', 'status', 'name')); } @@ -117,6 +124,11 @@ final class ArcanistRevisionRef return ($status === 'accepted'); } + public function isStatusNeedsReview() { + $status = $this->getStatus(); + return ($status === 'needs-review'); + } + public function getStatus() { return idxv($this->parameters, array('fields', 'status', 'value')); } diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php index 4d271ec3..b58180c1 100644 --- a/src/repository/graph/view/ArcanistCommitGraphSetView.php +++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php @@ -249,9 +249,8 @@ final class ArcanistCommitGraphSetView substr($max->getCommitHash(), 0, 7)); } - // TODO: Make this a function of terminal width? - - $max_depth = 25; + $terminal_width = phutil_console_get_terminal_width(); + $max_depth = (int)floor(3 + (max(0, $terminal_width - 72) / 6)); if ($depth <= $max_depth) { $indent = str_repeat(' ', ($depth * 2)); } else { @@ -262,20 +261,29 @@ final class ArcanistCommitGraphSetView $empty_indent = str_repeat(' ', strlen($indent)); + $max_width = ($max_depth * 2) + 16; + $available_width = $max_width - (min($max_depth, $depth) * 2); + $is_first = true; $cell = array(); foreach ($items as $item) { $marker_ref = idx($item, 'marker'); if ($marker_ref) { + $marker_name = $marker_ref->getName(); + + $marker_name = id(new PhutilUTF8StringTruncator()) + ->setMaximumGlyphs($available_width) + ->truncateString($marker_name); + if ($marker_ref->getIsActive()) { $label = tsprintf( '**%s**', - $marker_ref->getName()); + $marker_name); } else { $label = tsprintf( '**%s**', - $marker_ref->getName()); + $marker_name); } } else if ($is_first) { $label = $commit_label; @@ -391,7 +399,11 @@ final class ArcanistCommitGraphSetView private function drawRevisionStatus(ArcanistRevisionRef $revision_ref) { - $status = $revision_ref->getStatusDisplayName(); + if (phutil_console_get_terminal_width() < 120) { + $status = $revision_ref->getStatusShortDisplayName(); + } else { + $status = $revision_ref->getStatusDisplayName(); + } $ansi_color = $revision_ref->getStatusANSIColor(); if ($ansi_color) { diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index e0daef52..ea9fe98d 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -151,7 +151,8 @@ abstract class ArcanistMarkersWorkflow $grid->newColumn('commits'); $grid->newColumn('status'); $grid->newColumn('revisions'); - $grid->newColumn('messages'); + $grid->newColumn('messages') + ->setMinimumWidth(12); foreach ($partition_lists as $row_lists) { foreach ($row_lists as $row_list) { From 5d305909eb916b60e1b90b6c36857c56479356ba Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jun 2020 09:16:41 -0700 Subject: [PATCH 71/82] When a commit graph set has many commits, summarize them Summary: Ref T13546. In cases where a given set has a large number of commits, summarize them in the output. Test Plan: Ran "arc branches", saw long lists of commits (like the history of "stable" summarized). Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21366 --- .../graph/view/ArcanistCommitGraphSetView.php | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php index b58180c1..fa418f20 100644 --- a/src/repository/graph/view/ArcanistCommitGraphSetView.php +++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php @@ -169,41 +169,43 @@ final class ArcanistCommitGraphSetView ); } - $marker_layout = array(); + $items = array(); foreach ($object_layout as $layout) { $commit_ref = idx($layout, 'commit'); if (!$commit_ref) { - $marker_layout[] = $layout; + $items[] = $layout; continue; } $commit_hash = $commit_ref->getCommitHash(); $markers = idx($marker_refs, $commit_hash); if (!$markers) { - $marker_layout[] = $layout; + $items[] = $layout; continue; } $head_marker = array_shift($markers); $layout['marker'] = $head_marker; - $marker_layout[] = $layout; + $items[] = $layout; if (!$markers) { continue; } foreach ($markers as $marker) { - $marker_layout[] = array( + $items[] = array( 'marker' => $marker, ); } } - $marker_view = $this->drawMarkerCell($marker_layout); - $commits_view = $this->drawCommitsCell($marker_layout); - $status_view = $this->drawStatusCell($marker_layout); - $revisions_view = $this->drawRevisionsCell($marker_layout); - $messages_view = $this->drawMessagesCell($marker_layout); + $items = $this->collapseItems($items); + + $marker_view = $this->drawMarkerCell($items); + $commits_view = $this->drawCommitsCell($items); + $status_view = $this->drawStatusCell($items); + $revisions_view = $this->drawRevisionsCell($items); + $messages_view = $this->drawMessagesCell($items); return array( id(new ArcanistGridCell()) @@ -311,6 +313,12 @@ final class ArcanistCommitGraphSetView private function drawCommitsCell(array $items) { $cell = array(); foreach ($items as $item) { + $count = idx($item, 'collapseCount'); + if ($count) { + $cell[] = tsprintf(" : \n"); + continue; + } + $commit_ref = idx($item, 'commit'); if (!$commit_ref) { $cell[] = tsprintf("\n"); @@ -361,6 +369,16 @@ final class ArcanistCommitGraphSetView $cell = array(); foreach ($items as $item) { + $count = idx($item, 'collapseCount'); + if ($count) { + $cell[] = tsprintf( + "%s\n", + pht( + '<... %s more commits ...>', + new PhutilNumber($count))); + continue; + } + $revision_ref = idx($item, 'revision'); if ($revision_ref) { $cell[] = tsprintf("%s\n", $revision_ref->getName()); @@ -415,5 +433,46 @@ final class ArcanistCommitGraphSetView return tsprintf('%s', $status); } + private function collapseItems(array $items) { + $show_context = 3; + + $map = array(); + foreach ($items as $key => $item) { + $can_collapse = + (isset($item['commit'])) && + (!isset($item['revision'])) && + (!isset($item['marker'])); + $map[$key] = $can_collapse; + } + + $map = phutil_partition($map); + foreach ($map as $partition) { + $value = head($partition); + + if (!$value) { + break; + } + + $count = count($partition); + if ($count < ($show_context * 2) + 3) { + continue; + } + + $partition = array_slice($partition, $show_context, -$show_context, true); + + $is_first = true; + foreach ($partition as $key => $value) { + if ($is_first) { + $items[$key]['collapseCount'] = $count; + } else { + unset($items[$key]); + } + + $is_first = false; + } + } + + return $items; + } } From c7093a2e5796aae13469fe832713a2ab4061f166 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 17 Jun 2020 10:25:22 -0700 Subject: [PATCH 72/82] In "arc branches", group linear sequences of published revisions together Summary: Ref T13546. If your history includes a long linear sequence of published revisions, summarize them. Test Plan: Ran "arc branches", saw better summarization of linear published revision sequences. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21367 --- .../view/ArcanistCommitGraphSetTreeView.php | 119 ++++++++++++++++-- .../graph/view/ArcanistCommitGraphSetView.php | 36 ++++++ 2 files changed, 145 insertions(+), 10 deletions(-) diff --git a/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php index a69641fb..a67db3e2 100644 --- a/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php +++ b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php @@ -54,16 +54,6 @@ final class ArcanistCommitGraphSetTreeView $view_root = $this->newSetViews($set); $view_list = $this->setViews; - foreach ($view_list as $view) { - $parent_view = $view->getParentView(); - if ($parent_view) { - $depth = $parent_view->getViewDepth() + 1; - } else { - $depth = 0; - } - $view->setViewDepth($depth); - } - $api = $this->getRepositoryAPI(); foreach ($view_list as $view) { @@ -81,6 +71,8 @@ final class ArcanistCommitGraphSetTreeView ->setMarkerRefs($marker_refs); } + $view_list = $this->collapseViews($view_root, $view_list); + $rows = array(); foreach ($view_list as $view) { $rows[] = $view->newCellViews(); @@ -144,4 +136,111 @@ final class ArcanistCommitGraphSetTreeView return $results; } + private function collapseViews($view_root, array $view_list) { + $this->groupViews($view_root); + + foreach ($view_list as $view) { + $group = $view->getGroupView(); + $group->addMemberView($view); + } + + foreach ($view_list as $view) { + $member_views = $view->getMemberViews(); + + // Break small groups apart. + $count = count($member_views); + if ($count > 1 && $count < 4) { + foreach ($member_views as $member_view) { + $member_view->setGroupView($member_view); + $member_view->setMemberViews(array($member_view)); + } + } + } + + foreach ($view_list as $view) { + $parent_view = $view->getParentView(); + if (!$parent_view) { + $depth = 0; + } else { + $parent_group = $parent_view->getGroupView(); + + $member_views = $parent_group->getMemberViews(); + if (count($member_views) > 1) { + $depth = $parent_group->getViewDepth() + 2; + } else { + $depth = $parent_group->getViewDepth() + 1; + } + } + + $view->setViewDepth($depth); + } + + foreach ($view_list as $key => $view) { + if (!$view->getMemberViews()) { + unset($view_list[$key]); + } + } + + return $view_list; + } + + private function groupViews($view) { + $group_view = $this->getGroupForView($view); + $view->setGroupView($group_view); + + + + $children = $view->getChildViews(); + foreach ($children as $child) { + $this->groupViews($child); + } + } + + private function getGroupForView($view) { + $revision_refs = $view->getRevisionRefs(); + if ($revision_refs) { + $has_unpublished_revision = false; + + foreach ($revision_refs as $revision_ref) { + if (!$revision_ref->isStatusPublished()) { + $has_unpublished_revision = true; + break; + } + } + + if ($has_unpublished_revision) { + return $view; + } + } + + $marker_lists = $view->getMarkerRefs(); + foreach ($marker_lists as $marker_refs) { + if ($marker_refs) { + return $view; + } + } + + // If a view has no children, it is never grouped with other views. + $children = $view->getChildViews(); + if (!$children) { + return $view; + } + + // If a view is a root, we can't group it. + $parent = $view->getParentView(); + if (!$parent) { + return $view; + } + + // If a view has siblings, we can't group it with other views. + $siblings = $parent->getChildViews(); + if (count($siblings) !== 1) { + return $view; + } + + // The view has no children and no other siblings, so add it to the + // parent's group. + return $parent->getGroupView(); + } + } diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php index fa418f20..b951b677 100644 --- a/src/repository/graph/view/ArcanistCommitGraphSetView.php +++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php @@ -11,6 +11,8 @@ final class ArcanistCommitGraphSetView private $revisionRefs; private $markerRefs; private $viewDepth; + private $groupView; + private $memberViews = array(); public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) { $this->repositoryAPI = $repository_api; @@ -39,6 +41,29 @@ final class ArcanistCommitGraphSetView return $this->parentView; } + public function setGroupView(ArcanistCommitGraphSetView $group_view) { + $this->groupView = $group_view; + return $this; + } + + public function getGroupView() { + return $this->groupView; + } + + public function addMemberView(ArcanistCommitGraphSetView $member_view) { + $this->memberViews[] = $member_view; + return $this; + } + + public function getMemberViews() { + return $this->memberViews; + } + + public function setMemberViews(array $member_views) { + $this->memberViews = $member_views; + return $this; + } + public function addChildView(ArcanistCommitGraphSetView $child_view) { $this->childViews[] = $child_view; return $this; @@ -307,6 +332,17 @@ final class ArcanistCommitGraphSetView $is_first = false; } + $member_views = $this->getMemberViews(); + $member_count = count($member_views); + if ($member_count > 1) { + $cell[] = tsprintf( + "%s%s\n", + $empty_indent, + pht( + '- <... %s more revisions ...>', + new PhutilNumber($member_count - 1))); + } + return $cell; } From 8c95dc0d295ee99a9a109b4a056aa0f6831037f1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 09:15:26 -0700 Subject: [PATCH 73/82] Support date-range commit graph queries, and multiple disjoint commits in Git Summary: Ref T13546. Allow the commit graph to be queried by date range, and Git to be queried for multiple disjoint commits. Test Plan: Ran "arc branches" and future code which searches for alternate commit ranges for revisions. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21379 --- src/ref/revision/ArcanistRevisionRef.php | 10 ++ .../graph/query/ArcanistCommitGraphQuery.php | 22 ++- .../query/ArcanistGitCommitGraphQuery.php | 151 ++++++++++++------ .../ArcanistMercurialCommitGraphQuery.php | 44 ++++- src/workflow/ArcanistMarkersWorkflow.php | 14 +- 5 files changed, 172 insertions(+), 69 deletions(-) diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index c36845bd..6f73390d 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -70,6 +70,11 @@ final class ArcanistRevisionRef $status_value = idx($value_map, idx($dict, 'status')); $ansi_color = idx($color_map, $status_value); + $date_created = null; + if (isset($dict['dateCreated'])) { + $date_created = (int)$dict['dateCreated']; + } + $dict['fields'] = array( 'uri' => idx($dict, 'uri'), 'title' => idx($dict, 'title'), @@ -80,6 +85,7 @@ final class ArcanistRevisionRef 'value' => $status_value, 'color.ansi' => $ansi_color, ), + 'dateCreated' => $date_created, ); return self::newFromConduit($dict); @@ -104,6 +110,10 @@ final class ArcanistRevisionRef return idxv($this->parameters, array('fields', 'status', 'color.ansi')); } + public function getDateCreated() { + return idxv($this->parameters, array('fields', 'dateCreated')); + } + public function isStatusChangesPlanned() { $status = $this->getStatus(); return ($status === 'changes-planned'); diff --git a/src/repository/graph/query/ArcanistCommitGraphQuery.php b/src/repository/graph/query/ArcanistCommitGraphQuery.php index 98cbb35c..6369c4b5 100644 --- a/src/repository/graph/query/ArcanistCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistCommitGraphQuery.php @@ -7,8 +7,9 @@ abstract class ArcanistCommitGraphQuery private $headHashes; private $tailHashes; private $exactHashes; - private $stopAtGCA; private $limit; + private $minimumEpoch; + private $maximumEpoch; final public function setGraph(ArcanistCommitGraph $graph) { $this->graph = $graph; @@ -46,11 +47,6 @@ abstract class ArcanistCommitGraphQuery return $this->exactHashes; } - final public function withStopAtGCA($stop_gca) { - $this->stopAtGCA = $stop_gca; - return $this; - } - final public function setLimit($limit) { $this->limit = $limit; return $this; @@ -60,6 +56,20 @@ abstract class ArcanistCommitGraphQuery return $this->limit; } + final public function withEpochRange($min, $max) { + $this->minimumEpoch = $min; + $this->maximumEpoch = $max; + return $this; + } + + final public function getMinimumEpoch() { + return $this->minimumEpoch; + } + + final public function getMaximumEpoch() { + return $this->maximumEpoch; + } + final public function getRepositoryAPI() { return $this->getGraph()->getRepositoryAPI(); } diff --git a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php index 028fbbe3..2491a549 100644 --- a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php @@ -3,17 +3,21 @@ final class ArcanistGitCommitGraphQuery extends ArcanistCommitGraphQuery { - private $queryFuture; private $seen = array(); + private $futures = array(); + private $iterators = array(); + private $cursors = array(); + private $iteratorKey = 0; public function execute() { - $this->beginExecute(); - $this->continueExecute(); + $this->newFutures(); + + $this->executeIterators(); return $this->seen; } - protected function beginExecute() { + private function newFutures() { $head_hashes = $this->getHeadHashes(); $exact_hashes = $this->getExactHashes(); @@ -22,72 +26,122 @@ final class ArcanistGitCommitGraphQuery } $api = $this->getRepositoryAPI(); + $ref_lists = array(); - $refs = array(); - if ($head_hashes !== null) { - foreach ($head_hashes as $hash) { - $refs[] = $hash; + if ($head_hashes) { + $refs = array(); + if ($head_hashes !== null) { + foreach ($head_hashes as $hash) { + $refs[] = $hash; + } } - } - $tail_hashes = $this->getTailHashes(); - if ($tail_hashes !== null) { - foreach ($tail_hashes as $tail_hash) { - $refs[] = sprintf('^%s^@', $tail_hash); + $tail_hashes = $this->getTailHashes(); + if ($tail_hashes !== null) { + foreach ($tail_hashes as $tail_hash) { + $refs[] = sprintf('^%s^@', $tail_hash); + } } + + $ref_lists[] = $refs; } if ($exact_hashes !== null) { - if (count($exact_hashes) > 1) { - // If "A" is a parent of "B" and we search for exact hashes ["A", "B"], - // the exclusion rule generated by "^B^@" is stronger than the inclusion - // rule generated by "A" and we don't get "A" in the result set. - throw new Exception( - pht( - 'TODO: Multiple exact hashes not supported under Git.')); - } foreach ($exact_hashes as $exact_hash) { - $refs[] = $exact_hash; - $refs[] = sprintf('^%s^@', $exact_hash); + $ref_list = array(); + $ref_list[] = $exact_hash; + $ref_list[] = sprintf('^%s^@', $exact_hash); + $ref_list[] = '--'; + $ref_lists[] = $ref_list; } } - $refs[] = '--'; - $refs = implode("\n", $refs)."\n"; + $flags = array(); - $fields = array( - '%e', - '%H', - '%P', - '%ct', - '%B', - ); + $min_epoch = $this->getMinimumEpoch(); + if ($min_epoch !== null) { + $flags[] = '--after'; + $flags[] = date('c', $min_epoch); + } - $format = implode('%x02', $fields).'%x01'; + $max_epoch = $this->getMaximumEpoch(); + if ($max_epoch !== null) { + $flags[] = '--before'; + $flags[] = date('c', $max_epoch); + } - $future = $api->newFuture( - 'log --format=%s --stdin', - $format); - $future->write($refs); - $future->setResolveOnError(true); - $future->start(); + foreach ($ref_lists as $ref_list) { + $ref_blob = implode("\n", $ref_list)."\n"; - $lines = id(new LinesOfALargeExecFuture($future)) - ->setDelimiter("\1"); - $lines->rewind(); + $fields = array( + '%e', + '%H', + '%P', + '%ct', + '%B', + ); - $this->queryFuture = $lines; + $format = implode('%x02', $fields).'%x01'; + + $future = $api->newFuture( + 'log --format=%s %Ls --stdin', + $format, + $flags); + $future->write($ref_blob); + $future->setResolveOnError(true); + + $this->futures[] = $future; + } } - protected function continueExecute() { + private function executeIterators() { + while ($this->futures || $this->iterators) { + $iterator_limit = 8; + + while (count($this->iterators) < $iterator_limit) { + if (!$this->futures) { + break; + } + + $future = array_pop($this->futures); + $future->startFuture(); + + $iterator = id(new LinesOfALargeExecFuture($future)) + ->setDelimiter("\1"); + $iterator->rewind(); + + $iterator_key = $this->getNextIteratorKey(); + $this->iterators[$iterator_key] = $iterator; + } + + $limit = $this->getLimit(); + + foreach ($this->iterators as $iterator_key => $iterator) { + $this->executeIterator($iterator_key, $iterator); + + if ($limit) { + if (count($this->seen) >= $limit) { + return; + } + } + } + } + } + + private function getNextIteratorKey() { + return $this->iteratorKey++; + } + + private function executeIterator($iterator_key, $lines) { $graph = $this->getGraph(); $limit = $this->getLimit(); - $lines = $this->queryFuture; + $is_done = false; while (true) { if (!$lines->valid()) { - return false; + $is_done = true; + break; } $line = $lines->current(); @@ -133,6 +187,7 @@ final class ArcanistGitCommitGraphQuery $parent_nodes[$parent] = $parent_node; $parent_node->addChildNode($node); + } $node->setParentNodes($parent_nodes); } else { @@ -145,6 +200,10 @@ final class ArcanistGitCommitGraphQuery } } } + + if ($is_done) { + unset($this->iterators[$iterator_key]); + } } } diff --git a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php index 0a3836a9..aadd08ad 100644 --- a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php @@ -47,7 +47,7 @@ final class ArcanistMercurialCommitGraphQuery if ($revsets) { $revsets = array( - $this->joinAndRevsets($revs), + $this->joinAndRevsets($revsets), ); } @@ -58,12 +58,10 @@ final class ArcanistMercurialCommitGraphQuery '%s', $exact_hash); } - $revsets[] = array( - $this->joinOrRevsets($revs), - ); + $revsets[] = $this->joinOrRevsets($revs); } - $revsets = $this->joinOrRevsets($revs); + $revsets = $this->joinOrRevsets($revsets); $fields = array( '', // Placeholder for "encoding". @@ -75,10 +73,42 @@ final class ArcanistMercurialCommitGraphQuery $template = implode("\2", $fields)."\1"; + $flags = array(); + + $min_epoch = $this->getMinimumEpoch(); + $max_epoch = $this->getMaximumEpoch(); + if ($min_epoch !== null || $max_epoch !== null) { + $flags[] = '--date'; + + if ($min_epoch !== null) { + $min_epoch = date('c', $min_epoch); + } + + if ($max_epoch !== null) { + $max_epoch = date('c', $max_epoch); + } + + if ($min_epoch !== null && $max_epoch !== null) { + $flags[] = sprintf( + '%s to %s', + $min_epoch, + $max_epoch); + } else if ($min_epoch) { + $flags[] = sprintf( + '>%s', + $min_epoch); + } else { + $flags[] = sprintf( + '<%s', + $max_epoch); + } + } + $future = $api->newFuture( - 'log --rev %s --template %s --', + 'log --rev %s --template %s %Ls --', $revsets, - $template); + $template, + $flags); $future->setResolveOnError(true); $future->start(); diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index ea9fe98d..d27ae2af 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -60,17 +60,11 @@ abstract class ArcanistMarkersWorkflow } if ($disjoint_heads) { + $disjoint_nodes = $graph->newQuery() + ->withExactHashes($disjoint_heads) + ->execute(); - // TODO: Git currently can not query for more than one exact hash at a - // time. - - foreach ($disjoint_heads as $disjoint_head) { - $disjoint_nodes = $graph->newQuery() - ->withExactHashes(array($disjoint_head)) - ->execute(); - - $nodes += $disjoint_nodes; - } + $nodes += $disjoint_nodes; } $state_refs = array(); From 4b8a32ee0273959727285a81bc239b61de837fff Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Jun 2020 15:20:57 -0700 Subject: [PATCH 74/82] Give Mercurial more plausible marker behavior Summary: Ref T13546. Fixes some issues where marker selection in Mercurial didn't work, and selects "draft()" as the set of commits to show, which is at least somewhat reasonable. Test Plan: Ran "arc branches" and "arc bookmarks" in Mercurial, got more reasonable output. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21380 --- src/repository/api/ArcanistMercurialAPI.php | 20 +++++++++++++++++++ .../ArcanistMercurialCommitGraphQuery.php | 12 ++++++++--- src/workflow/ArcanistLookWorkflow.php | 2 +- src/workflow/ArcanistMarkersWorkflow.php | 8 +------- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php index 9915a879..cffb9306 100644 --- a/src/repository/api/ArcanistMercurialAPI.php +++ b/src/repository/api/ArcanistMercurialAPI.php @@ -1035,4 +1035,24 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI { return new ArcanistMercurialCommitGraphQuery(); } + protected function newPublishedCommitHashes() { + $future = $this->newFuture( + 'log --rev %s --template %s', + hgsprintf('parents(draft()) - draft()'), + '{node}\n'); + list($lines) = $future->resolve(); + + $lines = phutil_split_lines($lines, false); + + $hashes = array(); + foreach ($lines as $line) { + if (!strlen(trim($line))) { + continue; + } + $hashes[] = $line; + } + + return $hashes; + } + } diff --git a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php index aadd08ad..4a87f162 100644 --- a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php @@ -66,9 +66,9 @@ final class ArcanistMercurialCommitGraphQuery $fields = array( '', // Placeholder for "encoding". '{node}', - '{parents}', + '{p1node} {p2node}', '{date|rfc822date}', - '{description|utf8}', + '{desc|utf8}', ); $template = implode("\2", $fields)."\1"; @@ -123,6 +123,9 @@ final class ArcanistMercurialCommitGraphQuery $graph = $this->getGraph(); $lines = $this->queryFuture; $limit = $this->getLimit(); + + $no_parent = str_repeat('0', 40); + while (true) { if (!$lines->valid()) { return false; @@ -159,9 +162,12 @@ final class ArcanistMercurialCommitGraphQuery if (strlen($parents)) { $parents = explode(' ', $parents); - $parent_nodes = array(); foreach ($parents as $parent) { + if ($parent === $no_parent) { + continue; + } + $parent_node = $graph->getNode($parent); if (!$parent_node) { $parent_node = $graph->newNode($parent); diff --git a/src/workflow/ArcanistLookWorkflow.php b/src/workflow/ArcanistLookWorkflow.php index e54fffe9..ac0cedcb 100644 --- a/src/workflow/ArcanistLookWorkflow.php +++ b/src/workflow/ArcanistLookWorkflow.php @@ -60,7 +60,7 @@ EOTEXT 'You stand in the middle of a small clearing in the woods.')); $now = time(); - $hour = (int)date('h', $now); + $hour = (int)date('G', $now); if ($hour >= 5 && $hour <= 7) { $time = pht( diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index d27ae2af..f3933566 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -16,7 +16,7 @@ abstract class ArcanistMarkersWorkflow ->withMarkerTypes(array($marker_type)) ->execute(); - $tail_hashes = $this->getTailHashes(); + $tail_hashes = $api->getPublishedCommitHashes(); $heads = mpull($markers, 'getCommitHash'); @@ -32,7 +32,6 @@ abstract class ArcanistMarkersWorkflow } $nodes = $query->execute(); - if (count($nodes) > $limit) { // TODO: Show what we can. @@ -168,11 +167,6 @@ abstract class ArcanistMarkersWorkflow return isset($types[$marker_type]); } - private function getTailHashes() { - $api = $this->getRepositoryAPI(); - return $api->getPublishedCommitHashes(); - } - private function sortSets( ArcanistCommitGraph $graph, array $sets, From 98ca5cfa817198a488a67256c033be45ccaa5abb Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 05:05:27 -0700 Subject: [PATCH 75/82] Remove an unused method in "ArcanistUploadWorkflow" Summary: This method is private and has no callers. The code has moved to "FileUploader" in a prior change. Test Plan: Grepped for callers, found none. Differential Revision: https://secure.phabricator.com/D21382 --- src/workflow/ArcanistUploadWorkflow.php | 79 ------------------------- 1 file changed, 79 deletions(-) diff --git a/src/workflow/ArcanistUploadWorkflow.php b/src/workflow/ArcanistUploadWorkflow.php index 1ed34f46..d888c464 100644 --- a/src/workflow/ArcanistUploadWorkflow.php +++ b/src/workflow/ArcanistUploadWorkflow.php @@ -141,83 +141,4 @@ EOTEXT $this->writeStatusMessage($line."\n"); } - private function uploadChunks($file_phid, $path) { - $conduit = $this->getConduit(); - - $f = @fopen($path, 'rb'); - if (!$f) { - throw new Exception(pht('Unable to open file "%s"', $path)); - } - - $this->writeStatus(pht('Beginning chunked upload of large file...')); - $chunks = $conduit->resolveCall( - 'file.querychunks', - array( - 'filePHID' => $file_phid, - )); - - $remaining = array(); - foreach ($chunks as $chunk) { - if (!$chunk['complete']) { - $remaining[] = $chunk; - } - } - - $done = (count($chunks) - count($remaining)); - - if ($done) { - $this->writeStatus( - pht( - 'Resuming upload (%s of %s chunks remain).', - phutil_count($remaining), - phutil_count($chunks))); - } else { - $this->writeStatus( - pht( - 'Uploading chunks (%s chunks to upload).', - phutil_count($remaining))); - } - - $progress = new PhutilConsoleProgressBar(); - $progress->setTotal(count($chunks)); - - for ($ii = 0; $ii < $done; $ii++) { - $progress->update(1); - } - - $progress->draw(); - - // TODO: We could do these in parallel to improve upload performance. - foreach ($remaining as $chunk) { - $offset = $chunk['byteStart']; - - $ok = fseek($f, $offset); - if ($ok !== 0) { - throw new Exception( - pht( - 'Failed to %s!', - 'fseek()')); - } - - $data = fread($f, $chunk['byteEnd'] - $chunk['byteStart']); - if ($data === false) { - throw new Exception( - pht( - 'Failed to %s!', - 'fread()')); - } - - $conduit->resolveCall( - 'file.uploadchunk', - array( - 'filePHID' => $file_phid, - 'byteStart' => $offset, - 'dataEncoding' => 'base64', - 'data' => base64_encode($data), - )); - - $progress->update(1); - } - } - } From 2daf9b16aeb1abbceefc889ede9f1304fdfecd90 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 05:06:19 -0700 Subject: [PATCH 76/82] Improve resolution behaviors of FutureProxy Summary: See PHI1764. See PHI1802. Address two resolution behaviors for FutureProxy: - FutureProxy may throw an exception directly from iteration via "FutureIterator" (see PHI1764). This is wrong: futures should throw only when resolved. - FutureProxy can not change an exception into a result, or a result into an exception, or an exception into a different exception. Being able to proxy the full range of result and exception behavior is useful, particularly for Conduit (see PHI1802). Make "FutureProxy" more robust in how it handles exceptions from proxied futures. Test Plan: Used this script to raise an exception during result processing: ``` resolve(); } catch (Exception $ex) { echo "Caught exception properly on resolution.\n"; } } ``` Before this change, the exception is raised in the `foreach()` loop. After this change, the exception is raised at resolution time. Differential Revision: https://secure.phabricator.com/D21383 --- src/future/Future.php | 2 +- src/future/FutureProxy.php | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/future/Future.php b/src/future/Future.php index 3c371bc4..1e52c48a 100644 --- a/src/future/Future.php +++ b/src/future/Future.php @@ -63,7 +63,7 @@ abstract class Future extends Phobject { $this->hasStarted = true; $this->startServiceProfiler(); - $this->isReady(); + $this->updateFuture(); } final public function updateFuture() { diff --git a/src/future/FutureProxy.php b/src/future/FutureProxy.php index 77c8c5bb..d7e00cd6 100644 --- a/src/future/FutureProxy.php +++ b/src/future/FutureProxy.php @@ -27,27 +27,29 @@ abstract class FutureProxy extends Future { } public function isReady() { - if ($this->hasResult()) { + if ($this->hasResult() || $this->hasException()) { return true; } $proxied = $this->getProxiedFuture(); + $proxied->updateFuture(); - $is_ready = $proxied->isReady(); + if ($proxied->hasResult() || $proxied->hasException()) { + try { + $result = $proxied->resolve(); + $result = $this->didReceiveResult($result); + } catch (Exception $ex) { + $result = $this->didReceiveException($ex); + } catch (Throwable $ex) { + $result = $this->didReceiveException($ex); + } - if ($proxied->hasResult()) { - $result = $proxied->getResult(); - $result = $this->didReceiveResult($result); $this->setResult($result); + + return true; } - return $is_ready; - } - - public function resolve() { - $this->getProxiedFuture()->resolve(); - $this->isReady(); - return $this->getResult(); + return false; } public function getReadSockets() { @@ -73,4 +75,8 @@ abstract class FutureProxy extends Future { abstract protected function didReceiveResult($result); + protected function didReceiveException($exception) { + throw $exception; + } + } From 7e9f80971bd646823ff804a5eb0c2f649ae99523 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 05:08:06 -0700 Subject: [PATCH 77/82] Implement Conduit login prompt behavior as a pure FutureProxy, not a Future-like object Summary: See PHI1802. Currently, we can't raise a "you must login" error in a generic way at the beginning of a workflow because we don't know if a workflow needs credentials or not. For example, "arc help" does not need credentials but "arc diff" does. Additionally, some actual Conduit calls do not need credentials ("conduit.ping", "conduit.getcapabilities") and others do. Although I'd like to simplify this eventually and move away from anonymous/unauthenticated "arc", this isn't trivial today. It's also possible for third-party code to add authenticated calls to "arc help", etc., so even if we could execute these tests upfront it's not obvious we'd want to. So, for now, we raise "you must login" at runtime, when we receive an authentication error from Conduit. This got implemented for Toolsets in a well-intentioned but not-so-great way somewhere in wilds/experimental, with an "ArcanistConduitCall" that behaves a bit like a future but is not really a future. This implementation made more sense when ConduitEngine was serving as a future engine, and FutureProxy could not rewrite exceptions. After the Toolsets code was first written, ConduitEngine has stopped serving as a future engine (this is now in "HardpointEngine"). Since HardpointEngine needs a real future, this "show the user a login message" code gets bypassed. This results in user-visible raw authentication exceptions on some workflows: ``` [2020-06-30 21:39:53] EXCEPTION: (ConduitClientException) ERR-INVALID-SESSION: Session key is not present. at [/src/conduit/ConduitFuture.php:76] ``` To fix this: - Allow FutureProxy to rewrite exceptions (see D21383). - Implement "ArcanistConduitCall" as a FutureProxy, not a future-like object. - Collapse the mixed-mode future/not-quite-a-future APIs into a single "real future" API. Test Plan: - Created a paste with "echo hi | arc paste --". - Uploaded a file with "arc upload". - Called a raw method with "echo {} | arc call-conduit conduit.ping --". - Invoked hardpoint behavior with "arc branches". - Grepped for calls to either "resolveCall()" method, found none. - Grepped for calls to "newCall()", found none. - Grepped for "ArcanistConduitCall", found no references. Then: - Removed my "~/.arcrc", ran "arc land", got a sensible and human-readable (but currently ugly) exception instead of a raw authentication stack trace. Differential Revision: https://secure.phabricator.com/D21384 --- src/__phutil_library_map__.php | 4 +- ...Call.php => ArcanistConduitCallFuture.php} | 105 ++++-------------- src/conduit/ArcanistConduitEngine.php | 21 +--- src/conduit/ConduitSearchFuture.php | 3 +- src/ref/paste/ArcanistPasteRef.php | 2 +- .../query/ArcanistRuntimeHardpointQuery.php | 3 +- src/upload/ArcanistFileUploader.php | 16 +-- src/workflow/ArcanistCallConduitWorkflow.php | 3 +- src/workflow/ArcanistPasteWorkflow.php | 3 +- src/workflow/ArcanistWorkflow.php | 19 +--- 10 files changed, 45 insertions(+), 134 deletions(-) rename src/conduit/{ArcanistConduitCall.php => ArcanistConduitCallFuture.php} (52%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c988291b..9bd23305 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -126,7 +126,7 @@ phutil_register_library_map(array( 'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php', - 'ArcanistConduitCall' => 'conduit/ArcanistConduitCall.php', + 'ArcanistConduitCallFuture' => 'conduit/ArcanistConduitCallFuture.php', 'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php', 'ArcanistConduitException' => 'conduit/ArcanistConduitException.php', 'ArcanistConfigOption' => 'config/option/ArcanistConfigOption.php', @@ -1170,7 +1170,7 @@ phutil_register_library_map(array( 'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', - 'ArcanistConduitCall' => 'Phobject', + 'ArcanistConduitCallFuture' => 'FutureProxy', 'ArcanistConduitEngine' => 'Phobject', 'ArcanistConduitException' => 'Exception', 'ArcanistConfigOption' => 'Phobject', diff --git a/src/conduit/ArcanistConduitCall.php b/src/conduit/ArcanistConduitCallFuture.php similarity index 52% rename from src/conduit/ArcanistConduitCall.php rename to src/conduit/ArcanistConduitCallFuture.php index bd25ff19..01d9954d 100644 --- a/src/conduit/ArcanistConduitCall.php +++ b/src/conduit/ArcanistConduitCallFuture.php @@ -1,22 +1,9 @@ key = $key; - return $this; - } - - public function getKey() { - return $this->key; - } public function setEngine(ArcanistConduitEngine $engine) { $this->engine = $engine; @@ -27,71 +14,6 @@ final class ArcanistConduitCall return $this->engine; } - public function setMethod($method) { - $this->method = $method; - return $this; - } - - public function getMethod() { - return $this->method; - } - - public function setParameters(array $parameters) { - $this->parameters = $parameters; - return $this; - } - - public function getParameters() { - return $this->parameters; - } - - private function newFuture() { - if ($this->future) { - throw new Exception( - pht( - 'Call has previously generated a future. Create a '. - 'new call object for each API method invocation.')); - } - - $method = $this->getMethod(); - $parameters = $this->getParameters(); - $future = $this->getEngine()->newFuture($this); - $this->future = $future; - - return $this->future; - } - - public function resolve() { - if (!$this->future) { - $this->newFuture(); - } - - return $this->resolveFuture(); - } - - private function resolveFuture() { - $future = $this->future; - - try { - $result = $future->resolve(); - } catch (ConduitClientException $ex) { - switch ($ex->getErrorCode()) { - case 'ERR-INVALID-SESSION': - if (!$this->getEngine()->getConduitToken()) { - $this->raiseLoginRequired(); - } - break; - case 'ERR-INVALID-AUTH': - $this->raiseInvalidAuth(); - break; - } - - throw $ex; - } - - return $result; - } - private function raiseLoginRequired() { $conduit_uri = $this->getEngine()->getConduitURI(); $conduit_uri = new PhutilURI($conduit_uri); @@ -119,7 +41,7 @@ final class ArcanistConduitCall " $ arc install-certificate %s\n", $conduit_uri)); - throw new ArcanistUsageException($block->drawConsoleString()); + throw new PhutilArgumentUsageException($block->drawConsoleString()); } private function raiseInvalidAuth() { @@ -147,7 +69,26 @@ final class ArcanistConduitCall " $ arc install-certificate %s\n", $conduit_uri)); - throw new ArcanistUsageException($block->drawConsoleString()); + throw new PhutilArgumentUsageException($block->drawConsoleString()); + } + + protected function didReceiveResult($result) { + return $result; + } + + protected function didReceiveException($exception) { + switch ($exception->getErrorCode()) { + case 'ERR-INVALID-SESSION': + if (!$this->getEngine()->getConduitToken()) { + $this->raiseLoginRequired(); + } + break; + case 'ERR-INVALID-AUTH': + $this->raiseInvalidAuth(); + break; + } + + throw $exception; } } diff --git a/src/conduit/ArcanistConduitEngine.php b/src/conduit/ArcanistConduitEngine.php index c9171245..cd2b411e 100644 --- a/src/conduit/ArcanistConduitEngine.php +++ b/src/conduit/ArcanistConduitEngine.php @@ -39,28 +39,17 @@ final class ArcanistConduitEngine return $this->conduitTimeout; } - public function newCall($method, array $parameters) { + public function newFuture($method, array $parameters) { if ($this->conduitURI == null && $this->client === null) { $this->raiseURIException(); } - return id(new ArcanistConduitCall()) - ->setEngine($this) - ->setMethod($method) - ->setParameters($parameters); - } - - public function resolveCall($method, array $parameters) { - return $this->newCall($method, $parameters)->resolve(); - } - - public function newFuture(ArcanistConduitCall $call) { - $method = $call->getMethod(); - $parameters = $call->getParameters(); - $future = $this->getClient()->callMethod($method, $parameters); - return $future; + $call_future = id(new ArcanistConduitCallFuture($future)) + ->setEngine($this); + + return $call_future; } private function getClient() { diff --git a/src/conduit/ConduitSearchFuture.php b/src/conduit/ConduitSearchFuture.php index 03c394d9..f4d9564e 100644 --- a/src/conduit/ConduitSearchFuture.php +++ b/src/conduit/ConduitSearchFuture.php @@ -104,8 +104,7 @@ final class ConduitSearchFuture $parameters['after'] = (string)$this->cursor; } - $conduit_call = $engine->newCall($method, $parameters); - $conduit_future = $engine->newFuture($conduit_call); + $conduit_future = $engine->newFuture($method, $parameters); return $conduit_future; } diff --git a/src/ref/paste/ArcanistPasteRef.php b/src/ref/paste/ArcanistPasteRef.php index bfeaa77b..d987656d 100644 --- a/src/ref/paste/ArcanistPasteRef.php +++ b/src/ref/paste/ArcanistPasteRef.php @@ -48,7 +48,7 @@ final class ArcanistPasteRef protected function buildRefView(ArcanistRefView $view) { $view ->setObjectName($this->getMonogram()) - ->setTitle($this->getName()); + ->setTitle($this->getTitle()); } } diff --git a/src/toolset/query/ArcanistRuntimeHardpointQuery.php b/src/toolset/query/ArcanistRuntimeHardpointQuery.php index aec1cd32..ed824da2 100644 --- a/src/toolset/query/ArcanistRuntimeHardpointQuery.php +++ b/src/toolset/query/ArcanistRuntimeHardpointQuery.php @@ -77,8 +77,7 @@ abstract class ArcanistRuntimeHardpointQuery $conduit_engine = $this->getRuntime() ->getConduitEngine(); - $call_object = $conduit_engine->newCall($method, $parameters); - $call_future = $conduit_engine->newFuture($call_object); + $call_future = $conduit_engine->newFuture($method, $parameters); return $call_future; } diff --git a/src/upload/ArcanistFileUploader.php b/src/upload/ArcanistFileUploader.php index 5e1841f4..cde462e7 100644 --- a/src/upload/ArcanistFileUploader.php +++ b/src/upload/ArcanistFileUploader.php @@ -118,11 +118,7 @@ final class ArcanistFileUploader extends Phobject { $params['deleteAfterEpoch'] = $delete_after; } - // TOOLSETS: This should be a real future, but ConduitEngine and - // ConduitCall are currently built oddly and return pretend futures. - - $futures[$key] = new ImmediateFuture( - $conduit->resolveCall('file.allocate', $params)); + $futures[$key] = $conduit->newFuture('file.allocate', $params); } $iterator = id(new FutureIterator($futures))->limit(4); @@ -217,11 +213,12 @@ final class ArcanistFileUploader extends Phobject { private function uploadChunks(ArcanistFileDataRef $file, $file_phid) { $conduit = $this->conduitEngine; - $chunks = $conduit->resolveCall( + $future = $conduit->newFuture( 'file.querychunks', array( 'filePHID' => $file_phid, )); + $chunks = $future->resolve(); $remaining = array(); foreach ($chunks as $chunk) { @@ -258,7 +255,7 @@ final class ArcanistFileUploader extends Phobject { foreach ($remaining as $chunk) { $data = $file->readBytes($chunk['byteStart'], $chunk['byteEnd']); - $conduit->resolveCall( + $future = $conduit->newFuture( 'file.uploadchunk', array( 'filePHID' => $file_phid, @@ -266,6 +263,7 @@ final class ArcanistFileUploader extends Phobject { 'dataEncoding' => 'base64', 'data' => base64_encode($data), )); + $future->resolve(); $progress->update(1); } @@ -282,11 +280,13 @@ final class ArcanistFileUploader extends Phobject { $data = $file->readBytes(0, $file->getByteSize()); - return $conduit->resolveCall( + $future = $conduit->newFuture( 'file.upload', $this->getUploadParameters($file) + array( 'data_base64' => base64_encode($data), )); + + return $future->resolve(); } diff --git a/src/workflow/ArcanistCallConduitWorkflow.php b/src/workflow/ArcanistCallConduitWorkflow.php index f5b302ae..025be3e1 100644 --- a/src/workflow/ArcanistCallConduitWorkflow.php +++ b/src/workflow/ArcanistCallConduitWorkflow.php @@ -53,8 +53,7 @@ EOTEXT } $engine = $this->getConduitEngine(); - $conduit_call = $engine->newCall($method, $params); - $conduit_future = $engine->newFuture($conduit_call); + $conduit_future = $engine->newFuture($method, $params); $error = null; $error_message = null; diff --git a/src/workflow/ArcanistPasteWorkflow.php b/src/workflow/ArcanistPasteWorkflow.php index f9e2a693..c447cf29 100644 --- a/src/workflow/ArcanistPasteWorkflow.php +++ b/src/workflow/ArcanistPasteWorkflow.php @@ -141,8 +141,7 @@ EOTEXT ); $conduit_engine = $this->getConduitEngine(); - $conduit_call = $conduit_engine->newCall($method, $parameters); - $conduit_future = $conduit_engine->newFuture($conduit_call); + $conduit_future = $conduit_engine->newFuture($method, $parameters); $result = $conduit_future->resolve(); $paste_phid = idxv($result, array('object', 'phid')); diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php index c6098ddb..edad2d8d 100644 --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -1808,22 +1808,6 @@ abstract class ArcanistWorkflow extends Phobject { return $parser; } - final protected function resolveCall(ConduitFuture $method) { - try { - return $method->resolve(); - } catch (ConduitClientException $ex) { - if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') { - echo phutil_console_wrap( - pht( - 'This feature requires a newer version of Phabricator. Please '. - 'update it using these instructions: %s', - 'https://secure.phabricator.com/book/phabricator/article/'. - 'upgrading/')."\n\n"); - } - throw $ex; - } - } - final protected function dispatchEvent($type, array $data) { $data += array( 'workflow' => $this, @@ -1964,7 +1948,8 @@ abstract class ArcanistWorkflow extends Phobject { try { $method = 'repository.query'; - $results = $this->getConduitEngine()->newCall($method, $query) + $results = $this->getConduitEngine() + ->newFuture($method, $query) ->resolve(); } catch (ConduitClientException $ex) { if ($ex->getErrorCode() == 'ERR-CONDUIT-CALL') { From 17f2668d1f757b8399bd1147c666adb3a1c3c056 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 05:33:33 -0700 Subject: [PATCH 78/82] When tab-completing "arc" commands, suggest paths if the argument is empty and a path wildcard argument exists Summary: Currently, if you type "arc upload ", we do not autocomplete the working directory. We should, but the "current argument" is the empty string and that's technically a prefix of every flag, so we suggest that you might want a flag instead. You probably don't. Suggest paths in this case. Test Plan: - Ran "arc upload ", saw path completions. - Ran "arc land " (this workflow does NOT take paths as arguments), saw flag completions. Differential Revision: https://secure.phabricator.com/D21385 --- .../workflow/ArcanistShellCompleteWorkflow.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/toolset/workflow/ArcanistShellCompleteWorkflow.php b/src/toolset/workflow/ArcanistShellCompleteWorkflow.php index 4fc1fb0a..f68d04a5 100644 --- a/src/toolset/workflow/ArcanistShellCompleteWorkflow.php +++ b/src/toolset/workflow/ArcanistShellCompleteWorkflow.php @@ -585,12 +585,18 @@ EOTEXT $matches = $this->getMatches($flags, $current); // If whatever the user is completing does not match the prefix of any - // flag, try to autcomplete a wildcard argument if it has some kind of - // meaningful completion. For example, "arc lint READ" should - // autocomplete a file. + // flag (or is entirely empty), try to autcomplete a wildcard argument + // if it has some kind of meaningful completion. For example, "arc lint + // READ" should autocomplete a file, and "arc lint " should + // suggest files in the current directory. - if (!$matches && $wildcard) { + if (!strlen($current) || !$matches) { + $try_paths = true; + } else { + $try_paths = false; + } + if ($try_paths && $wildcard) { // TOOLSETS: There was previously some very questionable support for // autocompleting branches here. This could be moved into Arguments // and Workflows. From 65e4927dca339bb3f25c83c95f1bb7f38f9fc9a2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 05:50:13 -0700 Subject: [PATCH 79/82] Drop intended support for "--anonymous" from Arcanist Toolsets Summary: Currently, modern "arc" workflows accept and parse "--anonymous" but don't do anything with it. I intend to move away from anonymous workflows, which only really supported a tiny subset of unusual workflows on open source installs but added significant complexity to login/auth behavior. Drop support for this flag. Test Plan: Grepped for "anonymous", didn't turn up any relevant hits. Differential Revision: https://secure.phabricator.com/D21386 --- src/toolset/ArcanistArcToolset.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/toolset/ArcanistArcToolset.php b/src/toolset/ArcanistArcToolset.php index fb3ceff4..5c1cb377 100644 --- a/src/toolset/ArcanistArcToolset.php +++ b/src/toolset/ArcanistArcToolset.php @@ -16,10 +16,6 @@ final class ArcanistArcToolset extends ArcanistToolset { 'param' => 'token', 'help' => pht('Use a specific authentication token.'), ), - array( - 'name' => 'anonymous', - 'help' => pht('Run workflow as a public user, without authenticating.'), - ), ); } From b8a5191e3b36f445f4bd5708472072a34427a0f1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 06:12:09 -0700 Subject: [PATCH 80/82] Improve login/auth messages from Arcanist toolset workflows Summary: See PHI1802. After D21384, "arc land" and similar with no credentials now properly raise a useful exception, but it isn't formatted readably. Update the display code to make it look prettier. Test Plan: Ran "arc land" with no and invalid credentials, got properly formatted output. Differential Revision: https://secure.phabricator.com/D21387 --- src/__phutil_library_map__.php | 2 + src/conduit/ArcanistConduitCallFuture.php | 104 +++++++++++------- ...ArcanistConduitAuthenticationException.php | 27 +++++ src/runtime/ArcanistRuntime.php | 2 + 4 files changed, 94 insertions(+), 41 deletions(-) create mode 100644 src/exception/ArcanistConduitAuthenticationException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9bd23305..90e2e2ef 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -126,6 +126,7 @@ phutil_register_library_map(array( 'ArcanistComprehensiveLintEngine' => 'lint/engine/ArcanistComprehensiveLintEngine.php', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistConcatenationOperatorXHPASTLinterRule.php', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistConcatenationOperatorXHPASTLinterRuleTestCase.php', + 'ArcanistConduitAuthenticationException' => 'exception/ArcanistConduitAuthenticationException.php', 'ArcanistConduitCallFuture' => 'conduit/ArcanistConduitCallFuture.php', 'ArcanistConduitEngine' => 'conduit/ArcanistConduitEngine.php', 'ArcanistConduitException' => 'conduit/ArcanistConduitException.php', @@ -1170,6 +1171,7 @@ phutil_register_library_map(array( 'ArcanistComprehensiveLintEngine' => 'ArcanistLintEngine', 'ArcanistConcatenationOperatorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistConcatenationOperatorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistConduitAuthenticationException' => 'Exception', 'ArcanistConduitCallFuture' => 'FutureProxy', 'ArcanistConduitEngine' => 'Phobject', 'ArcanistConduitException' => 'Exception', diff --git a/src/conduit/ArcanistConduitCallFuture.php b/src/conduit/ArcanistConduitCallFuture.php index 01d9954d..187b4c27 100644 --- a/src/conduit/ArcanistConduitCallFuture.php +++ b/src/conduit/ArcanistConduitCallFuture.php @@ -15,61 +15,56 @@ final class ArcanistConduitCallFuture } private function raiseLoginRequired() { - $conduit_uri = $this->getEngine()->getConduitURI(); - $conduit_uri = new PhutilURI($conduit_uri); - $conduit_uri->setPath('/'); + $conduit_domain = $this->getConduitDomain(); - $conduit_domain = $conduit_uri->getDomain(); - - $block = id(new PhutilConsoleBlock()) - ->addParagraph( - tsprintf( - '** %s **', - pht('LOGIN REQUIRED'))) - ->addParagraph( + $message = array( + tsprintf( + "\n\n%W\n\n", pht( 'You are trying to connect to a server ("%s") that you do not '. 'have any stored credentials for, but the command you are '. 'running requires authentication.', - $conduit_domain)) - ->addParagraph( + $conduit_domain)), + tsprintf( + "%W\n\n", pht( - 'To login and save credentials for this server, run this '. - 'command:')) - ->addParagraph( - tsprintf( - " $ arc install-certificate %s\n", - $conduit_uri)); + 'To log in and save credentials for this server, run this '. + 'command:')), + tsprintf( + '%>', + $this->getInstallCommand()), + ); - throw new PhutilArgumentUsageException($block->drawConsoleString()); + $this->raiseException( + pht('Conduit API login required.'), + pht('LOGIN REQUIRED'), + $message); } private function raiseInvalidAuth() { - $conduit_uri = $this->getEngine()->getConduitURI(); - $conduit_uri = new PhutilURI($conduit_uri); - $conduit_uri->setPath('/'); + $conduit_domain = $this->getConduitDomain(); - $conduit_domain = $conduit_uri->getDomain(); - - $block = id(new PhutilConsoleBlock()) - ->addParagraph( - tsprintf( - '** %s **', - pht('INVALID CREDENTIALS'))) - ->addParagraph( + $message = array( + tsprintf( + "\n\n%W\n\n", pht( - 'Your stored credentials for this server ("%s") are not valid.', - $conduit_domain)) - ->addParagraph( + 'Your stored credentials for the server you are trying to connect '. + 'to ("%s") are not valid.', + $conduit_domain)), + tsprintf( + "%W\n\n", pht( - 'To login and save valid credentials for this server, run this '. - 'command:')) - ->addParagraph( - tsprintf( - " $ arc install-certificate %s\n", - $conduit_uri)); + 'To log in and save valid credentials for this server, run this '. + 'command:')), + tsprintf( + '%>', + $this->getInstallCommand()), + ); - throw new PhutilArgumentUsageException($block->drawConsoleString()); + $this->raiseException( + pht('Invalid Conduit API credentials.'), + pht('INVALID CREDENTIALS'), + $message); } protected function didReceiveResult($result) { @@ -91,4 +86,31 @@ final class ArcanistConduitCallFuture throw $exception; } + private function getInstallCommand() { + $conduit_uri = $this->getConduitURI(); + + return csprintf( + 'arc install-certificate %s', + $conduit_uri); + } + + private function getConduitURI() { + $conduit_uri = $this->getEngine()->getConduitURI(); + $conduit_uri = new PhutilURI($conduit_uri); + $conduit_uri->setPath('/'); + + return $conduit_uri; + } + + private function getConduitDomain() { + $conduit_uri = $this->getConduitURI(); + return $conduit_uri->getDomain(); + } + + private function raiseException($summary, $title, $body) { + throw id(new ArcanistConduitAuthenticationException($summary)) + ->setTitle($title) + ->setBody($body); + } + } diff --git a/src/exception/ArcanistConduitAuthenticationException.php b/src/exception/ArcanistConduitAuthenticationException.php new file mode 100644 index 00000000..72345453 --- /dev/null +++ b/src/exception/ArcanistConduitAuthenticationException.php @@ -0,0 +1,27 @@ +title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function setBody($body) { + $this->body = $body; + return $this; + } + + public function getBody() { + return $this->body; + } + +} diff --git a/src/runtime/ArcanistRuntime.php b/src/runtime/ArcanistRuntime.php index 485857f3..87d574a5 100644 --- a/src/runtime/ArcanistRuntime.php +++ b/src/runtime/ArcanistRuntime.php @@ -41,6 +41,8 @@ final class ArcanistRuntime { $log->writeError(pht('USAGE EXCEPTION'), $ex->getMessage()); } catch (ArcanistUserAbortException $ex) { $log->writeError(pht('---'), $ex->getMessage()); + } catch (ArcanistConduitAuthenticationException $ex) { + $log->writeError($ex->getTitle(), $ex->getBody()); } return 1; From 01e91dc260a737376701a4908af04b67ff2791cf Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 1 Jul 2020 06:23:23 -0700 Subject: [PATCH 81/82] Clean up some service profiler behavior in Conduit futures Summary: Correct two minor Conduit future issues: - FutureAgent shows up in the trace log as "". Since it isn't doing anything useful, solve the mystery and drop it from the log. - Simply the ConduitFuture code for interacting with the service profiler now that a more structured integration is available. Test Plan: Ran "arc branches --trace", saw conduit calls and no more "" clutter. Differential Revision: https://secure.phabricator.com/D21388 --- src/conduit/ConduitFuture.php | 29 +++++++++-------------------- src/conduit/FutureAgent.php | 7 +++++++ src/future/Future.php | 4 ++++ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/conduit/ConduitFuture.php b/src/conduit/ConduitFuture.php index 20c6906a..351d2d11 100644 --- a/src/conduit/ConduitFuture.php +++ b/src/conduit/ConduitFuture.php @@ -5,7 +5,6 @@ final class ConduitFuture extends FutureProxy { private $client; private $engine; private $conduitMethod; - private $profilerCallID; public function setClient(ConduitClient $client, $method) { $this->client = $client; @@ -13,29 +12,19 @@ final class ConduitFuture extends FutureProxy { return $this; } - public function isReady() { - if ($this->profilerCallID === null) { - $profiler = PhutilServiceProfiler::getInstance(); + protected function getServiceProfilerStartParameters() { + return array( + 'type' => 'conduit', + 'method' => $this->conduitMethod, + 'size' => $this->getProxiedFuture()->getHTTPRequestByteLength(), + ); + } - $this->profilerCallID = $profiler->beginServiceCall( - array( - 'type' => 'conduit', - 'method' => $this->conduitMethod, - 'size' => $this->getProxiedFuture()->getHTTPRequestByteLength(), - )); - } - - return parent::isReady(); + protected function getServiceProfilerResultParameters() { + return array(); } protected function didReceiveResult($result) { - if ($this->profilerCallID !== null) { - $profiler = PhutilServiceProfiler::getInstance(); - $profiler->endServiceCall( - $this->profilerCallID, - array()); - } - list($status, $body, $headers) = $result; if ($status->isError()) { throw $status; diff --git a/src/conduit/FutureAgent.php b/src/conduit/FutureAgent.php index 6000c6b2..3b84ab88 100644 --- a/src/conduit/FutureAgent.php +++ b/src/conduit/FutureAgent.php @@ -35,4 +35,11 @@ abstract class FutureAgent return $sockets; } + protected function getServiceProfilerStartParameters() { + // At least today, the agent construct doesn't add anything interesting + // to the trace and the underlying futures always show up in the trace + // themselves. + return null; + } + } diff --git a/src/future/Future.php b/src/future/Future.php index 1e52c48a..8078aacc 100644 --- a/src/future/Future.php +++ b/src/future/Future.php @@ -115,6 +115,10 @@ abstract class Future extends Phobject { $params = $this->getServiceProfilerStartParameters(); + if ($params === null) { + return; + } + $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall($params); From a5480609f8708d2c7a01aab1a28c4a6c959d1d19 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 3 Jul 2020 11:32:55 -0700 Subject: [PATCH 82/82] Render the state tree in "arc branches" slightly more cleanly Summary: Ref T13546. Try using unicode box drawing characters to render a more obvious tree struture in "arc branches". Unclear if this has enough support to use, but seems okay so far. Test Plan: Ran "arc branches", saw a nicer tree display. Maniphest Tasks: T13546 Differential Revision: https://secure.phabricator.com/D21390 --- .../graph/view/ArcanistCommitGraphSetView.php | 108 +++++++++++++----- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php index b951b677..46e65595 100644 --- a/src/repository/graph/view/ArcanistCommitGraphSetView.php +++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php @@ -253,7 +253,6 @@ final class ArcanistCommitGraphSetView private function drawMarkerCell(array $items) { $api = $this->getRepositoryAPI(); - $depth = $this->getViewDepth(); $marker_refs = $this->getMarkerRefs(); $commit_refs = $this->getCommitRefs(); @@ -276,54 +275,120 @@ final class ArcanistCommitGraphSetView substr($max->getCommitHash(), 0, 7)); } + $member_views = $this->getMemberViews(); + $member_count = count($member_views); + if ($member_count > 1) { + $items[] = array( + 'group' => $member_views, + ); + } + $terminal_width = phutil_console_get_terminal_width(); $max_depth = (int)floor(3 + (max(0, $terminal_width - 72) / 6)); - if ($depth <= $max_depth) { - $indent = str_repeat(' ', ($depth * 2)); - } else { - $more = ' ... '; - $indent = str_repeat(' ', ($max_depth * 2) - strlen($more)).$more; - } - $indent .= '- '; - $empty_indent = str_repeat(' ', strlen($indent)); + $depth = $this->getViewDepth(); + + if ($depth <= $max_depth) { + $display_depth = ($depth * 2); + $is_squished = false; + } else { + $display_depth = ($max_depth * 2); + $is_squished = true; + } $max_width = ($max_depth * 2) + 16; - $available_width = $max_width - (min($max_depth, $depth) * 2); + $available_width = $max_width - $display_depth; + + $mark_ne = "\xE2\x94\x97"; + $mark_ew = "\xE2\x94\x81"; + $mark_esw = "\xE2\x94\xB3"; + $mark_sw = "\xE2\x94\x93"; + $mark_bullet = "\xE2\x80\xA2"; + $mark_ns_light = "\xE2\x94\x82"; + $mark_ne_light = "\xE2\x94\x94"; + $mark_esw_light = "\xE2\x94\xAF"; + + $has_children = $this->getChildViews(); $is_first = true; + $last_key = last_key($items); $cell = array(); - foreach ($items as $item) { + foreach ($items as $item_key => $item) { $marker_ref = idx($item, 'marker'); + $group_ref = idx($item, 'group'); + + $is_last = ($item_key === $last_key); if ($marker_ref) { $marker_name = $marker_ref->getName(); + $is_active = $marker_ref->getIsActive(); + + if ($is_active) { + $marker_width = $available_width - 4; + } else { + $marker_width = $available_width; + } $marker_name = id(new PhutilUTF8StringTruncator()) - ->setMaximumGlyphs($available_width) + ->setMaximumGlyphs($marker_width) ->truncateString($marker_name); if ($marker_ref->getIsActive()) { $label = tsprintf( - '**%s**', + '**%s** **%s**', + ' * ', $marker_name); } else { $label = tsprintf( '**%s**', $marker_name); } + } else if ($group_ref) { + $label = pht( + '(... %s more revisions ...)', + new PhutilNumber(count($group_ref) - 1)); } else if ($is_first) { $label = $commit_label; } else { $label = ''; } - if ($is_first) { - $indent_text = $indent; + if ($display_depth > 2) { + $indent = str_repeat(' ', $display_depth - 2); } else { - $indent_text = $empty_indent; + $indent = ''; } + if ($is_first) { + if ($display_depth === 0) { + $path = $mark_bullet.' '; + } else { + if ($has_children) { + $path = $mark_ne.$mark_ew.$mark_esw.' '; + } else if (!$is_last) { + $path = $mark_ne.$mark_ew.$mark_esw_light.' '; + } else { + $path = $mark_ne.$mark_ew.$mark_ew.' '; + } + } + } else if ($group_ref) { + $path = $mark_ne.'/'.$mark_sw.' '; + } else { + if ($is_last && !$has_children) { + $path = $mark_ne_light.' '; + } else { + $path = $mark_ns_light.' '; + } + if ($display_depth > 0) { + $path = ' '.$path; + } + } + + $indent_text = sprintf( + '%s%s', + $indent, + $path); + $cell[] = tsprintf( "%s%s\n", $indent_text, @@ -332,17 +397,6 @@ final class ArcanistCommitGraphSetView $is_first = false; } - $member_views = $this->getMemberViews(); - $member_count = count($member_views); - if ($member_count > 1) { - $cell[] = tsprintf( - "%s%s\n", - $empty_indent, - pht( - '- <... %s more revisions ...>', - new PhutilNumber($member_count - 1))); - } - return $cell; }