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

Fix project hashtag bugs: duplicate tags, uppercase tags

Summary:
Ref T8509. This fixes three issues:

- Adding a slug like `UPPERCASE` would not give you a normalized slug. (Now: normalizes as `uppercase`.)
- Adding a slug like `UPPERCASE` would allow you to give two different projects the different tags `UPPERCASE` and `uppercase` (and `UpPeRcAsE`, etc). (Now: second tag is rejected as a duplicate.)
- Adding multiple identical or similar slugs would produce a duplicate key exception. (Now: ignores the duplicates.)

Test Plan:
- Added test coverage.
- Made tests pass.
- Hit these cases in the UI.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T8509

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

+105 -7
+84
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 249 249 $milestone->getMemberPHIDs()); 250 250 } 251 251 252 + public function testDuplicateSlugs() { 253 + // Creating a project with multiple duplicate slugs should succeed. 254 + 255 + $user = $this->createUser(); 256 + $user->save(); 257 + 258 + $project = $this->createProject($user); 259 + 260 + $input = 'duplicate'; 261 + 262 + $xactions = array(); 263 + 264 + $xactions[] = id(new PhabricatorProjectTransaction()) 265 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) 266 + ->setNewValue(array($input, $input)); 267 + 268 + $this->applyTransactions($project, $user, $xactions); 269 + 270 + $project = id(new PhabricatorProjectQuery()) 271 + ->setViewer($user) 272 + ->withPHIDs(array($project->getPHID())) 273 + ->needSlugs(true) 274 + ->executeOne(); 275 + 276 + $slugs = $project->getSlugs(); 277 + $slugs = mpull($slugs, 'getSlug'); 278 + 279 + $this->assertTrue(in_array($input, $slugs)); 280 + } 281 + 282 + public function testNormalizeSlugs() { 283 + // When a user creates a project with slug "XxX360n0sc0perXxX", normalize 284 + // it before writing it. 285 + 286 + $user = $this->createUser(); 287 + $user->save(); 288 + 289 + $project = $this->createProject($user); 290 + 291 + $input = 'NoRmAlIzE'; 292 + $expect = 'normalize'; 293 + 294 + $xactions = array(); 295 + 296 + $xactions[] = id(new PhabricatorProjectTransaction()) 297 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) 298 + ->setNewValue(array($input)); 299 + 300 + $this->applyTransactions($project, $user, $xactions); 301 + 302 + $project = id(new PhabricatorProjectQuery()) 303 + ->setViewer($user) 304 + ->withPHIDs(array($project->getPHID())) 305 + ->needSlugs(true) 306 + ->executeOne(); 307 + 308 + $slugs = $project->getSlugs(); 309 + $slugs = mpull($slugs, 'getSlug'); 310 + 311 + $this->assertTrue(in_array($expect, $slugs)); 312 + 313 + 314 + // If another user tries to add the same slug in denormalized form, it 315 + // should be caught and fail, even though the database version of the slug 316 + // is normalized. 317 + 318 + $project2 = $this->createProject($user); 319 + 320 + $xactions = array(); 321 + 322 + $xactions[] = id(new PhabricatorProjectTransaction()) 323 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) 324 + ->setNewValue(array($input)); 325 + 326 + $caught = null; 327 + try { 328 + $this->applyTransactions($project2, $user, $xactions); 329 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 330 + $caught = $ex; 331 + } 332 + 333 + $this->assertTrue((bool)$caught); 334 + } 335 + 252 336 public function testParentProject() { 253 337 $user = $this->createUser(); 254 338 $user->save();
+18 -4
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 68 68 69 69 switch ($xaction->getTransactionType()) { 70 70 case PhabricatorProjectTransaction::TYPE_NAME: 71 - case PhabricatorProjectTransaction::TYPE_SLUGS: 72 71 case PhabricatorProjectTransaction::TYPE_STATUS: 73 72 case PhabricatorProjectTransaction::TYPE_IMAGE: 74 73 case PhabricatorProjectTransaction::TYPE_ICON: ··· 76 75 case PhabricatorProjectTransaction::TYPE_LOCKED: 77 76 case PhabricatorProjectTransaction::TYPE_PARENT: 78 77 return $xaction->getNewValue(); 78 + case PhabricatorProjectTransaction::TYPE_SLUGS: 79 + return $this->normalizeSlugs($xaction->getNewValue()); 79 80 case PhabricatorProjectTransaction::TYPE_MILESTONE: 80 81 $current = queryfx_one( 81 82 $object->establishConnection('w'), ··· 313 314 } 314 315 315 316 $slug_xaction = last($xactions); 317 + 316 318 $new = $slug_xaction->getNewValue(); 319 + $new = $this->normalizeSlugs($new); 317 320 318 321 if ($new) { 319 322 $slugs_used_already = id(new PhabricatorProjectSlug()) ··· 332 335 $type, 333 336 pht('Invalid'), 334 337 pht( 335 - 'Project hashtag %s is already the primary hashtag.', 338 + 'Project hashtag "%s" is already the primary hashtag.', 336 339 $object->getPrimarySlug()), 337 340 $slug_xaction); 338 341 $errors[] = $error; ··· 344 347 $type, 345 348 pht('Invalid'), 346 349 pht( 347 - '%d project hashtag(s) are already used: %s.', 348 - count($used_slug_strs), 350 + '%s project hashtag(s) are already used by other projects: %s.', 351 + phutil_count($used_slug_strs), 349 352 implode(', ', $used_slug_strs)), 350 353 $slug_xaction); 351 354 $errors[] = $error; ··· 638 641 } 639 642 640 643 return parent::applyFinalEffects($object, $xactions); 644 + } 645 + 646 + private function normalizeSlugs(array $slugs) { 647 + foreach ($slugs as $key => $slug) { 648 + $slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug); 649 + } 650 + 651 + $slugs = array_unique($slugs); 652 + $slugs = array_values($slugs); 653 + 654 + return $slugs; 641 655 } 642 656 643 657 }
+3 -3
src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
··· 766 766 ), 767 767 ), 768 768 769 - '%d project hashtag(s) are already used: %s.' => array( 770 - 'Project hashtag %2$s is already used.', 771 - '%d project hashtags are already used: %2$s.', 769 + '%s project hashtag(s) are already used by other projects: %s.' => array( 770 + 'Project hashtag "%2$s" is already used by another project.', 771 + 'Some project hashtags are already used by other projects: %2$s.', 772 772 ), 773 773 774 774 '%s changed project hashtag(s), added %d: %s; removed %d: %s.' =>