Page MenuHomePhabricator

D21363.id50889.diff
No OneTemporary

D21363.id50889.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -106,6 +106,16 @@
'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 @@
'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 @@
'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 @@
'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 @@
'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 @@
'ArcanistGeneratedLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistGetConfigWorkflow' => 'ArcanistWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
+ 'ArcanistGitCommitGraphQuery' => 'ArcanistCommitGraphQuery',
'ArcanistGitCommitMessageHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
@@ -1362,6 +1386,7 @@
'ArcanistMarkerRef' => 'ArcanistRef',
'ArcanistMarkersWorkflow' => 'ArcanistArcWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
+ 'ArcanistMercurialCommitGraphQuery' => 'ArcanistCommitGraphQuery',
'ArcanistMercurialCommitMessageHardpointQuery' => 'ArcanistWorkflowMercurialHardpointQuery',
'ArcanistMercurialLandEngine' => 'ArcanistLandEngine',
'ArcanistMercurialLocalState' => 'ArcanistRepositoryLocalState',
@@ -1498,6 +1523,7 @@
'ArcanistSetting' => 'Phobject',
'ArcanistSettings' => 'Phobject',
'ArcanistShellCompleteWorkflow' => 'ArcanistWorkflow',
+ 'ArcanistSimpleCommitGraphQuery' => 'ArcanistCommitGraphQuery',
'ArcanistSimpleSymbolHardpointQuery' => 'ArcanistRuntimeHardpointQuery',
'ArcanistSimpleSymbolRef' => 'ArcanistSymbolRef',
'ArcanistSimpleSymbolRefInspector' => 'ArcanistRefInspector',
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1816,4 +1816,8 @@
return $hashes;
}
+ protected function newCommitGraphQueryTemplate() {
+ return new ArcanistGitCommitGraphQuery();
+ }
+
}
diff --git a/src/repository/api/ArcanistMercurialAPI.php b/src/repository/api/ArcanistMercurialAPI.php
--- a/src/repository/api/ArcanistMercurialAPI.php
+++ b/src/repository/api/ArcanistMercurialAPI.php
@@ -1031,4 +1031,8 @@
$uri);
}
+ protected function newCommitGraphQueryTemplate() {
+ return new ArcanistMercurialCommitGraphQuery();
+ }
+
}
diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php
--- a/src/repository/api/ArcanistRepositoryAPI.php
+++ b/src/repository/api/ArcanistRepositoryAPI.php
@@ -42,6 +42,7 @@
private $runtime;
private $currentWorkingCopyStateRef = false;
private $currentCommitRef = false;
+ private $graph;
abstract public function getSourceControlSystemName();
@@ -794,10 +795,19 @@
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 @@
return array();
}
+ final public function getGraph() {
+ if (!$this->graph) {
+ $this->graph = id(new ArcanistCommitGraph())
+ ->setRepositoryAPI($this);
+ }
+
+ return $this->graph;
+ }
+
}
diff --git a/src/repository/graph/ArcanistCommitGraph.php b/src/repository/graph/ArcanistCommitGraph.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitGraph.php
@@ -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);
+ }
+
+}
diff --git a/src/repository/graph/ArcanistCommitGraphPartition.php b/src/repository/graph/ArcanistCommitGraphPartition.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitGraphPartition.php
@@ -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);
+ }
+
+}
diff --git a/src/repository/graph/ArcanistCommitGraphPartitionQuery.php b/src/repository/graph/ArcanistCommitGraphPartitionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitGraphPartitionQuery.php
@@ -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;
+ }
+
+}
diff --git a/src/repository/graph/ArcanistCommitGraphSet.php b/src/repository/graph/ArcanistCommitGraphSet.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitGraphSet.php
@@ -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;
+ }
+
+}
diff --git a/src/repository/graph/ArcanistCommitGraphSetQuery.php b/src/repository/graph/ArcanistCommitGraphSetQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitGraphSetQuery.php
@@ -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;
+ }
+
+
+}
diff --git a/src/repository/graph/ArcanistCommitNode.php b/src/repository/graph/ArcanistCommitNode.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/ArcanistCommitNode.php
@@ -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;
+ }
+
+}
diff --git a/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php b/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/__tests__/ArcanistCommitGraphTestCase.php
@@ -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));
+ }
+
+}
diff --git a/src/repository/graph/query/ArcanistCommitGraphQuery.php b/src/repository/graph/query/ArcanistCommitGraphQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/query/ArcanistCommitGraphQuery.php
@@ -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();
+
+}
diff --git a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php
@@ -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;
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php
@@ -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).')';
+ }
+ }
+
+}
diff --git a/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php b/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/query/ArcanistSimpleCommitGraphQuery.php
@@ -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);
+ }
+ }
+
+}
diff --git a/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/view/ArcanistCommitGraphSetTreeView.php
@@ -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;
+ }
+
+}
diff --git a/src/repository/graph/view/ArcanistCommitGraphSetView.php b/src/repository/graph/view/ArcanistCommitGraphSetView.php
new file mode 100644
--- /dev/null
+++ b/src/repository/graph/view/ArcanistCommitGraphSetView.php
@@ -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);
+ }
+
+
+}
diff --git a/src/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php
--- a/src/workflow/ArcanistMarkersWorkflow.php
+++ b/src/workflow/ArcanistMarkersWorkflow.php
@@ -3,6 +3,8 @@
abstract class ArcanistMarkersWorkflow
extends ArcanistArcWorkflow {
+ private $nodes;
+
abstract protected function getWorkflowMarkerType();
public function runWorkflow() {
@@ -14,96 +16,152 @@
->withMarkerTypes(array($marker_type))
->execute();
- $states = array();
- foreach ($markers as $marker) {
+ $tail_hashes = $this->getTailHashes();
+
+ $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($marker->getCommitRef());
+ ->setCommitRef($commit_ref);
- $states[] = array(
- 'marker' => $marker,
- 'state' => $state_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'];
-
- $vector = id(new PhutilSortVector())
- ->addInt($marker_ref->getIsActive() ? 1 : 0)
- ->addInt($marker_ref->getEpoch());
+ $partitions = $graph->newPartitionQuery()
+ ->withHeads($heads)
+ ->withHashes(array_keys($nodes))
+ ->execute();
- $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();
-
- $ansi_color = $revision_ref->getStatusANSIColor();
- if ($ansi_color) {
- $status = tsprintf(
- sprintf('<fg:%s>%%s</fg>', $ansi_color),
- $status);
- }
+ $partition_lists = array();
+ foreach ($partitions as $partition_key => $partition) {
+ $sets = $partition_sets[$partition_key];
+
+ $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 = ' ';
+ $row_lists = array();
+ foreach ($roots as $set) {
+ $view = id(new ArcanistCommitGraphSetTreeView())
+ ->setRepositoryAPI($api)
+ ->setRootSet($set)
+ ->setMarkers($markers)
+ ->setStateRefs($state_refs);
+
+ $row_lists[] = $view->draw();
}
- $is_active = tsprintf('** %s **', $active_mark);
-
- $rows[] = array(
- 'active' => $is_active,
- 'name' => $marker_name,
- 'status' => $status,
- 'description' => $description,
- );
+ $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 @@
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);
+ }
+
}

File Metadata

Mime Type
text/plain
Expires
Thu, Mar 20, 4:56 PM (1 d, 23 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/lt/6t/ixmp4qphbuvmy3ro
Default Alt Text
D21363.id50889.diff (63 KB)

Event Timeline