@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.

Convert Maniphest merge operations to modern Relationship code

Summary:
Ref T4788. Fixes T7820. This updates the "Merge Duplicates In" interaction, and adds a "Close as Duplicate" action.

These are the last interactions that were using the old code, so it removes that code.

Merges are now recorded as real edges, so we can show them in the UI later on (originally from T9390, etc).

Also provides more general support for relationships which need EDIT permission, not-undoable relationships like merges, preventing relating an object to itself, and relationship side effects like merges.

Finally, fixes a couple of behaviors around typing an exact object name (like `T123`) to find the related object.

Test Plan:
- Merged tasks into the current task.
- Closed the current task as a duplicate of another task.
- Edited other relationships.
- Searched for tasks, commits, etc., by object monogram.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4788, T7820

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

+336 -440
+8 -4
src/__phutil_library_map__.php
··· 1420 1420 'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php', 1421 1421 'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php', 1422 1422 'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php', 1423 + 'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php', 1423 1424 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 1424 1425 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 1425 1426 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', ··· 1430 1431 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 1431 1432 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 1432 1433 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', 1434 + 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', 1433 1435 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', 1434 1436 'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php', 1435 1437 'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php', ··· 1438 1440 'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php', 1439 1441 'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php', 1440 1442 'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php', 1443 + 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php', 1441 1444 'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php', 1442 1445 'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php', 1443 1446 'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php', 1444 1447 'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php', 1448 + 'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php', 1445 1449 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 1446 1450 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 1447 1451 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', ··· 3372 3376 'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php', 3373 3377 'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php', 3374 3378 'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php', 3375 - 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', 3376 3379 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 3377 3380 'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php', 3378 3381 'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php', ··· 3415 3418 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 3416 3419 'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php', 3417 3420 'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php', 3418 - 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', 3419 3421 'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php', 3420 3422 'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', 3421 3423 'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php', ··· 5929 5931 'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField', 5930 5932 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 5931 5933 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', 5934 + 'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship', 5932 5935 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 5933 5936 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 5934 5937 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', ··· 5939 5942 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 5940 5943 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 5941 5944 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', 5945 + 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', 5942 5946 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', 5943 5947 'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship', 5944 5948 'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship', ··· 5947 5951 'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship', 5948 5952 'ManiphestTaskHeraldField' => 'HeraldField', 5949 5953 'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup', 5954 + 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType', 5950 5955 'ManiphestTaskListController' => 'ManiphestController', 5951 5956 'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType', 5952 5957 'ManiphestTaskListView' => 'ManiphestView', 5953 5958 'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver', 5959 + 'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship', 5954 5960 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 5955 5961 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 5956 5962 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', ··· 8210 8216 'PhabricatorSearchApplication' => 'PhabricatorApplication', 8211 8217 'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine', 8212 8218 'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 8213 - 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 8214 8219 'PhabricatorSearchBaseController' => 'PhabricatorController', 8215 8220 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', 8216 8221 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', ··· 8253 8258 'PhabricatorSearchResultView' => 'AphrontView', 8254 8259 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', 8255 8260 'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', 8256 - 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', 8257 8261 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', 8258 8262 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 8259 8263 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField',
+7 -6
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 201 201 202 202 $parent_key = ManiphestTaskHasParentRelationship::RELATIONSHIPKEY; 203 203 $subtask_key = ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY; 204 + $merge_key = ManiphestTaskMergeInRelationship::RELATIONSHIPKEY; 205 + $close_key = ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY; 204 206 205 207 $task_submenu[] = $relationship_list->getRelationship($parent_key) 206 208 ->newAction($task); ··· 208 210 $task_submenu[] = $relationship_list->getRelationship($subtask_key) 209 211 ->newAction($task); 210 212 211 - $task_submenu[] = id(new PhabricatorActionView()) 212 - ->setName(pht('Merge Duplicates In')) 213 - ->setHref("/search/attach/{$phid}/TASK/merge/") 214 - ->setIcon('fa-compress') 215 - ->setDisabled(!$can_edit) 216 - ->setWorkflow(true); 213 + $task_submenu[] = $relationship_list->getRelationship($merge_key) 214 + ->newAction($task); 215 + 216 + $task_submenu[] = $relationship_list->getRelationship($close_key) 217 + ->newAction($task); 217 218 218 219 $curtain->addAction( 219 220 id(new PhabricatorActionView())
+16
src/applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskHasDuplicateTaskEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 62; 7 + 8 + public function getInverseEdgeConstant() { 9 + return ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST; 10 + } 11 + 12 + public function shouldWriteInverseTransactions() { 13 + return true; 14 + } 15 + 16 + }
+16
src/applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskIsDuplicateOfTaskEdgeType 4 + extends PhabricatorEdgeType { 5 + 6 + const EDGECONST = 63; 7 + 8 + public function getInverseEdgeConstant() { 9 + return ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST; 10 + } 11 + 12 + public function shouldWriteInverseTransactions() { 13 + return true; 14 + } 15 + 16 + }
+84
src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskCloseAsDuplicateRelationship 4 + extends ManiphestTaskRelationship { 5 + 6 + const RELATIONSHIPKEY = 'task.close-as-duplicate'; 7 + 8 + public function getEdgeConstant() { 9 + return ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST; 10 + } 11 + 12 + protected function getActionName() { 13 + return pht('Close As Duplicate'); 14 + } 15 + 16 + protected function getActionIcon() { 17 + return 'fa-times'; 18 + } 19 + 20 + public function canRelateObjects($src, $dst) { 21 + return ($dst instanceof ManiphestTask); 22 + } 23 + 24 + public function shouldAppearInActionMenu() { 25 + return false; 26 + } 27 + 28 + public function getDialogTitleText() { 29 + return pht('Close As Duplicate'); 30 + } 31 + 32 + public function getDialogHeaderText() { 33 + return pht('Close This Task As a Duplicate Of'); 34 + } 35 + 36 + public function getDialogButtonText() { 37 + return pht('Merge Into Selected Task'); 38 + } 39 + 40 + protected function newRelationshipSource() { 41 + return new ManiphestTaskRelationshipSource(); 42 + } 43 + 44 + public function getRequiredRelationshipCapabilities() { 45 + return array( 46 + PhabricatorPolicyCapability::CAN_VIEW, 47 + PhabricatorPolicyCapability::CAN_EDIT, 48 + ); 49 + } 50 + 51 + public function canUndoRelationship() { 52 + return false; 53 + } 54 + 55 + public function willUpdateRelationships($object, array $add, array $rem) { 56 + 57 + // TODO: Communicate this in the UI before users hit this error. 58 + if (count($add) > 1) { 59 + throw new Exception( 60 + pht( 61 + 'A task can only be closed as a duplicate of exactly one other '. 62 + 'task.')); 63 + } 64 + 65 + $task = head($add); 66 + return $this->newMergeIntoTransactions($task); 67 + } 68 + 69 + public function didUpdateRelationships($object, array $add, array $rem) { 70 + $viewer = $this->getViewer(); 71 + $content_source = $this->getContentSource(); 72 + 73 + $task = head($add); 74 + $xactions = $this->newMergeFromTransactions(array($object)); 75 + 76 + $task->getApplicationTransactionEditor() 77 + ->setActor($viewer) 78 + ->setContentSource($content_source) 79 + ->setContinueOnMissingFields(true) 80 + ->setContinueOnNoEffect(true) 81 + ->applyTransactions($task, $xactions); 82 + } 83 + 84 + }
+75
src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php
··· 1 + <?php 2 + 3 + final class ManiphestTaskMergeInRelationship 4 + extends ManiphestTaskRelationship { 5 + 6 + const RELATIONSHIPKEY = 'task.merge-in'; 7 + 8 + public function getEdgeConstant() { 9 + return ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST; 10 + } 11 + 12 + protected function getActionName() { 13 + return pht('Merge Duplicates In'); 14 + } 15 + 16 + protected function getActionIcon() { 17 + return 'fa-compress'; 18 + } 19 + 20 + public function canRelateObjects($src, $dst) { 21 + return ($dst instanceof ManiphestTask); 22 + } 23 + 24 + public function shouldAppearInActionMenu() { 25 + return false; 26 + } 27 + 28 + public function getDialogTitleText() { 29 + return pht('Merge Duplicates Into This Task'); 30 + } 31 + 32 + public function getDialogHeaderText() { 33 + return pht('Tasks to Close and Merge'); 34 + } 35 + 36 + public function getDialogButtonText() { 37 + return pht('Close and Merge Selected Tasks'); 38 + } 39 + 40 + protected function newRelationshipSource() { 41 + return new ManiphestTaskRelationshipSource(); 42 + } 43 + 44 + public function getRequiredRelationshipCapabilities() { 45 + return array( 46 + PhabricatorPolicyCapability::CAN_VIEW, 47 + PhabricatorPolicyCapability::CAN_EDIT, 48 + ); 49 + } 50 + 51 + public function canUndoRelationship() { 52 + return false; 53 + } 54 + 55 + public function willUpdateRelationships($object, array $add, array $rem) { 56 + return $this->newMergeFromTransactions($add); 57 + } 58 + 59 + public function didUpdateRelationships($object, array $add, array $rem) { 60 + $viewer = $this->getViewer(); 61 + $content_source = $this->getContentSource(); 62 + 63 + foreach ($add as $task) { 64 + $xactions = $this->newMergeIntoTransactions($object); 65 + 66 + $task->getApplicationTransactionEditor() 67 + ->setActor($viewer) 68 + ->setContentSource($content_source) 69 + ->setContinueOnMissingFields(true) 70 + ->setContinueOnNoEffect(true) 71 + ->applyTransactions($task, $xactions); 72 + } 73 + } 74 + 75 + }
+48
src/applications/maniphest/relationship/ManiphestTaskRelationship.php
··· 16 16 return ($object instanceof ManiphestTask); 17 17 } 18 18 19 + protected function newMergeIntoTransactions(ManiphestTask $task) { 20 + return array( 21 + id(new ManiphestTransaction()) 22 + ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) 23 + ->setNewValue($task->getPHID()), 24 + ); 25 + } 26 + 27 + protected function newMergeFromTransactions(array $tasks) { 28 + $xactions = array(); 29 + 30 + $subscriber_phids = $this->loadMergeSubscriberPHIDs($tasks); 31 + 32 + $xactions[] = id(new ManiphestTransaction()) 33 + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 34 + ->setNewValue(array('+' => $subscriber_phids)); 35 + 36 + $xactions[] = id(new ManiphestTransaction()) 37 + ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) 38 + ->setNewValue(mpull($tasks, 'getPHID')); 39 + 40 + return $xactions; 41 + } 42 + 43 + private function loadMergeSubscriberPHIDs(array $tasks) { 44 + $phids = array(); 45 + 46 + foreach ($tasks as $task) { 47 + $phids[] = $task->getAuthorPHID(); 48 + $phids[] = $task->getOwnerPHID(); 49 + } 50 + 51 + $subscribers = id(new PhabricatorSubscribersQuery()) 52 + ->withObjectPHIDs(mpull($tasks, 'getPHID')) 53 + ->execute(); 54 + 55 + foreach ($subscribers as $phid => $subscriber_list) { 56 + foreach ($subscriber_list as $subscriber) { 57 + $phids[] = $subscriber; 58 + } 59 + } 60 + 61 + $phids = array_unique($phids); 62 + $phids = array_filter($phids); 63 + 64 + return $phids; 65 + } 66 + 19 67 }
-4
src/applications/search/application/PhabricatorSearchApplication.php
··· 30 30 return array( 31 31 '/search/' => array( 32 32 '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSearchController', 33 - 'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?' 34 - => 'PhabricatorSearchAttachController', 35 - 'select/(?P<type>\w+)/(?:(?P<action>\w+)/)?' 36 - => 'PhabricatorSearchSelectController', 37 33 'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController', 38 34 'hovercard/' 39 35 => 'PhabricatorSearchHovercardController',
-330
src/applications/search/controller/PhabricatorSearchAttachController.php
··· 1 - <?php 2 - 3 - final class PhabricatorSearchAttachController 4 - extends PhabricatorSearchBaseController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $user = $request->getUser(); 8 - $phid = $request->getURIData('phid'); 9 - $attach_type = $request->getURIData('type'); 10 - $action = $request->getURIData('action', self::ACTION_ATTACH); 11 - 12 - $handle = id(new PhabricatorHandleQuery()) 13 - ->setViewer($user) 14 - ->withPHIDs(array($phid)) 15 - ->executeOne(); 16 - 17 - $object_type = $handle->getType(); 18 - 19 - $object = id(new PhabricatorObjectQuery()) 20 - ->setViewer($user) 21 - ->requireCapabilities( 22 - array( 23 - PhabricatorPolicyCapability::CAN_VIEW, 24 - PhabricatorPolicyCapability::CAN_EDIT, 25 - )) 26 - ->withPHIDs(array($phid)) 27 - ->executeOne(); 28 - 29 - if (!$object) { 30 - return new Aphront404Response(); 31 - } 32 - 33 - $edge_type = null; 34 - switch ($action) { 35 - case self::ACTION_EDGE: 36 - case self::ACTION_DEPENDENCIES: 37 - case self::ACTION_BLOCKS: 38 - case self::ACTION_ATTACH: 39 - $edge_type = $this->getEdgeType($object_type, $attach_type); 40 - break; 41 - } 42 - 43 - if ($request->isFormPost()) { 44 - $phids = explode(';', $request->getStr('phids')); 45 - $phids = array_filter($phids); 46 - $phids = array_values($phids); 47 - 48 - if ($edge_type) { 49 - if (!$object instanceof PhabricatorApplicationTransactionInterface) { 50 - throw new Exception( 51 - pht( 52 - 'Expected object ("%s") to implement interface "%s".', 53 - get_class($object), 54 - 'PhabricatorApplicationTransactionInterface')); 55 - } 56 - 57 - $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 58 - $phid, 59 - $edge_type); 60 - $add_phids = $phids; 61 - $rem_phids = array_diff($old_phids, $add_phids); 62 - 63 - $txn_editor = $object->getApplicationTransactionEditor() 64 - ->setActor($user) 65 - ->setContentSourceFromRequest($request) 66 - ->setContinueOnMissingFields(true) 67 - ->setContinueOnNoEffect(true); 68 - 69 - $txn_template = $object->getApplicationTransactionTemplate() 70 - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 71 - ->setMetadataValue('edge:type', $edge_type) 72 - ->setNewValue(array( 73 - '+' => array_fuse($add_phids), 74 - '-' => array_fuse($rem_phids), 75 - )); 76 - 77 - try { 78 - $txn_editor->applyTransactions( 79 - $object->getApplicationTransactionObject(), 80 - array($txn_template)); 81 - } catch (PhabricatorEdgeCycleException $ex) { 82 - $this->raiseGraphCycleException($ex); 83 - } 84 - 85 - return id(new AphrontReloadResponse())->setURI($handle->getURI()); 86 - } else { 87 - return $this->performMerge($object, $handle, $phids); 88 - } 89 - } else { 90 - if ($edge_type) { 91 - $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 92 - $phid, 93 - $edge_type); 94 - } else { 95 - // This is a merge. 96 - $phids = array(); 97 - } 98 - } 99 - 100 - $strings = $this->getStrings($attach_type, $action); 101 - 102 - $handles = $this->loadViewerHandles($phids); 103 - 104 - $obj_dialog = new PhabricatorObjectSelectorDialog(); 105 - $obj_dialog 106 - ->setUser($user) 107 - ->setHandles($handles) 108 - ->setFilters($this->getFilters($strings, $attach_type)) 109 - ->setSelectedFilter($strings['selected']) 110 - ->setExcluded($phid) 111 - ->setCancelURI($handle->getURI()) 112 - ->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/') 113 - ->setTitle($strings['title']) 114 - ->setHeader($strings['header']) 115 - ->setButtonText($strings['button']) 116 - ->setInstructions($strings['instructions']); 117 - 118 - $dialog = $obj_dialog->buildDialog(); 119 - 120 - return id(new AphrontDialogResponse())->setDialog($dialog); 121 - } 122 - 123 - private function performMerge( 124 - ManiphestTask $task, 125 - PhabricatorObjectHandle $handle, 126 - array $phids) { 127 - 128 - $user = $this->getRequest()->getUser(); 129 - $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); 130 - 131 - $phids = array_fill_keys($phids, true); 132 - unset($phids[$task->getPHID()]); // Prevent merging a task into itself. 133 - 134 - if (!$phids) { 135 - return $response; 136 - } 137 - 138 - $targets = id(new ManiphestTaskQuery()) 139 - ->setViewer($user) 140 - ->requireCapabilities( 141 - array( 142 - PhabricatorPolicyCapability::CAN_VIEW, 143 - PhabricatorPolicyCapability::CAN_EDIT, 144 - )) 145 - ->withPHIDs(array_keys($phids)) 146 - ->needSubscriberPHIDs(true) 147 - ->needProjectPHIDs(true) 148 - ->execute(); 149 - 150 - if (empty($targets)) { 151 - return $response; 152 - } 153 - 154 - $editor = id(new ManiphestTransactionEditor()) 155 - ->setActor($user) 156 - ->setContentSourceFromRequest($this->getRequest()) 157 - ->setContinueOnNoEffect(true) 158 - ->setContinueOnMissingFields(true); 159 - 160 - $cc_vector = array(); 161 - // since we loaded this via a generic object query, go ahead and get the 162 - // attach the subscriber and project phids now 163 - $task->attachSubscriberPHIDs( 164 - PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID())); 165 - $task->attachProjectPHIDs( 166 - PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(), 167 - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)); 168 - 169 - $cc_vector[] = $task->getSubscriberPHIDs(); 170 - foreach ($targets as $target) { 171 - $cc_vector[] = $target->getSubscriberPHIDs(); 172 - $cc_vector[] = array( 173 - $target->getAuthorPHID(), 174 - $target->getOwnerPHID(), 175 - ); 176 - 177 - $merged_into_txn = id(new ManiphestTransaction()) 178 - ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) 179 - ->setNewValue($task->getPHID()); 180 - 181 - $editor->applyTransactions( 182 - $target, 183 - array($merged_into_txn)); 184 - 185 - } 186 - $all_ccs = array_mergev($cc_vector); 187 - $all_ccs = array_filter($all_ccs); 188 - $all_ccs = array_unique($all_ccs); 189 - 190 - $add_ccs = id(new ManiphestTransaction()) 191 - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) 192 - ->setNewValue(array('=' => $all_ccs)); 193 - 194 - $merged_from_txn = id(new ManiphestTransaction()) 195 - ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) 196 - ->setNewValue(mpull($targets, 'getPHID')); 197 - 198 - $editor->applyTransactions( 199 - $task, 200 - array($add_ccs, $merged_from_txn)); 201 - 202 - return $response; 203 - } 204 - 205 - private function getStrings($attach_type, $action) { 206 - switch ($attach_type) { 207 - case DifferentialRevisionPHIDType::TYPECONST: 208 - $noun = pht('Revisions'); 209 - $selected = pht('created'); 210 - break; 211 - case ManiphestTaskPHIDType::TYPECONST: 212 - $noun = pht('Tasks'); 213 - $selected = pht('assigned'); 214 - break; 215 - case PhabricatorRepositoryCommitPHIDType::TYPECONST: 216 - $noun = pht('Commits'); 217 - $selected = pht('created'); 218 - break; 219 - case PholioMockPHIDType::TYPECONST: 220 - $noun = pht('Mocks'); 221 - $selected = pht('created'); 222 - break; 223 - } 224 - 225 - switch ($action) { 226 - case self::ACTION_EDGE: 227 - case self::ACTION_ATTACH: 228 - $dialog_title = pht('Manage Attached %s', $noun); 229 - $header_text = pht('Currently Attached %s', $noun); 230 - $button_text = pht('Save %s', $noun); 231 - $instructions = null; 232 - break; 233 - case self::ACTION_MERGE: 234 - $dialog_title = pht('Merge Duplicate Tasks'); 235 - $header_text = pht('Tasks To Merge'); 236 - $button_text = pht('Merge %s', $noun); 237 - $instructions = pht( 238 - 'These tasks will be merged into the current task and then closed. '. 239 - 'The current task will grow stronger.'); 240 - break; 241 - case self::ACTION_DEPENDENCIES: 242 - $dialog_title = pht('Edit Dependencies'); 243 - $header_text = pht('Current Dependencies'); 244 - $button_text = pht('Save Dependencies'); 245 - $instructions = null; 246 - break; 247 - case self::ACTION_BLOCKS: 248 - $dialog_title = pht('Edit Blocking Tasks'); 249 - $header_text = pht('Current Blocking Tasks'); 250 - $button_text = pht('Save Blocking Tasks'); 251 - $instructions = null; 252 - break; 253 - } 254 - 255 - return array( 256 - 'target_plural_noun' => $noun, 257 - 'selected' => $selected, 258 - 'title' => $dialog_title, 259 - 'header' => $header_text, 260 - 'button' => $button_text, 261 - 'instructions' => $instructions, 262 - ); 263 - } 264 - 265 - private function getFilters(array $strings, $attach_type) { 266 - if ($attach_type == PholioMockPHIDType::TYPECONST) { 267 - $filters = array( 268 - 'created' => pht('Created By Me'), 269 - 'all' => pht('All %s', $strings['target_plural_noun']), 270 - ); 271 - } else { 272 - $filters = array( 273 - 'assigned' => pht('Assigned to Me'), 274 - 'created' => pht('Created By Me'), 275 - 'open' => pht('All Open %s', $strings['target_plural_noun']), 276 - 'all' => pht('All %s', $strings['target_plural_noun']), 277 - ); 278 - } 279 - 280 - return $filters; 281 - } 282 - 283 - private function getEdgeType($src_type, $dst_type) { 284 - $t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST; 285 - $t_task = ManiphestTaskPHIDType::TYPECONST; 286 - $t_drev = DifferentialRevisionPHIDType::TYPECONST; 287 - $t_mock = PholioMockPHIDType::TYPECONST; 288 - 289 - $map = array( 290 - $t_cmit => array( 291 - $t_task => DiffusionCommitHasTaskEdgeType::EDGECONST, 292 - ), 293 - $t_task => array( 294 - $t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST, 295 - $t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST, 296 - $t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST, 297 - $t_mock => ManiphestTaskHasMockEdgeType::EDGECONST, 298 - ), 299 - $t_drev => array( 300 - $t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, 301 - $t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST, 302 - ), 303 - $t_mock => array( 304 - $t_task => PholioMockHasTaskEdgeType::EDGECONST, 305 - ), 306 - ); 307 - 308 - if (empty($map[$src_type][$dst_type])) { 309 - return null; 310 - } 311 - 312 - return $map[$src_type][$dst_type]; 313 - } 314 - 315 - private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { 316 - $cycle = $ex->getCycle(); 317 - 318 - $handles = $this->loadViewerHandles($cycle); 319 - $names = array(); 320 - foreach ($cycle as $cycle_phid) { 321 - $names[] = $handles[$cycle_phid]->getFullName(); 322 - } 323 - throw new Exception( 324 - pht( 325 - 'You can not create that dependency, because it would create a '. 326 - 'circular dependency: %s.', 327 - implode(" \xE2\x86\x92 ", $names))); 328 - } 329 - 330 - }
+48 -6
src/applications/search/controller/PhabricatorSearchRelationshipController.php
··· 19 19 $src_phid = $object->getPHID(); 20 20 $edge_type = $relationship->getEdgeConstant(); 21 21 22 - $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 23 - $src_phid, 24 - $edge_type); 22 + // If this is a normal relationship, users can remove related objects. If 23 + // it's a special relationship like a merge, we can't undo it, so we won't 24 + // prefill the current related objects. 25 + if ($relationship->canUndoRelationship()) { 26 + $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( 27 + $src_phid, 28 + $edge_type); 29 + } else { 30 + $dst_phids = array(); 31 + } 25 32 26 33 $all_phids = $dst_phids; 27 34 $all_phids[] = $src_phid; ··· 44 51 // relationships at the same time don't race and overwrite one another. 45 52 $add_phids = array_diff($phids, $initial_phids); 46 53 $rem_phids = array_diff($initial_phids, $phids); 54 + $all_phids = array_merge($add_phids, $rem_phids); 47 55 48 - if ($add_phids) { 56 + $capabilities = $relationship->getRequiredRelationshipCapabilities(); 57 + 58 + if ($all_phids) { 49 59 $dst_objects = id(new PhabricatorObjectQuery()) 50 60 ->setViewer($viewer) 51 - ->withPHIDs($phids) 61 + ->withPHIDs($all_phids) 52 62 ->setRaisePolicyExceptions(true) 63 + ->requireCapabilities($capabilities) 53 64 ->execute(); 54 65 $dst_objects = mpull($dst_objects, null, 'getPHID'); 55 66 } else { ··· 67 78 $add_phid)); 68 79 } 69 80 81 + if ($add_phid == $src_phid) { 82 + throw new Exception( 83 + pht( 84 + 'You can not create a relationship to object "%s" because '. 85 + 'objects can not be related to themselves.', 86 + $add_phid)); 87 + } 88 + 70 89 if (!$relationship->canRelateObjects($object, $dst_object)) { 71 90 throw new Exception( 72 91 pht( ··· 81 100 return $this->newUnrelatableObjectResponse($ex, $done_uri); 82 101 } 83 102 103 + $content_source = PhabricatorContentSource::newFromRequest($request); 104 + $relationship->setContentSource($content_source); 105 + 84 106 $editor = $object->getApplicationTransactionEditor() 85 107 ->setActor($viewer) 86 - ->setContentSourceFromRequest($request) 108 + ->setContentSource($content_source) 87 109 ->setContinueOnMissingFields(true) 88 110 ->setContinueOnNoEffect(true); 89 111 ··· 96 118 '-' => array_fuse($rem_phids), 97 119 )); 98 120 121 + $add_objects = array_select_keys($dst_objects, $add_phids); 122 + $rem_objects = array_select_keys($dst_objects, $rem_phids); 123 + 124 + if ($add_objects || $rem_objects) { 125 + $more_xactions = $relationship->willUpdateRelationships( 126 + $object, 127 + $add_objects, 128 + $rem_objects); 129 + foreach ($more_xactions as $xaction) { 130 + $xactions[] = $xaction; 131 + } 132 + } 133 + 99 134 try { 100 135 $editor->applyTransactions($object, $xactions); 136 + 137 + if ($add_objects || $rem_objects) { 138 + $relationship->didUpdateRelationships( 139 + $object, 140 + $add_objects, 141 + $rem_objects); 142 + } 101 143 102 144 return id(new AphrontRedirectResponse())->setURI($done_uri); 103 145 } catch (PhabricatorEdgeCycleException $ex) {
+10 -4
src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php
··· 58 58 ->execute(); 59 59 60 60 $phids = array_fill_keys(mpull($results, 'getPHID'), true); 61 - $phids += $this->queryObjectNames($query_str, $capabilities); 61 + $phids = $this->queryObjectNames($query, $capabilities) + $phids; 62 62 63 63 $phids = array_keys($phids); 64 64 $handles = $viewer->loadHandles($phids); ··· 72 72 return id(new AphrontAjaxResponse())->setContent($data); 73 73 } 74 74 75 - private function queryObjectNames($query, $capabilities) { 75 + private function queryObjectNames( 76 + PhabricatorSavedQuery $query, 77 + array $capabilities) { 78 + 76 79 $request = $this->getRequest(); 77 80 $viewer = $request->getUser(); 78 81 82 + $types = $query->getParameter('types'); 83 + $match = $query->getParameter('query'); 84 + 79 85 $objects = id(new PhabricatorObjectQuery()) 80 86 ->setViewer($viewer) 81 87 ->requireCapabilities($capabilities) 82 - ->withTypes(array($request->getURIData('type'))) 83 - ->withNames(array($query)) 88 + ->withTypes($query->getParameter('types')) 89 + ->withNames(array($match)) 84 90 ->execute(); 85 91 86 92 return mpull($objects, 'getPHID');
-86
src/applications/search/controller/PhabricatorSearchSelectController.php
··· 1 - <?php 2 - 3 - final class PhabricatorSearchSelectController 4 - extends PhabricatorSearchBaseController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $user = $request->getUser(); 8 - $type = $request->getURIData('type'); 9 - $action = $request->getURIData('action'); 10 - 11 - $query = new PhabricatorSavedQuery(); 12 - $query_str = $request->getStr('query'); 13 - 14 - $query->setEngineClassName('PhabricatorSearchApplicationSearchEngine'); 15 - $query->setParameter('query', $query_str); 16 - $query->setParameter('types', array($type)); 17 - 18 - $status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; 19 - 20 - switch ($request->getStr('filter')) { 21 - case 'assigned': 22 - $query->setParameter('ownerPHIDs', array($user->getPHID())); 23 - $query->setParameter('statuses', array($status_open)); 24 - break; 25 - case 'created'; 26 - $query->setParameter('authorPHIDs', array($user->getPHID())); 27 - // TODO - if / when we allow pholio mocks to be archived, etc 28 - // update this 29 - if ($type != PholioMockPHIDType::TYPECONST) { 30 - $query->setParameter('statuses', array($status_open)); 31 - } 32 - break; 33 - case 'open': 34 - $query->setParameter('statuses', array($status_open)); 35 - break; 36 - } 37 - 38 - $query->setParameter('excludePHIDs', array($request->getStr('exclude'))); 39 - 40 - $capabilities = array(PhabricatorPolicyCapability::CAN_VIEW); 41 - switch ($action) { 42 - case self::ACTION_MERGE: 43 - $capabilities[] = PhabricatorPolicyCapability::CAN_EDIT; 44 - break; 45 - default: 46 - break; 47 - } 48 - 49 - $results = id(new PhabricatorSearchDocumentQuery()) 50 - ->setViewer($user) 51 - ->requireObjectCapabilities($capabilities) 52 - ->withSavedQuery($query) 53 - ->setOffset(0) 54 - ->setLimit(100) 55 - ->execute(); 56 - 57 - $phids = array_fill_keys(mpull($results, 'getPHID'), true); 58 - $phids += $this->queryObjectNames($query_str, $capabilities); 59 - 60 - $phids = array_keys($phids); 61 - $handles = $this->loadViewerHandles($phids); 62 - 63 - $data = array(); 64 - foreach ($handles as $handle) { 65 - $view = new PhabricatorHandleObjectSelectorDataView($handle); 66 - $data[] = $view->renderData(); 67 - } 68 - 69 - return id(new AphrontAjaxResponse())->setContent($data); 70 - } 71 - 72 - private function queryObjectNames($query, $capabilities) { 73 - $request = $this->getRequest(); 74 - $viewer = $request->getUser(); 75 - 76 - $objects = id(new PhabricatorObjectQuery()) 77 - ->setViewer($viewer) 78 - ->requireCapabilities($capabilities) 79 - ->withTypes(array($request->getURIData('type'))) 80 - ->withNames(array($query)) 81 - ->execute(); 82 - 83 - return mpull($objects, 'getPHID'); 84 - } 85 - 86 - }
+22
src/applications/search/relationship/PhabricatorObjectRelationship.php
··· 3 3 abstract class PhabricatorObjectRelationship extends Phobject { 4 4 5 5 private $viewer; 6 + private $contentSource; 6 7 7 8 public function setViewer(PhabricatorUser $viewer) { 8 9 $this->viewer = $viewer; ··· 11 12 12 13 public function getViewer() { 13 14 return $this->viewer; 15 + } 16 + 17 + public function setContentSource(PhabricatorContentSource $content_source) { 18 + $this->contentSource = $content_source; 19 + return $this; 20 + } 21 + 22 + public function getContentSource() { 23 + return $this->contentSource; 14 24 } 15 25 16 26 final public function getRelationshipConstant() { ··· 92 102 $phid = $object->getPHID(); 93 103 $type = $this->getRelationshipConstant(); 94 104 return "/search/rel/{$type}/{$phid}/"; 105 + } 106 + 107 + public function canUndoRelationship() { 108 + return true; 109 + } 110 + 111 + public function willUpdateRelationships($object, array $add, array $rem) { 112 + return array(); 113 + } 114 + 115 + public function didUpdateRelationships($object, array $add, array $rem) { 116 + return; 95 117 } 96 118 97 119 }
+2
src/applications/transactions/storage/PhabricatorApplicationTransaction.php
··· 609 609 $edge_type = $this->getMetadataValue('edge:type'); 610 610 switch ($edge_type) { 611 611 case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: 612 + case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST: 613 + case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST: 612 614 return true; 613 615 break; 614 616 case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: