diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index 8263b91dbb..5cf6f82198 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -1,226 +1,281 @@ needFlags(true) ->needDrafts(true) ->needRelationships(true) ->needReviewerStatus(true); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['responsiblePHIDs']) { $query->withResponsibleUsers($map['responsiblePHIDs']); } if ($map['authorPHIDs']) { $query->withAuthors($map['authorPHIDs']); } if ($map['reviewerPHIDs']) { $query->withReviewers($map['reviewerPHIDs']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } if ($map['status']) { $query->withStatus($map['status']); } return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Responsible Users')) ->setKey('responsiblePHIDs') ->setAliases(array('responsiblePHID', 'responsibles', 'responsible')) ->setDatasource(new DifferentialResponsibleDatasource()) ->setDescription( pht('Find revisions that a given user is responsible for.')), id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setAliases(array('author', 'authors', 'authorPHID')) ->setDescription( pht('Find revisions with specific authors.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Reviewers')) ->setKey('reviewerPHIDs') ->setAliases(array('reviewer', 'reviewers', 'reviewerPHID')) ->setDatasource(new DiffusionAuditorFunctionDatasource()) ->setDescription( pht('Find revisions with specific reviewers.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setAliases(array('repository', 'repositories', 'repositoryPHID')) ->setDatasource(new DiffusionRepositoryDatasource()) ->setDescription( pht('Find revisions from specific repositories.')), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Status')) ->setKey('status') ->setOptions($this->getStatusOptions()) ->setDescription( pht('Find revisions with particular statuses.')), ); } protected function getURI($path) { return '/differential/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['active'] = pht('Active Revisions'); $names['authored'] = pht('Authored'); } $names['all'] = pht('All Revisions'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer = $this->requireViewer(); switch ($query_key) { case 'active': $bucket_key = DifferentialRevisionRequiredActionResultBucket::BUCKETKEY; return $query ->setParameter('responsiblePHIDs', array($viewer->getPHID())) ->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN) ->setParameter('bucket', $bucket_key); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer->getPHID())); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( DifferentialRevisionQuery::STATUS_ANY => pht('All'), DifferentialRevisionQuery::STATUS_OPEN => pht('Open'), DifferentialRevisionQuery::STATUS_ACCEPTED => pht('Accepted'), DifferentialRevisionQuery::STATUS_NEEDS_REVIEW => pht('Needs Review'), DifferentialRevisionQuery::STATUS_NEEDS_REVISION => pht('Needs Revision'), DifferentialRevisionQuery::STATUS_CLOSED => pht('Closed'), DifferentialRevisionQuery::STATUS_ABANDONED => pht('Abandoned'), ); } protected function renderResultList( array $revisions, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($revisions, 'DifferentialRevision'); $viewer = $this->requireViewer(); $template = id(new DifferentialRevisionListView()) ->setUser($viewer) ->setNoBox($this->isPanelContext()); $bucket = $this->getResultBucket($query); + $unlanded = $this->loadUnlandedDependencies($revisions); + $views = array(); if ($bucket) { $bucket->setViewer($viewer); try { $groups = $bucket->newResultGroups($query, $revisions); foreach ($groups as $group) { $views[] = id(clone $template) ->setHeader($group->getName()) ->setNoDataString($group->getNoDataString()) ->setRevisions($group->getObjects()); } } catch (Exception $ex) { $this->addError($ex->getMessage()); } } else { $views[] = id(clone $template) ->setRevisions($revisions) ->setHandles(array()); } $phids = array_mergev(mpull($views, 'getRequiredHandlePHIDs')); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } else { $handles = array(); } foreach ($views as $view) { $view->setHandles($handles); + $view->setUnlandedDependencies($unlanded); } if (count($views) == 1) { // Reduce this to a PHUIObjectItemListView so we can get the free // support from ApplicationSearch. $list = head($views)->render(); } else { $list = $views; } $result = new PhabricatorApplicationSearchResultView(); $result->setContent($list); return $result; } protected function getNewUserBody() { $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Create a Diff')) ->setHref('/differential/diff/create/') ->setColor(PHUIButtonView::GREEN); $icon = $this->getApplication()->getIcon(); $app_name = $this->getApplication()->getName(); $view = id(new PHUIBigInfoView()) ->setIcon($icon) ->setTitle(pht('Welcome to %s', $app_name)) ->setDescription( pht('Pre-commit code review. Revisions that are waiting on your input '. 'will appear here.')) ->addAction($create_button); return $view; } + private function loadUnlandedDependencies(array $revisions) { + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; + + $phids = array(); + foreach ($revisions as $revision) { + if ($revision->getStatus() != $status_accepted) { + continue; + } + + $phids[] = $revision->getPHID(); + } + + if (!$phids) { + return array(); + } + + $query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($phids) + ->withEdgeTypes( + array( + DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, + )); + + $query->execute(); + + $revision_phids = $query->getDestinationPHIDs(); + if (!$revision_phids) { + return array(); + } + + $viewer = $this->requireViewer(); + + $blocking_revisions = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withPHIDs($revision_phids) + ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) + ->execute(); + $blocking_revisions = mpull($blocking_revisions, null, 'getPHID'); + + $result = array(); + foreach ($revisions as $revision) { + $revision_phid = $revision->getPHID(); + $blocking_phids = $query->getDestinationPHIDs(array($revision_phid)); + $blocking = array_select_keys($blocking_revisions, $blocking_phids); + if ($blocking) { + $result[$revision_phid] = $blocking; + } + } + + return $result; + } + } diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 826536355e..6451bd9d2a 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -1,185 +1,205 @@ unlandedDependencies = $unlanded_dependencies; + return $this; + } + + public function getUnlandedDependencies() { + return $this->unlandedDependencies; + } public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $this->revisions = $revisions; return $this; } public function setHighlightAge($bool) { $this->highlightAge = $bool; return $this; } public function setNoBox($box) { $this->noBox = $box; return $this; } public function setBackground($background) { $this->background = $background; return $this; } public function getRequiredHandlePHIDs() { $phids = array(); foreach ($this->revisions as $revision) { $phids[] = array($revision->getAuthorPHID()); // TODO: Switch to getReviewerStatus(), but not all callers pass us // revisions with this data loaded. $phids[] = $revision->getReviewers(); } return array_mergev($phids); } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function render() { $viewer = $this->getViewer(); $this->initBehavior('phabricator-tooltips', array()); $this->requireResource('aphront-tooltip-css'); $list = new PHUIObjectItemListView(); foreach ($this->revisions as $revision) { $item = id(new PHUIObjectItemView()) ->setUser($viewer); $icons = array(); $phid = $revision->getPHID(); $flag = $revision->getFlag($viewer); if ($flag) { $flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $icons['flag'] = phutil_tag( 'div', array( 'class' => 'phabricator-flag-icon '.$flag_class, ), ''); } if ($revision->getDrafts($viewer)) { $icons['draft'] = true; } $modified = $revision->getDateModified(); $status = $revision->getStatus(); $status_name = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); if (isset($icons['flag'])) { $item->addHeadIcon($icons['flag']); } $item->setObjectName('D'.$revision->getID()); $item->setHeader($revision->getTitle()); $item->setHref('/D'.$revision->getID()); if (isset($icons['draft'])) { $draft = id(new PHUIIconView()) ->setIcon('fa-comment yellow') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Unsubmitted Comments'), )); $item->addAttribute($draft); } // Author $author_handle = $this->handles[$revision->getAuthorPHID()]; $item->addByline(pht('Author: %s', $author_handle->renderLink())); + $unlanded = idx($this->unlandedDependencies, $phid); + if ($unlanded) { + $item->addAttribute( + array( + id(new PHUIIconView())->setIcon('fa-chain-broken', 'red'), + ' ', + pht('Open Dependencies'), + )); + } + $reviewers = array(); // TODO: As above, this should be based on `getReviewerStatus()`. foreach ($revision->getReviewers() as $reviewer) { $reviewers[] = $this->handles[$reviewer]->renderLink(); } if (!$reviewers) { $reviewers = phutil_tag('em', array(), pht('None')); } else { $reviewers = phutil_implode_html(', ', $reviewers); } $item->addAttribute(pht('Reviewers: %s', $reviewers)); $item->setEpoch($revision->getDateModified()); switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $item->setStatusIcon('fa-code grey', pht('Needs Review')); break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $item->setStatusIcon('fa-refresh red', pht('Needs Revision')); break; case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: $item->setStatusIcon('fa-headphones red', pht('Changes Planned')); break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $item->setStatusIcon('fa-check green', pht('Accepted')); break; case ArcanistDifferentialRevisionStatus::CLOSED: $item->setDisabled(true); $item->setStatusIcon('fa-check-square-o black', pht('Closed')); break; case ArcanistDifferentialRevisionStatus::ABANDONED: $item->setDisabled(true); $item->setStatusIcon('fa-plane black', pht('Abandoned')); break; } $list->addItem($item); } $list->setNoDataString($this->noDataString); if ($this->header && !$this->noBox) { $list->setFlush(true); $list = id(new PHUIObjectBoxView()) ->setBackground($this->background) ->setObjectList($list); if ($this->header instanceof PHUIHeaderView) { $list->setHeader($this->header); } else { $list->setHeaderText($this->header); } } else { $list->setHeader($this->header); } return $list; } }