mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-21 22:32:41 +01:00
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
This commit is contained in:
parent
80f5166b70
commit
cd19216ea2
18 changed files with 2114 additions and 68 deletions
|
@ -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',
|
||||
|
|
|
@ -1816,4 +1816,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
return $hashes;
|
||||
}
|
||||
|
||||
protected function newCommitGraphQueryTemplate() {
|
||||
return new ArcanistGitCommitGraphQuery();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1031,4 +1031,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
|
|||
$uri);
|
||||
}
|
||||
|
||||
protected function newCommitGraphQueryTemplate() {
|
||||
return new ArcanistMercurialCommitGraphQuery();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
55
src/repository/graph/ArcanistCommitGraph.php
Normal file
55
src/repository/graph/ArcanistCommitGraph.php
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraph
|
||||
extends Phobject {
|
||||
|
||||
private $repositoryAPI;
|
||||
private $nodes = array();
|
||||
|
||||
public function setRepositoryAPI(ArcanistRepositoryAPI $api) {
|
||||
$this->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);
|
||||
}
|
||||
|
||||
}
|
62
src/repository/graph/ArcanistCommitGraphPartition.php
Normal file
62
src/repository/graph/ArcanistCommitGraphPartition.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphPartition
|
||||
extends Phobject {
|
||||
|
||||
private $graph;
|
||||
private $hashes = array();
|
||||
private $heads = array();
|
||||
private $tails = array();
|
||||
private $waypoints = array();
|
||||
|
||||
public function setGraph(ArcanistCommitGraph $graph) {
|
||||
$this->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);
|
||||
}
|
||||
|
||||
}
|
153
src/repository/graph/ArcanistCommitGraphPartitionQuery.php
Normal file
153
src/repository/graph/ArcanistCommitGraphPartitionQuery.php
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphPartitionQuery
|
||||
extends Phobject {
|
||||
|
||||
private $graph;
|
||||
private $heads;
|
||||
private $hashes;
|
||||
|
||||
public function setGraph(ArcanistCommitGraph $graph) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
97
src/repository/graph/ArcanistCommitGraphSet.php
Normal file
97
src/repository/graph/ArcanistCommitGraphSet.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphSet
|
||||
extends Phobject {
|
||||
|
||||
private $setID;
|
||||
private $color;
|
||||
private $hashes;
|
||||
private $parentHashes;
|
||||
private $childHashes;
|
||||
private $parentSets;
|
||||
private $childSets;
|
||||
private $displayDepth;
|
||||
private $displayChildSets;
|
||||
|
||||
public function setColor($color) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
305
src/repository/graph/ArcanistCommitGraphSetQuery.php
Normal file
305
src/repository/graph/ArcanistCommitGraphSetQuery.php
Normal file
|
@ -0,0 +1,305 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphSetQuery
|
||||
extends Phobject {
|
||||
|
||||
private $partition;
|
||||
private $waypointMap;
|
||||
private $visitedDisplaySets;
|
||||
|
||||
public function setPartition($partition) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
|
||||
}
|
78
src/repository/graph/ArcanistCommitNode.php
Normal file
78
src/repository/graph/ArcanistCommitNode.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitNode
|
||||
extends Phobject {
|
||||
|
||||
private $commitHash;
|
||||
private $childNodes = array();
|
||||
private $parentNodes = array();
|
||||
private $commitRef;
|
||||
private $commitMessage;
|
||||
private $commitEpoch;
|
||||
|
||||
public function setCommitHash($commit_hash) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphTestCase
|
||||
extends PhutilTestCase {
|
||||
|
||||
public function testGraphQuery() {
|
||||
$this->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));
|
||||
}
|
||||
|
||||
}
|
69
src/repository/graph/query/ArcanistCommitGraphQuery.php
Normal file
69
src/repository/graph/query/ArcanistCommitGraphQuery.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
abstract class ArcanistCommitGraphQuery
|
||||
extends Phobject {
|
||||
|
||||
private $graph;
|
||||
private $headHashes;
|
||||
private $tailHashes;
|
||||
private $exactHashes;
|
||||
private $stopAtGCA;
|
||||
private $limit;
|
||||
|
||||
final public function setGraph(ArcanistCommitGraph $graph) {
|
||||
$this->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();
|
||||
|
||||
}
|
150
src/repository/graph/query/ArcanistGitCommitGraphQuery.php
Normal file
150
src/repository/graph/query/ArcanistGitCommitGraphQuery.php
Normal file
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistGitCommitGraphQuery
|
||||
extends ArcanistCommitGraphQuery {
|
||||
|
||||
private $queryFuture;
|
||||
private $seen = array();
|
||||
|
||||
public function execute() {
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
180
src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php
Normal file
180
src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php
Normal file
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistMercurialCommitGraphQuery
|
||||
extends ArcanistCommitGraphQuery {
|
||||
|
||||
private $seen = array();
|
||||
private $queryFuture;
|
||||
|
||||
public function execute() {
|
||||
$this->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).')';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistSimpleCommitGraphQuery
|
||||
extends ArcanistCommitGraphQuery {
|
||||
|
||||
private $corpus;
|
||||
|
||||
public function setCorpus($corpus) {
|
||||
$this->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<parent>\S+)>(?P<child>\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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
147
src/repository/graph/view/ArcanistCommitGraphSetTreeView.php
Normal file
147
src/repository/graph/view/ArcanistCommitGraphSetTreeView.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphSetTreeView
|
||||
extends Phobject {
|
||||
|
||||
private $repositoryAPI;
|
||||
private $rootSet;
|
||||
private $markers;
|
||||
private $markerGroups;
|
||||
private $stateRefs;
|
||||
private $setViews;
|
||||
|
||||
public function setRootSet($root_set) {
|
||||
$this->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;
|
||||
}
|
||||
|
||||
}
|
407
src/repository/graph/view/ArcanistCommitGraphSetView.php
Normal file
407
src/repository/graph/view/ArcanistCommitGraphSetView.php
Normal file
|
@ -0,0 +1,407 @@
|
|||
<?php
|
||||
|
||||
final class ArcanistCommitGraphSetView
|
||||
extends Phobject {
|
||||
|
||||
private $repositoryAPI;
|
||||
private $set;
|
||||
private $parentView;
|
||||
private $childViews = array();
|
||||
private $commitRefs;
|
||||
private $revisionRefs;
|
||||
private $markerRefs;
|
||||
private $viewDepth;
|
||||
|
||||
public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
|
||||
$this->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(
|
||||
'<bg:green>**%s**</bg>',
|
||||
$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('<fg:%s>%%s</fg>', $ansi_color),
|
||||
$status);
|
||||
}
|
||||
|
||||
return tsprintf('%s', $status);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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(
|
||||
'<fg:blue>%s</fg>',
|
||||
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('<fg:%s>%%s</fg>', $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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue