mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +01:00
Add PackagesVersion
Summary: Ref T8116. A version has: - a package (like "Arcanist") which it belongs to; - a name (like "v3.1.5"). The name is immutable and unique, like the package key and publisher key. Policy stuff: - Versions have the exact same policies as their packages. - You must be able to edit a package to create new versions of it. This is still entirely uninteresting. Test Plan: {F1731703} Reviewers: chad Reviewed By: chad Maniphest Tasks: T8116 Differential Revision: https://secure.phabricator.com/D16316
This commit is contained in:
parent
704afea281
commit
ee37eca2e4
26 changed files with 1095 additions and 27 deletions
10
resources/sql/autopatches/20160721.pack.06.version.sql
Normal file
10
resources/sql/autopatches/20160721.pack.06.version.sql
Normal file
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE {$NAMESPACE}_packages.packages_version (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
phid VARBINARY(64) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL COLLATE {$COLLATE_SORT},
|
||||
packagePHID VARBINARY(64) NOT NULL,
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (phid),
|
||||
UNIQUE KEY `key_package` (packagePHID, name)
|
||||
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
|
@ -0,0 +1,19 @@
|
|||
CREATE TABLE {$NAMESPACE}_packages.packages_versiontransaction (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
phid VARBINARY(64) NOT NULL,
|
||||
authorPHID VARBINARY(64) NOT NULL,
|
||||
objectPHID VARBINARY(64) NOT NULL,
|
||||
viewPolicy VARBINARY(64) NOT NULL,
|
||||
editPolicy VARBINARY(64) NOT NULL,
|
||||
commentPHID VARBINARY(64) DEFAULT NULL,
|
||||
commentVersion INT UNSIGNED NOT NULL,
|
||||
transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL,
|
||||
oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
|
||||
newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
|
||||
contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
|
||||
metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (`phid`),
|
||||
KEY `key_object` (`objectPHID`)
|
||||
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
|
@ -2979,6 +2979,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPackagesEditor' => 'applications/packages/editor/PhabricatorPackagesEditor.php',
|
||||
'PhabricatorPackagesPackage' => 'applications/packages/storage/PhabricatorPackagesPackage.php',
|
||||
'PhabricatorPackagesPackageController' => 'applications/packages/controller/PhabricatorPackagesPackageController.php',
|
||||
'PhabricatorPackagesPackageDatasource' => 'applications/packages/typeahead/PhabricatorPackagesPackageDatasource.php',
|
||||
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesPackageEditConduitAPIMethod.php',
|
||||
'PhabricatorPackagesPackageEditController' => 'applications/packages/controller/PhabricatorPackagesPackageEditController.php',
|
||||
'PhabricatorPackagesPackageEditEngine' => 'applications/packages/editor/PhabricatorPackagesPackageEditEngine.php',
|
||||
|
@ -3016,6 +3017,23 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPackagesQuery' => 'applications/packages/query/PhabricatorPackagesQuery.php',
|
||||
'PhabricatorPackagesSchemaSpec' => 'applications/packages/storage/PhabricatorPackagesSchemaSpec.php',
|
||||
'PhabricatorPackagesTransactionType' => 'applications/packages/xaction/PhabricatorPackagesTransactionType.php',
|
||||
'PhabricatorPackagesVersion' => 'applications/packages/storage/PhabricatorPackagesVersion.php',
|
||||
'PhabricatorPackagesVersionController' => 'applications/packages/controller/PhabricatorPackagesVersionController.php',
|
||||
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionEditConduitAPIMethod.php',
|
||||
'PhabricatorPackagesVersionEditController' => 'applications/packages/controller/PhabricatorPackagesVersionEditController.php',
|
||||
'PhabricatorPackagesVersionEditEngine' => 'applications/packages/editor/PhabricatorPackagesVersionEditEngine.php',
|
||||
'PhabricatorPackagesVersionEditor' => 'applications/packages/editor/PhabricatorPackagesVersionEditor.php',
|
||||
'PhabricatorPackagesVersionListController' => 'applications/packages/controller/PhabricatorPackagesVersionListController.php',
|
||||
'PhabricatorPackagesVersionNameTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionNameTransaction.php',
|
||||
'PhabricatorPackagesVersionPHIDType' => 'applications/packages/phid/PhabricatorPackagesVersionPHIDType.php',
|
||||
'PhabricatorPackagesVersionPackageTransaction' => 'applications/packages/xaction/version/PhabricatorPackagesVersionPackageTransaction.php',
|
||||
'PhabricatorPackagesVersionQuery' => 'applications/packages/query/PhabricatorPackagesVersionQuery.php',
|
||||
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'applications/packages/conduit/PhabricatorPackagesVersionSearchConduitAPIMethod.php',
|
||||
'PhabricatorPackagesVersionSearchEngine' => 'applications/packages/query/PhabricatorPackagesVersionSearchEngine.php',
|
||||
'PhabricatorPackagesVersionTransaction' => 'applications/packages/storage/PhabricatorPackagesVersionTransaction.php',
|
||||
'PhabricatorPackagesVersionTransactionQuery' => 'applications/packages/query/PhabricatorPackagesVersionTransactionQuery.php',
|
||||
'PhabricatorPackagesVersionTransactionType' => 'applications/packages/xaction/version/PhabricatorPackagesVersionTransactionType.php',
|
||||
'PhabricatorPackagesVersionViewController' => 'applications/packages/controller/PhabricatorPackagesVersionViewController.php',
|
||||
'PhabricatorPagerUIExample' => 'applications/uiexample/examples/PhabricatorPagerUIExample.php',
|
||||
'PhabricatorPassphraseApplication' => 'applications/passphrase/application/PhabricatorPassphraseApplication.php',
|
||||
'PhabricatorPasswordAuthProvider' => 'applications/auth/provider/PhabricatorPasswordAuthProvider.php',
|
||||
|
@ -7784,6 +7802,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorConduitResultInterface',
|
||||
),
|
||||
'PhabricatorPackagesPackageController' => 'PhabricatorPackagesController',
|
||||
'PhabricatorPackagesPackageDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||
'PhabricatorPackagesPackageEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
|
||||
'PhabricatorPackagesPackageEditController' => 'PhabricatorPackagesPackageController',
|
||||
'PhabricatorPackagesPackageEditEngine' => 'PhabricatorPackagesEditEngine',
|
||||
|
@ -7829,6 +7848,32 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPackagesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorPackagesSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||
'PhabricatorPackagesTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorPackagesVersion' => array(
|
||||
'PhabricatorPackagesDAO',
|
||||
'PhabricatorPolicyInterface',
|
||||
'PhabricatorExtendedPolicyInterface',
|
||||
'PhabricatorApplicationTransactionInterface',
|
||||
'PhabricatorDestructibleInterface',
|
||||
'PhabricatorSubscribableInterface',
|
||||
'PhabricatorProjectInterface',
|
||||
'PhabricatorConduitResultInterface',
|
||||
),
|
||||
'PhabricatorPackagesVersionController' => 'PhabricatorPackagesController',
|
||||
'PhabricatorPackagesVersionEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
|
||||
'PhabricatorPackagesVersionEditController' => 'PhabricatorPackagesVersionController',
|
||||
'PhabricatorPackagesVersionEditEngine' => 'PhabricatorPackagesEditEngine',
|
||||
'PhabricatorPackagesVersionEditor' => 'PhabricatorPackagesEditor',
|
||||
'PhabricatorPackagesVersionListController' => 'PhabricatorPackagesVersionController',
|
||||
'PhabricatorPackagesVersionNameTransaction' => 'PhabricatorPackagesVersionTransactionType',
|
||||
'PhabricatorPackagesVersionPHIDType' => 'PhabricatorPHIDType',
|
||||
'PhabricatorPackagesVersionPackageTransaction' => 'PhabricatorPackagesVersionTransactionType',
|
||||
'PhabricatorPackagesVersionQuery' => 'PhabricatorPackagesQuery',
|
||||
'PhabricatorPackagesVersionSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
|
||||
'PhabricatorPackagesVersionSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorPackagesVersionTransaction' => 'PhabricatorModularTransaction',
|
||||
'PhabricatorPackagesVersionTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorPackagesVersionTransactionType' => 'PhabricatorPackagesTransactionType',
|
||||
'PhabricatorPackagesVersionViewController' => 'PhabricatorPackagesVersionController',
|
||||
'PhabricatorPagerUIExample' => 'PhabricatorUIExample',
|
||||
'PhabricatorPassphraseApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPasswordAuthProvider' => 'PhabricatorAuthProvider',
|
||||
|
|
|
@ -33,6 +33,8 @@ final class PhabricatorPackagesApplication extends PhabricatorApplication {
|
|||
'' => 'PhabricatorPackagesPublisherViewController',
|
||||
'(?P<packageKey>[^/]+)/' => array(
|
||||
'' => 'PhabricatorPackagesPackageViewController',
|
||||
'(?P<versionKey>[^/]+)/' =>
|
||||
'PhabricatorPackagesVersionViewController',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -49,6 +51,12 @@ final class PhabricatorPackagesApplication extends PhabricatorApplication {
|
|||
$this->getEditRoutePattern('edit/') =>
|
||||
'PhabricatorPackagesPackageEditController',
|
||||
),
|
||||
'version/' => array(
|
||||
$this->getQueryRoutePattern() =>
|
||||
'PhabricatorPackagesVersionListController',
|
||||
$this->getEditRoutePattern('edit/') =>
|
||||
'PhabricatorPackagesVersionEditController',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionEditConduitAPIMethod
|
||||
extends PhabricatorEditEngineAPIMethod {
|
||||
|
||||
public function getAPIMethodName() {
|
||||
return 'packages.version.edit';
|
||||
}
|
||||
|
||||
public function newEditEngine() {
|
||||
return new PhabricatorPackagesVersionEditEngine();
|
||||
}
|
||||
|
||||
public function getMethodSummary() {
|
||||
return pht(
|
||||
'Apply transactions to create a new version or edit an existing one.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionSearchConduitAPIMethod
|
||||
extends PhabricatorSearchEngineAPIMethod {
|
||||
|
||||
public function getAPIMethodName() {
|
||||
return 'packages.version.search';
|
||||
}
|
||||
|
||||
public function newSearchEngine() {
|
||||
return new PhabricatorPackagesVersionSearchEngine();
|
||||
}
|
||||
|
||||
public function getMethodSummary() {
|
||||
return pht('Read information about versions.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorPackagesVersionController
|
||||
extends PhabricatorPackagesController {}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionEditController
|
||||
extends PhabricatorPackagesVersionController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
return id(new PhabricatorPackagesVersionEditEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionListController
|
||||
extends PhabricatorPackagesVersionController {
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
return id(new PhabricatorPackagesVersionSearchEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
}
|
||||
|
||||
protected function buildApplicationCrumbs() {
|
||||
$crumbs = parent::buildApplicationCrumbs();
|
||||
|
||||
id(new PhabricatorPackagesVersionEditEngine())
|
||||
->setViewer($this->getViewer())
|
||||
->addActionToCrumbs($crumbs);
|
||||
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionViewController
|
||||
extends PhabricatorPackagesVersionController {
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$publisher_key = $request->getURIData('publisherKey');
|
||||
$package_key = $request->getURIData('packageKey');
|
||||
$full_key = $publisher_key.'/'.$package_key;
|
||||
$version_key = $request->getURIData('versionKey');
|
||||
|
||||
$version = id(new PhabricatorPackagesVersionQuery())
|
||||
->setViewer($viewer)
|
||||
->withFullKeys(array($full_key))
|
||||
->withNames(array($version_key))
|
||||
->executeOne();
|
||||
if (!$version) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$package = $version->getPackage();
|
||||
$publisher = $package->getPublisher();
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs()
|
||||
->addTextCrumb($publisher->getName(), $publisher->getURI())
|
||||
->addTextCrumb($package->getName(), $package->getURI())
|
||||
->addTextCrumb($version->getName())
|
||||
->setBorder(true);
|
||||
|
||||
$header = $this->buildHeaderView($version);
|
||||
$curtain = $this->buildCurtain($version);
|
||||
|
||||
$timeline = $this->buildTransactionTimeline(
|
||||
$version,
|
||||
new PhabricatorPackagesVersionTransactionQuery());
|
||||
|
||||
$version_view = id(new PHUITwoColumnView())
|
||||
->setHeader($header)
|
||||
->setCurtain($curtain)
|
||||
->setMainColumn($timeline);
|
||||
|
||||
return $this->newPage()
|
||||
->setCrumbs($crumbs)
|
||||
->setPageObjectPHIDs(
|
||||
array(
|
||||
$version->getPHID(),
|
||||
))
|
||||
->appendChild($version_view);
|
||||
}
|
||||
|
||||
|
||||
private function buildHeaderView(PhabricatorPackagesVersion $version) {
|
||||
$viewer = $this->getViewer();
|
||||
$name = $version->getName();
|
||||
|
||||
return id(new PHUIHeaderView())
|
||||
->setViewer($viewer)
|
||||
->setHeader($name)
|
||||
->setPolicyObject($version)
|
||||
->setHeaderIcon('fa-tag');
|
||||
}
|
||||
|
||||
private function buildCurtain(PhabricatorPackagesVersion $version) {
|
||||
$viewer = $this->getViewer();
|
||||
$curtain = $this->newCurtainView($version);
|
||||
|
||||
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||
$viewer,
|
||||
$version,
|
||||
PhabricatorPolicyCapability::CAN_EDIT);
|
||||
|
||||
$id = $version->getID();
|
||||
$edit_uri = $this->getApplicationURI("version/edit/{$id}/");
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Edit Version'))
|
||||
->setIcon('fa-pencil')
|
||||
->setDisabled(!$can_edit)
|
||||
->setHref($edit_uri));
|
||||
|
||||
return $curtain;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionEditEngine
|
||||
extends PhabricatorPackagesEditEngine {
|
||||
|
||||
const ENGINECONST = 'packages.version';
|
||||
|
||||
public function getEngineName() {
|
||||
return pht('Package Versions');
|
||||
}
|
||||
|
||||
public function getSummaryHeader() {
|
||||
return pht('Edit Package Version Configurations');
|
||||
}
|
||||
|
||||
public function getSummaryText() {
|
||||
return pht('This engine is used to edit Packages versions.');
|
||||
}
|
||||
|
||||
protected function newEditableObject() {
|
||||
$viewer = $this->getViewer();
|
||||
return PhabricatorPackagesVersion::initializeNewVersion($viewer);
|
||||
}
|
||||
|
||||
protected function newObjectQuery() {
|
||||
return new PhabricatorPackagesVersionQuery();
|
||||
}
|
||||
|
||||
protected function getObjectCreateTitleText($object) {
|
||||
return pht('Create Version');
|
||||
}
|
||||
|
||||
protected function getObjectCreateButtonText($object) {
|
||||
return pht('Create Version');
|
||||
}
|
||||
|
||||
protected function getObjectEditTitleText($object) {
|
||||
return pht('Edit Version: %s', $object->getName());
|
||||
}
|
||||
|
||||
protected function getObjectEditShortText($object) {
|
||||
return pht('Edit Version');
|
||||
}
|
||||
|
||||
protected function getObjectCreateShortText() {
|
||||
return pht('Create Version');
|
||||
}
|
||||
|
||||
protected function getObjectName() {
|
||||
return pht('Version');
|
||||
}
|
||||
|
||||
protected function getEditorURI() {
|
||||
return '/packages/version/edit/';
|
||||
}
|
||||
|
||||
protected function getObjectCreateCancelURI($object) {
|
||||
return '/packages/version/';
|
||||
}
|
||||
|
||||
protected function getObjectViewURI($object) {
|
||||
return $object->getURI();
|
||||
}
|
||||
|
||||
protected function buildCustomEditFields($object) {
|
||||
$fields = array();
|
||||
|
||||
if ($this->getIsCreate()) {
|
||||
$fields[] = id(new PhabricatorDatasourceEditField())
|
||||
->setKey('package')
|
||||
->setAliases(array('packagePHID'))
|
||||
->setLabel(pht('Package'))
|
||||
->setDescription(pht('Package for this version.'))
|
||||
->setTransactionType(
|
||||
PhabricatorPackagesVersionPackageTransaction::TRANSACTIONTYPE)
|
||||
->setIsRequired(true)
|
||||
->setDatasource(new PhabricatorPackagesPackageDatasource())
|
||||
->setSingleValue($object->getPackagePHID());
|
||||
|
||||
$fields[] = id(new PhabricatorTextEditField())
|
||||
->setKey('name')
|
||||
->setLabel(pht('Name'))
|
||||
->setDescription(pht('Name of the version.'))
|
||||
->setTransactionType(
|
||||
PhabricatorPackagesVersionNameTransaction::TRANSACTIONTYPE)
|
||||
->setIsRequired(true)
|
||||
->setValue($object->getName());
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionEditor
|
||||
extends PhabricatorPackagesEditor {
|
||||
|
||||
public function getEditorObjectsDescription() {
|
||||
return pht('Package Versions');
|
||||
}
|
||||
|
||||
public function getCreateObjectTitle($author, $object) {
|
||||
return pht('%s created this version.', $author);
|
||||
}
|
||||
|
||||
public function getCreateObjectTitleForFeed($author, $object) {
|
||||
return pht('%s created %s.', $author, $object);
|
||||
}
|
||||
|
||||
protected function shouldPublishFeedStory(
|
||||
PhabricatorLiskDAO $object,
|
||||
array $xactions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getMailTo(PhabricatorLiskDAO $object) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function didCatchDuplicateKeyException(
|
||||
PhabricatorLiskDAO $object,
|
||||
array $xactions,
|
||||
Exception $ex) {
|
||||
|
||||
$errors = array();
|
||||
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||
PhabricatorPackagesVersionNameTransaction::TRANSACTIONTYPE,
|
||||
pht('Duplicate'),
|
||||
pht(
|
||||
'The version "%s" already exists for this package. Each version '.
|
||||
'must have a unique name.',
|
||||
$object->getName()),
|
||||
null);
|
||||
|
||||
throw new PhabricatorApplicationTransactionValidationException($errors);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionPHIDType
|
||||
extends PhabricatorPHIDType {
|
||||
|
||||
const TYPECONST = 'PVER';
|
||||
|
||||
public function getTypeName() {
|
||||
return pht('Version');
|
||||
}
|
||||
|
||||
public function newObject() {
|
||||
return new PhabricatorPackagesVersion();
|
||||
}
|
||||
|
||||
public function getPHIDTypeApplicationClass() {
|
||||
return 'PhabricatorPackagesApplication';
|
||||
}
|
||||
|
||||
protected function buildQueryForObjects(
|
||||
PhabricatorObjectQuery $query,
|
||||
array $phids) {
|
||||
|
||||
return id(new PhabricatorPackagesVersionQuery())
|
||||
->withPHIDs($phids);
|
||||
}
|
||||
|
||||
public function loadHandles(
|
||||
PhabricatorHandleQuery $query,
|
||||
array $handles,
|
||||
array $objects) {
|
||||
|
||||
foreach ($handles as $phid => $handle) {
|
||||
$version = $objects[$phid];
|
||||
|
||||
$name = $version->getName();
|
||||
$uri = $version->getURI();
|
||||
|
||||
$handle
|
||||
->setName($name)
|
||||
->setURI($uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -74,31 +74,8 @@ final class PhabricatorPackagesPackageQuery
|
|||
}
|
||||
|
||||
if ($this->fullKeys !== null) {
|
||||
$parts = array();
|
||||
foreach ($this->fullKeys as $full_key) {
|
||||
$key_parts = explode('/', $full_key, 2);
|
||||
|
||||
if (count($key_parts) != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = qsprintf(
|
||||
$conn,
|
||||
'(u.publisherKey = %s AND p.packageKey = %s)',
|
||||
$key_parts[0],
|
||||
$key_parts[1]);
|
||||
}
|
||||
|
||||
// If none of the full keys we were provided were valid, we don't
|
||||
// match any results.
|
||||
if (!$parts) {
|
||||
throw new PhabricatorEmptyQueryException();
|
||||
}
|
||||
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'%Q',
|
||||
implode(' OR ', $parts));
|
||||
$parts = $this->buildFullKeyClauseParts($conn, $this->fullKeys);
|
||||
$where[] = qsprintf($conn, '%Q', $parts);
|
||||
}
|
||||
|
||||
return $where;
|
||||
|
|
|
@ -7,4 +7,32 @@ abstract class PhabricatorPackagesQuery
|
|||
return 'PhabricatorPackagesApplication';
|
||||
}
|
||||
|
||||
protected function buildFullKeyClauseParts(
|
||||
AphrontDatabaseConnection $conn,
|
||||
array $full_keys) {
|
||||
|
||||
$parts = array();
|
||||
foreach ($full_keys as $full_key) {
|
||||
$key_parts = explode('/', $full_key, 2);
|
||||
|
||||
if (count($key_parts) != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = qsprintf(
|
||||
$conn,
|
||||
'(u.publisherKey = %s AND p.packageKey = %s)',
|
||||
$key_parts[0],
|
||||
$key_parts[1]);
|
||||
}
|
||||
|
||||
// If none of the full keys we were provided were valid, we don't
|
||||
// match any results.
|
||||
if (!$parts) {
|
||||
throw new PhabricatorEmptyQueryException();
|
||||
}
|
||||
|
||||
return implode(' OR ', $parts);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionQuery
|
||||
extends PhabricatorPackagesQuery {
|
||||
|
||||
private $ids;
|
||||
private $phids;
|
||||
private $packagePHIDs;
|
||||
private $fullKeys;
|
||||
private $names;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withPHIDs(array $phids) {
|
||||
$this->phids = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withPackagePHIDs(array $phids) {
|
||||
$this->packagePHIDs = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withFullKeys(array $keys) {
|
||||
$this->fullKeys = $keys;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withNames(array $names) {
|
||||
$this->names = $names;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function newResultObject() {
|
||||
return new PhabricatorPackagesVersion();
|
||||
}
|
||||
|
||||
protected function loadPage() {
|
||||
return $this->loadStandardPage($this->newResultObject());
|
||||
}
|
||||
|
||||
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$where = parent::buildWhereClauseParts($conn);
|
||||
|
||||
if ($this->ids !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'v.id IN (%Ld)',
|
||||
$this->ids);
|
||||
}
|
||||
|
||||
if ($this->phids !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'v.phid IN (%Ls)',
|
||||
$this->phids);
|
||||
}
|
||||
|
||||
if ($this->packagePHIDs !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'v.packagePHID IN (%Ls)',
|
||||
$this->packagePHIDs);
|
||||
}
|
||||
|
||||
if ($this->names !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'v.name IN (%Ls)',
|
||||
$this->names);
|
||||
}
|
||||
|
||||
if ($this->fullKeys !== null) {
|
||||
$parts = $this->buildFullKeyClauseParts($conn, $this->fullKeys);
|
||||
$where[] = qsprintf($conn, '%Q', $parts);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$joins = parent::buildJoinClauseParts($conn);
|
||||
|
||||
$join_publisher = ($this->fullKeys !== null);
|
||||
$join_package = ($this->fullKeys !== null) || $join_publisher;
|
||||
|
||||
if ($join_package) {
|
||||
$package_table = new PhabricatorPackagesPackage();
|
||||
|
||||
$joins[] = qsprintf(
|
||||
$conn,
|
||||
'JOIN %T p ON v.packagePHID = p.phid',
|
||||
$package_table->getTableName());
|
||||
}
|
||||
|
||||
if ($join_publisher) {
|
||||
$publisher_table = new PhabricatorPackagesPublisher();
|
||||
|
||||
$joins[] = qsprintf(
|
||||
$conn,
|
||||
'JOIN %T u ON u.phid = p.publisherPHID',
|
||||
$publisher_table->getTableName());
|
||||
}
|
||||
|
||||
return $joins;
|
||||
}
|
||||
|
||||
protected function willFilterPage(array $versions) {
|
||||
$package_phids = mpull($versions, 'getPackagePHID');
|
||||
|
||||
$packages = id(new PhabricatorPackagesPackageQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->setParentQuery($this)
|
||||
->withPHIDs($package_phids)
|
||||
->execute();
|
||||
$packages = mpull($packages, null, 'getPHID');
|
||||
|
||||
foreach ($versions as $key => $version) {
|
||||
$package = idx($packages, $version->getPackagePHID());
|
||||
|
||||
if (!$package) {
|
||||
unset($versions[$key]);
|
||||
$this->didRejectResult($version);
|
||||
continue;
|
||||
}
|
||||
|
||||
$version->attachPackage($package);
|
||||
}
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
protected function getPrimaryTableAlias() {
|
||||
return 'v';
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionSearchEngine
|
||||
extends PhabricatorApplicationSearchEngine {
|
||||
|
||||
public function getResultTypeDescription() {
|
||||
return pht('Package Versions');
|
||||
}
|
||||
|
||||
public function getApplicationClassName() {
|
||||
return 'PhabricatorPackagesApplication';
|
||||
}
|
||||
|
||||
public function newQuery() {
|
||||
return id(new PhabricatorPackagesVersionQuery());
|
||||
}
|
||||
|
||||
protected function buildQueryFromParameters(array $map) {
|
||||
$query = $this->newQuery();
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function buildCustomSearchFields() {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function getURI($path) {
|
||||
return '/packages/version/'.$path;
|
||||
}
|
||||
|
||||
protected function getBuiltinQueryNames() {
|
||||
$names = array(
|
||||
'all' => pht('All Versions'),
|
||||
);
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
public function buildSavedQueryFromBuiltin($query_key) {
|
||||
$query = $this->newSavedQuery();
|
||||
$query->setQueryKey($query_key);
|
||||
|
||||
switch ($query_key) {
|
||||
case 'all':
|
||||
return $query;
|
||||
}
|
||||
|
||||
return parent::buildSavedQueryFromBuiltin($query_key);
|
||||
}
|
||||
|
||||
protected function renderResultList(
|
||||
array $versions,
|
||||
PhabricatorSavedQuery $query,
|
||||
array $handles) {
|
||||
|
||||
assert_instances_of($versions, 'PhabricatorPackagesVersion');
|
||||
|
||||
$viewer = $this->requireViewer();
|
||||
|
||||
$list = id(new PHUIObjectItemListView())
|
||||
->setViewer($viewer);
|
||||
foreach ($versions as $version) {
|
||||
$item = id(new PHUIObjectItemView())
|
||||
->setHeader($version->getName())
|
||||
->setHref($version->getURI());
|
||||
|
||||
$list->addItem($item);
|
||||
}
|
||||
|
||||
return id(new PhabricatorApplicationSearchResultView())
|
||||
->setObjectList($list)
|
||||
->setNoDataString(pht('No versions found.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionTransactionQuery
|
||||
extends PhabricatorApplicationTransactionQuery {
|
||||
|
||||
public function getTemplateApplicationTransaction() {
|
||||
return new PhabricatorPackagesVersionTransaction();
|
||||
}
|
||||
|
||||
}
|
|
@ -154,7 +154,21 @@ final class PhabricatorPackagesPackage
|
|||
|
||||
public function destroyObjectPermanently(
|
||||
PhabricatorDestructionEngine $engine) {
|
||||
$this->delete();
|
||||
$viewer = $engine->getViewer();
|
||||
|
||||
$this->openTransaction();
|
||||
|
||||
$versions = id(new PhabricatorPackagesVersionQuery())
|
||||
->setViewer($viewer)
|
||||
->withPackagePHIDs(array($this->getPHID()))
|
||||
->execute();
|
||||
foreach ($versions as $version) {
|
||||
$engine->destroyObject($version);
|
||||
}
|
||||
|
||||
$this->delete();
|
||||
|
||||
$this->saveTransaction();
|
||||
}
|
||||
|
||||
|
||||
|
|
200
src/applications/packages/storage/PhabricatorPackagesVersion.php
Normal file
200
src/applications/packages/storage/PhabricatorPackagesVersion.php
Normal file
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersion
|
||||
extends PhabricatorPackagesDAO
|
||||
implements
|
||||
PhabricatorPolicyInterface,
|
||||
PhabricatorExtendedPolicyInterface,
|
||||
PhabricatorApplicationTransactionInterface,
|
||||
PhabricatorDestructibleInterface,
|
||||
PhabricatorSubscribableInterface,
|
||||
PhabricatorProjectInterface,
|
||||
PhabricatorConduitResultInterface {
|
||||
|
||||
protected $name;
|
||||
protected $packagePHID;
|
||||
|
||||
private $package;
|
||||
|
||||
public static function initializeNewVersion(PhabricatorUser $actor) {
|
||||
return id(new self());
|
||||
}
|
||||
|
||||
protected function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'name' => 'sort64',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_package' => array(
|
||||
'columns' => array('packagePHID', 'name'),
|
||||
'unique' => true,
|
||||
),
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
public function generatePHID() {
|
||||
return PhabricatorPHID::generateNewPHID(
|
||||
PhabricatorPackagesVersionPHIDType::TYPECONST);
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
$package = $this->getPackage();
|
||||
$full_key = $package->getFullKey();
|
||||
$name = $this->getName();
|
||||
|
||||
return "/package/{$full_key}/{$name}/";
|
||||
}
|
||||
|
||||
public function attachPackage(PhabricatorPackagesPackage $package) {
|
||||
$this->package = $package;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPackage() {
|
||||
return $this->assertAttached($this->package);
|
||||
}
|
||||
|
||||
public static function assertValidVersionName($value) {
|
||||
$length = phutil_utf8_strlen($value);
|
||||
if (!$length) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Version name "%s" is not valid: version names are required.',
|
||||
$value));
|
||||
}
|
||||
|
||||
$max_length = 64;
|
||||
if ($length > $max_length) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Version name "%s" is not valid: version names must not be '.
|
||||
'more than %s characters long.',
|
||||
$value,
|
||||
new PhutilNumber($max_length)));
|
||||
}
|
||||
|
||||
if (!preg_match('/^[A-Za-z0-9.-]+\z/', $value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Version name "%s" is not valid: version names may only contain '.
|
||||
'latin letters, digits, periods, and hyphens.',
|
||||
$value));
|
||||
}
|
||||
|
||||
if (preg_match('/^[.-]|[.-]$/', $value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Version name "%s" is not valid: version names may not start or '.
|
||||
'end with a period or hyphen.',
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorSubscribableInterface )----------------------------------- */
|
||||
|
||||
|
||||
public function isAutomaticallySubscribed($phid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* -( Policy Interface )--------------------------------------------------- */
|
||||
|
||||
|
||||
public function getCapabilities() {
|
||||
return array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
);
|
||||
}
|
||||
|
||||
public function getPolicy($capability) {
|
||||
switch ($capability) {
|
||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||
return PhabricatorPolicies::getMostOpenPolicy();
|
||||
case PhabricatorPolicyCapability::CAN_EDIT:
|
||||
return PhabricatorPolicies::POLICY_USER;
|
||||
}
|
||||
}
|
||||
|
||||
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function describeAutomaticCapability($capability) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
|
||||
|
||||
|
||||
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
|
||||
return array(
|
||||
array(
|
||||
$this->getPackage(),
|
||||
$capability,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorDestructibleInterface )----------------------------------- */
|
||||
|
||||
|
||||
public function destroyObjectPermanently(
|
||||
PhabricatorDestructionEngine $engine) {
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||
|
||||
|
||||
public function getApplicationTransactionEditor() {
|
||||
return new PhabricatorPackagesVersionEditor();
|
||||
}
|
||||
|
||||
public function getApplicationTransactionObject() {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApplicationTransactionTemplate() {
|
||||
return new PhabricatorPackagesVersionTransaction();
|
||||
}
|
||||
|
||||
public function willRenderTimeline(
|
||||
PhabricatorApplicationTransactionView $timeline,
|
||||
AphrontRequest $request) {
|
||||
return $timeline;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorConduitResultInterface )---------------------------------- */
|
||||
|
||||
|
||||
public function getFieldSpecificationsForConduit() {
|
||||
return array(
|
||||
id(new PhabricatorConduitSearchFieldSpecification())
|
||||
->setKey('name')
|
||||
->setType('string')
|
||||
->setDescription(pht('The name of the version.')),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFieldValuesForConduit() {
|
||||
return array(
|
||||
'name' => $this->getName(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getConduitSearchAttachments() {
|
||||
return array();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionTransaction
|
||||
extends PhabricatorModularTransaction {
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'packages';
|
||||
}
|
||||
|
||||
public function getApplicationTransactionType() {
|
||||
return PhabricatorPackagesVersionPHIDType::TYPECONST;
|
||||
}
|
||||
|
||||
public function getBaseTransactionClass() {
|
||||
return 'PhabricatorPackagesVersionTransactionType';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesPackageDatasource
|
||||
extends PhabricatorTypeaheadDatasource {
|
||||
|
||||
public function getBrowseTitle() {
|
||||
return pht('Browse Packagess');
|
||||
}
|
||||
|
||||
public function getPlaceholderText() {
|
||||
return pht('Type a package name...');
|
||||
}
|
||||
|
||||
public function getDatasourceApplicationClass() {
|
||||
return 'PhabricatorPackagesApplication';
|
||||
}
|
||||
|
||||
public function loadResults() {
|
||||
$viewer = $this->getViewer();
|
||||
$raw_query = $this->getRawQuery();
|
||||
|
||||
$package_query = id(new PhabricatorPackagesPackageQuery());
|
||||
$packages = $this->executeQuery($package_query);
|
||||
|
||||
$results = array();
|
||||
foreach ($packages as $package) {
|
||||
$results[] = id(new PhabricatorTypeaheadResult())
|
||||
->setName($package->getName())
|
||||
->setPHID($package->getPHID());
|
||||
}
|
||||
|
||||
return $this->filterResultsAgainstTokens($results);
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,8 @@ final class PhabricatorPackagesPackagePublisherTransaction
|
|||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
|
||||
$current_value = $object->getPublisherPHID();
|
||||
if ($this->isEmptyTextTransaction($current_value, $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht(
|
||||
'You must select a publisher when creating a package.'));
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionNameTransaction
|
||||
extends PhabricatorPackagesVersionTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'packages.version.name';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getName();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setName($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the name of this version from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
public function getTitleForFeed() {
|
||||
return pht(
|
||||
'%s updated the name for %s from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderObject(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Versions must have a name.'));
|
||||
return $errors;
|
||||
}
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$value = $xaction->getNewValue();
|
||||
try {
|
||||
PhabricatorPackagesVersion::assertValidVersionName($value);
|
||||
} catch (Exception $ex) {
|
||||
$errors[] = $this->newInvalidError($ex->getMessage(), $xaction);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->isNewObject()) {
|
||||
foreach ($xactions as $xaction) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('Once a version is created, its name can not be changed.'),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPackagesVersionPackageTransaction
|
||||
extends PhabricatorPackagesVersionTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'packages.version.package';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getPackagePHID();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setPackagePHID($value);
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
if ($this->isEmptyTextTransaction($object->getPackagePHID(), $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht(
|
||||
'You must select a package when creating a version'));
|
||||
return $errors;
|
||||
}
|
||||
|
||||
if (!$this->isNewObject()) {
|
||||
foreach ($xactions as $xaction) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('Once a version is created, its package can not be changed.'),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
$viewer = $this->getActor();
|
||||
foreach ($xactions as $xaction) {
|
||||
$package_phid = $xaction->getNewValue();
|
||||
|
||||
$package = id(new PhabricatorPackagesPackageQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($package_phid))
|
||||
->setRaisePolicyExceptions(false)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
|
||||
if (!$package) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Package "%s" is invalid: the package must exist and you '.
|
||||
'must have permission to edit it in order to create a new '.
|
||||
'package.',
|
||||
$package_phid),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
|
||||
$object->attachPackage($package);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorPackagesVersionTransactionType
|
||||
extends PhabricatorPackagesTransactionType {}
|
Loading…
Reference in a new issue