@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

Add additional flags to "bin/repository rebuild-identities" to improve flexibility

Summary:
Ref T13444. Repository identities have, at a minimum, some bugs where they do not update relationships properly after many types of email address changes.

It is currently very difficult to fix this once the damage is done since there's no good way to inspect or rebuild them.

Take some steps toward improving observability and providing repair tools: allow `bin/repository rebuild-identities` to effect more repairs and operate on identities more surgically.

Test Plan: Ran `bin/repository rebuild-identities` with all new flags, saw what looked like reasonable rebuilds occur.

Maniphest Tasks: T13444

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

+460 -36
+2 -1
src/applications/config/check/PhabricatorManualActivitySetupCheck.php
··· 113 113 'pre', 114 114 array(), 115 115 (string)csprintf( 116 - 'phabricator/ $ ./bin/repository rebuild-identities --all')); 116 + 'phabricator/ $ '. 117 + './bin/repository rebuild-identities --all-repositories')); 117 118 118 119 $message[] = pht( 119 120 'You can find more information about this new identity mapping '.
+18
src/applications/diffusion/identity/DiffusionRepositoryIdentityEngine.php
··· 68 68 } 69 69 70 70 private function updateIdentity(PhabricatorRepositoryIdentity $identity) { 71 + 72 + // If we're updating an identity and it has a manual user PHID associated 73 + // with it but the user is no longer valid, remove the value. This likely 74 + // corresponds to a user that was destroyed. 75 + 76 + $assigned_phid = $identity->getManuallySetUserPHID(); 77 + $unassigned = DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN; 78 + if ($assigned_phid && ($assigned_phid !== $unassigned)) { 79 + $viewer = $this->getViewer(); 80 + $user = id(new PhabricatorPeopleQuery()) 81 + ->setViewer($viewer) 82 + ->withPHIDs(array($assigned_phid)) 83 + ->executeOne(); 84 + if (!$user) { 85 + $identity->setManuallySetUserPHID(null); 86 + } 87 + } 88 + 71 89 $resolved_phid = $this->resolveIdentity($identity); 72 90 73 91 $identity
+292 -25
src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php
··· 14 14 ->setArguments( 15 15 array( 16 16 array( 17 - 'name' => 'repositories', 18 - 'wildcard' => true, 17 + 'name' => 'all-repositories', 18 + 'help' => pht('Rebuild identities across all repositories.'), 19 19 ), 20 20 array( 21 - 'name' => 'all', 22 - 'help' => pht('Rebuild identities across all repositories.'), 23 - ), 21 + 'name' => 'all-identities', 22 + 'help' => pht('Rebuild all currently-known identities.'), 23 + ), 24 + array( 25 + 'name' => 'repository', 26 + 'param' => 'repository', 27 + 'repeat' => true, 28 + 'help' => pht('Rebuild identities in a repository.'), 29 + ), 30 + array( 31 + 'name' => 'commit', 32 + 'param' => 'commit', 33 + 'repeat' => true, 34 + 'help' => pht('Rebuild identities for a commit.'), 35 + ), 36 + array( 37 + 'name' => 'user', 38 + 'param' => 'user', 39 + 'repeat' => true, 40 + 'help' => pht('Rebuild identities for a user.'), 41 + ), 42 + array( 43 + 'name' => 'email', 44 + 'param' => 'email', 45 + 'repeat' => true, 46 + 'help' => pht('Rebuild identities for an email address.'), 47 + ), 48 + array( 49 + 'name' => 'raw', 50 + 'param' => 'raw', 51 + 'repeat' => true, 52 + 'help' => pht('Rebuild identities for a raw commit string.'), 53 + ), 24 54 )); 25 55 } 26 56 27 57 public function execute(PhutilArgumentParser $args) { 28 - $console = PhutilConsole::getConsole(); 58 + $viewer = $this->getViewer(); 29 59 30 - $all = $args->getArg('all'); 31 - $repositories = $args->getArg('repositories'); 60 + $rebuilt_anything = false; 32 61 33 - if ($all xor empty($repositories)) { 62 + 63 + $all_repositories = $args->getArg('all-repositories'); 64 + $repositories = $args->getArg('repository'); 65 + 66 + if ($all_repositories && $repositories) { 34 67 throw new PhutilArgumentUsageException( 35 - pht('Specify --all or a list of repositories, but not both.')); 68 + pht( 69 + 'Flags "--all-repositories" and "--repository" are not '. 70 + 'compatible.')); 71 + } 72 + 73 + 74 + $all_identities = $args->getArg('all-identities'); 75 + $raw = $args->getArg('raw'); 76 + 77 + if ($all_identities && $raw) { 78 + throw new PhutilArgumentUsageException( 79 + pht( 80 + 'Flags "--all-identities" and "--raw" are not '. 81 + 'compatible.')); 82 + } 83 + 84 + if ($all_repositories || $repositories) { 85 + $rebuilt_anything = true; 86 + 87 + if ($repositories) { 88 + $repository_list = $this->loadRepositories($args, 'repository'); 89 + } else { 90 + $repository_query = id(new PhabricatorRepositoryQuery()) 91 + ->setViewer($viewer); 92 + $repository_list = new PhabricatorQueryIterator($repository_query); 93 + } 94 + 95 + foreach ($repository_list as $repository) { 96 + $commit_query = id(new DiffusionCommitQuery()) 97 + ->setViewer($viewer) 98 + ->needCommitData(true) 99 + ->withRepositoryIDs(array($repository->getID())); 100 + 101 + $commit_iterator = new PhabricatorQueryIterator($commit_query); 102 + 103 + $this->rebuildCommits($commit_iterator); 104 + } 36 105 } 37 106 38 - $query = id(new DiffusionCommitQuery()) 39 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 40 - ->needCommitData(true); 107 + $commits = $args->getArg('commit'); 108 + if ($commits) { 109 + $rebuilt_anything = true; 110 + $commit_list = $this->loadCommits($args, 'commit'); 111 + 112 + // Reload commits to get commit data. 113 + $commit_list = id(new DiffusionCommitQuery()) 114 + ->setViewer($viewer) 115 + ->needCommitData(true) 116 + ->withIDs(mpull($commit_list, 'getID')) 117 + ->execute(); 118 + 119 + $this->rebuildCommits($commit_list); 120 + } 121 + 122 + $users = $args->getArg('user'); 123 + if ($users) { 124 + $rebuilt_anything = true; 125 + 126 + $user_list = $this->loadUsersFromArguments($users); 127 + $this->rebuildUsers($user_list); 128 + } 129 + 130 + $emails = $args->getArg('email'); 131 + if ($emails) { 132 + $rebuilt_anything = true; 133 + $this->rebuildEmails($emails); 134 + } 135 + 136 + if ($all_identities || $raw) { 137 + $rebuilt_anything = true; 138 + 139 + if ($raw) { 140 + $identities = id(new PhabricatorRepositoryIdentityQuery()) 141 + ->setViewer($viewer) 142 + ->withIdentityNames($raw) 143 + ->execute(); 144 + 145 + $identities = mpull($identities, null, 'getIdentityNameRaw'); 146 + foreach ($raw as $raw_identity) { 147 + if (!isset($identities[$raw_identity])) { 148 + throw new PhutilArgumentUsageException( 149 + pht( 150 + 'No identity "%s" exists. When selecting identities with '. 151 + '"--raw", the entire identity must match exactly.', 152 + $raw_identity)); 153 + } 154 + } 155 + 156 + $identity_list = $identities; 157 + } else { 158 + $identity_query = id(new PhabricatorRepositoryIdentityQuery()) 159 + ->setViewer($viewer); 160 + 161 + $identity_list = new PhabricatorQueryIterator($identity_query); 162 + 163 + $this->logInfo( 164 + pht('REBUILD'), 165 + pht('Rebuilding all existing identities.')); 166 + } 167 + 168 + $this->rebuildIdentities($identity_list); 169 + } 41 170 42 - if ($repositories) { 43 - $repos = $this->loadRepositories($args, 'repositories'); 44 - $query->withRepositoryIDs(mpull($repos, 'getID')); 171 + if (!$rebuilt_anything) { 172 + throw new PhutilArgumentUsageException( 173 + pht( 174 + 'Nothing specified to rebuild. Use flags to choose which '. 175 + 'identities to rebuild, or "--help" for help.')); 45 176 } 46 177 47 - $iterator = new PhabricatorQueryIterator($query); 48 - foreach ($iterator as $commit) { 178 + return 0; 179 + } 180 + 181 + private function rebuildCommits($commits) { 182 + foreach ($commits as $commit) { 49 183 $needs_update = false; 50 184 51 185 $data = $commit->getCommitData(); ··· 57 191 58 192 $author_phid = $commit->getAuthorIdentityPHID(); 59 193 $identity_phid = $author_identity->getPHID(); 194 + 195 + $aidentity_phid = $identity_phid; 60 196 if ($author_phid !== $identity_phid) { 61 197 $commit->setAuthorIdentityPHID($identity_phid); 62 198 $data->setCommitDetail('authorIdentityPHID', $identity_phid); ··· 83 219 if ($needs_update) { 84 220 $commit->save(); 85 221 $data->save(); 86 - echo tsprintf( 87 - "Rebuilt identities for %s.\n", 88 - $commit->getDisplayName()); 222 + 223 + $this->logInfo( 224 + pht('COMMIT'), 225 + pht( 226 + 'Rebuilt identities for "%s".', 227 + $commit->getDisplayName())); 89 228 } else { 90 - echo tsprintf( 91 - "No changes for %s.\n", 92 - $commit->getDisplayName()); 229 + $this->logInfo( 230 + pht('SKIP'), 231 + pht( 232 + 'No changes for commit "%s".', 233 + $commit->getDisplayName())); 93 234 } 94 235 } 95 - 96 236 } 97 237 98 238 private function getIdentityForCommit( ··· 111 251 } 112 252 113 253 return $this->identityCache[$raw_identity]; 254 + } 255 + 256 + 257 + private function rebuildUsers($users) { 258 + $viewer = $this->getViewer(); 259 + 260 + foreach ($users as $user) { 261 + $this->logInfo( 262 + pht('USER'), 263 + pht( 264 + 'Rebuilding identities for user "%s".', 265 + $user->getMonogram())); 266 + 267 + $emails = id(new PhabricatorUserEmail())->loadAllWhere( 268 + 'userPHID = %s', 269 + $user->getPHID()); 270 + if ($emails) { 271 + $this->rebuildEmails(mpull($emails, 'getAddress')); 272 + } 273 + 274 + $identities = id(new PhabricatorRepositoryIdentityQuery()) 275 + ->setViewer($viewer) 276 + ->withRelatedPHIDs(array($user->getPHID())) 277 + ->execute(); 278 + 279 + if (!$identities) { 280 + $this->logWarn( 281 + pht('NO IDENTITIES'), 282 + pht('Found no identities directly related to user.')); 283 + continue; 284 + } 285 + 286 + $this->rebuildIdentities($identities); 287 + } 288 + } 289 + 290 + private function rebuildEmails($emails) { 291 + $viewer = $this->getViewer(); 292 + 293 + foreach ($emails as $email) { 294 + $this->logInfo( 295 + pht('EMAIL'), 296 + pht('Rebuilding identities for email address "%s".', $email)); 297 + 298 + $identities = id(new PhabricatorRepositoryIdentityQuery()) 299 + ->setViewer($viewer) 300 + ->withEmailAddresses(array($email)) 301 + ->execute(); 302 + 303 + if (!$identities) { 304 + $this->logWarn( 305 + pht('NO IDENTITIES'), 306 + pht('Found no identities for email address "%s".', $email)); 307 + continue; 308 + } 309 + 310 + $this->rebuildIdentities($identities); 311 + } 312 + } 313 + 314 + private function rebuildIdentities($identities) { 315 + $viewer = $this->getViewer(); 316 + 317 + foreach ($identities as $identity) { 318 + $raw_identity = $identity->getIdentityName(); 319 + 320 + if (isset($this->identityCache[$raw_identity])) { 321 + $this->logInfo( 322 + pht('SKIP'), 323 + pht( 324 + 'Identity "%s" has already been rebuilt.', 325 + $raw_identity)); 326 + continue; 327 + } 328 + 329 + $this->logInfo( 330 + pht('IDENTITY'), 331 + pht( 332 + 'Rebuilding identity "%s".', 333 + $raw_identity)); 334 + 335 + $old_auto = $identity->getAutomaticGuessedUserPHID(); 336 + $old_assign = $identity->getManuallySetUserPHID(); 337 + 338 + $identity = id(new DiffusionRepositoryIdentityEngine()) 339 + ->setViewer($viewer) 340 + ->newUpdatedIdentity($identity); 341 + 342 + $this->identityCache[$raw_identity] = $identity; 343 + 344 + $new_auto = $identity->getAutomaticGuessedUserPHID(); 345 + $new_assign = $identity->getManuallySetUserPHID(); 346 + 347 + $same_auto = ($old_auto === $new_auto); 348 + $same_assign = ($old_assign === $new_assign); 349 + 350 + if ($same_auto && $same_assign) { 351 + $this->logInfo( 352 + pht('UNCHANGED'), 353 + pht('No changes to identity.')); 354 + } else { 355 + if (!$same_auto) { 356 + $this->logWarn( 357 + pht('AUTOMATIC PHID'), 358 + pht( 359 + 'Automatic user updated from "%s" to "%s".', 360 + $this->renderPHID($old_auto), 361 + $this->renderPHID($new_auto))); 362 + } 363 + if (!$same_assign) { 364 + $this->logWarn( 365 + pht('ASSIGNED PHID'), 366 + pht( 367 + 'Assigned user updated from "%s" to "%s".', 368 + $this->renderPHID($old_assign), 369 + $this->renderPHID($new_assign))); 370 + } 371 + } 372 + } 373 + } 374 + 375 + private function renderPHID($phid) { 376 + if ($phid == null) { 377 + return pht('NULL'); 378 + } else { 379 + return $phid; 380 + } 114 381 } 115 382 116 383 }
+27 -10
src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php
··· 11 11 private $effectivePHIDs; 12 12 private $identityNameLike; 13 13 private $hasEffectivePHID; 14 + private $relatedPHIDs; 14 15 15 16 public function withIDs(array $ids) { 16 17 $this->ids = $ids; ··· 47 48 return $this; 48 49 } 49 50 51 + public function withRelatedPHIDs(array $related) { 52 + $this->relatedPHIDs = $related; 53 + return $this; 54 + } 55 + 50 56 public function withHasEffectivePHID($has_effective_phid) { 51 57 $this->hasEffectivePHID = $has_effective_phid; 52 58 return $this; ··· 57 63 } 58 64 59 65 protected function getPrimaryTableAlias() { 60 - return 'repository_identity'; 66 + return 'identity'; 61 67 } 62 68 63 69 protected function loadPage() { ··· 70 76 if ($this->ids !== null) { 71 77 $where[] = qsprintf( 72 78 $conn, 73 - 'repository_identity.id IN (%Ld)', 79 + 'identity.id IN (%Ld)', 74 80 $this->ids); 75 81 } 76 82 77 83 if ($this->phids !== null) { 78 84 $where[] = qsprintf( 79 85 $conn, 80 - 'repository_identity.phid IN (%Ls)', 86 + 'identity.phid IN (%Ls)', 81 87 $this->phids); 82 88 } 83 89 84 90 if ($this->assignedPHIDs !== null) { 85 91 $where[] = qsprintf( 86 92 $conn, 87 - 'repository_identity.manuallySetUserPHID IN (%Ls)', 93 + 'identity.manuallySetUserPHID IN (%Ls)', 88 94 $this->assignedPHIDs); 89 95 } 90 96 91 97 if ($this->effectivePHIDs !== null) { 92 98 $where[] = qsprintf( 93 99 $conn, 94 - 'repository_identity.currentEffectiveUserPHID IN (%Ls)', 100 + 'identity.currentEffectiveUserPHID IN (%Ls)', 95 101 $this->effectivePHIDs); 96 102 } 97 103 ··· 99 105 if ($this->hasEffectivePHID) { 100 106 $where[] = qsprintf( 101 107 $conn, 102 - 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); 108 + 'identity.currentEffectiveUserPHID IS NOT NULL'); 103 109 } else { 104 110 $where[] = qsprintf( 105 111 $conn, 106 - 'repository_identity.currentEffectiveUserPHID IS NULL'); 112 + 'identity.currentEffectiveUserPHID IS NULL'); 107 113 } 108 114 } 109 115 ··· 115 121 116 122 $where[] = qsprintf( 117 123 $conn, 118 - 'repository_identity.identityNameHash IN (%Ls)', 124 + 'identity.identityNameHash IN (%Ls)', 119 125 $name_hashes); 120 126 } 121 127 122 128 if ($this->emailAddresses !== null) { 123 129 $where[] = qsprintf( 124 130 $conn, 125 - 'repository_identity.emailAddress IN (%Ls)', 131 + 'identity.emailAddress IN (%Ls)', 126 132 $this->emailAddresses); 127 133 } 128 134 129 135 if ($this->identityNameLike != null) { 130 136 $where[] = qsprintf( 131 137 $conn, 132 - 'repository_identity.identityNameRaw LIKE %~', 138 + 'identity.identityNameRaw LIKE %~', 133 139 $this->identityNameLike); 140 + } 141 + 142 + if ($this->relatedPHIDs !== null) { 143 + $where[] = qsprintf( 144 + $conn, 145 + '(identity.manuallySetUserPHID IN (%Ls) OR 146 + identity.currentEffectiveUserPHID IN (%Ls) OR 147 + identity.automaticGuessedUserPHID IN (%Ls))', 148 + $this->relatedPHIDs, 149 + $this->relatedPHIDs, 150 + $this->relatedPHIDs); 134 151 } 135 152 136 153 return $where;
+121
src/infrastructure/management/PhabricatorManagementWorkflow.php
··· 67 67 fprintf(STDERR, '%s', $message); 68 68 } 69 69 70 + final protected function loadUsersFromArguments(array $identifiers) { 71 + if (!$identifiers) { 72 + return array(); 73 + } 74 + 75 + $ids = array(); 76 + $phids = array(); 77 + $usernames = array(); 78 + 79 + $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; 80 + 81 + foreach ($identifiers as $identifier) { 82 + // If the value is a user PHID, treat as a PHID. 83 + if (phid_get_type($identifier) === $user_type) { 84 + $phids[$identifier] = $identifier; 85 + continue; 86 + } 87 + 88 + // If the value is "@..." and then some text, treat it as a username. 89 + if ((strlen($identifier) > 1) && ($identifier[0] == '@')) { 90 + $usernames[$identifier] = substr($identifier, 1); 91 + continue; 92 + } 93 + 94 + // If the value is digits, treat it as both an ID and a username. 95 + // Entirely numeric usernames, like "1234", are valid. 96 + if (ctype_digit($identifier)) { 97 + $ids[$identifier] = $identifier; 98 + $usernames[$identifier] = $identifier; 99 + continue; 100 + } 101 + 102 + // Otherwise, treat it as an unescaped username. 103 + $usernames[$identifier] = $identifier; 104 + } 105 + 106 + $viewer = $this->getViewer(); 107 + $results = array(); 108 + 109 + if ($phids) { 110 + $users = id(new PhabricatorPeopleQuery()) 111 + ->setViewer($viewer) 112 + ->withPHIDs($phids) 113 + ->execute(); 114 + foreach ($users as $user) { 115 + $phid = $user->getPHID(); 116 + $results[$phid][] = $user; 117 + } 118 + } 119 + 120 + if ($usernames) { 121 + $users = id(new PhabricatorPeopleQuery()) 122 + ->setViewer($viewer) 123 + ->withUsernames($usernames) 124 + ->execute(); 125 + 126 + $reverse_map = array(); 127 + foreach ($usernames as $identifier => $username) { 128 + $username = phutil_utf8_strtolower($username); 129 + $reverse_map[$username][] = $identifier; 130 + } 131 + 132 + foreach ($users as $user) { 133 + $username = $user->getUsername(); 134 + $username = phutil_utf8_strtolower($username); 135 + 136 + $reverse_identifiers = idx($reverse_map, $username, array()); 137 + 138 + if (count($reverse_identifiers) > 1) { 139 + throw new PhutilArgumentUsageException( 140 + pht( 141 + 'Multiple user identifiers (%s) correspond to the same user. '. 142 + 'Identify each user exactly once.', 143 + implode(', ', $reverse_identifiers))); 144 + } 145 + 146 + foreach ($reverse_identifiers as $reverse_identifier) { 147 + $results[$reverse_identifier][] = $user; 148 + } 149 + } 150 + } 151 + 152 + if ($ids) { 153 + $users = id(new PhabricatorPeopleQuery()) 154 + ->setViewer($viewer) 155 + ->withIDs($ids) 156 + ->execute(); 157 + 158 + foreach ($users as $user) { 159 + $id = $user->getID(); 160 + $results[$id][] = $user; 161 + } 162 + } 163 + 164 + $list = array(); 165 + foreach ($identifiers as $identifier) { 166 + $users = idx($results, $identifier, array()); 167 + if (!$users) { 168 + throw new PhutilArgumentUsageException( 169 + pht( 170 + 'No user "%s" exists. Specify users by username, ID, or PHID.', 171 + $identifier)); 172 + } 173 + 174 + if (count($users) > 1) { 175 + // This can happen if you have a user "@25", a user with ID 25, and 176 + // specify "--user 25". You can disambiguate this by specifying 177 + // "--user @25". 178 + throw new PhutilArgumentUsageException( 179 + pht( 180 + 'Identifier "%s" matches multiple users. Specify each user '. 181 + 'unambiguously with "@username" or by using user PHIDs.', 182 + $identifier)); 183 + } 184 + 185 + $list[] = head($users); 186 + } 187 + 188 + return $list; 189 + } 190 + 70 191 }