1
0
Fork 0
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:
epriestley 2020-06-15 08:12:54 -07:00
parent 80f5166b70
commit cd19216ea2
18 changed files with 2114 additions and 68 deletions

View file

@ -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',

View file

@ -1816,4 +1816,8 @@ final class ArcanistGitAPI extends ArcanistRepositoryAPI {
return $hashes;
}
protected function newCommitGraphQueryTemplate() {
return new ArcanistGitCommitGraphQuery();
}
}

View file

@ -1031,4 +1031,8 @@ final class ArcanistMercurialAPI extends ArcanistRepositoryAPI {
$uri);
}
protected function newCommitGraphQueryTemplate() {
return new ArcanistMercurialCommitGraphQuery();
}
}

View file

@ -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;
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View file

@ -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));
}
}

View 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();
}

View 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;
}
}
}
}
}

View 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).')';
}
}
}

View file

@ -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);
}
}
}

View 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;
}
}

View 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);
}
}

View file

@ -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;
}
}
// TODO: When no parent of a set is in the node list, we should render
// a marker showing that the commit sequence is historic.
$row_lists = array();
foreach ($roots as $set) {
$view = id(new ArcanistCommitGraphSetTreeView())
->setRepositoryAPI($api)
->setRootSet($set)
->setMarkers($markers)
->setStateRefs($state_refs);
$row_lists[] = $view->draw();
}
$partition_lists[] = $row_lists;
}
$grid = id(new ArcanistGridView());
$grid->newColumn('marker');
$grid->newColumn('commits');
$grid->newColumn('status');
$grid->newColumn('revisions');
$grid->newColumn('messages');
foreach ($partition_lists as $row_lists) {
foreach ($row_lists as $row_list) {
foreach ($row_list as $row) {
$grid->newRow($row);
}
}
}
if ($revision_ref) {
$description = $revision_ref->getFullName();
} else {
$description = $commit_ref->getSummary();
}
if ($marker_ref->getIsActive()) {
$active_mark = '*';
} else {
$active_mark = ' ';
}
$is_active = tsprintf('** %s **', $active_mark);
$rows[] = array(
'active' => $is_active,
'name' => $marker_name,
'status' => $status,
'description' => $description,
);
}
$table->drawRows($rows);
return 0;
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);
}
}