diff --git a/src/ref/revision/ArcanistRevisionRef.php b/src/ref/revision/ArcanistRevisionRef.php index c36845bd..6f73390d 100644 --- a/src/ref/revision/ArcanistRevisionRef.php +++ b/src/ref/revision/ArcanistRevisionRef.php @@ -1,209 +1,219 @@ getMonogram()); } protected function newHardpoints() { $object_list = new ArcanistObjectListHardpoint(); return array( $this->newHardpoint(self::HARDPOINT_COMMITMESSAGE), $this->newHardpoint(self::HARDPOINT_AUTHORREF), $this->newHardpoint(self::HARDPOINT_BUILDABLEREF), $this->newTemplateHardpoint( self::HARDPOINT_PARENTREVISIONREFS, $object_list), ); } public static function newFromConduit(array $dict) { $ref = new self(); $ref->parameters = $dict; return $ref; } public static function newFromConduitQuery(array $dict) { // Mangle an older "differential.query" result to look like a modern // "differential.revision.search" result. $status_name = idx($dict, 'statusName'); switch ($status_name) { case 'Abandoned': case 'Closed': $is_closed = true; break; default: $is_closed = false; break; } $value_map = array( '0' => 'needs-review', '1' => 'needs-revision', '2' => 'accepted', '3' => 'published', '4' => 'abandoned', '5' => 'changes-planned', ); $color_map = array( 'needs-review' => 'magenta', 'needs-revision' => 'red', 'accepted' => 'green', 'published' => 'cyan', 'abandoned' => null, 'changes-planned' => 'red', ); $status_value = idx($value_map, idx($dict, 'status')); $ansi_color = idx($color_map, $status_value); + $date_created = null; + if (isset($dict['dateCreated'])) { + $date_created = (int)$dict['dateCreated']; + } + $dict['fields'] = array( 'uri' => idx($dict, 'uri'), 'title' => idx($dict, 'title'), 'authorPHID' => idx($dict, 'authorPHID'), 'status' => array( 'name' => $status_name, 'closed' => $is_closed, 'value' => $status_value, 'color.ansi' => $ansi_color, ), + 'dateCreated' => $date_created, ); return self::newFromConduit($dict); } public function getMonogram() { return 'D'.$this->getID(); } public function getStatusShortDisplayName() { if ($this->isStatusNeedsReview()) { return pht('Review'); } return idxv($this->parameters, array('fields', 'status', 'name')); } public function getStatusDisplayName() { return idxv($this->parameters, array('fields', 'status', 'name')); } public function getStatusANSIColor() { return idxv($this->parameters, array('fields', 'status', 'color.ansi')); } + public function getDateCreated() { + return idxv($this->parameters, array('fields', 'dateCreated')); + } + public function isStatusChangesPlanned() { $status = $this->getStatus(); return ($status === 'changes-planned'); } public function isStatusAbandoned() { $status = $this->getStatus(); return ($status === 'abandoned'); } public function isStatusPublished() { $status = $this->getStatus(); return ($status === 'published'); } public function isStatusAccepted() { $status = $this->getStatus(); return ($status === 'accepted'); } public function isStatusNeedsReview() { $status = $this->getStatus(); return ($status === 'needs-review'); } public function getStatus() { return idxv($this->parameters, array('fields', 'status', 'value')); } public function isClosed() { return idxv($this->parameters, array('fields', 'status', 'closed')); } public function getURI() { $uri = idxv($this->parameters, array('fields', 'uri')); if ($uri === null) { // TODO: The "uri" field was added at the same time as this callsite, // so we may not have it yet if the server is running an older version // of Phabricator. Fake our way through. $uri = '/'.$this->getMonogram(); } return $uri; } public function getFullName() { return pht('%s: %s', $this->getMonogram(), $this->getName()); } public function getID() { return (int)idx($this->parameters, 'id'); } public function getPHID() { return idx($this->parameters, 'phid'); } public function getDiffPHID() { return idxv($this->parameters, array('fields', 'diffPHID')); } public function getName() { return idxv($this->parameters, array('fields', 'title')); } public function getAuthorPHID() { return idxv($this->parameters, array('fields', 'authorPHID')); } public function addSource(ArcanistRevisionRefSource $source) { $this->sources[] = $source; return $this; } public function getSources() { return $this->sources; } public function getCommitMessage() { return $this->getHardpoint(self::HARDPOINT_COMMITMESSAGE); } public function getAuthorRef() { return $this->getHardpoint(self::HARDPOINT_AUTHORREF); } public function getParentRevisionRefs() { return $this->getHardpoint(self::HARDPOINT_PARENTREVISIONREFS); } public function getBuildableRef() { return $this->getHardpoint(self::HARDPOINT_BUILDABLEREF); } protected function buildRefView(ArcanistRefView $view) { $view ->setObjectName($this->getMonogram()) ->setTitle($this->getName()); } } diff --git a/src/repository/graph/query/ArcanistCommitGraphQuery.php b/src/repository/graph/query/ArcanistCommitGraphQuery.php index 98cbb35c..6369c4b5 100644 --- a/src/repository/graph/query/ArcanistCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistCommitGraphQuery.php @@ -1,69 +1,79 @@ 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 withEpochRange($min, $max) { + $this->minimumEpoch = $min; + $this->maximumEpoch = $max; + return $this; + } + + final public function getMinimumEpoch() { + return $this->minimumEpoch; + } + + final public function getMaximumEpoch() { + return $this->maximumEpoch; + } + final public function getRepositoryAPI() { return $this->getGraph()->getRepositoryAPI(); } abstract public function execute(); } diff --git a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php index 028fbbe3..2491a549 100644 --- a/src/repository/graph/query/ArcanistGitCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistGitCommitGraphQuery.php @@ -1,150 +1,209 @@ beginExecute(); - $this->continueExecute(); + $this->newFutures(); + + $this->executeIterators(); return $this->seen; } - protected function beginExecute() { + private function newFutures() { $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(); + $ref_lists = array(); - $refs = array(); - if ($head_hashes !== null) { - foreach ($head_hashes as $hash) { - $refs[] = $hash; + if ($head_hashes) { + $refs = array(); + if ($head_hashes !== null) { + foreach ($head_hashes as $hash) { + $refs[] = $hash; + } } - } - $tail_hashes = $this->getTailHashes(); - if ($tail_hashes !== null) { - foreach ($tail_hashes as $tail_hash) { - $refs[] = sprintf('^%s^@', $tail_hash); + $tail_hashes = $this->getTailHashes(); + if ($tail_hashes !== null) { + foreach ($tail_hashes as $tail_hash) { + $refs[] = sprintf('^%s^@', $tail_hash); + } } + + $ref_lists[] = $refs; } if ($exact_hashes !== null) { - if (count($exact_hashes) > 1) { - // If "A" is a parent of "B" and we search for exact hashes ["A", "B"], - // the exclusion rule generated by "^B^@" is stronger than the inclusion - // rule generated by "A" and we don't get "A" in the result set. - throw new Exception( - pht( - 'TODO: Multiple exact hashes not supported under Git.')); - } foreach ($exact_hashes as $exact_hash) { - $refs[] = $exact_hash; - $refs[] = sprintf('^%s^@', $exact_hash); + $ref_list = array(); + $ref_list[] = $exact_hash; + $ref_list[] = sprintf('^%s^@', $exact_hash); + $ref_list[] = '--'; + $ref_lists[] = $ref_list; } } - $refs[] = '--'; - $refs = implode("\n", $refs)."\n"; + $flags = array(); + + $min_epoch = $this->getMinimumEpoch(); + if ($min_epoch !== null) { + $flags[] = '--after'; + $flags[] = date('c', $min_epoch); + } + + $max_epoch = $this->getMaximumEpoch(); + if ($max_epoch !== null) { + $flags[] = '--before'; + $flags[] = date('c', $max_epoch); + } + + foreach ($ref_lists as $ref_list) { + $ref_blob = implode("\n", $ref_list)."\n"; + + $fields = array( + '%e', + '%H', + '%P', + '%ct', + '%B', + ); + + $format = implode('%x02', $fields).'%x01'; + + $future = $api->newFuture( + 'log --format=%s %Ls --stdin', + $format, + $flags); + $future->write($ref_blob); + $future->setResolveOnError(true); + + $this->futures[] = $future; + } + } + + private function executeIterators() { + while ($this->futures || $this->iterators) { + $iterator_limit = 8; - $fields = array( - '%e', - '%H', - '%P', - '%ct', - '%B', - ); + while (count($this->iterators) < $iterator_limit) { + if (!$this->futures) { + break; + } - $format = implode('%x02', $fields).'%x01'; + $future = array_pop($this->futures); + $future->startFuture(); - $future = $api->newFuture( - 'log --format=%s --stdin', - $format); - $future->write($refs); - $future->setResolveOnError(true); - $future->start(); + $iterator = id(new LinesOfALargeExecFuture($future)) + ->setDelimiter("\1"); + $iterator->rewind(); - $lines = id(new LinesOfALargeExecFuture($future)) - ->setDelimiter("\1"); - $lines->rewind(); + $iterator_key = $this->getNextIteratorKey(); + $this->iterators[$iterator_key] = $iterator; + } + + $limit = $this->getLimit(); - $this->queryFuture = $lines; + foreach ($this->iterators as $iterator_key => $iterator) { + $this->executeIterator($iterator_key, $iterator); + + if ($limit) { + if (count($this->seen) >= $limit) { + return; + } + } + } + } + } + + private function getNextIteratorKey() { + return $this->iteratorKey++; } - protected function continueExecute() { + private function executeIterator($iterator_key, $lines) { $graph = $this->getGraph(); $limit = $this->getLimit(); - $lines = $this->queryFuture; + $is_done = false; while (true) { if (!$lines->valid()) { - return false; + $is_done = true; + break; } $line = $lines->current(); $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; } } } + + if ($is_done) { + unset($this->iterators[$iterator_key]); + } } } diff --git a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php index 0a3836a9..aadd08ad 100644 --- a/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php +++ b/src/repository/graph/query/ArcanistMercurialCommitGraphQuery.php @@ -1,180 +1,210 @@ 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), + $this->joinAndRevsets($revsets), ); } 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); } - $revsets = $this->joinOrRevsets($revs); + $revsets = $this->joinOrRevsets($revsets); $fields = array( '', // Placeholder for "encoding". '{node}', '{parents}', '{date|rfc822date}', '{description|utf8}', ); $template = implode("\2", $fields)."\1"; + $flags = array(); + + $min_epoch = $this->getMinimumEpoch(); + $max_epoch = $this->getMaximumEpoch(); + if ($min_epoch !== null || $max_epoch !== null) { + $flags[] = '--date'; + + if ($min_epoch !== null) { + $min_epoch = date('c', $min_epoch); + } + + if ($max_epoch !== null) { + $max_epoch = date('c', $max_epoch); + } + + if ($min_epoch !== null && $max_epoch !== null) { + $flags[] = sprintf( + '%s to %s', + $min_epoch, + $max_epoch); + } else if ($min_epoch) { + $flags[] = sprintf( + '>%s', + $min_epoch); + } else { + $flags[] = sprintf( + '<%s', + $max_epoch); + } + } + $future = $api->newFuture( - 'log --rev %s --template %s --', + 'log --rev %s --template %s %Ls --', $revsets, - $template); + $template, + $flags); $future->setResolveOnError(true); $future->start(); $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/workflow/ArcanistMarkersWorkflow.php b/src/workflow/ArcanistMarkersWorkflow.php index ea9fe98d..d27ae2af 100644 --- a/src/workflow/ArcanistMarkersWorkflow.php +++ b/src/workflow/ArcanistMarkersWorkflow.php @@ -1,303 +1,297 @@ getRepositoryAPI(); $marker_type = $this->getWorkflowMarkerType(); $markers = $api->newMarkerRefQuery() ->withMarkerTypes(array($marker_type)) ->execute(); $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) { + $disjoint_nodes = $graph->newQuery() + ->withExactHashes($disjoint_heads) + ->execute(); - // TODO: Git currently can not query for more than one exact hash at a - // time. - - foreach ($disjoint_heads as $disjoint_head) { - $disjoint_nodes = $graph->newQuery() - ->withExactHashes(array($disjoint_head)) - ->execute(); - - $nodes += $disjoint_nodes; - } + $nodes += $disjoint_nodes; } $state_refs = array(); foreach ($nodes as $node) { $commit_ref = $node->getCommitRef(); $state_ref = id(new ArcanistWorkingCopyStateRef()) ->setCommitRef($commit_ref); $state_refs[$node->getCommitHash()] = $state_ref; } $this->loadHardpoints( $state_refs, ArcanistWorkingCopyStateRef::HARDPOINT_REVISIONREFS); $partitions = $graph->newPartitionQuery() ->withHeads($heads) ->withHashes(array_keys($nodes)) ->execute(); $revision_refs = array(); foreach ($state_refs as $hash => $state_ref) { $revision_ids = mpull($state_ref->getRevisionRefs(), 'getID'); $revision_refs[$hash] = array_fuse($revision_ids); } $partition_sets = array(); $partition_vectors = array(); foreach ($partitions as $partition_key => $partition) { $sets = $partition->newSetQuery() ->setWaypointMap($revision_refs) ->execute(); list($sets, $partition_vector) = $this->sortSets( $graph, $sets, $markers); $partition_sets[$partition_key] = $sets; $partition_vectors[$partition_key] = $partition_vector; } $partition_vectors = msortv($partition_vectors, 'getSelf'); $partitions = array_select_keys( $partitions, array_keys($partition_vectors)); $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; } } // 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') ->setMinimumWidth(12); 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) { $api = $this->getRepositoryAPI(); $types = $api->getSupportedMarkerTypes(); $types = array_fuse($types); 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); } }