@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: allow simultaneously changing name and adding same name as a tag

Summary:
Fixes T8509. Changes these behaviors:

- If you create a project named "QQQ" and add "qqq" as a hashtag at the same time, it fails in an unhelpful way. (Now: succeeds.)
- If you add "qqq" as a hashtag to a project with primary hashtag "qqq", it fails in a correct but probably unnecessary way (Now: just works).

We could make one or both of these behaviors show the user an error instead, but I think it's likely that this behavior is just what they always want.

Test Plan:
- Added failing tests and made them pass.
- Executed both scenarios described above from the web UI.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T8509

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

+119 -49
+64
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 249 249 $milestone->getMemberPHIDs()); 250 250 } 251 251 252 + public function testSameSlugAsName() { 253 + // It should be OK to type the primary hashtag into "additional hashtags", 254 + // even if the primary hashtag doesn't exist yet because you're creating 255 + // or renaming the project. 256 + 257 + $user = $this->createUser(); 258 + $user->save(); 259 + 260 + $project = $this->createProject($user); 261 + 262 + // In this first case, set the name and slugs at the same time. 263 + $name = 'slugproject'; 264 + 265 + $xactions = array(); 266 + $xactions[] = id(new PhabricatorProjectTransaction()) 267 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME) 268 + ->setNewValue($name); 269 + $this->applyTransactions($project, $user, $xactions); 270 + 271 + $xactions = array(); 272 + $xactions[] = id(new PhabricatorProjectTransaction()) 273 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) 274 + ->setNewValue(array($name)); 275 + $this->applyTransactions($project, $user, $xactions); 276 + 277 + $project = id(new PhabricatorProjectQuery()) 278 + ->setViewer($user) 279 + ->withPHIDs(array($project->getPHID())) 280 + ->needSlugs(true) 281 + ->executeOne(); 282 + 283 + $slugs = $project->getSlugs(); 284 + $slugs = mpull($slugs, 'getSlug'); 285 + 286 + $this->assertTrue(in_array($name, $slugs)); 287 + 288 + // In this second case, set the name first and then the slugs separately. 289 + $name2 = 'slugproject2'; 290 + 291 + $xactions = array(); 292 + 293 + $xactions[] = id(new PhabricatorProjectTransaction()) 294 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME) 295 + ->setNewValue($name2); 296 + 297 + $xactions[] = id(new PhabricatorProjectTransaction()) 298 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_SLUGS) 299 + ->setNewValue(array($name2)); 300 + 301 + $this->applyTransactions($project, $user, $xactions); 302 + 303 + $project = id(new PhabricatorProjectQuery()) 304 + ->setViewer($user) 305 + ->withPHIDs(array($project->getPHID())) 306 + ->needSlugs(true) 307 + ->executeOne(); 308 + 309 + $slugs = $project->getSlugs(); 310 + $slugs = mpull($slugs, 'getSlug'); 311 + 312 + $this->assertTrue(in_array($name2, $slugs)); 313 + 314 + } 315 + 252 316 public function testDuplicateSlugs() { 253 317 // Creating a project with multiple duplicate slugs should succeed. 254 318
+55 -49
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 146 146 // First, add the old name as a secondary slug; this is helpful 147 147 // for renames and generally a good thing to do. 148 148 if ($old !== null) { 149 - $this->addSlug($object, $old); 149 + $this->addSlug($object, $old, false); 150 150 } 151 - $this->addSlug($object, $new); 151 + $this->addSlug($object, $new, false); 152 152 153 153 return; 154 154 case PhabricatorProjectTransaction::TYPE_SLUGS: ··· 157 157 $add = array_diff($new, $old); 158 158 $rem = array_diff($old, $new); 159 159 160 - if ($add) { 161 - $add_slug_template = id(new PhabricatorProjectSlug()) 162 - ->setProjectPHID($object->getPHID()); 163 - foreach ($add as $add_slug_str) { 164 - $add_slug = id(clone $add_slug_template) 165 - ->setSlug($add_slug_str) 166 - ->save(); 167 - } 168 - } 169 - if ($rem) { 170 - $rem_slugs = id(new PhabricatorProjectSlug()) 171 - ->loadAllWhere('slug IN (%Ls)', $rem); 172 - foreach ($rem_slugs as $rem_slug) { 173 - $rem_slug->delete(); 174 - } 160 + foreach ($add as $slug) { 161 + $this->addSlug($object, $slug, true); 175 162 } 176 163 164 + $this->removeSlugs($object, $rem); 177 165 return; 178 166 case PhabricatorProjectTransaction::TYPE_STATUS: 179 167 case PhabricatorProjectTransaction::TYPE_IMAGE: ··· 328 316 329 317 $slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID'); 330 318 foreach ($slugs_used_already as $project_phid => $used_slugs) { 331 - $used_slug_strs = mpull($used_slugs, 'getSlug'); 332 319 if ($project_phid == $object->getPHID()) { 333 - if (in_array($object->getPrimarySlug(), $used_slug_strs)) { 334 - $error = new PhabricatorApplicationTransactionValidationError( 335 - $type, 336 - pht('Invalid'), 337 - pht( 338 - 'Project hashtag "%s" is already the primary hashtag.', 339 - $object->getPrimarySlug()), 340 - $slug_xaction); 341 - $errors[] = $error; 342 - } 343 320 continue; 344 321 } 322 + 323 + $used_slug_strs = mpull($used_slugs, 'getSlug'); 345 324 346 325 $error = new PhabricatorApplicationTransactionValidationError( 347 326 $type, ··· 594 573 return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); 595 574 } 596 575 597 - private function addSlug( 598 - PhabricatorLiskDAO $object, 599 - $name) { 600 - 601 - $slug = PhabricatorSlug::normalizeProjectSlug($name); 602 - 603 - $slug_object = id(new PhabricatorProjectSlug())->loadOneWhere( 604 - 'slug = %s', 605 - $slug); 606 - 607 - if ($slug_object) { 608 - return; 609 - } 610 - 611 - $new_slug = id(new PhabricatorProjectSlug()) 612 - ->setSlug($slug) 613 - ->setProjectPHID($object->getPHID()) 614 - ->save(); 615 - } 616 - 617 - 618 576 protected function applyFinalEffects( 619 577 PhabricatorLiskDAO $object, 620 578 array $xactions) { ··· 641 599 } 642 600 643 601 return parent::applyFinalEffects($object, $xactions); 602 + } 603 + 604 + private function addSlug(PhabricatorProject $project, $slug, $force) { 605 + $slug = PhabricatorSlug::normalizeProjectSlug($slug); 606 + $table = new PhabricatorProjectSlug(); 607 + $project_phid = $project->getPHID(); 608 + 609 + if ($force) { 610 + // If we have the `$force` flag set, we only want to ignore an existing 611 + // slug if it's for the same project. We'll error on collisions with 612 + // other projects. 613 + $current = $table->loadOneWhere( 614 + 'slug = %s AND projectPHID = %s', 615 + $slug, 616 + $project_phid); 617 + } else { 618 + // Without the `$force` flag, we'll just return without doing anything 619 + // if any other project already has the slug. 620 + $current = $table->loadOneWhere( 621 + 'slug = %s', 622 + $slug); 623 + } 624 + 625 + if ($current) { 626 + return; 627 + } 628 + 629 + return id(new PhabricatorProjectSlug()) 630 + ->setSlug($slug) 631 + ->setProjectPHID($project_phid) 632 + ->save(); 633 + } 634 + 635 + private function removeSlugs(PhabricatorProject $project, array $slugs) { 636 + $slugs = $this->normalizeSlugs($slugs); 637 + 638 + if (!$slugs) { 639 + return; 640 + } 641 + 642 + $objects = id(new PhabricatorProjectSlug())->loadAllWhere( 643 + 'projectPHID = %s AND slug IN (%Ls)', 644 + $project->getPHID(), 645 + $slugs); 646 + 647 + foreach ($objects as $object) { 648 + $object->delete(); 649 + } 644 650 } 645 651 646 652 private function normalizeSlugs(array $slugs) {