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

Extract textual object list parsing from Differential

Summary:
Ref T2222. Currently, Differential has a fairly hairy piece of logic to parse object lists, like `Reviewers: alincoln, htaft`. Extract, generalize, and cover this.

- Some of the logic can be simplified with modern ObjectQuery stuff.
- Make `@username` the formal monogram for users.
- Make `list@domain.com` the formal monogram for mailing lists.
- Add test coverage.

Test Plan:
- Ran unit tests.
- Called `differential.parsecommitmessage` with a bunch of real-world inputs and got sensible results.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2222

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

+361 -84
+3
src/__phutil_library_map__.php
··· 1771 1771 'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php', 1772 1772 'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php', 1773 1773 'PhabricatorObjectHandleStatus' => 'applications/phid/handle/const/PhabricatorObjectHandleStatus.php', 1774 + 'PhabricatorObjectListQuery' => 'applications/phid/query/PhabricatorObjectListQuery.php', 1775 + 'PhabricatorObjectListQueryTestCase' => 'applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php', 1774 1776 'PhabricatorObjectListView' => 'view/control/PhabricatorObjectListView.php', 1775 1777 'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php', 1776 1778 'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php', ··· 4545 4547 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 4546 4548 'PhabricatorObjectHandle' => 'PhabricatorPolicyInterface', 4547 4549 'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants', 4550 + 'PhabricatorObjectListQueryTestCase' => 'PhabricatorTestCase', 4548 4551 'PhabricatorObjectListView' => 'AphrontView', 4549 4552 'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver', 4550 4553 'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase',
+13 -82
src/applications/differential/field/specification/DifferentialFieldSpecification.php
··· 783 783 return $this->parseCommitMessageObjectList( 784 784 $value, 785 785 $mailables = false, 786 - $allow_partial = false, 787 - $projects = true); 786 + $allow_partial = false); 788 787 } 789 788 790 789 /** ··· 814 813 $include_mailables, 815 814 $allow_partial = false) { 816 815 817 - $value = array_unique(array_filter(preg_split('/[\s,]+/', $value))); 818 - if (!$value) { 819 - return array(); 820 - } 821 - 822 - $object_map = array(); 823 - 824 - $project_names = array(); 825 - $other_names = array(); 826 - foreach ($value as $item) { 827 - if (preg_match('/^#/', $item)) { 828 - $project_names[$item] = ltrim(phutil_utf8_strtolower($item), '#').'/'; 829 - } else { 830 - $other_names[] = $item; 831 - } 832 - } 833 - 834 - if ($project_names) { 835 - // TODO: (T603) This should probably be policy-aware, although maybe not, 836 - // since we generally don't want to destroy data and it doesn't leak 837 - // anything? 838 - $projects = id(new PhabricatorProjectQuery()) 839 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 840 - ->withPhrictionSlugs($project_names) 841 - ->execute(); 816 + $types = array( 817 + PhabricatorPeoplePHIDTypeUser::TYPECONST, 818 + PhabricatorProjectPHIDTypeProject::TYPECONST, 819 + ); 842 820 843 - $reverse_map = array_flip($project_names); 844 - foreach ($projects as $project) { 845 - $reverse_key = $project->getPhrictionSlug(); 846 - if (isset($reverse_map[$reverse_key])) { 847 - $object_map[$reverse_map[$reverse_key]] = $project->getPHID(); 848 - } 849 - } 821 + if ($include_mailables) { 822 + $types[] = PhabricatorMailingListPHIDTypeList::TYPECONST; 850 823 } 851 824 852 - if ($other_names) { 853 - $users = id(new PhabricatorUser())->loadAllWhere( 854 - '(username IN (%Ls))', 855 - $other_names); 856 - 857 - $user_map = mpull($users, 'getPHID', 'getUsername'); 858 - foreach ($user_map as $username => $phid) { 859 - // Usernames may have uppercase letters in them. Put both names in the 860 - // map so we can try the original case first, so that username *always* 861 - // works in weird edge cases where some other mailable object collides. 862 - $object_map[$username] = $phid; 863 - $object_map[strtolower($username)] = $phid; 864 - } 865 - 866 - if ($include_mailables) { 867 - $mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere( 868 - '(email IN (%Ls)) OR (name IN (%Ls))', 869 - $other_names, 870 - $other_names); 871 - $object_map += mpull($mailables, 'getPHID', 'getName'); 872 - $object_map += mpull($mailables, 'getPHID', 'getEmail'); 873 - } 874 - } 875 - 876 - $invalid = array(); 877 - $results = array(); 878 - foreach ($value as $name) { 879 - if (empty($object_map[$name])) { 880 - if (empty($object_map[phutil_utf8_strtolower($name)])) { 881 - $invalid[] = $name; 882 - } else { 883 - $results[] = $object_map[phutil_utf8_strtolower($name)]; 884 - } 885 - } else { 886 - $results[] = $object_map[$name]; 887 - } 888 - } 889 - 890 - if ($invalid && !$allow_partial) { 891 - $invalid = implode(', ', $invalid); 892 - $what = $include_mailables 893 - ? "users and mailing lists" 894 - : "users"; 895 - throw new DifferentialFieldParseException( 896 - "Commit message references nonexistent {$what}: {$invalid}."); 897 - } 898 - 899 - return array_unique($results); 825 + return id(new PhabricatorObjectListQuery()) 826 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 827 + ->setAllowPartialResults($allow_partial) 828 + ->setAllowedTypes($types) 829 + ->setObjectList($value) 830 + ->execute(); 900 831 } 901 832 902 833
+31
src/applications/mailinglists/phid/PhabricatorMailingListPHIDTypeList.php
··· 37 37 } 38 38 } 39 39 40 + public function canLoadNamedObject($name) { 41 + return preg_match('/^.+@.+/', $name); 42 + } 43 + 44 + public function loadNamedObjects( 45 + PhabricatorObjectQuery $query, 46 + array $names) { 47 + 48 + $id_map = array(); 49 + foreach ($names as $name) { 50 + // Maybe normalize these some day? 51 + $id = $name; 52 + $id_map[$id][] = $name; 53 + } 54 + 55 + $objects = id(new PhabricatorMailingListQuery()) 56 + ->setViewer($query->getViewer()) 57 + ->withEmails(array_keys($id_map)) 58 + ->execute(); 59 + 60 + $results = array(); 61 + foreach ($objects as $id => $object) { 62 + $email = $object->getEmail(); 63 + foreach (idx($id_map, $email, array()) as $name) { 64 + $results[$name] = $object; 65 + } 66 + } 67 + 68 + return $results; 69 + } 70 + 40 71 }
+26
src/applications/mailinglists/query/PhabricatorMailingListQuery.php
··· 5 5 6 6 private $phids; 7 7 private $ids; 8 + private $emails; 9 + private $names; 8 10 9 11 public function withIDs($ids) { 10 12 $this->ids = $ids; ··· 13 15 14 16 public function withPHIDs($phids) { 15 17 $this->phids = $phids; 18 + return $this; 19 + } 20 + 21 + public function withEmails(array $emails) { 22 + $this->emails = $emails; 23 + return $this; 24 + } 25 + 26 + public function withNames(array $names) { 27 + $this->names = $names; 16 28 return $this; 17 29 } 18 30 ··· 46 58 $conn_r, 47 59 'phid IN (%Ls)', 48 60 $this->phids); 61 + } 62 + 63 + if ($this->names) { 64 + $where[] = qsprintf( 65 + $conn_r, 66 + 'name IN (%Ls)', 67 + $this->names); 68 + } 69 + 70 + if ($this->emails) { 71 + $where[] = qsprintf( 72 + $conn_r, 73 + 'email IN (%Ls)', 74 + $this->emails); 49 75 } 50 76 51 77 $where[] = $this->buildPagingClause($conn_r);
+30
src/applications/people/phid/PhabricatorPeoplePHIDTypeUser.php
··· 52 52 53 53 } 54 54 55 + public function canLoadNamedObject($name) { 56 + return preg_match('/^@.+/', $name); 57 + } 58 + 59 + public function loadNamedObjects( 60 + PhabricatorObjectQuery $query, 61 + array $names) { 62 + 63 + $id_map = array(); 64 + foreach ($names as $name) { 65 + $id = substr($name, 1); 66 + $id_map[$id][] = $name; 67 + } 68 + 69 + $objects = id(new PhabricatorPeopleQuery()) 70 + ->setViewer($query->getViewer()) 71 + ->withUsernames(array_keys($id_map)) 72 + ->execute(); 73 + 74 + $results = array(); 75 + foreach ($objects as $id => $object) { 76 + $username = $object->getUsername(); 77 + foreach (idx($id_map, $username, array()) as $name) { 78 + $results[$name] = $object; 79 + } 80 + } 81 + 82 + return $results; 83 + } 84 + 55 85 }
+171
src/applications/phid/query/PhabricatorObjectListQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorObjectListQuery { 4 + 5 + private $viewer; 6 + private $objectList; 7 + private $allowedTypes = array(); 8 + private $allowPartialResults; 9 + 10 + public function setAllowPartialResults($allow_partial_results) { 11 + $this->allowPartialResults = $allow_partial_results; 12 + return $this; 13 + } 14 + 15 + public function getAllowPartialResults() { 16 + return $this->allowPartialResults; 17 + } 18 + 19 + public function setAllowedTypes(array $allowed_types) { 20 + $this->allowedTypes = $allowed_types; 21 + return $this; 22 + } 23 + 24 + public function getAllowedTypes() { 25 + return $this->allowedTypes; 26 + } 27 + 28 + public function setViewer(PhabricatorUser $viewer) { 29 + $this->viewer = $viewer; 30 + return $this; 31 + } 32 + 33 + public function getViewer() { 34 + return $this->viewer; 35 + } 36 + 37 + public function setObjectList($object_list) { 38 + $this->objectList = $object_list; 39 + return $this; 40 + } 41 + 42 + public function getObjectList() { 43 + return $this->objectList; 44 + } 45 + 46 + public function execute() { 47 + $names = $this->getObjectList(); 48 + $names = array_unique(array_filter(preg_split('/[\s,]+/', $names))); 49 + 50 + $objects = $this->loadObjects($names); 51 + 52 + $types = array(); 53 + foreach ($objects as $name => $object) { 54 + $types[phid_get_type($object->getPHID())][] = $name; 55 + } 56 + 57 + $invalid = array(); 58 + if ($this->getAllowedTypes()) { 59 + $allowed = array_fuse($this->getAllowedTypes()); 60 + foreach ($types as $type => $names_of_type) { 61 + if (empty($allowed[$type])) { 62 + $invalid[] = $names_of_type; 63 + } 64 + } 65 + } 66 + $invalid = array_mergev($invalid); 67 + 68 + $missing = array(); 69 + foreach ($names as $name) { 70 + if (empty($objects[$name])) { 71 + $missing[] = $name; 72 + } 73 + } 74 + 75 + // NOTE: We could couple this less tightly with Differential, but it is 76 + // currently the only thing that uses it, and we'd have to add a lot of 77 + // extra API to loosen this. It's not clear that this will be useful 78 + // elsewhere any time soon, so let's cross that bridge when we come to it. 79 + 80 + if (!$this->getAllowPartialResults()) { 81 + if ($invalid && $missing) { 82 + throw new DifferentialFieldParseException( 83 + pht( 84 + 'The objects you have listed include objects of the wrong '. 85 + 'type (%s) and objects which do not exist (%s).', 86 + implode(', ', $invalid), 87 + implode(', ', $missing))); 88 + } else if ($invalid) { 89 + throw new DifferentialFieldParseException( 90 + pht( 91 + 'The objects you have listed include objects of the wrong '. 92 + 'type (%s).', 93 + implode(', ', $invalid))); 94 + } else if ($missing) { 95 + throw new DifferentialFieldParseException( 96 + pht( 97 + 'The objects you have listed include objects which do not '. 98 + 'exist (%s).', 99 + implode(', ', $missing))); 100 + } 101 + } 102 + 103 + return array_values(array_unique(mpull($objects, 'getPHID'))); 104 + } 105 + 106 + private function loadObjects($names) { 107 + // First, try to load visible objects using monograms. This covers most 108 + // object types, but does not cover users or user email addresses. 109 + $query = id(new PhabricatorObjectQuery()) 110 + ->setViewer($this->getViewer()) 111 + ->withNames($names); 112 + 113 + $query->execute(); 114 + $objects = $query->getNamedResults(); 115 + 116 + $results = array(); 117 + foreach ($names as $key => $name) { 118 + if (isset($objects[$name])) { 119 + $results[$name] = $objects[$name]; 120 + unset($names[$key]); 121 + } 122 + } 123 + 124 + if ($names) { 125 + // We still have some symbols we haven't been able to resolve, so try to 126 + // load users. Try by username first... 127 + $users = id(new PhabricatorPeopleQuery()) 128 + ->setViewer($this->getViewer()) 129 + ->withUsernames($names) 130 + ->execute(); 131 + 132 + $user_map = array(); 133 + foreach ($users as $user) { 134 + $user_map[phutil_utf8_strtolower($user->getUsername())] = $user; 135 + } 136 + 137 + foreach ($names as $key => $name) { 138 + $normal_name = phutil_utf8_strtolower($name); 139 + if (isset($user_map[$normal_name])) { 140 + $results[$name] = $user_map[$normal_name]; 141 + unset($names[$key]); 142 + } 143 + } 144 + } 145 + 146 + $mailing_list_app = PhabricatorApplication::getByClass( 147 + 'PhabricatorApplicationMailingLists'); 148 + if ($mailing_list_app->isInstalled()) { 149 + if ($names) { 150 + // We still haven't been able to resolve everything; try mailing lists 151 + // by name as a last resort. 152 + $lists = id(new PhabricatorMailingListQuery()) 153 + ->setViewer($this->getViewer()) 154 + ->withNames($names) 155 + ->execute(); 156 + 157 + $lists = mpull($lists, null, 'getName'); 158 + foreach ($names as $key => $name) { 159 + if (isset($lists[$name])) { 160 + $results[$name] = $lists[$name]; 161 + unset($names[$key]); 162 + } 163 + } 164 + } 165 + } 166 + 167 + return $results; 168 + } 169 + 170 + 171 + }
+87
src/applications/phid/query/__tests__/PhabricatorObjectListQueryTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorObjectListQueryTestCase extends PhabricatorTestCase { 4 + 5 + protected function getPhabricatorTestCaseConfiguration() { 6 + return array( 7 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 8 + ); 9 + } 10 + 11 + public function testObjectListQuery() { 12 + $user = $this->generateNewTestUser(); 13 + $name = $user->getUsername(); 14 + $phid = $user->getPHID(); 15 + 16 + 17 + $result = $this->parseObjectList("@{$name}"); 18 + $this->assertEqual(array($phid), $result); 19 + 20 + $result = $this->parseObjectList("{$name}"); 21 + $this->assertEqual(array($phid), $result); 22 + 23 + $result = $this->parseObjectList("{$name}, {$name}"); 24 + $this->assertEqual(array($phid), $result); 25 + 26 + $result = $this->parseObjectList("@{$name}, {$name}"); 27 + $this->assertEqual(array($phid), $result); 28 + 29 + $result = $this->parseObjectList(""); 30 + $this->assertEqual(array(), $result); 31 + 32 + // Expect failure when loading a user if objects must be of type "DUCK". 33 + $caught = null; 34 + try { 35 + $result = $this->parseObjectList("{$name}", array("DUCK")); 36 + } catch (Exception $ex) { 37 + $caught = $ex; 38 + } 39 + $this->assertEqual(true, ($caught instanceof Exception)); 40 + 41 + 42 + // Expect failure when loading an invalid object. 43 + $caught = null; 44 + try { 45 + $result = $this->parseObjectList("invalid"); 46 + } catch (Exception $ex) { 47 + $caught = $ex; 48 + } 49 + $this->assertEqual(true, ($caught instanceof Exception)); 50 + 51 + 52 + // Expect failure when loading ANY invalid object, by default. 53 + $caught = null; 54 + try { 55 + $result = $this->parseObjectList("{$name}, invalid"); 56 + } catch (Exception $ex) { 57 + $caught = $ex; 58 + } 59 + $this->assertEqual(true, ($caught instanceof Exception)); 60 + 61 + 62 + // With partial results, this should load the valid user. 63 + $result = $this->parseObjectList("{$name}, invalid", array(), true); 64 + $this->assertEqual(array($phid), $result); 65 + } 66 + 67 + private function parseObjectList( 68 + $list, 69 + array $types = array(), 70 + $allow_partial = false) { 71 + 72 + $query = id(new PhabricatorObjectListQuery()) 73 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 74 + ->setObjectList($list); 75 + 76 + if ($types) { 77 + $query->setAllowedTypes($types); 78 + } 79 + 80 + if ($allow_partial) { 81 + $query->setAllowPartialResults(true); 82 + } 83 + 84 + return $query->execute(); 85 + } 86 + 87 + }
-2
src/applications/search/engine/PhabricatorJumpNavHandler.php
··· 16 16 '/^p$/i' => 'uri:/project/', 17 17 '/^u$/i' => 'uri:/people/', 18 18 '/^p\s+(.+)$/i' => 'project', 19 - '/^#(.+)$/i' => 'project', 20 19 '/^u\s+(\S+)$/i' => 'user', 21 - '/^@(.+)$/i' => 'user', 22 20 '/^task:\s*(.+)/i' => 'create-task', 23 21 '/^(?:s|symbol)\s+(\S+)/i' => 'find-symbol', 24 22 '/^r\s+(.+)$/i' => 'find-repository',