@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Change Differential revision buckets to focus on "next required action"

Summary:
Ref T10939. Ref T4144. This splits the existing buckets ("Blocking Others", "Action Required", "Waiting on Others") into 6-7 buckets with a stronger focus on what the next action you need to take is.

See T10939#175423 for some discussion.

Overall, I think some of the root problems here are caused by reviewer laziness and shotgun review workflows (where a ton of people get automatically added to everything, probably unnecessarily), but these buckets haven't been updated since the introduction of blocking reviewers or project/package reviewers and I think splitting the 3 buckets into 6 buckets isn't unreasonable, even though it's kind of a lot of buckets and the root problem here is approximately "I want to ignore a bunch of stuff on my dashboard".

I didn't remove the old bucketing code yet since it's still in use on the default homepage.

This also isn't quite right until I fix the tokenizer to work properly, since it won't bucket project/package reviewers accurately.

Test Plan: {F1395972}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4144, T10939

Differential Revision: https://secure.phabricator.com/D15924

+339 -27
+2
src/__phutil_library_map__.php
··· 3320 3320 'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php', 3321 3321 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 3322 3322 'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php', 3323 + 'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php', 3323 3324 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 3324 3325 'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php', 3325 3326 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', ··· 8015 8016 'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel', 8016 8017 'PhabricatorSearchRelationship' => 'Phobject', 8017 8018 'PhabricatorSearchResultBucket' => 'Phobject', 8019 + 'PhabricatorSearchResultBucketGroup' => 'Phobject', 8018 8020 'PhabricatorSearchResultView' => 'AphrontView', 8019 8021 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', 8020 8022 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController',
+188
src/applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php
··· 5 5 6 6 const BUCKETKEY = 'action'; 7 7 8 + private $objects; 9 + 8 10 public function getResultBucketName() { 9 11 return pht('Bucket by Required Action'); 12 + } 13 + 14 + protected function buildResultGroups( 15 + PhabricatorSavedQuery $query, 16 + array $objects) { 17 + 18 + $this->objects = $objects; 19 + 20 + $phids = $query->getParameter('responsiblePHIDs', array()); 21 + if (!$phids) { 22 + throw new Exception( 23 + pht( 24 + 'You can not bucket results by required action without '. 25 + 'specifying "Responsible Users".')); 26 + } 27 + $phids = array_fuse($phids); 28 + 29 + $groups = array(); 30 + 31 + $groups[] = $this->newGroup() 32 + ->setName(pht('Must Review')) 33 + ->setNoDataString(pht('No revisions are blocked on your review.')) 34 + ->setObjects($this->filterMustReview($phids)); 35 + 36 + $groups[] = $this->newGroup() 37 + ->setName(pht('Ready to Review')) 38 + ->setNoDataString(pht('No revisions are waiting on you to review them.')) 39 + ->setObjects($this->filterShouldReview($phids)); 40 + 41 + $groups[] = $this->newGroup() 42 + ->setName(pht('Ready to Land')) 43 + ->setNoDataString(pht('No revisions are ready to land.')) 44 + ->setObjects($this->filterShouldLand($phids)); 45 + 46 + $groups[] = $this->newGroup() 47 + ->setName(pht('Ready to Update')) 48 + ->setNoDataString(pht('No revisions are waiting for updates.')) 49 + ->setObjects($this->filterShouldUpdate($phids)); 50 + 51 + $groups[] = $this->newGroup() 52 + ->setName(pht('Waiting on Review')) 53 + ->setNoDataString(pht('None of your revisions are waiting on review.')) 54 + ->setObjects($this->filterWaitingForReview($phids)); 55 + 56 + $groups[] = $this->newGroup() 57 + ->setName(pht('Waiting on Authors')) 58 + ->setNoDataString(pht('No revisions are waiting on author action.')) 59 + ->setObjects($this->filterWaitingOnAuthors($phids)); 60 + 61 + // Because you can apply these buckets to queries which include revisions 62 + // that have been closed, add an "Other" bucket if we still have stuff 63 + // that didn't get filtered into any of the previous buckets. 64 + if ($this->objects) { 65 + $groups[] = $this->newGroup() 66 + ->setName(pht('Other Revisions')) 67 + ->setObjects($this->objects); 68 + } 69 + 70 + return $groups; 71 + } 72 + 73 + private function filterMustReview(array $phids) { 74 + $blocking = array( 75 + DifferentialReviewerStatus::STATUS_BLOCKING, 76 + DifferentialReviewerStatus::STATUS_REJECTED, 77 + DifferentialReviewerStatus::STATUS_REJECTED_OLDER, 78 + ); 79 + $blocking = array_fuse($blocking); 80 + 81 + $objects = $this->getRevisionsUnderReview($this->objects, $phids); 82 + 83 + $results = array(); 84 + foreach ($objects as $key => $object) { 85 + if (!$this->hasReviewersWithStatus($object, $phids, $blocking)) { 86 + continue; 87 + } 88 + 89 + $results[$key] = $object; 90 + unset($this->objects[$key]); 91 + } 92 + 93 + return $results; 94 + } 95 + 96 + private function filterShouldReview(array $phids) { 97 + $reviewing = array( 98 + DifferentialReviewerStatus::STATUS_ADDED, 99 + ); 100 + $reviewing = array_fuse($reviewing); 101 + 102 + $objects = $this->getRevisionsUnderReview($this->objects, $phids); 103 + 104 + $results = array(); 105 + foreach ($objects as $key => $object) { 106 + if (!$this->hasReviewersWithStatus($object, $phids, $reviewing)) { 107 + continue; 108 + } 109 + 110 + $results[$key] = $object; 111 + unset($this->objects[$key]); 112 + } 113 + 114 + return $results; 115 + } 116 + 117 + private function filterShouldLand(array $phids) { 118 + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; 119 + 120 + $objects = $this->getRevisionsAuthored($this->objects, $phids); 121 + 122 + $results = array(); 123 + foreach ($objects as $key => $object) { 124 + if ($object->getStatus() != $status_accepted) { 125 + continue; 126 + } 127 + 128 + $results[$key] = $object; 129 + unset($this->objects[$key]); 130 + } 131 + 132 + return $results; 133 + } 134 + 135 + private function filterShouldUpdate(array $phids) { 136 + $statuses = array( 137 + ArcanistDifferentialRevisionStatus::NEEDS_REVISION, 138 + ArcanistDifferentialRevisionStatus::CHANGES_PLANNED, 139 + ArcanistDifferentialRevisionStatus::IN_PREPARATION, 140 + ); 141 + $statuses = array_fuse($statuses); 142 + 143 + $objects = $this->getRevisionsAuthored($this->objects, $phids); 144 + 145 + $results = array(); 146 + foreach ($objects as $key => $object) { 147 + if (empty($statuses[$object->getStatus()])) { 148 + continue; 149 + } 150 + 151 + $results[$key] = $object; 152 + unset($this->objects[$key]); 153 + } 154 + 155 + return $results; 156 + } 157 + 158 + private function filterWaitingForReview(array $phids) { 159 + $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; 160 + 161 + $objects = $this->getRevisionsAuthored($this->objects, $phids); 162 + 163 + $results = array(); 164 + foreach ($objects as $key => $object) { 165 + if ($object->getStatus() != $status_review) { 166 + continue; 167 + } 168 + 169 + $results[$key] = $object; 170 + unset($this->objects[$key]); 171 + } 172 + 173 + return $results; 174 + } 175 + 176 + private function filterWaitingOnAuthors(array $phids) { 177 + $statuses = array( 178 + ArcanistDifferentialRevisionStatus::ACCEPTED, 179 + ArcanistDifferentialRevisionStatus::NEEDS_REVISION, 180 + ArcanistDifferentialRevisionStatus::CHANGES_PLANNED, 181 + ArcanistDifferentialRevisionStatus::IN_PREPARATION, 182 + ); 183 + $statuses = array_fuse($statuses); 184 + 185 + $objects = $this->getRevisionsNotAuthored($this->objects, $phids); 186 + 187 + $results = array(); 188 + foreach ($objects as $key => $object) { 189 + if (empty($statuses[$object->getStatus()])) { 190 + continue; 191 + } 192 + 193 + $results[$key] = $object; 194 + unset($this->objects[$key]); 195 + } 196 + 197 + return $results; 10 198 } 11 199 12 200 }
+64
src/applications/differential/query/DifferentialRevisionResultBucket.php
··· 10 10 ->execute(); 11 11 } 12 12 13 + protected function getRevisionsUnderReview(array $objects, array $phids) { 14 + $results = array(); 15 + 16 + $objects = $this->getRevisionsNotAuthored($objects, $phids); 17 + 18 + $status_review = ArcanistDifferentialRevisionStatus::NEEDS_REVIEW; 19 + foreach ($objects as $key => $object) { 20 + if ($object->getStatus() !== $status_review) { 21 + continue; 22 + } 23 + 24 + $results[$key] = $object; 25 + } 26 + 27 + return $results; 28 + } 29 + 30 + protected function getRevisionsAuthored(array $objects, array $phids) { 31 + $results = array(); 32 + 33 + foreach ($objects as $key => $object) { 34 + if (isset($phids[$object->getAuthorPHID()])) { 35 + $results[$key] = $object; 36 + } 37 + } 38 + 39 + return $results; 40 + } 41 + 42 + protected function getRevisionsNotAuthored(array $objects, array $phids) { 43 + $results = array(); 44 + 45 + foreach ($objects as $key => $object) { 46 + if (empty($phids[$object->getAuthorPHID()])) { 47 + $results[$key] = $object; 48 + } 49 + } 50 + 51 + return $results; 52 + } 53 + 54 + protected function hasReviewersWithStatus( 55 + DifferentialRevision $revision, 56 + array $phids, 57 + array $statuses) { 58 + 59 + foreach ($revision->getReviewerStatus() as $reviewer) { 60 + $reviewer_phid = $reviewer->getReviewerPHID(); 61 + if (empty($phids[$reviewer_phid])) { 62 + continue; 63 + } 64 + 65 + $status = $reviewer->getStatus(); 66 + if (empty($statuses[$status])) { 67 + continue; 68 + } 69 + 70 + return true; 71 + } 72 + 73 + return false; 74 + } 75 + 76 + 13 77 }
+14 -26
src/applications/differential/query/DifferentialRevisionSearchEngine.php
··· 19 19 return id(new DifferentialRevisionQuery()) 20 20 ->needFlags(true) 21 21 ->needDrafts(true) 22 - ->needRelationships(true); 22 + ->needRelationships(true) 23 + ->needReviewerStatus(true); 23 24 } 24 25 25 26 protected function buildQueryFromParameters(array $map) { ··· 153 154 154 155 $views = array(); 155 156 if ($bucket) { 156 - $split = DifferentialRevisionQuery::splitResponsible( 157 - $revisions, 158 - $query->getParameter('responsiblePHIDs')); 159 - list($blocking, $active, $waiting) = $split; 157 + $bucket->setViewer($viewer); 160 158 161 - $views[] = id(clone $template) 162 - ->setHeader(pht('Blocking Others')) 163 - ->setNoDataString( 164 - pht('No revisions are blocked on your action.')) 165 - ->setHighlightAge(true) 166 - ->setRevisions($blocking) 167 - ->setHandles(array()); 159 + try { 160 + $groups = $bucket->newResultGroups($query, $revisions); 168 161 169 - $views[] = id(clone $template) 170 - ->setHeader(pht('Action Required')) 171 - ->setNoDataString( 172 - pht('No revisions require your action.')) 173 - ->setHighlightAge(true) 174 - ->setRevisions($active) 175 - ->setHandles(array()); 176 - 177 - $views[] = id(clone $template) 178 - ->setHeader(pht('Waiting on Others')) 179 - ->setNoDataString( 180 - pht('You have no revisions waiting on others.')) 181 - ->setRevisions($waiting) 182 - ->setHandles(array()); 162 + foreach ($groups as $group) { 163 + $views[] = id(clone $template) 164 + ->setHeader($group->getName()) 165 + ->setNoDataString($group->getNoDataString()) 166 + ->setRevisions($group->getObjects()); 167 + } 168 + } catch (Exception $ex) { 169 + $this->addError($ex->getMessage()); 170 + } 183 171 } else { 184 172 $views[] = id(clone $template) 185 173 ->setRevisions($revisions)
+23
src/applications/search/buckets/PhabricatorSearchResultBucket.php
··· 3 3 abstract class PhabricatorSearchResultBucket 4 4 extends Phobject { 5 5 6 + private $viewer; 6 7 private $pageSize; 7 8 8 9 final public function setPageSize($page_size) { ··· 18 19 return $this->pageSize; 19 20 } 20 21 22 + public function setViewer(PhabricatorUser $viewer) { 23 + $this->viewer = $viewer; 24 + return $this; 25 + } 26 + 27 + public function getViewer() { 28 + return $this->viewer; 29 + } 30 + 21 31 protected function getDefaultPageSize() { 22 32 return 1000; 23 33 } 24 34 25 35 abstract public function getResultBucketName(); 36 + abstract protected function buildResultGroups( 37 + PhabricatorSavedQuery $query, 38 + array $objects); 39 + 40 + final public function newResultGroups( 41 + PhabricatorSavedQuery $query, 42 + array $objects) { 43 + return $this->buildResultGroups($query, $objects); 44 + } 26 45 27 46 final public function getResultBucketKey() { 28 47 return $this->getPhobjectClassConstant('BUCKETKEY'); 48 + } 49 + 50 + final protected function newGroup() { 51 + return new PhabricatorSearchResultBucketGroup(); 29 52 } 30 53 31 54 }
+37
src/applications/search/buckets/PhabricatorSearchResultBucketGroup.php
··· 1 + <?php 2 + 3 + final class PhabricatorSearchResultBucketGroup 4 + extends Phobject { 5 + 6 + private $name; 7 + private $noDataString; 8 + private $objects; 9 + 10 + public function setNoDataString($no_data_string) { 11 + $this->noDataString = $no_data_string; 12 + return $this; 13 + } 14 + 15 + public function getNoDataString() { 16 + return $this->noDataString; 17 + } 18 + 19 + public function setName($name) { 20 + $this->name = $name; 21 + return $this; 22 + } 23 + 24 + public function getName() { 25 + return $this->name; 26 + } 27 + 28 + public function setObjects(array $objects) { 29 + $this->objects = $objects; 30 + return $this; 31 + } 32 + 33 + public function getObjects() { 34 + return $this->objects; 35 + } 36 + 37 + }
+11 -1
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 213 213 214 214 215 215 if ($run_query) { 216 + $exec_errors = array(); 217 + 216 218 $box->setAnchor( 217 219 id(new PhabricatorAnchorView()) 218 220 ->setAnchorName('R')); ··· 280 282 } 281 283 } 282 284 } catch (PhabricatorTypeaheadInvalidTokenException $ex) { 283 - $errors[] = pht( 285 + $exec_errors[] = pht( 284 286 'This query specifies an invalid parameter. Review the '. 285 287 'query parameters and correct errors.'); 286 288 } 289 + 290 + // The engine may have encountered additional errors during rendering; 291 + // merge them in and show everything. 292 + foreach ($engine->getErrors() as $error) { 293 + $exec_errors[] = $error; 294 + } 295 + 296 + $errors = $exec_errors; 287 297 } 288 298 289 299 if ($errors) {