1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-10 00:42:40 +01:00

Rebuild "arc branch" on new "hardpoint" infrastructure

Summary:
Ref T11355. Ref T10895. Ref T11518. This is heading to `experiemntal`. This may or may not be a good idea, but basically it's a more generic version of `Query` classes in Phabricator.

This starts creating generic objects ("CommitRef", "BranchRef") which have attachable properties, like many Phabricator objects do. Here, they're formalized (and theoretically extensible), as "hardpoints".

So: a hardpoint is something on an object which you can attach stuff to, but which we don't start with the data for.

All of the logic for actually figuruing out how to attach stuff to hardpoints is also modular. `Loader` classes have code for loading stuff onto objects. For example, `ArcanistMercurialBranchCommitHardpointLoader` knows how to run `hg log` to build the commit for a branch.

One issue is that `arc feature` in Mercurial is 100% bookmarks, so maybe I should actually be making `ArcanistRefRef` here. But we can probbbably deal with that later.

This moves us somewhat closer to T11355 and T11518, although the immediate thing I want to do with it is define an `ArcanistObjectNameRef` and use hardpoints to load URIs for it for T10895.

Overall, I expect this will see some revision in future changes, and perhaps most of it will go away.

Test Plan: Ran `arc branch` / `arc feature` in Git and Mercurial repositories.

Reviewers: avivey, chad

Reviewed By: chad

Maniphest Tasks: T10895, T11518, T11355

Differential Revision: https://secure.phabricator.com/D16857
This commit is contained in:
epriestley 2016-11-13 12:41:42 -08:00
parent 45c2152988
commit 71473af895
21 changed files with 1023 additions and 145 deletions

View file

@ -40,6 +40,7 @@ phutil_register_library_map(array(
'ArcanistBookmarkWorkflow' => 'workflow/ArcanistBookmarkWorkflow.php',
'ArcanistBraceFormattingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistBraceFormattingXHPASTLinterRule.php',
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistBraceFormattingXHPASTLinterRuleTestCase.php',
'ArcanistBranchRef' => 'ref/ArcanistBranchRef.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
'ArcanistBrowseWorkflow' => 'workflow/ArcanistBrowseWorkflow.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
@ -76,6 +77,7 @@ 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',
'ArcanistCommitRef' => 'ref/ArcanistCommitRef.php',
'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php',
'ArcanistCompilerLintRenderer' => 'lint/renderer/ArcanistCompilerLintRenderer.php',
'ArcanistComposerLinter' => 'lint/linter/ArcanistComposerLinter.php',
@ -158,7 +160,9 @@ phutil_register_library_map(array(
'ArcanistGeneratedLinterTestCase' => 'lint/linter/__tests__/ArcanistGeneratedLinterTestCase.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
'ArcanistGitHardpointLoader' => 'loader/ArcanistGitHardpointLoader.php',
'ArcanistGitLandEngine' => 'land/ArcanistGitLandEngine.php',
'ArcanistGitRevisionHardpointLoader' => 'loader/ArcanistGitRevisionHardpointLoader.php',
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
'ArcanistGlobalVariableXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistGlobalVariableXHPASTLinterRule.php',
'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistGlobalVariableXHPASTLinterRuleTestCase.php',
@ -168,6 +172,7 @@ phutil_register_library_map(array(
'ArcanistGoTestResultParserTestCase' => 'unit/parser/__tests__/ArcanistGoTestResultParserTestCase.php',
'ArcanistHLintLinter' => 'lint/linter/ArcanistHLintLinter.php',
'ArcanistHLintLinterTestCase' => 'lint/linter/__tests__/ArcanistHLintLinterTestCase.php',
'ArcanistHardpointLoader' => 'loader/ArcanistHardpointLoader.php',
'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule.php',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase.php',
@ -241,10 +246,14 @@ 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',
'ArcanistMercurialBranchCommitHardpointLoader' => 'loader/ArcanistMercurialBranchCommitHardpointLoader.php',
'ArcanistMercurialHardpointLoader' => 'loader/ArcanistMercurialHardpointLoader.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'loader/ArcanistMercurialWorkingCopyCommitHardpointLoader.php',
'ArcanistMergeConflictLinter' => 'lint/linter/ArcanistMergeConflictLinter.php',
'ArcanistMergeConflictLinterTestCase' => 'lint/linter/__tests__/ArcanistMergeConflictLinterTestCase.php',
'ArcanistMessageRevisionHardpointLoader' => 'loader/ArcanistMessageRevisionHardpointLoader.php',
'ArcanistMissingLinterException' => 'lint/linter/exception/ArcanistMissingLinterException.php',
'ArcanistModifierOrderingXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistModifierOrderingXHPASTLinterRule.php',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistModifierOrderingXHPASTLinterRuleTestCase.php',
@ -308,6 +317,8 @@ phutil_register_library_map(array(
'ArcanistPyLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPyLintLinterTestCase.php',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistRaggedClassTreeEdgeXHPASTLinterRule.php',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php',
'ArcanistRef' => 'ref/ArcanistRef.php',
'ArcanistRefQuery' => 'ref/ArcanistRefQuery.php',
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php',
'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php',
@ -318,6 +329,8 @@ phutil_register_library_map(array(
'ArcanistReusedIteratorXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistReusedIteratorXHPASTLinterRule.php',
'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistReusedIteratorXHPASTLinterRuleTestCase.php',
'ArcanistRevertWorkflow' => 'workflow/ArcanistRevertWorkflow.php',
'ArcanistRevisionRef' => 'ref/ArcanistRevisionRef.php',
'ArcanistRevisionRefSource' => 'ref/ArcanistRevisionRefSource.php',
'ArcanistRuboCopLinter' => 'lint/linter/ArcanistRuboCopLinter.php',
'ArcanistRuboCopLinterTestCase' => 'lint/linter/__tests__/ArcanistRuboCopLinterTestCase.php',
'ArcanistRubyLinter' => 'lint/linter/ArcanistRubyLinter.php',
@ -399,6 +412,7 @@ phutil_register_library_map(array(
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWorkflow' => 'workflow/ArcanistWorkflow.php',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php',
'ArcanistWorkingCopyStateRef' => 'ref/ArcanistWorkingCopyStateRef.php',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php',
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLintSwitchHook' => 'lint/linter/xhpast/ArcanistXHPASTLintSwitchHook.php',
@ -456,6 +470,7 @@ phutil_register_library_map(array(
'ArcanistBookmarkWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBraceFormattingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistBraceFormattingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistBranchRef' => 'ArcanistRef',
'ArcanistBranchWorkflow' => 'ArcanistFeatureWorkflow',
'ArcanistBrowseWorkflow' => 'ArcanistWorkflow',
'ArcanistBundle' => 'Phobject',
@ -492,6 +507,7 @@ phutil_register_library_map(array(
'ArcanistCommentSpacingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistCommentStyleXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistCommentStyleXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistCommitRef' => 'ArcanistRef',
'ArcanistCommitWorkflow' => 'ArcanistWorkflow',
'ArcanistCompilerLintRenderer' => 'ArcanistLintRenderer',
'ArcanistComposerLinter' => 'ArcanistLinter',
@ -574,7 +590,9 @@ phutil_register_library_map(array(
'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
'ArcanistGitRevisionHardpointLoader' => 'ArcanistGitHardpointLoader',
'ArcanistGitUpstreamPath' => 'Phobject',
'ArcanistGlobalVariableXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistGlobalVariableXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@ -584,6 +602,7 @@ phutil_register_library_map(array(
'ArcanistGoTestResultParserTestCase' => 'PhutilTestCase',
'ArcanistHLintLinter' => 'ArcanistExternalLinter',
'ArcanistHLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistHardpointLoader' => 'Phobject',
'ArcanistHelpWorkflow' => 'ArcanistWorkflow',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistHexadecimalNumericScalarCasingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@ -657,10 +676,14 @@ phutil_register_library_map(array(
'ArcanistLowercaseFunctionsXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistLowercaseFunctionsXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialBranchCommitHardpointLoader' => 'ArcanistMercurialHardpointLoader',
'ArcanistMercurialHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistMercurialParser' => 'Phobject',
'ArcanistMercurialParserTestCase' => 'PhutilTestCase',
'ArcanistMercurialWorkingCopyCommitHardpointLoader' => 'ArcanistMercurialHardpointLoader',
'ArcanistMergeConflictLinter' => 'ArcanistLinter',
'ArcanistMergeConflictLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistMessageRevisionHardpointLoader' => 'ArcanistHardpointLoader',
'ArcanistMissingLinterException' => 'Exception',
'ArcanistModifierOrderingXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistModifierOrderingXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
@ -724,6 +747,8 @@ phutil_register_library_map(array(
'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistRef' => 'Phobject',
'ArcanistRefQuery' => 'Phobject',
'ArcanistRepositoryAPI' => 'Phobject',
'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase',
'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase',
@ -734,6 +759,8 @@ phutil_register_library_map(array(
'ArcanistReusedIteratorXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistReusedIteratorXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistRevertWorkflow' => 'ArcanistWorkflow',
'ArcanistRevisionRef' => 'ArcanistRef',
'ArcanistRevisionRefSource' => 'Phobject',
'ArcanistRuboCopLinter' => 'ArcanistExternalLinter',
'ArcanistRuboCopLinterTestCase' => 'ArcanistExternalLinterTestCase',
'ArcanistRubyLinter' => 'ArcanistExternalLinter',
@ -815,6 +842,7 @@ phutil_register_library_map(array(
'ArcanistWhichWorkflow' => 'ArcanistWorkflow',
'ArcanistWorkflow' => 'Phobject',
'ArcanistWorkingCopyIdentity' => 'Phobject',
'ArcanistWorkingCopyStateRef' => 'ArcanistRef',
'ArcanistXHPASTLintNamingHook' => 'Phobject',
'ArcanistXHPASTLintNamingHookTestCase' => 'PhutilTestCase',
'ArcanistXHPASTLintSwitchHook' => 'Phobject',

View file

@ -78,6 +78,10 @@ final class ArcanistConduitEngine
->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();

View file

@ -0,0 +1,10 @@
<?php
abstract class ArcanistGitHardpointLoader
extends ArcanistHardpointLoader {
public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
return ($api instanceof ArcanistGitAPI);
}
}

View file

@ -0,0 +1,80 @@
<?php
final class ArcanistGitRevisionHardpointLoader
extends ArcanistGitHardpointLoader {
const LOADERKEY = 'git.revision';
public function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistWorkingCopyStateRef);
}
public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
return ($hardpoint == 'revisionRefs');
}
public function loadHardpoints(array $refs, $hardpoint) {
$this->newQuery($refs)
->needHardpoints(
array(
'commitRef',
))
->execute();
$hashes = array();
$map = array();
foreach ($refs as $ref_key => $ref) {
$commit = $ref->getCommitRef();
$commit_hashes = array();
$commit_hashes[] = array(
'gtcm',
$commit->getCommitHash(),
);
$commit_hashes[] = array(
'gttr',
$commit->getTreeHash(),
);
foreach ($commit_hashes as $hash) {
$hashes[] = $hash;
$hash_key = $this->getHashKey($hash);
$map[$hash_key][$ref_key] = $ref;
}
}
$results = array();
if ($hashes) {
$revisions = $this->resolveCall(
'differential.query',
array(
'commitHashes' => $hashes,
));
foreach ($revisions as $dict) {
$revision_hashes = idx($dict, 'hashes');
if (!$revision_hashes) {
continue;
}
$revision_ref = ArcanistRevisionRef::newFromConduit($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;
}
}
}
}
return $results;
}
private function getHashKey(array $hash) {
return $hash[0].':'.$hash[1];
}
}

View file

@ -0,0 +1,58 @@
<?php
abstract class ArcanistHardpointLoader
extends Phobject {
private $query;
private $conduitEngine;
abstract public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api);
abstract public function canLoadRef(ArcanistRef $ref);
abstract public function canLoadHardpoint(ArcanistRef $ref, $hardpoint);
abstract public function loadHardpoints(array $refs, $hardpoint);
final public function setQuery(ArcanistRefQuery $query) {
$this->query = $query;
return $this;
}
final public function getQuery() {
return $this->query;
}
final public function getConduitEngine() {
return $this->getQuery()->getConduitEngine();
}
final protected function newQuery(array $refs) {
return id(new ArcanistRefQuery())
->setRepositoryAPI($this->getQuery()->getRepositoryAPI())
->setConduitEngine($this->getQuery()->getConduitEngine())
->setRefs($refs);
}
final public function getLoaderKey() {
return $this->getPhobjectClassConstant('LOADERKEY', 64);
}
final public static function getAllLoaders() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getLoaderKey')
->execute();
}
final public function resolveCall($method, array $parameters) {
return $this->newCall($method, $parameters)->resolve();
}
final public function newCall($method, array $parameters) {
return $this->getConduitEngine()->newCall($method, $parameters);
}
final protected function newFutureIterator(array $futures) {
return id(new FutureIterator($futures))
->limit(16);
}
}

View file

@ -0,0 +1,49 @@
<?php
final class ArcanistMercurialBranchCommitHardpointLoader
extends ArcanistMercurialHardpointLoader {
const LOADERKEY = 'hg.branch.commit';
public function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistBranchRef);
}
public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
return ($hardpoint == 'commitRef');
}
public function loadHardpoints(array $refs, $hardpoint) {
$api = $this->getQuery()->getRepositoryAPI();
$futures = array();
foreach ($refs as $ref_key => $branch) {
$branch_name = $branch->getBranchName();
$futures[$ref_key] = $api->execFutureLocal(
'log -l 1 --template %s -r %s',
"{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}",
hgsprintf('%s', $branch_name));
}
$results = array();
$iterator = $this->newFutureIterator($futures);
foreach ($iterator as $ref_key => $future) {
list($info) = $future->resolvex();
$fields = explode("\1", trim($info), 5);
list($hash, $epoch, $parent, $desc, $text) = $fields;
$commit_ref = $api->newCommitRef()
->setCommitHash($hash)
->setCommitEpoch((int)$epoch)
->attachMessage($text);
$results[$ref_key] = $commit_ref;
}
return $results;
}
}

View file

@ -0,0 +1,10 @@
<?php
abstract class ArcanistMercurialHardpointLoader
extends ArcanistHardpointLoader {
public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
return ($api instanceof ArcanistMercurialAPI);
}
}

View file

@ -0,0 +1,36 @@
<?php
final class ArcanistMercurialWorkingCopyCommitHardpointLoader
extends ArcanistMercurialHardpointLoader {
const LOADERKEY = 'hg.state.commit';
public function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistWorkingCopyStateRef);
}
public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
return ($hardpoint == 'commitRef');
}
public function loadHardpoints(array $refs, $hardpoint) {
$branch_refs = array();
foreach ($refs as $ref_key => $ref) {
if ($ref->hasAttachedHardpoint('branchRef')) {
$branch_refs[$ref_key] = $ref->getBranchRef();
}
}
if ($branch_refs) {
$this->newQuery($branch_refs)
->needHardpoints(
array(
'commitRef',
))
->execute();
}
return mpull($branch_refs, 'getCommitRef');
}
}

View file

@ -0,0 +1,82 @@
<?php
final class ArcanistMessageRevisionHardpointLoader
extends ArcanistHardpointLoader {
const LOADERKEY = 'message.revision';
public function canLoadRepositoryAPI(ArcanistRepositoryAPI $api) {
return true;
}
public function canLoadRef(ArcanistRef $ref) {
return ($ref instanceof ArcanistWorkingCopyStateRef);
}
public function canLoadHardpoint(ArcanistRef $ref, $hardpoint) {
return ($hardpoint == 'revisionRefs');
}
public function loadHardpoints(array $refs, $hardpoint) {
$this->newQuery($refs)
->needHardpoints(
array(
'commitRef',
))
->execute();
$commit_refs = array();
foreach ($refs as $ref) {
$commit_refs[] = $ref->getCommitRef();
}
$this->newQuery($commit_refs)
->needHardpoints(
array(
'message',
))
->execute();
$map = array();
foreach ($refs as $ref_key => $ref) {
$commit_ref = $ref->getCommitRef();
$corpus = $commit_ref->getMessage();
$id = null;
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($corpus);
$id = $message->getRevisionID();
} catch (ArcanistUsageException $ex) {
continue;
}
if (!$id) {
continue;
}
$map[$id][$ref_key] = $ref;
}
$results = array();
if ($map) {
$revisions = $this->resolveCall(
'differential.query',
array(
'ids' => array_keys($map),
));
foreach ($revisions as $dict) {
$revision_ref = ArcanistRevisionRef::newFromConduit($dict);
$id = $dict['id'];
$state_refs = idx($map, $id, array());
foreach ($state_refs as $ref_key => $state_ref) {
$results[$ref_key][] = $revision_ref;
}
}
}
return $results;
}
}

View file

@ -0,0 +1,57 @@
<?php
final class ArcanistBranchRef
extends ArcanistRef {
private $branchName;
private $refName;
private $isCurrentBranch;
public function getRefIdentifier() {
return pht('Branch %s', $this->getBranchName());
}
public function defineHardpoints() {
return array(
'commitRef' => array(
'type' => 'ArcanistCommitRef',
),
);
}
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('commitRef', $ref);
}
public function getCommitRef() {
return $this->getHardpoint('commitRef');
}
}

View file

@ -0,0 +1,76 @@
<?php
final class ArcanistCommitRef
extends ArcanistRef {
private $commitHash;
private $treeHash;
private $commitEpoch;
private $authorEpoch;
public function getRefIdentifier() {
return pht('Commit %s', $this->getCommitHash());
}
public function defineHardpoints() {
return array(
'message' => array(
'type' => 'string',
),
);
}
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 setCommitEpoch($commit_epoch) {
$this->commitEpoch = $commit_epoch;
return $this;
}
public function getCommitEpoch() {
return $this->commitEpoch;
}
public function setAuthorEpoch($author_epoch) {
$this->authorEpoch = $author_epoch;
return $this;
}
public function getAuthorEpoch() {
return $this->authorEpoch;
}
public function getSummary() {
$message = $this->getMessage();
$message = trim($message);
$lines = phutil_split_lines($message, false);
return head($lines);
}
public function attachMessage($message) {
return $this->attachHardpoint('message', $message);
}
public function getMessage() {
return $this->getHardpoint('message');
}
}

106
src/ref/ArcanistRef.php Normal file
View file

@ -0,0 +1,106 @@
<?php
abstract class ArcanistRef
extends Phobject {
private $hardpoints = array();
abstract public function getRefIdentifier();
abstract public function defineHardpoints();
final public function hasHardpoint($hardpoint) {
$map = $this->getHardpointMap();
return isset($map[$hardpoint]);
}
final public function hasAttachedHardpoint($hardpoint) {
if (array_key_exists($hardpoint, $this->hardpoints)) {
return true;
}
return $this->canReadHardpoint($hardpoint);
}
final public function attachHardpoint($hardpoint, $value) {
if (!$this->hasHardpoint($hardpoint)) {
throw new Exception(pht('No hardpoint "%s".', $hardpoint));
}
$this->hardpoints[$hardpoint] = $value;
return $this;
}
final public function appendHardpoint($hardpoint, array $value) {
if (!$this->isVectorHardpoint($hardpoint)) {
throw new Exception(
pht(
'Hardpoint "%s" is not a vector hardpoint.',
$hardpoint));
}
if (!isset($this->hardpoints[$hardpoint])) {
$this->hardpoints[$hardpoint] = array();
}
$this->hardpoints[$hardpoint] = $this->mergeHardpoint(
$hardpoint,
$this->hardpoints[$hardpoint],
$value);
return $this;
}
protected function mergeHardpoint($hardpoint, array $src, array $new) {
foreach ($new as $value) {
$src[] = $value;
}
return $src;
}
final public function isVectorHardpoint($hardpoint) {
if (!$this->hasHardpoint($hardpoint)) {
return false;
}
$map = $this->getHardpointMap();
$spec = idx($map, $hardpoint, array());
return (idx($spec, 'vector') === true);
}
final public function getHardpoint($hardpoint) {
if (!$this->hasAttachedHardpoint($hardpoint)) {
if (!$this->hasHardpoint($hardpoint)) {
throw new Exception(
pht(
'Ref does not have hardpoint "%s"!',
$hardpoint));
} else {
throw new Exception(
pht(
'Hardpoint "%s" is not attached!',
$hardpoint));
}
}
if (array_key_exists($hardpoint, $this->hardpoints)) {
return $this->hardpoints[$hardpoint];
}
return $this->readHardpoint($hardpoint);
}
private function getHardpointMap() {
return $this->defineHardpoints();
}
protected function canReadHardpoint($hardpoint) {
return false;
}
protected function readHardpoint($hardpoint) {
throw new Exception(pht('Can not read hardpoint "%s".', $hardpoint));
}
}

View file

@ -0,0 +1,147 @@
<?php
final class ArcanistRefQuery extends Phobject {
private $repositoryAPI;
private $conduitEngine;
private $refs;
private $hardpoints;
public function setRefs(array $refs) {
assert_instances_of($refs, 'ArcanistRef');
$this->refs = $refs;
return $this;
}
public function getRefs() {
return $this->refs;
}
public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
$this->repositoryAPI = $repository_api;
return $this;
}
public function getRepositoryAPI() {
return $this->repositoryAPI;
}
public function setConduitEngine(ArcanistConduitEngine $conduit_engine) {
$this->conduitEngine = $conduit_engine;
return $this;
}
public function getConduitEngine() {
return $this->conduitEngine;
}
public function needHardpoints(array $hardpoints) {
$this->hardpoints = $hardpoints;
return $this;
}
public function execute() {
$refs = $this->getRefs();
if ($this->refs === null) {
throw new PhutilInvalidStateException('setRefs');
}
if ($this->hardpoints === null) {
throw new PhutilInvalidStateException('needHardpoints');
}
$api = $this->getRepositoryAPI();
$all_loaders = ArcanistHardpointLoader::getAllLoaders();
$loaders = array();
foreach ($all_loaders as $loader_key => $loader) {
if (!$loader->canLoadRepositoryAPI($api)) {
continue;
}
$loaders[$loader_key] = id(clone $loader)
->setQuery($this);
}
foreach ($this->hardpoints as $hardpoint) {
$load = array();
$need = array();
$has_hardpoint = false;
foreach ($refs as $ref_key => $ref) {
if (!$ref->hasHardpoint($hardpoint)) {
continue;
}
$has_hardpoint = true;
if ($ref->hasAttachedHardpoint($hardpoint)) {
continue;
}
foreach ($loaders as $loader_key => $loader) {
if (!$loader->canLoadRef($ref)) {
continue;
}
if (!$loader->canLoadHardpoint($ref, $hardpoint)) {
continue;
}
$load[$loader_key][$ref_key] = $ref;
}
$need[$ref_key] = $ref_key;
}
if ($refs && !$has_hardpoint) {
throw new Exception(
pht(
'No ref in query has hardpoint "%s".',
$hardpoint));
}
$vectors = array();
foreach ($need as $ref_key) {
$ref = $refs[$ref_key];
if ($ref->isVectorHardpoint($hardpoint)) {
$vectors[$ref_key] = $ref_key;
$ref->attachHardpoint($hardpoint, array());
}
}
foreach ($load as $loader_key => $loader_refs) {
$loader_refs = array_select_keys($loader_refs, $need);
$loader = $loaders[$loader_key];
$data = $loader->loadHardpoints($loader_refs, $hardpoint);
foreach ($data as $ref_key => $value) {
$ref = $refs[$ref_key];
if (isset($vectors[$ref_key])) {
$ref->appendHardpoint($hardpoint, $value);
} else {
unset($need[$ref_key]);
$ref->attachHardpoint($hardpoint, $value);
}
}
}
foreach ($vectors as $ref_key) {
unset($need[$ref_key]);
}
if ($need) {
throw new Exception(
pht(
'Nothing could attach data to hardpoint "%s" for ref "%s".',
$hardpoint,
$refs[head($need)]->getRefIdentifier()));
}
}
return $refs;
}
}

View file

@ -0,0 +1,52 @@
<?php
final class ArcanistRevisionRef
extends ArcanistRef {
private $parameters;
private $sources = array();
public function getRefIdentifier() {
return pht('Revision %s', $this->getMonogram());
}
public function defineHardpoints() {
return array();
}
public static function newFromConduit(array $dict) {
$ref = new self();
$ref->parameters = $dict;
return $ref;
}
public function getMonogram() {
return 'D'.$this->getID();
}
public function getStatusDisplayName() {
return idx($this->parameters, 'statusName');
}
public function getFullName() {
return pht('%s: %s', $this->getMonogram(), $this->getName());
}
public function getID() {
return idx($this->parameters, 'id');
}
public function getName() {
return idx($this->parameters, 'title');
}
public function addSource(ArcanistRevisionRefSource $source) {
$this->sources[] = $source;
return $this;
}
public function getSources() {
return $this->sources;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class ArcanistRevisionRefSource
extends Phobject {}

View file

@ -0,0 +1,111 @@
<?php
final class ArcanistWorkingCopyStateRef
extends ArcanistRef {
public function getRefIdentifier() {
// TODO: This could check attached hardpoints and render something more
// insightful.
return pht('Working Copy State');
}
public function defineHardpoints() {
return array(
'commitRef' => array(
'type' => 'ArcanistCommitRef',
),
'branchRef' => array(
'type' => 'ArcanistBranchRef',
),
'revisionRefs' => array(
'type' => 'ArcanistRevisionRef',
'vector' => true,
),
);
}
public function attachBranchRef(ArcanistBranchRef $branch_ref) {
return $this->attachHardpoint('branchRef', $branch_ref);
}
public function getBranchRef() {
return $this->getHardpoint('branchRef');
}
public function setCommitRef(ArcanistCommitRef $commit_ref) {
return $this->attachHardpoint('commitRef', $commit_ref);
}
public function getCommitRef() {
return $this->getHardpoint('commitRef');
}
public function getRevisionRefs() {
return $this->getHardpoint('revisionRefs');
}
public function getRevisionRef() {
if ($this->hasAmbiguousRevisionRefs()) {
throw new Exception(
pht('State has multiple ambiguous revisions refs.'));
}
$refs = $this->getRevisionRefs();
if ($refs) {
return head($refs);
}
return null;
}
public function hasAmbiguousRevisionRefs() {
return (count($this->getRevisionRefs()) > 1);
}
protected function canReadHardpoint($hardpoint) {
switch ($hardpoint) {
case 'commitRef':
// If we have a branch ref, we can try to read the commit ref from the
// branch ref.
if ($this->hasAttachedHardpoint('branchRef')) {
if ($this->getBranchRef()->hasAttachedHardpoint('commitRef')) {
return true;
}
}
break;
}
return false;
}
protected function readHardpoint($hardpoint) {
switch ($hardpoint) {
case 'commitRef':
return $this->getBranchRef()->getCommitRef();
}
return parent::readHardpoint($hardpoint);
}
protected function mergeHardpoint($hardpoint, array $src, array $new) {
if ($hardpoint == 'revisionRefs') {
$src = mpull($src, null, 'getID');
$new = mpull($new, null, 'getID');
foreach ($new as $id => $ref) {
if (isset($src[$id])) {
foreach ($ref->getSources() as $source) {
$src[$id]->addSource($source);
}
} else {
$src[$id] = $ref;
}
}
return array_values($src);
}
return parent::mergeHardpoint($hardpoint, $src, $new);
}
}

View file

@ -1014,6 +1014,7 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
$result[] = array(
'current' => ($branch === $current),
'name' => $branch,
'ref' => $ref,
'hash' => $hash,
'tree' => $tree,
'epoch' => (int)$epoch,
@ -1026,6 +1027,27 @@ 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->newBranchRef()
->setBranchName($branch['name'])
->setRefName($branch['ref'])
->setIsCurrentBranch($branch['current'])
->attachCommitRef($commit_ref);
}
return $refs;
}
public function getWorkingCopyRevision() {
list($stdout) = $this->execxLocal('rev-parse HEAD');
return rtrim($stdout, "\n");

View file

@ -583,6 +583,19 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
return $return;
}
public function getAllBranchRefs() {
$branches = $this->getAllBranches();
$refs = array();
foreach ($branches as $branch) {
$refs[] = $this->newBranchRef()
->setBranchName($branch['name'])
->setIsCurrentBranch($branch['current']);
}
return $refs;
}
public function hasLocalCommit($commit) {
try {
$this->getCanonicalRevisionName($commit);

View file

@ -375,6 +375,10 @@ abstract class ArcanistRepositoryAPI extends Phobject {
return array();
}
public function getAllBranchRefs() {
throw new ArcanistCapabilityNotSupportedException($this);
}
public function hasLocalCommit($commit) {
throw new ArcanistCapabilityNotSupportedException($this);
}
@ -668,4 +672,12 @@ abstract class ArcanistRepositoryAPI extends Phobject {
return null;
}
final public function newCommitRef() {
return new ArcanistCommitRef();
}
final public function newBranchRef() {
return new ArcanistBranchRef();
}
}

View file

@ -38,18 +38,10 @@ EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return !$this->getArgument('branch');
}
public function getArguments() {
return array(
'view-all' => array(
@ -86,15 +78,29 @@ EOTEXT
return $this->checkoutBranch($names);
}
$branches = $repository_api->getAllBranches();
// 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.'));
}
$branches = $this->loadCommitInfo($branches);
$revisions = $this->loadRevisions($branches);
$this->printBranches($branches, $revisions);
$states = array();
foreach ($branches as $branch) {
$states[] = $this->newWorkingCopyStateRef()
->attachBranchRef($branch);
}
$this->newRefQuery($states)
->needHardpoints(
array(
'revisionRefs',
))
->execute();
$this->printBranches($states);
return 0;
}
@ -125,21 +131,19 @@ EOTEXT
if ($err) {
$match = null;
if (preg_match('/^D(\d+)$/', $name, $match)) {
try {
$diff = $this->getConduit()->callMethodSynchronous(
'differential.querydiffs',
array(
'revisionIDs' => array($match[1]),
));
$diff = head($diff);
$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);
}
} catch (ConduitClientException $ex) {}
if ($diff['branch'] != '') {
$name = $diff['branch'];
list($err, $stdout, $stderr) = $api->execManualLocal(
$command,
$name);
}
}
}
@ -171,99 +175,7 @@ EOTEXT
return $err;
}
private function loadCommitInfo(array $branches) {
$repository_api = $this->getRepositoryAPI();
$branches = ipull($branches, null, 'name');
if ($repository_api instanceof ArcanistMercurialAPI) {
$futures = array();
foreach ($branches as $branch) {
$futures[$branch['name']] = $repository_api->execFutureLocal(
'log -l 1 --template %s -r %s',
"{node}\1{date|hgdate}\1{p1node}\1{desc|firstline}\1{desc}",
hgsprintf('%s', $branch['name']));
}
$futures = id(new FutureIterator($futures))
->limit(16);
foreach ($futures as $name => $future) {
list($info) = $future->resolvex();
$fields = explode("\1", trim($info), 5);
list($hash, $epoch, $tree, $desc, $text) = $fields;
$branches[$name] += array(
'hash' => $hash,
'desc' => $desc,
'tree' => $tree,
'epoch' => (int)$epoch,
'text' => $text,
);
}
}
foreach ($branches as $name => $branch) {
$text = $branch['text'];
try {
$message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
$id = $message->getRevisionID();
$branch['revisionID'] = $id;
} catch (ArcanistUsageException $ex) {
// In case of invalid commit message which fails the parsing,
// do nothing.
$branch['revisionID'] = null;
}
$branches[$name] = $branch;
}
return $branches;
}
private function loadRevisions(array $branches) {
$ids = array();
$hashes = array();
foreach ($branches as $branch) {
if ($branch['revisionID']) {
$ids[] = $branch['revisionID'];
}
$hashes[] = array('gtcm', $branch['hash']);
$hashes[] = array('gttr', $branch['tree']);
}
$calls = array();
if ($ids) {
$calls[] = $this->getConduit()->callMethod(
'differential.query',
array(
'ids' => $ids,
));
}
if ($hashes) {
$calls[] = $this->getConduit()->callMethod(
'differential.query',
array(
'commitHashes' => $hashes,
));
}
$results = array();
foreach (new FutureIterator($calls) as $call) {
$results[] = $call->resolve();
}
return array_mergev($results);
}
private function printBranches(array $branches, array $revisions) {
$revisions = ipull($revisions, null, 'id');
private function printBranches(array $states) {
static $color_map = array(
'Closed' => 'cyan',
'Needs Review' => 'magenta',
@ -282,48 +194,45 @@ EOTEXT
);
$out = array();
foreach ($branches as $branch) {
$revision = idx($revisions, idx($branch, 'revisionID'));
foreach ($states as $state) {
$branch = $state->getBranchRef();
// If we haven't identified a revision by ID, try to identify it by hash.
if (!$revision) {
foreach ($revisions as $rev) {
$hashes = idx($rev, 'hashes', array());
foreach ($hashes as $hash) {
if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) ||
($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) {
$revision = $rev;
break;
}
}
$revision = null;
if ($state->hasAmbiguousRevisionRefs()) {
$status = pht('Ambiguous Revision');
} else {
$revision = $state->getRevisionRef();
if ($revision) {
$status = $revision->getStatusDisplayName();
} else {
$status = pht('No Revision');
}
}
if ($revision) {
$desc = 'D'.$revision['id'].': '.$revision['title'];
$status = $revision['statusName'];
} else {
$desc = $branch['desc'];
$status = pht('No Revision');
}
if (!$this->getArgument('view-all') && !$branch['current']) {
if (!$this->getArgument('view-all') && !$branch->getIsCurrentBranch()) {
if ($status == 'Closed' || $status == 'Abandoned') {
continue;
}
}
$epoch = $branch['epoch'];
$commit = $branch->getCommitRef();
$epoch = $commit->getCommitEpoch();
$color = idx($color_map, $status, 'default');
$ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
if ($revision) {
$desc = $revision->getFullName();
} else {
$desc = $commit->getSummary();
}
$out[] = array(
'name' => $branch['name'],
'current' => $branch['current'],
'name' => $branch->getBranchName(),
'current' => $branch->getIsCurrentBranch(),
'status' => $status,
'desc' => $desc,
'revision' => $revision ? $revision['id'] : null,
'revision' => $revision ? $revision->getID() : null,
'color' => $color,
'esort' => $epoch,
'epoch' => $epoch,

View file

@ -2073,4 +2073,16 @@ abstract class ArcanistWorkflow extends Phobject {
return $this->conduitEngine;
}
final protected function newWorkingCopyStateRef() {
return new ArcanistWorkingCopyStateRef();
}
final protected function newRefQuery(array $refs) {
assert_instances_of($refs, 'ArcanistRef');
return id(new ArcanistRefQuery())
->setRepositoryAPI($this->getRepositoryAPI())
->setConduitEngine($this->getConduitEngine())
->setRefs($refs);
}
}