Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15386123
D9250.id21984.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
24 KB
Referenced Files
None
Subscribers
None
D9250.id21984.diff
View Options
diff --git a/resources/sql/autopatches/20140521.projectslug.1.create.sql b/resources/sql/autopatches/20140521.projectslug.1.create.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140521.projectslug.1.create.sql
@@ -0,0 +1,9 @@
+CREATE TABLE {$NAMESPACE}_project.project_slug (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ projectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin,
+ slug VARCHAR(128) NOT NULL COLLATE utf8_bin,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+ UNIQUE KEY `key_slug` (slug),
+ KEY `key_projectPHID` (projectPHID)
+) ENGINE=InnoDB, COLLATE utf8_general_ci;
diff --git a/resources/sql/autopatches/20140521.projectslug.2.mig.php b/resources/sql/autopatches/20140521.projectslug.2.mig.php
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20140521.projectslug.2.mig.php
@@ -0,0 +1,33 @@
+<?php
+
+$project_table = new PhabricatorProject();
+$table_name = $project_table->getTableName();
+$conn_w = $project_table->establishConnection('w');
+$slug_table_name = id(new PhabricatorProjectSlug())->getTableName();
+$time = time();
+
+echo "Migrating project phriction slugs...\n";
+foreach (new LiskMigrationIterator($project_table) as $project) {
+ $id = $project->getID();
+
+ echo "Migrating project {$id}...\n";
+ $phriction_slug = rtrim($project->getPhrictionSlug(), '/');
+ $slug = id(new PhabricatorProjectSlug())
+ ->loadOneWhere('slug = %s', $phriction_slug);
+ if ($slug) {
+ echo "Already migrated {$id}... Continuing.\n";
+ continue;
+ }
+ queryfx(
+ $conn_w,
+ 'INSERT INTO %T (projectPHID, slug, dateCreated, dateModified) '.
+ 'VALUES (%s, %s, %d, %d)',
+ $slug_table_name,
+ $project->getPHID(),
+ $phriction_slug,
+ $time,
+ $time);
+ echo "Migrated {$id}.\n";
+}
+
+echo "Done.\n";
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1963,6 +1963,7 @@
'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php',
'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php',
'PhabricatorProjectSearchIndexer' => 'applications/project/search/PhabricatorProjectSearchIndexer.php',
+ 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
@@ -4787,6 +4788,7 @@
'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorProjectSearchIndexer' => 'PhabricatorSearchDocumentIndexer',
+ 'PhabricatorProjectSlug' => 'PhabricatorProjectDAO',
'PhabricatorProjectStandardCustomField' =>
array(
0 => 'PhabricatorProjectCustomField',
diff --git a/src/applications/project/controller/PhabricatorProjectCreateController.php b/src/applications/project/controller/PhabricatorProjectCreateController.php
--- a/src/applications/project/controller/PhabricatorProjectCreateController.php
+++ b/src/applications/project/controller/PhabricatorProjectCreateController.php
@@ -15,13 +15,16 @@
$project = PhabricatorProject::initializeNewProject($user);
$e_name = true;
- $errors = array();
+ $type_name = PhabricatorProjectTransaction::TYPE_NAME;
+ $v_name = $project->getName();
+ $validation_exception = null;
if ($request->isFormPost()) {
$xactions = array();
+ $v_name = $request->getStr('name');
$xactions[] = id(new PhabricatorProjectTransaction())
- ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME)
- ->setNewValue($request->getStr('name'));
+ ->setTransactionType($type_name)
+ ->setNewValue($v_name);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
@@ -34,14 +37,9 @@
$editor = id(new PhabricatorProjectTransactionEditor())
->setActor($user)
->setContinueOnNoEffect(true)
- ->setContentSourceFromRequest($request)
- ->applyTransactions($project, $xactions);
-
- // TODO: Deal with name collision exceptions more gracefully.
-
- if (!$errors) {
- $project->save();
-
+ ->setContentSourceFromRequest($request);
+ try {
+ $editor->applyTransactions($project, $xactions);
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())
->setContent(array(
@@ -52,15 +50,12 @@
return id(new AphrontRedirectResponse())
->setURI('/project/view/'.$project->getID().'/');
}
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ $validation_exception = $ex;
+ $e_name = $ex->getShortMessage($type_name);
}
}
- $error_view = null;
- if ($errors) {
- $error_view = new AphrontErrorView();
- $error_view->setErrors($errors);
- }
-
if ($request->isAjax()) {
$form = new PHUIFormLayoutView();
} else {
@@ -73,15 +68,19 @@
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
- ->setValue($project->getName())
+ ->setValue($v_name)
->setError($e_name));
if ($request->isAjax()) {
+ $errors = array();
+ if ($validation_exception) {
+ $errors = mpull($ex->getErrors(), 'getMessage');
+ }
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('Create a New Project'))
- ->appendChild($error_view)
+ ->setErrors($errors)
->appendChild($form)
->addSubmitButton(pht('Create Project'))
->addCancelButton('/project/');
@@ -101,7 +100,7 @@
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Create New Project'))
- ->setFormErrors($errors)
+ ->setValidationException($validation_exception)
->setForm($form);
return $this->buildApplicationPage(
diff --git a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
--- a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
+++ b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
@@ -16,6 +16,7 @@
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withIDs(array($this->id))
+ ->needSlugs(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
@@ -37,16 +38,24 @@
$edit_uri = $this->getApplicationURI('edit/'.$project->getID().'/');
$e_name = true;
+ $e_slugs = false;
$e_edit = null;
$v_name = $project->getName();
+ $project_slugs = $project->getSlugs();
+ $project_slugs = mpull($project_slugs, 'getSlug', 'getSlug');
+ $v_primary_slug = $project->getPrimarySlug();
+ unset($project_slugs[$v_primary_slug]);
+ $v_slugs = $project_slugs;
$validation_exception = null;
if ($request->isFormPost()) {
$e_name = null;
+ $e_slugs = null;
$v_name = $request->getStr('name');
+ $v_slugs = $request->getStrList('slugs');
$v_view = $request->getStr('can_view');
$v_edit = $request->getStr('can_edit');
$v_join = $request->getStr('can_join');
@@ -56,11 +65,16 @@
$request);
$type_name = PhabricatorProjectTransaction::TYPE_NAME;
+ $type_slugs = PhabricatorProjectTransaction::TYPE_SLUGS;
$type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType($type_name)
- ->setNewValue($request->getStr('name'));
+ ->setNewValue($v_name);
+
+ $xactions[] = id(new PhabricatorProjectTransaction())
+ ->setTransactionType($type_slugs)
+ ->setNewValue($v_slugs);
$xactions[] = id(new PhabricatorProjectTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
@@ -87,6 +101,7 @@
$validation_exception = $ex;
$e_name = $ex->getShortMessage($type_name);
+ $e_slugs = $ex->getShortMessage($type_slugs);
$e_edit = $ex->getShortMessage($type_edit);
$project->setViewPolicy($v_view);
@@ -102,6 +117,7 @@
->setViewer($viewer)
->setObject($project)
->execute();
+ $v_slugs = implode(', ', $v_slugs);
$form = new AphrontFormView();
$form
@@ -112,11 +128,23 @@
->setName('name')
->setValue($v_name)
->setError($e_name));
-
$field_list->appendFieldsToForm($form);
$form
->appendChild(
+ id(new AphrontFormStaticControl())
+ ->setLabel(pht('Primary Hashtag'))
+ ->setCaption(pht('The primary hashtag is derived from the name.'))
+ ->setValue($v_primary_slug))
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Additional Hashtags'))
+ ->setCaption(pht(
+ 'Specify a comma-separated list of additional hashtags.'))
+ ->setName('slugs')
+ ->setValue($v_slugs)
+ ->setError($e_slugs))
+ ->appendChild(
id(new AphrontFormPolicyControl())
->setUser($viewer)
->setName('can_view')
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -12,6 +12,7 @@
$types[] = PhabricatorTransactions::TYPE_JOIN_POLICY;
$types[] = PhabricatorProjectTransaction::TYPE_NAME;
+ $types[] = PhabricatorProjectTransaction::TYPE_SLUGS;
$types[] = PhabricatorProjectTransaction::TYPE_STATUS;
$types[] = PhabricatorProjectTransaction::TYPE_IMAGE;
@@ -25,6 +26,11 @@
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
return $object->getName();
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
+ $slugs = $object->getSlugs();
+ $slugs = mpull($slugs, 'getSlug', 'getSlug');
+ unset($slugs[$object->getPrimarySlug()]);
+ return $slugs;
case PhabricatorProjectTransaction::TYPE_STATUS:
return $object->getStatus();
case PhabricatorProjectTransaction::TYPE_IMAGE:
@@ -40,6 +46,7 @@
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
case PhabricatorProjectTransaction::TYPE_STATUS:
case PhabricatorProjectTransaction::TYPE_IMAGE:
return $xaction->getNewValue();
@@ -57,6 +64,8 @@
$object->setName($xaction->getNewValue());
$object->setPhrictionSlug($xaction->getNewValue());
return;
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
+ return;
case PhabricatorProjectTransaction::TYPE_STATUS:
$object->setStatus($xaction->getNewValue());
return;
@@ -85,6 +94,24 @@
switch ($xaction->getTransactionType()) {
case PhabricatorProjectTransaction::TYPE_NAME:
+ $new_slug = id(new PhabricatorProjectSlug())
+ ->setSlug($object->getPrimarySlug())
+ ->setProjectPHID($object->getPHID())
+ ->save();
+
+ if ($xaction->getOldValue() !== null) {
+ $clone_object = clone $object;
+ $clone_object->setPhrictionSlug($xaction->getOldValue());
+ $old_slug = $clone_object->getPrimarySlug();
+ $old_slug = id(new PhabricatorProjectSlug())
+ ->loadOneWhere('slug = %s', $old_slug);
+ if ($old_slug) {
+ $old_slug->delete();
+ }
+ }
+
+ // TODO -- delete all of the below once we sever automagical project
+ // to phriction stuff
if ($xaction->getOldValue() === null) {
// Project was just created, we don't need to move anything.
return;
@@ -118,6 +145,29 @@
$from_editor->moveAway($target_document->getID());
}
return;
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+ $add = array_diff($new, $old);
+ $rem = array_diff($old, $new);
+
+ if ($add) {
+ $add_slug_template = id(new PhabricatorProjectSlug())
+ ->setProjectPHID($object->getPHID());
+ foreach ($add as $add_slug_str) {
+ $add_slug = id(clone $add_slug_template)
+ ->setSlug($add_slug_str)
+ ->save();
+ }
+ }
+ if ($rem) {
+ $rem_slugs = id(new PhabricatorProjectSlug())
+ ->loadAllWhere('slug IN (%Ls)', $rem);
+ foreach ($rem_slugs as $rem_slug) {
+ $rem_slug->delete();
+ }
+ }
+ return;
case PhabricatorTransactions::TYPE_VIEW_POLICY:
case PhabricatorTransactions::TYPE_EDIT_POLICY:
case PhabricatorTransactions::TYPE_JOIN_POLICY:
@@ -219,11 +269,64 @@
($name_used_already->getPHID() != $object->getPHID())) {
$error = new PhabricatorApplicationTransactionValidationError(
$type,
- pht(''),
+ pht('Duplicate'),
pht('Project name is already used.'),
nonempty(last($xactions), null));
$errors[] = $error;
}
+
+ $slug_builder = clone $object;
+ $slug_builder->setPhrictionSlug($name);
+ $slug = $slug_builder->getPrimarySlug();
+ $slug_used_already = id(new PhabricatorProjectSlug())
+ ->loadOneWhere('slug = %s', $slug);
+ if ($slug_used_already &&
+ $slug_used_already->getProjectPHID() != $object->getPHID()) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Duplicate'),
+ pht('Project name can not be used due to hashtag collision.'),
+ nonempty(last($xactions), null));
+ $errors[] = $error;
+ }
+ break;
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
+ if (!$xactions) {
+ break;
+ }
+
+ $slug_xaction = last($xactions);
+ $new = $slug_xaction->getNewValue();
+ $slugs_used_already = id(new PhabricatorProjectSlug())
+ ->loadAllWhere('slug IN (%Ls)', $new);
+ $slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID');
+ foreach ($slugs_used_already as $project_phid => $used_slugs) {
+ $used_slug_strs = mpull($used_slugs, 'getSlug');
+ if ($project_phid == $object->getPHID()) {
+ if (in_array($object->getPrimarySlug(), $used_slug_strs)) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht(''),
+ pht(
+ 'Project hashtag %s is already the primary hashtag.',
+ $object->getPrimarySlug()),
+ $slug_xaction);
+ $errors[] = $error;
+ }
+ continue;
+ }
+
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht(''),
+ pht(
+ '%d project hashtag(s) are already used: %s',
+ count($used_slug_strs),
+ implode(', ', $used_slug_strs)),
+ $slug_xaction);
+ $errors[] = $error;
+ }
+
break;
}
diff --git a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php
--- a/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php
+++ b/src/applications/project/phid/PhabricatorProjectPHIDTypeProject.php
@@ -79,14 +79,17 @@
$projects = id(new PhabricatorProjectQuery())
->setViewer($query->getViewer())
- ->withPhrictionSlugs(array_keys($map))
+ ->withSlugs(array_keys($map))
+ ->needSlugs(true)
->execute();
$result = array();
foreach ($projects as $project) {
- $slugs = array($project->getPhrictionSlug());
- foreach ($slugs as $slug) {
- foreach ($map[$slug] as $original) {
+ $slugs = $project->getSlugs();
+ $slug_strs = mpull($slugs, 'getSlug');
+ foreach ($slug_strs as $slug) {
+ $slug_map = idx($map, $slug, array());
+ foreach ($slug_map as $original) {
$result[$original] = $project;
}
}
@@ -102,7 +105,7 @@
// should not. normalize() strips out most punctuation and leads to
// excessively aggressive matches.
- return phutil_utf8_strtolower($slug).'/';
+ return phutil_utf8_strtolower($slug);
}
diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php
--- a/src/applications/project/query/PhabricatorProjectQuery.php
+++ b/src/applications/project/query/PhabricatorProjectQuery.php
@@ -7,6 +7,7 @@
private $phids;
private $memberPHIDs;
private $slugs;
+ private $phrictionSlugs;
private $names;
private $status = 'status-any';
@@ -16,6 +17,7 @@
const STATUS_ACTIVE = 'status-active';
const STATUS_ARCHIVED = 'status-archived';
+ private $needSlugs;
private $needMembers;
private $needWatchers;
private $needImages;
@@ -40,11 +42,16 @@
return $this;
}
- public function withPhrictionSlugs(array $slugs) {
+ public function withSlugs(array $slugs) {
$this->slugs = $slugs;
return $this;
}
+ public function withPhrictionSlugs(array $slugs) {
+ $this->phrictionSlugs = $slugs;
+ return $this;
+ }
+
public function withNames(array $names) {
$this->names = $names;
return $this;
@@ -65,6 +72,11 @@
return $this;
}
+ public function needSlugs($need_slugs) {
+ $this->needSlugs = $need_slugs;
+ return $this;
+ }
+
protected function getPagingColumn() {
return 'name';
}
@@ -184,6 +196,18 @@
}
}
+ if ($this->needSlugs) {
+ $slugs = id(new PhabricatorProjectSlug())
+ ->loadAllWhere(
+ 'projectPHID IN (%Ls)',
+ mpull($projects, 'getPHID'));
+ $slugs = mgroup($slugs, 'getProjectPHID');
+ foreach ($projects as $project) {
+ $project_slugs = idx($slugs, $project->getPHID(), array());
+ $project->attachSlugs($project_slugs);
+ }
+ }
+
return $projects;
}
@@ -238,10 +262,17 @@
if ($this->slugs) {
$where[] = qsprintf(
$conn_r,
- 'phrictionSlug IN (%Ls)',
+ 'slug.slug IN (%Ls)',
$this->slugs);
}
+ if ($this->phrictionSlugs) {
+ $where[] = qsprintf(
+ $conn_r,
+ 'phrictionSlug IN (%Ls)',
+ $this->phrictionSlugs);
+ }
+
if ($this->names) {
$where[] = qsprintf(
$conn_r,
@@ -282,6 +313,13 @@
PhabricatorEdgeConfig::TYPE_PROJ_MEMBER);
}
+ if ($this->slugs) {
+ $joins[] = qsprintf(
+ $conn_r,
+ 'JOIN %T slug on slug.projectPHID = p.phid',
+ id(new PhabricatorProjectSlug())->getTableName());
+ }
+
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
return implode(' ', $joins);
diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php
--- a/src/applications/project/storage/PhabricatorProject.php
+++ b/src/applications/project/storage/PhabricatorProject.php
@@ -24,6 +24,7 @@
private $sparseMembers = self::ATTACHABLE;
private $customFields = self::ATTACHABLE;
private $profileImageFile = self::ATTACHABLE;
+ private $slugs = self::ATTACHABLE;
public static function initializeNewProject(PhabricatorUser $actor) {
return id(new PhabricatorProject())
@@ -143,6 +144,14 @@
return 'projects/'.$slug;
}
+ // TODO - once we sever project => phriction automagicalness,
+ // migrate getPhrictionSlug to have no trailing slash and be called
+ // getPrimarySlug
+ public function getPrimarySlug() {
+ $slug = $this->getPhrictionSlug();
+ return rtrim($slug, '/');
+ }
+
public function isArchived() {
return ($this->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED);
}
@@ -185,6 +194,15 @@
return $this->assertAttached($this->watcherPHIDs);
}
+ public function attachSlugs(array $slugs) {
+ $this->slugs = $slugs;
+ return $this;
+ }
+
+ public function getSlugs() {
+ return $this->assertAttached($this->slugs);
+ }
+
/* -( PhabricatorSubscribableInterface )----------------------------------- */
diff --git a/src/applications/project/storage/PhabricatorProjectSlug.php b/src/applications/project/storage/PhabricatorProjectSlug.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/storage/PhabricatorProjectSlug.php
@@ -0,0 +1,8 @@
+<?php
+
+final class PhabricatorProjectSlug extends PhabricatorProjectDAO {
+
+ protected $slug;
+ protected $projectPHID;
+
+}
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -4,6 +4,7 @@
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'project:name';
+ const TYPE_SLUGS = 'project:slugs';
const TYPE_STATUS = 'project:status';
const TYPE_IMAGE = 'project:image';
@@ -84,6 +85,33 @@
$this->renderHandleLink($old),
$this->renderHandleLink($new));
}
+
+ case PhabricatorProjectTransaction::TYPE_SLUGS:
+ $add = array_diff($new, $old);
+ $rem = array_diff($old, $new);
+
+ if ($add && $rem) {
+ return pht(
+ '%s changed project hashtag(s), added %d: %s; removed %d: %s',
+ $author_handle,
+ count($add),
+ $this->renderSlugList($add),
+ count($rem),
+ $this->renderSlugList($rem));
+ } else if ($add) {
+ return pht(
+ '%s added %d project hashtag(s): %s',
+ $author_handle,
+ count($add),
+ $this->renderSlugList($add));
+ } else if ($rem) {
+ return pht(
+ '%s removed %d project hashtag(s): %s',
+ $author_handle,
+ count($rem),
+ $this->renderSlugList($rem));
+ }
+
case PhabricatorProjectTransaction::TYPE_MEMBERS:
$add = array_diff($new, $old);
$rem = array_diff($old, $new);
@@ -126,5 +154,8 @@
return parent::getTitle();
}
+ private function renderSlugList($slugs) {
+ return implode(', ', $slugs);
+ }
}
diff --git a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php
--- a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php
@@ -835,6 +835,28 @@
),
),
+ '%d project hashtag(s) are already used: %s' => array(
+ 'Project hashtag %2$s is already used.',
+ '%d project hashtags are already used: %2$s',
+ ),
+
+ '%s changed project hashtag(s), added %d: %s; removed %d: %s' =>
+ '%s changed project hashtags, added %3$s; removed %5$s',
+
+ '%s added %d project hashtag(s): %s' => array(
+ array(
+ '%s added a hashtag: %3$s',
+ '%s added hashtags: %3$s',
+ ),
+ ),
+
+ '%s removed %d project hashtag(s): %s' => array(
+ array(
+ '%s removed a hashtag: %3$s',
+ '%s removed hashtags: %3$s',
+ ),
+ ),
+
'%d User(s) Need Approval' => array(
'%d User Needs Approval',
'%d Users Need Approval',
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mar 16 2025, 12:05 AM (4 w, 3 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7704580
Default Alt Text
D9250.id21984.diff (24 KB)
Attached To
Mode
D9250: Projects - add "Additional Hashtags" to projects
Attached
Detach File
Event Timeline
Log In to Comment