diff --git a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php index 68a6284929..e74408a421 100644 --- a/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php +++ b/src/applications/differential/conduit/ConduitAPI_differential_creatediff_Method.php @@ -1,157 +1,156 @@ 'required list', 'sourceMachine' => 'required string', 'sourcePath' => 'required string', 'branch' => 'required string', 'bookmark' => 'optional string', 'sourceControlSystem' => 'required enum', 'sourceControlPath' => 'required string', 'sourceControlBaseRevision' => 'required string', 'parentRevisionID' => 'optional revisionid', 'creationMethod' => 'optional string', 'authorPHID' => 'optional phid', 'arcanistProject' => 'optional string', 'repositoryUUID' => 'optional string', 'lintStatus' => 'required enum', 'unitStatus' => 'required enum', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $change_data = $request->getValue('changes'); $changes = array(); foreach ($change_data as $dict) { $changes[] = ArcanistDiffChange::newFromDictionary($dict); } $diff = DifferentialDiff::newFromRawChanges($changes); $diff->setSourcePath($request->getValue('sourcePath')); $diff->setSourceMachine($request->getValue('sourceMachine')); $diff->setBranch($request->getValue('branch')); $diff->setCreationMethod($request->getValue('creationMethod')); $diff->setAuthorPHID($request->getValue('authorPHID')); $diff->setBookmark($request->getValue('bookmark')); $parent_id = $request->getValue('parentRevisionID'); if ($parent_id) { // NOTE: If the viewer can't see the parent revision, just don't set // a parent revision ID. This isn't used for anything meaningful. // TODO: Can we delete this entirely? $parent_rev = id(new DifferentialRevisionQuery()) ->setViewer($request->getUser()) ->withIDs(array($parent_id)) ->execute(); if ($parent_rev) { $parent_rev = head($parent_rev); - if ($parent_rev->getStatus() != - ArcanistDifferentialRevisionStatus::CLOSED) { + if (!$parent_rev->isClosed()) { $diff->setParentRevisionID($parent_id); } } } $system = $request->getValue('sourceControlSystem'); $diff->setSourceControlSystem($system); $diff->setSourceControlPath($request->getValue('sourceControlPath')); $diff->setSourceControlBaseRevision( $request->getValue('sourceControlBaseRevision')); $project_name = $request->getValue('arcanistProject'); $project_phid = null; if ($project_name) { $arcanist_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere( 'name = %s', $project_name); if (!$arcanist_project) { $arcanist_project = new PhabricatorRepositoryArcanistProject(); $arcanist_project->setName($project_name); $arcanist_project->save(); } $project_phid = $arcanist_project->getPHID(); } $diff->setArcanistProjectPHID($project_phid); $diff->setRepositoryUUID($request->getValue('repositoryUUID')); switch ($request->getValue('lintStatus')) { case 'skip': $diff->setLintStatus(DifferentialLintStatus::LINT_SKIP); break; case 'okay': $diff->setLintStatus(DifferentialLintStatus::LINT_OKAY); break; case 'warn': $diff->setLintStatus(DifferentialLintStatus::LINT_WARN); break; case 'fail': $diff->setLintStatus(DifferentialLintStatus::LINT_FAIL); break; case 'postponed': $diff->setLintStatus(DifferentialLintStatus::LINT_POSTPONED); break; case 'none': default: $diff->setLintStatus(DifferentialLintStatus::LINT_NONE); break; } switch ($request->getValue('unitStatus')) { case 'skip': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP); break; case 'okay': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_OKAY); break; case 'warn': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_WARN); break; case 'fail': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_FAIL); break; case 'postponed': $diff->setUnitStatus(DifferentialUnitStatus::UNIT_POSTPONED); break; case 'none': default: $diff->setUnitStatus(DifferentialUnitStatus::UNIT_NONE); break; } $diff->save(); $path = '/differential/diff/'.$diff->getID().'/'; $uri = PhabricatorEnv::getURI($path); return array( 'diffid' => $diff->getID(), 'uri' => $uri, ); } } diff --git a/src/applications/differential/constants/DifferentialRevisionStatus.php b/src/applications/differential/constants/DifferentialRevisionStatus.php index 1088ac40e3..95576fc80b 100644 --- a/src/applications/differential/constants/DifferentialRevisionStatus.php +++ b/src/applications/differential/constants/DifferentialRevisionStatus.php @@ -1,73 +1,99 @@ self::COLOR_STATUS_DEFAULT, ArcanistDifferentialRevisionStatus::NEEDS_REVISION => self::COLOR_STATUS_RED, ArcanistDifferentialRevisionStatus::ACCEPTED => self::COLOR_STATUS_GREEN, ArcanistDifferentialRevisionStatus::CLOSED => self::COLOR_STATUS_DARK, ArcanistDifferentialRevisionStatus::ABANDONED => self::COLOR_STATUS_DARK, ); return idx($map, $status, $default); } public static function getRevisionStatusIcon($status) { $default = 'oh-open'; $map = array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW => 'oh-open', ArcanistDifferentialRevisionStatus::NEEDS_REVISION => 'oh-open-red', ArcanistDifferentialRevisionStatus::ACCEPTED => 'oh-open-green', ArcanistDifferentialRevisionStatus::CLOSED => 'oh-closed-dark', ArcanistDifferentialRevisionStatus::ABANDONED => 'oh-closed-dark', ); return idx($map, $status, $default); } public static function renderFullDescription($status) { $color = self::getRevisionStatusColor($status); $status_name = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); $img = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_STATUS) ->setSpriteIcon(self::getRevisionStatusIcon($status)); $tag = phutil_tag( 'span', array( 'class' => 'phui-header-'.$color.' plr', ), array( $img, $status_name, )); return $tag; } + + public static function getClosedStatuses() { + return array( + ArcanistDifferentialRevisionStatus::CLOSED, + ArcanistDifferentialRevisionStatus::ABANDONED, + ); + } + + public static function getOpenStatuses() { + return array_diff(self::getAllStatuses(), self::getClosedStatuses()); + } + + public static function getAllStatuses() { + return array( + ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, + ArcanistDifferentialRevisionStatus::NEEDS_REVISION, + ArcanistDifferentialRevisionStatus::ACCEPTED, + ArcanistDifferentialRevisionStatus::CLOSED, + ArcanistDifferentialRevisionStatus::ABANDONED, + ); + } + + public static function isClosedStatus($status) { + return in_array($status, self::getClosedStatuses()); + } + } diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php index 74f9117ced..5bfd5ee836 100644 --- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php +++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php @@ -1,112 +1,106 @@ getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CREATE); } public function isStoryAboutObjectClosure($object) { $story = $this->getFeedStory(); $action = $story->getStoryData()->getValue('action'); return ($action == DifferentialAction::ACTION_CLOSE) || ($action == DifferentialAction::ACTION_ABANDON); } public function willPublishStory($object) { return id(new DifferentialRevisionQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) ->needRelationships(true) ->executeOne(); } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return $object->getReviewers(); } else { return array(); } } public function getPassiveUserPHIDs($object) { $status = $object->getStatus(); if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { return array(); } else { return $object->getReviewers(); } } public function getCCUserPHIDs($object) { return $object->getCCPHIDs(); } public function getObjectTitle($object) { $prefix = $this->getTitlePrefix($object); $lines = new PhutilNumber($object->getLineCount()); $lines = pht('[Request, %d lines]', $lines); $id = $object->getID(); $title = $object->getTitle(); return ltrim("{$prefix} {$lines} D{$id}: {$title}"); } public function getObjectURI($object) { return PhabricatorEnv::getProductionURI('/D'.$object->getID()); } public function getObjectDescription($object) { return $object->getSummary(); } public function isObjectClosed($object) { - switch ($object->getStatus()) { - case ArcanistDifferentialRevisionStatus::CLOSED: - case ArcanistDifferentialRevisionStatus::ABANDONED: - return true; - default: - return false; - } + return $object->isClosed(); } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Review Request', $prefix); } public function getStoryText($object) { $implied_context = $this->getRenderWithImpliedContext(); $story = $this->getFeedStory(); if ($story instanceof PhabricatorFeedStoryDifferential) { $text = $story->renderForAsanaBridge($implied_context); } else { $text = $story->renderText(); } return $text; } private function getTitlePrefix(DifferentialRevision $revision) { $prefix_key = 'metamta.differential.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/differential/phid/DifferentialPHIDTypeRevision.php b/src/applications/differential/phid/DifferentialPHIDTypeRevision.php index f13bd6770f..f576e0e062 100644 --- a/src/applications/differential/phid/DifferentialPHIDTypeRevision.php +++ b/src/applications/differential/phid/DifferentialPHIDTypeRevision.php @@ -1,83 +1,78 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { - static $closed_statuses = array( - ArcanistDifferentialRevisionStatus::CLOSED => true, - ArcanistDifferentialRevisionStatus::ABANDONED => true, - ); - foreach ($handles as $phid => $handle) { $revision = $objects[$phid]; $title = $revision->getTitle(); $id = $revision->getID(); $status = $revision->getStatus(); $handle->setName("D{$id}"); $handle->setURI("/D{$id}"); $handle->setFullName("D{$id}: {$title}"); - if (isset($closed_statuses[$status])) { + if ($revision->isClosed()) { $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); } } } public function canLoadNamedObject($name) { return preg_match('/^D\d*[1-9]\d*$/i', $name); } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { $id_map = array(); foreach ($names as $name) { $id = (int)substr($name, 1); $id_map[$id][] = $name; } $objects = id(new DifferentialRevisionQuery()) ->setViewer($query->getViewer()) ->withIDs(array_keys($id_map)) ->execute(); $results = array(); foreach ($objects as $id => $object) { foreach (idx($id_map, $id, array()) as $name) { $results[$name] = $object; } } return $results; } } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index de52a5cf74..ffa7bc2ff3 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -1,1203 +1,1190 @@ withStatus(DifferentialRevisionQuery::STATUS_OPEN) * ->execute(); * * @task config Query Configuration * @task exec Query Execution * @task internal Internals */ final class DifferentialRevisionQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $pathIDs = array(); private $status = 'status-any'; const STATUS_ANY = 'status-any'; const STATUS_OPEN = 'status-open'; const STATUS_ACCEPTED = 'status-accepted'; const STATUS_NEEDS_REVIEW = 'status-needs-review'; const STATUS_NEEDS_REVISION = 'status-needs-revision'; - const STATUS_CLOSED = 'status-closed'; // NOTE: Same as 'committed' - const STATUS_COMMITTED = 'status-committed'; // TODO: Remove. + const STATUS_CLOSED = 'status-closed'; const STATUS_ABANDONED = 'status-abandoned'; private $authors = array(); private $draftAuthors = array(); private $ccs = array(); private $reviewers = array(); private $revIDs = array(); private $commitHashes = array(); private $phids = array(); private $subscribers = array(); private $responsibles = array(); private $branches = array(); private $arcanistProjectPHIDs = array(); private $draftRevisions = array(); private $repositoryPHIDs; private $order = 'order-modified'; const ORDER_MODIFIED = 'order-modified'; const ORDER_CREATED = 'order-created'; /** * This is essentially a denormalized copy of the revision modified time that * should perform better for path queries with a LIMIT. Critically, when you * browse "/", every revision in that repository for all time will match so * the query benefits from being able to stop before fully materializing the * result set. */ const ORDER_PATH_MODIFIED = 'order-path-modified'; private $needRelationships = false; private $needActiveDiffs = false; private $needDiffIDs = false; private $needCommitPHIDs = false; private $needHashes = false; private $needReviewerStatus = false; private $needReviewerAuthority; private $buildingGlobalOrder; /* -( Query Configuration )------------------------------------------------ */ /** * Filter results to revisions which affect a Diffusion path ID in a given * repository. You can call this multiple times to select revisions for * several paths. * * @param int Diffusion repository ID. * @param int Diffusion path ID. * @return this * @task config */ public function withPath($repository_id, $path_id) { $this->pathIDs[] = array( 'repositoryID' => $repository_id, 'pathID' => $path_id, ); return $this; } /** * Filter results to revisions authored by one of the given PHIDs. Calling * this function will clear anything set by previous calls to * @{method:withAuthors}. * * @param array List of PHIDs of authors * @return this * @task config */ public function withAuthors(array $author_phids) { $this->authors = $author_phids; return $this; } /** * Filter results to revisions with comments authored bythe given PHIDs * * @param array List of PHIDs of authors * @return this * @task config */ public function withDraftRepliesByAuthors(array $author_phids) { $this->draftAuthors = $author_phids; return $this; } /** * Filter results to revisions which CC one of the listed people. Calling this * function will clear anything set by previous calls to @{method:withCCs}. * * @param array List of PHIDs of subscribers * @return this * @task config */ public function withCCs(array $cc_phids) { $this->ccs = $cc_phids; return $this; } /** * Filter results to revisions that have one of the provided PHIDs as * reviewers. Calling this function will clear anything set by previous calls * to @{method:withReviewers}. * * @param array List of PHIDs of reviewers * @return this * @task config */ public function withReviewers(array $reviewer_phids) { $this->reviewers = $reviewer_phids; return $this; } /** * Filter results to revisions that have one of the provided commit hashes. * Calling this function will clear anything set by previous calls to * @{method:withCommitHashes}. * * @param array List of pairs * @return this * @task config */ public function withCommitHashes(array $commit_hashes) { $this->commitHashes = $commit_hashes; return $this; } /** * Filter results to revisions with a given status. Provide a class constant, * such as ##DifferentialRevisionQuery::STATUS_OPEN##. * * @param const Class STATUS constant, like STATUS_OPEN. * @return this * @task config */ public function withStatus($status_constant) { $this->status = $status_constant; return $this; } /** * Filter results to revisions on given branches. * * @param list List of branch names. * @return this * @task config */ public function withBranches(array $branches) { $this->branches = $branches; return $this; } /** * Filter results to only return revisions whose ids are in the given set. * * @param array List of revision ids * @return this * @task config */ public function withIDs(array $ids) { $this->revIDs = $ids; return $this; } /** * Filter results to only return revisions whose PHIDs are in the given set. * * @param array List of revision PHIDs * @return this * @task config */ public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } /** * Given a set of users, filter results to return only revisions they are * responsible for (i.e., they are either authors or reviewers). * * @param array List of user PHIDs. * @return this * @task config */ public function withResponsibleUsers(array $responsible_phids) { $this->responsibles = $responsible_phids; return $this; } /** * Filter results to only return revisions with a given set of subscribers * (i.e., they are authors, reviewers or CC'd). * * @param array List of user PHIDs. * @return this * @task config */ public function withSubscribers(array $subscriber_phids) { $this->subscribers = $subscriber_phids; return $this; } /** * Filter results to only return revisions with a given set of arcanist * projects. * * @param array List of project PHIDs. * @return this * @task config */ public function withArcanistProjectPHIDs(array $arc_project_phids) { $this->arcanistProjectPHIDs = $arc_project_phids; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } /** * Set result ordering. Provide a class constant, such as * ##DifferentialRevisionQuery::ORDER_CREATED##. * * @task config */ public function setOrder($order_constant) { $this->order = $order_constant; return $this; } /** * Set whether or not the query will load and attach relationships. * * @param bool True to load and attach relationships. * @return this * @task config */ public function needRelationships($need_relationships) { $this->needRelationships = $need_relationships; return $this; } /** * Set whether or not the query should load the active diff for each * revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needActiveDiffs($need_active_diffs) { $this->needActiveDiffs = $need_active_diffs; return $this; } /** * Set whether or not the query should load the associated commit PHIDs for * each revision. * * @param bool True to load and attach diffs. * @return this * @task config */ public function needCommitPHIDs($need_commit_phids) { $this->needCommitPHIDs = $need_commit_phids; return $this; } /** * Set whether or not the query should load associated diff IDs for each * revision. * * @param bool True to load and attach diff IDs. * @return this * @task config */ public function needDiffIDs($need_diff_ids) { $this->needDiffIDs = $need_diff_ids; return $this; } /** * Set whether or not the query should load associated commit hashes for each * revision. * * @param bool True to load and attach commit hashes. * @return this * @task config */ public function needHashes($need_hashes) { $this->needHashes = $need_hashes; return $this; } /** * Set whether or not the query should load associated reviewer status. * * @param bool True to load and attach reviewers. * @return this * @task config */ public function needReviewerStatus($need_reviewer_status) { $this->needReviewerStatus = $need_reviewer_status; return $this; } /** * Request information about the viewer's authority to act on behalf of each * reviewer. In particular, they have authority to act on behalf of projects * they are a member of. * * @param bool True to load and attach authority. * @return this * @task config */ public function needReviewerAuthority($need_reviewer_authority) { $this->needReviewerAuthority = $need_reviewer_authority; return $this; } /* -( Query Execution )---------------------------------------------------- */ /** * Execute the query as configured, returning matching * @{class:DifferentialRevision} objects. * * @return list List of matching DifferentialRevision objects. * @task exec */ public function loadPage() { $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); $data = $this->loadData(); return $table->loadAllFromArray($data); } public function willFilterPage(array $revisions) { $viewer = $this->getViewer(); $repository_phids = mpull($revisions, 'getRepositoryPHID'); $repository_phids = array_filter($repository_phids); $repositories = array(); if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } // If a revision is associated with a repository: // // - the viewer must be able to see the repository; or // - the viewer must have an automatic view capability. // // In the latter case, we'll load the revision but not load the repository. $can_view = PhabricatorPolicyCapability::CAN_VIEW; foreach ($revisions as $key => $revision) { $repo_phid = $revision->getRepositoryPHID(); if (!$repo_phid) { // The revision has no associated repository. Attach `null` and move on. $revision->attachRepository(null); continue; } $repository = idx($repositories, $repo_phid); if ($repository) { // The revision has an associated repository, and the viewer can see // it. Attach it and move on. $revision->attachRepository($repository); continue; } if ($revision->hasAutomaticCapability($can_view, $viewer)) { // The revision has an associated repository which the viewer can not // see, but the viewer has an automatic capability on this revision. // Load the revision without attaching a repository. $revision->attachRepository(null); continue; } if ($this->getViewer()->isOmnipotent()) { // The viewer is omnipotent. Allow the revision to load even without // a repository. $revision->attachRepository(null); continue; } // The revision has an associated repository, and the viewer can't see // it, and the viewer has no special capabilities. Filter out this // revision. $this->didRejectResult($revision); unset($revisions[$key]); } if (!$revisions) { return array(); } $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); if ($this->needRelationships) { $this->loadRelationships($conn_r, $revisions); } if ($this->needCommitPHIDs) { $this->loadCommitPHIDs($conn_r, $revisions); } $need_active = $this->needActiveDiffs; $need_ids = $need_active || $this->needDiffIDs; if ($need_ids) { $this->loadDiffIDs($conn_r, $revisions); } if ($need_active) { $this->loadActiveDiffs($conn_r, $revisions); } if ($this->needHashes) { $this->loadHashes($conn_r, $revisions); } if ($this->needReviewerStatus || $this->needReviewerAuthority) { $this->loadReviewers($conn_r, $revisions); } return $revisions; } private function loadData() { $table = new DifferentialRevision(); $conn_r = $table->establishConnection('r'); if ($this->draftAuthors) { $this->draftRevisions = array(); $draft_key = 'differential-comment-'; $drafts = id(new PhabricatorDraft())->loadAllWhere( 'authorPHID IN (%Ls) AND draftKey LIKE %> AND draft != %s', $this->draftAuthors, $draft_key, ''); $len = strlen($draft_key); foreach ($drafts as $draft) { $this->draftRevisions[] = substr($draft->getDraftKey(), $len); } // TODO: Restore this after drafts are sorted out. It's now very // expensive to get revision IDs. /* $inlines = id(new DifferentialInlineCommentQuery()) ->withDraftsByAuthors($this->draftAuthors) ->execute(); foreach ($inlines as $inline) { $this->draftRevisions[] = $inline->getRevisionID(); } */ if (!$this->draftRevisions) { return array(); } } $selects = array(); // NOTE: If the query includes "responsiblePHIDs", we execute it as a // UNION of revisions they own and revisions they're reviewing. This has // much better performance than doing it with JOIN/WHERE. if ($this->responsibles) { $basic_authors = $this->authors; $basic_reviewers = $this->reviewers; $authority_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($this->responsibles) ->execute(); $authority_phids = mpull($authority_projects, 'getPHID'); try { // Build the query where the responsible users are authors. $this->authors = array_merge($basic_authors, $this->responsibles); $this->reviewers = $basic_reviewers; $selects[] = $this->buildSelectStatement($conn_r); // Build the query where the responsible users are reviewers, or // projects they are members of are reviewers. $this->authors = $basic_authors; $this->reviewers = array_merge( $basic_reviewers, $this->responsibles, $authority_phids); $selects[] = $this->buildSelectStatement($conn_r); // Put everything back like it was. $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; } catch (Exception $ex) { $this->authors = $basic_authors; $this->reviewers = $basic_reviewers; throw $ex; } } else { $selects[] = $this->buildSelectStatement($conn_r); } if (count($selects) > 1) { $this->buildingGlobalOrder = true; $query = qsprintf( $conn_r, '%Q %Q %Q', implode(' UNION DISTINCT ', $selects), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); } else { $query = head($selects); } return queryfx_all($conn_r, '%Q', $query); } private function buildSelectStatement(AphrontDatabaseConnection $conn_r) { $table = new DifferentialRevision(); $select = qsprintf( $conn_r, 'SELECT r.* FROM %T r', $table->getTableName()); $joins = $this->buildJoinsClause($conn_r); $where = $this->buildWhereClause($conn_r); $group_by = $this->buildGroupByClause($conn_r); $this->buildingGlobalOrder = false; $order_by = $this->buildOrderClause($conn_r); $limit = $this->buildLimitClause($conn_r); return qsprintf( $conn_r, '(%Q %Q %Q %Q %Q %Q)', $select, $joins, $where, $group_by, $order_by, $limit); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function buildJoinsClause($conn_r) { $joins = array(); if ($this->pathIDs) { $path_table = new DifferentialAffectedPath(); $joins[] = qsprintf( $conn_r, 'JOIN %T p ON p.revisionID = r.id', $path_table->getTableName()); } if ($this->commitHashes) { $joins[] = qsprintf( $conn_r, 'JOIN %T hash_rel ON hash_rel.revisionID = r.id', ArcanistDifferentialRevisionHash::TABLE_NAME); } if ($this->ccs) { $joins[] = qsprintf( $conn_r, 'JOIN %T cc_rel ON cc_rel.revisionID = r.id '. 'AND cc_rel.relation = %s '. 'AND cc_rel.objectPHID in (%Ls)', DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_SUBSCRIBED, $this->ccs); } if ($this->reviewers) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_reviewers ON e_reviewers.src = r.phid '. 'AND e_reviewers.type = %s '. 'AND e_reviewers.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $this->reviewers); } if ($this->subscribers) { // TODO: These can be expressed as a JOIN again (and the corresponding // WHERE clause removed) once subscribers move to edges. $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T sub_rel_cc ON sub_rel_cc.revisionID = r.id '. 'AND sub_rel_cc.relation = %s '. 'AND sub_rel_cc.objectPHID in (%Ls)', DifferentialRevision::RELATIONSHIP_TABLE, DifferentialRevision::RELATION_SUBSCRIBED, $this->subscribers); $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T sub_rel_reviewer ON sub_rel_reviewer.src = r.phid '. 'AND sub_rel_reviewer.type = %s '. 'AND sub_rel_reviewer.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER, $this->subscribers); } $joins = implode(' ', $joins); return $joins; } /** * @task internal */ private function buildWhereClause($conn_r) { $where = array(); if ($this->pathIDs) { $path_clauses = array(); $repo_info = igroup($this->pathIDs, 'repositoryID'); foreach ($repo_info as $repository_id => $paths) { $path_clauses[] = qsprintf( $conn_r, '(p.repositoryID = %d AND p.pathID IN (%Ld))', $repository_id, ipull($paths, 'pathID')); } $path_clauses = '('.implode(' OR ', $path_clauses).')'; $where[] = $path_clauses; } if ($this->authors) { $where[] = qsprintf( $conn_r, 'r.authorPHID IN (%Ls)', $this->authors); } if ($this->draftRevisions) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->draftRevisions); } if ($this->revIDs) { $where[] = qsprintf( $conn_r, 'r.id IN (%Ld)', $this->revIDs); } if ($this->repositoryPHIDs) { $where[] = qsprintf( $conn_r, 'r.repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->commitHashes) { $hash_clauses = array(); foreach ($this->commitHashes as $info) { list($type, $hash) = $info; $hash_clauses[] = qsprintf( $conn_r, '(hash_rel.type = %s AND hash_rel.hash = %s)', $type, $hash); } $hash_clauses = '('.implode(' OR ', $hash_clauses).')'; $where[] = $hash_clauses; } if ($this->phids) { $where[] = qsprintf( $conn_r, 'r.phid IN (%Ls)', $this->phids); } if ($this->branches) { $where[] = qsprintf( $conn_r, 'r.branchName in (%Ls)', $this->branches); } if ($this->arcanistProjectPHIDs) { $where[] = qsprintf( $conn_r, 'r.arcanistProjectPHID in (%Ls)', $this->arcanistProjectPHIDs); } if ($this->subscribers) { $where[] = qsprintf( $conn_r, '(sub_rel_cc.objectPHID IS NOT NULL) OR (sub_rel_reviewer.dst IS NOT NULL)'); } switch ($this->status) { case self::STATUS_ANY: break; case self::STATUS_OPEN: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', - array( - ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, - ArcanistDifferentialRevisionStatus::NEEDS_REVISION, - ArcanistDifferentialRevisionStatus::ACCEPTED, - )); + DifferentialRevisionStatus::getOpenStatuses()); break; case self::STATUS_NEEDS_REVIEW: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW, )); break; case self::STATUS_NEEDS_REVISION: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::NEEDS_REVISION, )); break; case self::STATUS_ACCEPTED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::ACCEPTED, )); break; - case self::STATUS_COMMITTED: - phlog( - "WARNING: DifferentialRevisionQuery using deprecated ". - "STATUS_COMMITTED constant. This will be removed soon. ". - "Use STATUS_CLOSED."); - // fallthrough case self::STATUS_CLOSED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', - array( - ArcanistDifferentialRevisionStatus::CLOSED, - )); + DifferentialRevisionStatus::getClosedStatuses()); break; case self::STATUS_ABANDONED: $where[] = qsprintf( $conn_r, 'r.status IN (%Ld)', array( ArcanistDifferentialRevisionStatus::ABANDONED, )); break; default: throw new Exception( "Unknown revision status filter constant '{$this->status}'!"); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } /** * @task internal */ private function buildGroupByClause($conn_r) { $join_triggers = array_merge( $this->pathIDs, $this->ccs, $this->reviewers, $this->subscribers); $needs_distinct = (count($join_triggers) > 1); if ($needs_distinct) { return 'GROUP BY r.id'; } else { return ''; } } private function loadCursorObject($id) { $results = id(new DifferentialRevisionQuery()) ->setViewer($this->getPagingViewer()) ->withIDs(array((int)$id)) ->execute(); return head($results); } protected function buildPagingClause(AphrontDatabaseConnection $conn_r) { $default = parent::buildPagingClause($conn_r); $before_id = $this->getBeforeID(); $after_id = $this->getAfterID(); if (!$before_id && !$after_id) { return $default; } if ($before_id) { $cursor = $this->loadCursorObject($before_id); } else { $cursor = $this->loadCursorObject($after_id); } if (!$cursor) { return null; } $columns = array(); switch ($this->order) { case self::ORDER_CREATED: return $default; case self::ORDER_MODIFIED: $columns[] = array( 'name' => 'r.dateModified', 'value' => $cursor->getDateModified(), 'type' => 'int', ); break; case self::ORDER_PATH_MODIFIED: $columns[] = array( 'name' => 'p.epoch', 'value' => $cursor->getDateCreated(), 'type' => 'int', ); break; } $columns[] = array( 'name' => 'r.id', 'value' => $cursor->getID(), 'type' => 'int', ); return $this->buildPagingClauseFromMultipleColumns( $conn_r, $columns, array( 'reversed' => (bool)($before_id xor $this->getReversePaging()), )); } protected function getPagingColumn() { $is_global = $this->buildingGlobalOrder; switch ($this->order) { case self::ORDER_MODIFIED: if ($is_global) { return 'dateModified'; } return 'r.dateModified'; case self::ORDER_CREATED: if ($is_global) { return 'id'; } return 'r.id'; case self::ORDER_PATH_MODIFIED: if (!$this->pathIDs) { throw new Exception( "To use ORDER_PATH_MODIFIED, you must specify withPath()."); } return 'p.epoch'; default: throw new Exception("Unknown query order constant '{$this->order}'."); } } private function loadRelationships($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $relationships = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID in (%Ld) AND relation != %s ORDER BY sequence', DifferentialRevision::RELATIONSHIP_TABLE, mpull($revisions, 'getID'), DifferentialRevision::RELATION_REVIEWER); $relationships = igroup($relationships, 'revisionID'); $type_reviewer = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withEdgeTypes(array($type_reviewer)) ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) ->execute(); foreach ($revisions as $revision) { $data = idx($relationships, $revision->getID(), array()); $revision_edges = $edges[$revision->getPHID()][$type_reviewer]; foreach ($revision_edges as $dst_phid => $edge_data) { $data[] = array( 'relation' => DifferentialRevision::RELATION_REVIEWER, 'objectPHID' => $dst_phid, 'reasonPHID' => null, ); } $revision->attachRelationships($data); } } private function loadCommitPHIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $commit_phids = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', DifferentialRevision::TABLE_COMMIT, mpull($revisions, 'getID')); $commit_phids = igroup($commit_phids, 'revisionID'); foreach ($revisions as $revision) { $phids = idx($commit_phids, $revision->getID(), array()); $phids = ipull($phids, 'commitPHID'); $revision->attachCommitPHIDs($phids); } } private function loadDiffIDs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $diff_ids = queryfx_all( $conn_r, 'SELECT revisionID, id FROM %T WHERE revisionID IN (%Ld) ORDER BY id DESC', $diff_table->getTableName(), mpull($revisions, 'getID')); $diff_ids = igroup($diff_ids, 'revisionID'); foreach ($revisions as $revision) { $ids = idx($diff_ids, $revision->getID(), array()); $ids = ipull($ids, 'id'); $revision->attachDiffIDs($ids); } } private function loadActiveDiffs($conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $diff_table = new DifferentialDiff(); $load_ids = array(); foreach ($revisions as $revision) { $diffs = $revision->getDiffIDs(); if ($diffs) { $load_ids[] = max($diffs); } } $active_diffs = array(); if ($load_ids) { $active_diffs = $diff_table->loadAllWhere( 'id IN (%Ld)', $load_ids); } $active_diffs = mpull($active_diffs, null, 'getRevisionID'); foreach ($revisions as $revision) { $revision->attachActiveDiff(idx($active_diffs, $revision->getID())); } } private function loadHashes( AphrontDatabaseConnection $conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE revisionID IN (%Ld)', 'differential_revisionhash', mpull($revisions, 'getID')); $data = igroup($data, 'revisionID'); foreach ($revisions as $revision) { $hashes = idx($data, $revision->getID(), array()); $list = array(); foreach ($hashes as $hash) { $list[] = array($hash['type'], $hash['hash']); } $revision->attachHashes($list); } } private function loadReviewers( AphrontDatabaseConnection $conn_r, array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($revisions, 'getPHID')) ->withEdgeTypes(array($edge_type)) ->needEdgeData(true) ->setOrder(PhabricatorEdgeQuery::ORDER_OLDEST_FIRST) ->execute(); $viewer = $this->getViewer(); $viewer_phid = $viewer->getPHID(); // Figure out which of these reviewers the viewer has authority to act as. if ($this->needReviewerAuthority && $viewer_phid) { $allow_key = 'differential.allow-self-accept'; $allow_self = PhabricatorEnv::getEnvConfig($allow_key); $authority = $this->loadReviewerAuthority( $revisions, $edges, $allow_self); } foreach ($revisions as $revision) { $revision_edges = $edges[$revision->getPHID()][$edge_type]; $reviewers = array(); foreach ($revision_edges as $reviewer_phid => $edge) { $reviewer = new DifferentialReviewer($reviewer_phid, $edge['data']); if ($this->needReviewerAuthority) { if (!$viewer_phid) { // Logged-out users never have authority. $has_authority = false; } else if ((!$allow_self) && ($revision->getAuthorPHID() == $viewer_phid)) { // The author can never have authority unless we allow self-accept. $has_authority = false; } else { // Otherwise, look up whether th viewer has authority. $has_authority = isset($authority[$reviewer_phid]); } $reviewer->attachAuthority($viewer, $has_authority); } $reviewers[$reviewer_phid] = $reviewer; } $revision->attachReviewerStatus($reviewers); } } public static function splitResponsible(array $revisions, array $user_phids) { $blocking = array(); $active = array(); $waiting = array(); $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; // Bucket revisions into $blocking (revisions where you are blocking // others), $active (revisions you need to do something about) and $waiting // (revisions you're waiting on someone else to do something about). foreach ($revisions as $revision) { $needs_review = ($revision->getStatus() == $status_review); $filter_is_author = in_array($revision->getAuthorPHID(), $user_phids); if (!$revision->getReviewers()) { $needs_review = false; } // If exactly one of "needs review" and "the user is the author" is // true, the user needs to act on it. Otherwise, they're waiting on // it. if ($needs_review ^ $filter_is_author) { if ($needs_review) { array_unshift($blocking, $revision); } else { $active[] = $revision; } } else { $waiting[] = $revision; } } return array($blocking, $active, $waiting); } private function loadReviewerAuthority( array $revisions, array $edges, $allow_self) { $revision_map = mpull($revisions, null, 'getPHID'); $viewer_phid = $this->getViewer()->getPHID(); // Find all the project reviewers which the user may have authority over. $project_phids = array(); $project_type = PhabricatorProjectPHIDTypeProject::TYPECONST; $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER; foreach ($edges as $src => $types) { if (!$allow_self) { if ($revision_map[$src]->getAuthorPHID() == $viewer_phid) { // If self-review isn't permitted, the user will never have // authority over projects on revisions they authored because you // can't accept your own revisions, so we don't need to load any // data about these reviewers. continue; } } $edge_data = idx($types, $edge_type, array()); foreach ($edge_data as $dst => $data) { if (phid_get_type($dst) == $project_type) { $project_phids[] = $dst; } } } // Now, figure out which of these projects the viewer is actually a // member of. $project_authority = array(); if ($project_phids) { $project_authority = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->withMemberPHIDs(array($viewer_phid)) ->execute(); $project_authority = mpull($project_authority, 'getPHID'); } // Finally, the viewer has authority over themselves. return array( $viewer_phid => true, ) + array_fuse($project_authority); } public function getQueryApplicationClass() { return 'PhabricatorApplicationDifferential'; } } diff --git a/src/applications/differential/search/DifferentialSearchIndexer.php b/src/applications/differential/search/DifferentialSearchIndexer.php index 01de4dc2e9..a371b6e4e6 100644 --- a/src/applications/differential/search/DifferentialSearchIndexer.php +++ b/src/applications/differential/search/DifferentialSearchIndexer.php @@ -1,109 +1,108 @@ loadDocumentByPHID($phid); $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($rev->getPHID()); $doc->setDocumentType(DifferentialPHIDTypeRevision::TYPECONST); $doc->setDocumentTitle($rev->getTitle()); $doc->setDocumentCreated($rev->getDateCreated()); $doc->setDocumentModified($rev->getDateModified()); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setUser(PhabricatorUser::getOmnipotentUser()); if (!$aux_field->shouldAddToSearchIndex()) { unset($aux_fields[$key]); } } $aux_fields = DifferentialAuxiliaryField::loadFromStorage( $rev, $aux_fields); foreach ($aux_fields as $aux_field) { $doc->addField( $aux_field->getKeyForSearchIndex(), $aux_field->getValueForSearchIndex()); } $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $rev->getAuthorPHID(), PhabricatorPeoplePHIDTypeUser::TYPECONST, $rev->getDateCreated()); - if ($rev->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED && - $rev->getStatus() != ArcanistDifferentialRevisionStatus::ABANDONED) { + if (!$rev->isClosed()) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OPEN, $rev->getPHID(), DifferentialPHIDTypeRevision::TYPECONST, time()); } $comments = id(new DifferentialCommentQuery()) ->withRevisionIDs(array($rev->getID())) ->execute(); $inlines = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($rev->getID())) ->withNotDraft(true) ->execute(); foreach (array_merge($comments, $inlines) as $comment) { if (strlen($comment->getContent())) { $doc->addField( PhabricatorSearchField::FIELD_COMMENT, $comment->getContent()); } } $rev->loadRelationships(); // If a revision needs review, the owners are the reviewers. Otherwise, the // owner is the author (e.g., accepted, rejected, closed). if ($rev->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) { foreach ($rev->getReviewers() as $phid) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OWNER, $phid, PhabricatorPeoplePHIDTypeUser::TYPECONST, $rev->getDateModified()); // Bogus timestamp. } } else { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_OWNER, $rev->getAuthorPHID(), PhabricatorPeoplePHIDTypeUser::TYPECONST, $rev->getDateCreated()); } $ccphids = $rev->getCCPHIDs(); $handles = id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($ccphids) ->execute(); foreach ($handles as $phid => $handle) { $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, $phid, $handle->getType(), $rev->getDateModified()); // Bogus timestamp. } return $doc; } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 350040e773..ba7870dda6 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,410 +1,415 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationDifferential')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialCapabilityDefaultView::CAPABILITY); return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRelationships(array()) ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialPHIDTypeRevision::TYPECONST); } public function loadComments() { if (!$this->getID()) { return array(); } return id(new DifferentialCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function delete() { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $diff->delete(); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::RELATIONSHIP_TABLE, $this->getID()); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $comments = id(new DifferentialCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($comments as $comment) { $comment->delete(); } $inlines = id(new DifferentialInlineCommentQuery()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($inlines as $inline) { $inline->delete(); } $fields = id(new DifferentialAuxiliaryField())->loadAllWhere( 'revisionPHID = %s', $this->getPHID()); foreach ($fields as $field) { $field->delete(); } // we have to do paths a little differentally as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function loadRelationships() { if (!$this->getID()) { $this->relationships = array(); return; } // Read "subscribed" and "unsubscribed" data out of the old relationship // table. $data = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE revisionID = %d AND relation != %s ORDER BY sequence', self::RELATIONSHIP_TABLE, $this->getID(), self::RELATION_REVIEWER); // Read "reviewer" data out of the new table. $reviewer_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_REVIEWER); $reviewer_phids = array_reverse($reviewer_phids); foreach ($reviewer_phids as $phid) { $data[] = array( 'relation' => self::RELATION_REVIEWER, 'objectPHID' => $phid, 'reasonPHID' => null, ); } return $this->attachRelationships($data); } public function attachRelationships(array $relationships) { $this->relationships = igroup($relationships, 'relation'); return $this; } public function getReviewers() { return $this->getRelatedPHIDs(self::RELATION_REVIEWER); } public function getCCPHIDs() { return $this->getRelatedPHIDs(self::RELATION_SUBSCRIBED); } private function getRelatedPHIDs($relation) { $this->assertAttached($this->relationships); return ipull($this->getRawRelations($relation), 'objectPHID'); } public function getRawRelations($relation) { return idx($this->relationships, $relation, array()); } public function loadUnsubscribedPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); } public function getPrimaryReviewer() { $reviewers = $this->getReviewers(); $last = $this->lastReviewerPHID; if (!$last || !in_array($last, $reviewers)) { return head($this->getReviewers()); } return $last; } public function loadReviewedBy() { $reviewer = null; if ($this->status == ArcanistDifferentialRevisionStatus::ACCEPTED || $this->status == ArcanistDifferentialRevisionStatus::CLOSED) { $comments = $this->loadComments(); foreach ($comments as $comment) { $action = $comment->getAction(); if ($action == DifferentialAction::ACTION_ACCEPT) { $reviewer = $comment->getAuthorPHID(); } else if ($action == DifferentialAction::ACTION_REJECT || $action == DifferentialAction::ACTION_ABANDON || $action == DifferentialAction::ACTION_RETHINK) { $reviewer = null; } } } return $reviewer; } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( "A revision's reviewers can always view it."); $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewerStatus() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewerStatus(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $this->reviewerStatus = $reviewers; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } + + public function isClosed() { + return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); + } + } diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index bc70460b26..86bab4dbc6 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -1,259 +1,253 @@ noDataString = $no_data_string; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setFields(array $fields) { assert_instances_of($fields, 'DifferentialFieldSpecification'); $this->fields = $fields; 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 getRequiredHandlePHIDs() { $phids = array(); foreach ($this->fields as $field) { foreach ($this->revisions as $revision) { $phids[] = $field->getRequiredHandlePHIDsForRevisionList($revision); } } return array_mergev($phids); } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function loadAssets() { $user = $this->user; if (!$user) { throw new Exception("Call setUser() before loadAssets()!"); } if ($this->revisions === null) { throw new Exception("Call setRevisions() before loadAssets()!"); } $this->flags = id(new PhabricatorFlagQuery()) ->setViewer($user) ->withOwnerPHIDs(array($user->getPHID())) ->withObjectPHIDs(mpull($this->revisions, 'getPHID')) ->execute(); $this->drafts = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withIDs(mpull($this->revisions, 'getID')) ->withDraftRepliesByAuthors(array($user->getPHID())) ->execute(); return $this; } public function render() { $user = $this->user; if (!$user) { throw new Exception("Call setUser() before render()!"); } $fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh'); if ($fresh) { $fresh = PhabricatorCalendarHoliday::getNthBusinessDay( time(), -$fresh); } $stale = PhabricatorEnv::getEnvConfig('differential.days-stale'); if ($stale) { $stale = PhabricatorCalendarHoliday::getNthBusinessDay( time(), -$stale); } Javelin::initBehavior('phabricator-tooltips', array()); require_celerity_resource('aphront-tooltip-css'); $flagged = mpull($this->flags, null, 'getObjectPHID'); foreach ($this->fields as $field) { $field->setHandles($this->handles); } $list = new PHUIObjectItemListView(); $list->setCards(true); - $do_not_display_age = array( - ArcanistDifferentialRevisionStatus::CLOSED => true, - ArcanistDifferentialRevisionStatus::ABANDONED => true, - ); - foreach ($this->revisions as $revision) { $item = id(new PHUIObjectItemView()) ->setUser($user); $rev_fields = array(); $icons = array(); $phid = $revision->getPHID(); if (isset($flagged[$phid])) { $flag = $flagged[$phid]; $flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $icons['flag'] = phutil_tag( 'div', array( 'class' => 'phabricator-flag-icon '.$flag_class, ), ''); } if (array_key_exists($revision->getID(), $this->drafts)) { $icons['draft'] = true; } $modified = $revision->getDateModified(); $status = $revision->getStatus(); $show_age = ($fresh || $stale) && $this->highlightAge && - empty($do_not_display_age[$status]); - + !$revision->isClosed(); $object_age = PHUIObjectItemView::AGE_FRESH; foreach ($this->fields as $field) { if ($show_age) { if ($field instanceof DifferentialDateModifiedFieldSpecification) { if ($stale && $modified < $stale) { $object_age = PHUIObjectItemView::AGE_OLD; } else if ($fresh && $modified < $fresh) { $object_age = PHUIObjectItemView::AGE_STALE; } } } $rev_header = $field->renderHeaderForRevisionList(); $rev_fields[$rev_header] = $field ->renderValueForRevisionList($revision); } $status_name = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); if (isset($icons['flag'])) { $item->addHeadIcon($icons['flag']); } $item->setObjectName('D'.$revision->getID()); $item->setHeader(phutil_tag('a', array('href' => '/D'.$revision->getID()), $revision->getTitle())); if (isset($icons['draft'])) { $draft = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ->setSpriteIcon('file-grey') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Unsubmitted Comments'), )); $item->addAttribute($draft); } $item->addAttribute($status_name); // Author $author_handle = $this->handles[$revision->getAuthorPHID()]; $item->addByline(pht('Author: %s', $author_handle->renderLink())); // Reviewers $item->addAttribute(pht('Reviewers: %s', $rev_fields['Reviewers'])); $item->setEpoch($revision->getDateModified(), $object_age); // First remove the fields we already have $count = 7; $rev_fields = array_slice($rev_fields, $count); // Then add each one of them // TODO: Add render-to-foot-icon support foreach ($rev_fields as $header => $field) { $item->addAttribute(pht('%s: %s', $header, $field)); } switch ($status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: break; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $item->setBarColor('red'); break; case ArcanistDifferentialRevisionStatus::ACCEPTED: $item->setBarColor('green'); break; case ArcanistDifferentialRevisionStatus::CLOSED: $item->setDisabled(true); break; case ArcanistDifferentialRevisionStatus::ABANDONED: $item->setBarColor('black'); break; } $list->addItem($item); } $list->setHeader($this->header); $list->setNoDataString($this->noDataString); return $list; } public static function getDefaultFields(PhabricatorUser $user) { $selector = DifferentialFieldSelector::newSelector(); $fields = $selector->getFieldSpecifications(); foreach ($fields as $key => $field) { $field->setUser($user); if (!$field->shouldAppearOnRevisionList()) { unset($fields[$key]); } } if (!$fields) { throw new Exception( "Phabricator configuration has no fields that appear on the list ". "interface!"); } return $selector->sortFieldsForRevisionList($fields); } }