Page MenuHomePhabricator

D16981.id40860.diff
No OneTemporary

D16981.id40860.diff

diff --git a/resources/sql/autopatches/20161201.release.1.sql b/resources/sql/autopatches/20161201.release.1.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161201.release.1.sql
@@ -0,0 +1,58 @@
+CREATE TABLE {$NAMESPACE}_release.release_release (
+ `id` INT unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY ,
+ `phid` VARBINARY(64) NOT NULL,
+ `name` VARCHAR(128) COLLATE {$COLLATE_SORT} NOT NULL,
+ `description` LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ `state` VARCHAR(12) COLLATE {$COLLATE_TEXT} NOT NULL,
+ `mailKey` BINARY(20) NOT NULL,
+ `dateCreated` INT unsigned NOT NULL,
+ `dateModified` INT unsigned NOT NULL,
+ `details` LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL,
+ `releaseTemplateKey` VARCHAR(12) COLLATE {$COLLATE_TEXT} NOT NULL,
+ `viewPolicy` VARBINARY(64) NOT NULL,
+ `editPolicy` VARBINARY(64) NOT NULL,
+
+ UNIQUE KEY `key_phid` (`phid`),
+ KEY `key_name` (`name`),
+ KEY `key_state` (`state`)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_release.release_releasetransaction (
+ 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};
+
+CREATE TABLE {$NAMESPACE}_release.release_releasetransaction_comment (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ phid VARBINARY(64) NOT NULL,
+ transactionPHID VARBINARY(64),
+ authorPHID VARBINARY(64) NOT NULL,
+ viewPolicy VARBINARY(64) NOT NULL,
+ editPolicy VARBINARY(64) NOT NULL,
+ commentVersion INT UNSIGNED NOT NULL,
+ content LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ contentSource LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ isDeleted BOOL NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ dateModified INT UNSIGNED NOT NULL,
+
+ UNIQUE KEY `key_phid` (phid),
+ UNIQUE KEY `key_version` (transactionPHID, commentVersion)
+
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20161201.release.2.edge.sql b/resources/sql/autopatches/20161201.release.2.edge.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161201.release.2.edge.sql
@@ -0,0 +1,16 @@
+CREATE TABLE {$NAMESPACE}_release.edge (
+ src VARBINARY(64) NOT NULL,
+ type INT UNSIGNED NOT NULL,
+ dst VARBINARY(64) NOT NULL,
+ dateCreated INT UNSIGNED NOT NULL,
+ seq INT UNSIGNED NOT NULL,
+ dataID INT UNSIGNED,
+ PRIMARY KEY (src, type, dst),
+ KEY `src` (src, type, dateCreated, seq),
+ UNIQUE KEY `key_dst` (dst, type, src)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_release.edgedata (
+ id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ data LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
diff --git a/resources/sql/autopatches/20161201.release.3.custom.sql b/resources/sql/autopatches/20161201.release.3.custom.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161201.release.3.custom.sql
@@ -0,0 +1,25 @@
+CREATE TABLE {$NAMESPACE}_release.release_customfieldstorage (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ fieldIndex BINARY(12) NOT NULL,
+ fieldValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
+ UNIQUE KEY (objectPHID, fieldIndex)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_release.release_customfieldstringindex (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ indexKey BINARY(12) NOT NULL,
+ indexValue LONGTEXT NOT NULL COLLATE {$COLLATE_SORT},
+ KEY `key_join` (objectPHID, indexKey, indexValue(64)),
+ KEY `key_find` (indexKey, indexValue(64))
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+
+CREATE TABLE {$NAMESPACE}_release.release_customfieldnumericindex (
+ id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ objectPHID VARBINARY(64) NOT NULL,
+ indexKey BINARY(12) NOT NULL,
+ indexValue BIGINT NOT NULL,
+ KEY `key_join` (objectPHID, indexKey, indexValue),
+ KEY `key_find` (indexKey, indexValue)
+) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
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
@@ -3500,6 +3500,33 @@
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
+ 'PhabricatorReleaseApplication' => 'applications/release/application/PhabricatorReleaseApplication.php',
+ 'PhabricatorReleaseDAO' => 'applications/release/storage/PhabricatorReleaseDAO.php',
+ 'PhabricatorReleaseRelease' => 'applications/release/storage/PhabricatorReleaseRelease.php',
+ 'PhabricatorReleaseReleaseCurrentRefTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseCurrentRefTransaction.php',
+ 'PhabricatorReleaseReleaseCutpointTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseCutpointTransaction.php',
+ 'PhabricatorReleaseReleaseDatasource' => 'applications/release/typeahead/PhabricatorReleaseReleaseDatasource.php',
+ 'PhabricatorReleaseReleaseDefaultEditCapability' => 'applications/release/capability/PhabricatorReleaseReleaseDefaultEditCapability.php',
+ 'PhabricatorReleaseReleaseDefaultViewCapability' => 'applications/release/capability/PhabricatorReleaseReleaseDefaultViewCapability.php',
+ 'PhabricatorReleaseReleaseDescribeTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseDescribeTransaction.php',
+ 'PhabricatorReleaseReleaseEditConduitAPIMethod' => 'applications/release/conduit/PhabricatorReleaseReleaseEditConduitAPIMethod.php',
+ 'PhabricatorReleaseReleaseEditEngine' => 'applications/release/editor/PhabricatorReleaseReleaseEditEngine.php',
+ 'PhabricatorReleaseReleaseEditor' => 'applications/release/editor/PhabricatorReleaseReleaseEditor.php',
+ 'PhabricatorReleaseReleaseNameTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseNameTransaction.php',
+ 'PhabricatorReleaseReleasePHIDType' => 'applications/release/phid/PhabricatorReleaseReleasePHIDType.php',
+ 'PhabricatorReleaseReleaseQuery' => 'applications/release/query/PhabricatorReleaseReleaseQuery.php',
+ 'PhabricatorReleaseReleaseReplyHandler' => 'applications/release/mail/PhabricatorReleaseReleaseReplyHandler.php',
+ 'PhabricatorReleaseReleaseSearchConduitAPIMethod' => 'applications/release/conduit/PhabricatorReleaseReleaseSearchConduitAPIMethod.php',
+ 'PhabricatorReleaseReleaseSearchEngine' => 'applications/release/query/PhabricatorReleaseReleaseSearchEngine.php',
+ 'PhabricatorReleaseReleaseStateTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseStateTransaction.php',
+ 'PhabricatorReleaseReleaseTemplateTransaction' => 'applications/release/xaction/PhabricatorReleaseReleaseTemplateTransaction.php',
+ 'PhabricatorReleaseReleaseTransaction' => 'applications/release/storage/PhabricatorReleaseReleaseTransaction.php',
+ 'PhabricatorReleaseReleaseTransactionComment' => 'applications/release/storage/PhabricatorReleaseReleaseTransactionComment.php',
+ 'PhabricatorReleaseReleaseTransactionQuery' => 'applications/release/query/PhabricatorReleaseReleaseTransactionQuery.php',
+ 'PhabricatorReleaseReleaseTransactionType' => 'applications/release/xaction/PhabricatorReleaseReleaseTransactionType.php',
+ 'PhabricatorReleaseRemarkupRule' => 'applications/release/remarkup/PhabricatorReleaseRemarkupRule.php',
+ 'PhabricatorReleaseSchemaSpec' => 'applications/release/storage/PhabricatorReleaseSchemaSpec.php',
+ 'PhabricatorReleaseTemplate' => 'applications/release/template/PhabricatorReleaseTemplate.php',
'PhabricatorReleephApplication' => 'applications/releeph/application/PhabricatorReleephApplication.php',
'PhabricatorReleephApplicationConfigOptions' => 'applications/releeph/config/PhabricatorReleephApplicationConfigOptions.php',
'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php',
@@ -4424,6 +4451,13 @@
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
+ 'ReleaseCustomField' => 'applications/release/customfield/ReleaseCustomField.php',
+ 'ReleaseCustomFieldNumericIndex' => 'applications/release/storage/ReleaseCustomFieldNumericIndex.php',
+ 'ReleaseCustomFieldStorage' => 'applications/release/storage/ReleaseCustomFieldStorage.php',
+ 'ReleaseCustomFieldStringIndex' => 'applications/release/storage/ReleaseCustomFieldStringIndex.php',
+ 'ReleaseReleaseEditController' => 'applications/release/controller/ReleaseReleaseEditController.php',
+ 'ReleaseReleaseListController' => 'applications/release/controller/ReleaseReleaseListController.php',
+ 'ReleaseReleaseViewController' => 'applications/release/controller/ReleaseReleaseViewController.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
@@ -8611,6 +8645,42 @@
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRegistrationProfile' => 'Phobject',
+ 'PhabricatorReleaseApplication' => 'PhabricatorApplication',
+ 'PhabricatorReleaseDAO' => 'PhabricatorLiskDAO',
+ 'PhabricatorReleaseRelease' => array(
+ 'PhabricatorReleaseDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorSubscribableInterface',
+ 'PhabricatorFlaggableInterface',
+ 'PhabricatorMentionableInterface',
+ 'PhabricatorConduitResultInterface',
+ 'PhabricatorCustomFieldInterface',
+ 'PhabricatorPolicyInterface',
+ ),
+ 'PhabricatorReleaseReleaseCurrentRefTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleaseCutpointTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleaseDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'PhabricatorReleaseReleaseDefaultEditCapability' => 'PhabricatorPolicyCapability',
+ 'PhabricatorReleaseReleaseDefaultViewCapability' => 'PhabricatorPolicyCapability',
+ 'PhabricatorReleaseReleaseDescribeTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleaseEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
+ 'PhabricatorReleaseReleaseEditEngine' => 'PhabricatorEditEngine',
+ 'PhabricatorReleaseReleaseEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'PhabricatorReleaseReleaseNameTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleasePHIDType' => 'PhabricatorPHIDType',
+ 'PhabricatorReleaseReleaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorReleaseReleaseReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
+ 'PhabricatorReleaseReleaseSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
+ 'PhabricatorReleaseReleaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhabricatorReleaseReleaseStateTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleaseTemplateTransaction' => 'PhabricatorReleaseReleaseTransactionType',
+ 'PhabricatorReleaseReleaseTransaction' => 'PhabricatorModularTransaction',
+ 'PhabricatorReleaseReleaseTransactionComment' => 'PhabricatorApplicationTransactionComment',
+ 'PhabricatorReleaseReleaseTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'PhabricatorReleaseReleaseTransactionType' => 'PhabricatorModularTransactionType',
+ 'PhabricatorReleaseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+ 'PhabricatorReleaseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+ 'PhabricatorReleaseTemplate' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
@@ -9773,6 +9843,13 @@
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
+ 'ReleaseCustomField' => 'PhabricatorCustomField',
+ 'ReleaseCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
+ 'ReleaseCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
+ 'ReleaseCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
+ 'ReleaseReleaseEditController' => 'PhabricatorController',
+ 'ReleaseReleaseListController' => 'PhabricatorController',
+ 'ReleaseReleaseViewController' => 'PhabricatorController',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => array(
'ReleephDAO',
diff --git a/src/applications/release/application/PhabricatorReleaseApplication.php b/src/applications/release/application/PhabricatorReleaseApplication.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/application/PhabricatorReleaseApplication.php
@@ -0,0 +1,66 @@
+<?php
+
+final class PhabricatorReleaseApplication extends PhabricatorApplication {
+
+ public function getName() {
+ return pht('Release');
+ }
+
+ public function getShortDescription() {
+ return pht('Release Code');
+ }
+
+ public function getFlavorText() {
+ return pht('Deliver Software');
+ }
+
+ public function getBaseURI() {
+ return '/release/';
+ }
+
+ public function getIcon() {
+ return 'fa-code-fork';
+ }
+
+ public function getApplicationGroup() {
+ return self::GROUP_UTILITIES;
+ }
+
+ public function isPrototype() {
+ return true;
+ }
+
+ public function getRemarkupRules() {
+ return array(
+ new PhabricatorReleaseRemarkupRule(),
+ );
+ }
+
+ protected function getCustomCapabilities() {
+ return array(
+ // TODO create
+ PhabricatorReleaseReleaseDefaultViewCapability::CAPABILITY => array(
+ 'caption' => pht('Default edit policy for newly created releases.'),
+ 'template' => PhabricatorReleaseReleasePHIDType::TYPECONST,
+ 'default' => PhabricatorPolicies::POLICY_PUBLIC,
+ ),
+ PhabricatorReleaseReleaseDefaultEditCapability::CAPABILITY => array(
+ 'caption' => pht('Default view policy for newly created releases.'),
+ 'template' => PhabricatorReleaseReleasePHIDType::TYPECONST,
+ 'default' => PhabricatorPolicies::POLICY_NOONE,
+ ),
+ );
+ }
+ public function getRoutes() {
+ return array(
+ '/X(?P<id>[1-9]\d*)' => 'ReleaseReleaseViewController',
+ '/release/' => array(
+ $this->getEditRoutePattern('edit/') => 'ReleaseReleaseEditController',
+ '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleaseReleaseListController',
+ // TODO new release
+
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/release/capability/PhabricatorReleaseReleaseDefaultEditCapability.php b/src/applications/release/capability/PhabricatorReleaseReleaseDefaultEditCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/capability/PhabricatorReleaseReleaseDefaultEditCapability.php
@@ -0,0 +1,12 @@
+<?php
+
+final class PhabricatorReleaseReleaseDefaultEditCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'release.default.edit';
+
+ public function getCapabilityName() {
+ return pht('Default Release Edit Policy');
+ }
+
+}
diff --git a/src/applications/release/capability/PhabricatorReleaseReleaseDefaultViewCapability.php b/src/applications/release/capability/PhabricatorReleaseReleaseDefaultViewCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/capability/PhabricatorReleaseReleaseDefaultViewCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class PhabricatorReleaseReleaseDefaultViewCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'release.default.view';
+
+ public function getCapabilityName() {
+ return pht('Default Release View Policy');
+ }
+
+ public function shouldAllowPublicPolicySetting() {
+ return true;
+ }
+
+}
diff --git a/src/applications/release/conduit/PhabricatorReleaseReleaseEditConduitAPIMethod.php b/src/applications/release/conduit/PhabricatorReleaseReleaseEditConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/conduit/PhabricatorReleaseReleaseEditConduitAPIMethod.php
@@ -0,0 +1,19 @@
+<?php
+
+final class PhabricatorReleaseReleaseEditConduitAPIMethod
+ extends PhabricatorEditEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'release.edit';
+ }
+
+ public function newEditEngine() {
+ return new PhabricatorReleaseReleaseEditEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht(
+ 'Apply transactions to create a new Release or edit an existing one.');
+ }
+
+}
diff --git a/src/applications/release/conduit/PhabricatorReleaseReleaseSearchConduitAPIMethod.php b/src/applications/release/conduit/PhabricatorReleaseReleaseSearchConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/conduit/PhabricatorReleaseReleaseSearchConduitAPIMethod.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorReleaseReleaseSearchConduitAPIMethod
+ extends PhabricatorSearchEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'release.search';
+ }
+
+ public function newSearchEngine() {
+ return new PhabricatorReleaseReleaseSearchEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht('Read information about releases.');
+ }
+
+}
diff --git a/src/applications/release/controller/ReleaseReleaseEditController.php b/src/applications/release/controller/ReleaseReleaseEditController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/controller/ReleaseReleaseEditController.php
@@ -0,0 +1,19 @@
+<?php
+
+final class ReleaseReleaseEditController extends PhabricatorController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $engine = id(new PhabricatorReleaseReleaseEditEngine())
+ ->setController($this);
+
+ $id = $request->getURIData('id');
+ if (!$id) {
+ // ??
+ }
+
+ return $engine->buildResponse();
+ }
+
+}
diff --git a/src/applications/release/controller/ReleaseReleaseListController.php b/src/applications/release/controller/ReleaseReleaseListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/controller/ReleaseReleaseListController.php
@@ -0,0 +1,35 @@
+<?php
+
+final class ReleaseReleaseListController extends PhabricatorController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $crumbs = $this->buildApplicationCrumbs();
+ $crumbs->addTextCrumb('Releases');
+
+ $querykey = $request->getURIData('queryKey');
+
+ $controller = id(new PhabricatorApplicationSearchController())
+ ->setQueryKey($querykey)
+ ->setSearchEngine(new PhabricatorReleaseReleaseSearchEngine())
+ ->setNavigation($this->buildSideNavView());
+ return $this->delegateToController($controller);
+ }
+
+ public function buildSideNavView() {
+ $viewer = $this->getViewer();
+
+ $nav = new AphrontSideNavFilterView();
+ $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
+
+ id(new PhabricatorReleaseReleaseSearchEngine())
+ ->setViewer($viewer)
+ ->addNavigationItems($nav->getMenu());
+
+ $nav->selectFilter(null);
+
+ return $nav;
+ }
+
+}
diff --git a/src/applications/release/controller/ReleaseReleaseViewController.php b/src/applications/release/controller/ReleaseReleaseViewController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/controller/ReleaseReleaseViewController.php
@@ -0,0 +1,205 @@
+<?php
+
+final class ReleaseReleaseViewController extends PhabricatorController {
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $id = $request->getURIData('id');
+
+ $crumbs = $this->buildApplicationCrumbs();
+
+ $release = id(new PhabricatorReleaseReleaseQuery())
+ ->setViewer($viewer)
+ ->withIds(array($id))
+ ->executeOne();
+ if (!$release) {
+ return new Aphront404Response();
+ }
+
+ $crumbs->addTextCrumb($release->getMonogram());
+ $release_name = $release->getName();
+
+ $properties = id(new PHUIPropertyListView())
+ ->addProperty(pht('Release Type'), $release->getReleaseTemplateName())
+ ->addProperty(pht('Status'), $release->getStateName());
+
+ $field_list = PhabricatorCustomField::getObjectFields(
+ $release,
+ PhabricatorCustomField::ROLE_VIEW);
+ $field_list
+ ->setViewer($viewer)
+ ->readFieldsFromStorage($release);
+
+ $properties->invokeWillRenderEvent();
+
+ $field_list->appendFieldsToPropertyList(
+ $release,
+ $viewer,
+ $properties);
+
+ $description = $release->getDescription();
+ if (strlen($description)) {
+ $properties
+ ->addSectionHeader('Description')
+ ->addTextContent(
+ new PHUIRemarkupView($viewer, $description));
+ }
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader('X'.$release->getID().' '.$release_name)
+ ->setUser($viewer)
+ ->setPolicyObject($release);
+
+ $curtain = $this->buildCurtain($release);
+
+ $timeline = $this->buildTransactionTimeline(
+ $release,
+ new PhabricatorReleaseReleaseTransactionQuery());
+ $timeline->setQuoteRef($release->getMonogram());
+
+ $comment_view = id(new PhabricatorReleaseReleaseEditEngine())
+ ->setViewer($viewer)
+ ->buildEditEngineCommentView($release);
+ $comment_view->setTransactionTimeline($timeline);
+
+ $commits = $this->buildCommitsSection($viewer, $release);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(array(
+ $timeline,
+ $comment_view,
+ ))
+ ->addPropertySection(pht('Details'), $properties)
+ ->addPropertySection(pht('Repositories'), $commits);
+
+ return $this->newPage()
+ ->setTitle('X'.$release->getID().' '.$release_name)
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $release->getPHID(),
+ ))
+ ->appendChild(
+ array(
+ $view,
+ ));
+ }
+
+ private function buildCommitsSection($viewer, $release) {
+ $cutpoints = $release->getCutpoints();
+ $currentrefs = $release->getCurrentRefs();
+
+ $repo_phids = array_keys($cutpoints);
+
+ if (!$repo_phids) {
+ return null;
+ }
+
+ $repositories = id(new PhabricatorRepositoryQuery())
+ ->setViewer($viewer)
+ ->withPHIDs($repo_phids)
+ ->execute();
+ $repositories = mpull($repositories, null, 'getPHID');
+
+ $body = array();
+
+ $commit_phids = id(new DiffusionCommitQuery())
+ ->setViewer($viewer)
+ ->withIdentifiers($cutpoints)
+ ->execute();
+ $commit_phids = mpull($commit_phids, 'getPHID', 'getCommitIdentifier');
+
+
+ $rows = array();
+ foreach ($repositories as $phid => $repo) {
+ $r = $viewer->renderHandle($phid);
+ $cut = idx($cutpoints, $phid);
+
+ $current = idx($currentrefs, $phid);
+
+ $cp = null;
+ if ($current) {
+ if ($cut) {
+ try {
+ list($cp) = $repo->execxLocalCommand(
+ 'rev-list --count %s..%s',
+ $cut,
+ $current);
+
+ if ($cp == 0) {
+ $cp = '--';
+ } else {
+ $cp = hsprintf('%d cherry-picks', $cp);
+ }
+ } catch (Exception $e) {
+ $cp = '??';
+ }
+ }
+
+ if ($release->isRefMutable($current)) {
+ $short = $release->getSimpleRefName($current);
+ $href = hsprintf(
+ '/diffusion/%d/history/%s/',
+ $repo->getID(),
+ $short);
+ $link = phutil_tag('a', array('href' => $href), $short);
+
+ $branch = hsprintf('Release branch: %s', $link);
+ } else {
+ $branch = 'Final commit: '.$repo->formatCommitName($current);
+ }
+
+ } else {
+ $branch = phutil_tag('i', array(), 'No current state');
+ }
+
+ if ($cut) {
+ $phid = idx($commit_phids, $cut);
+ if ($phid) {
+ $cut = $viewer->renderHandle($phid);
+ }
+ } else {
+ $cut = phutil_tag('i', array(), 'No cutpoint specified');
+ }
+
+ $rows[] = array($r, $cut, $cp, $branch);
+ }
+
+ return id(new AphrontTableView($rows))
+ ->setHeaders(array(
+ pht('Repository'),
+ pht('Cut From'),
+ pht('Cherry-picked commits'),
+ pht('Current State'),
+ ))
+ ->setColumnClasses(array(
+ 'link',
+ null,
+ null,
+ 'right',
+ ));
+ }
+
+ private function buildCurtain($release) {
+ $viewer = $this->getViewer();
+ $curtain = $this->newCurtainView($release);
+
+ $id = $release->getID();
+ $edit_uri = $this->getApplicationURI("edit/{$id}/");
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $release,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $curtain->addAction(id(new PhabricatorActionView())
+ ->setName(pht('Edit Release'))
+ ->setIcon('fa-pencil')
+ ->setHref($edit_uri)
+ ->setDisabled(!$can_edit));
+
+ return $curtain;
+ }
+}
diff --git a/src/applications/release/customfield/ReleaseCustomField.php b/src/applications/release/customfield/ReleaseCustomField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/customfield/ReleaseCustomField.php
@@ -0,0 +1,17 @@
+<?php
+
+abstract class ReleaseCustomField
+ extends PhabricatorCustomField {
+
+ public function newStorageObject() {
+ return new ReleaseCustomFieldStorage();
+ }
+
+ protected function newStringIndexStorage() {
+ return new ReleaseCustomFieldStringIndex();
+ }
+
+ protected function newNumericIndexStorage() {
+ return new ReleaseCustomFieldNumericIndex();
+ }
+}
diff --git a/src/applications/release/editor/PhabricatorReleaseReleaseEditEngine.php b/src/applications/release/editor/PhabricatorReleaseReleaseEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/editor/PhabricatorReleaseReleaseEditEngine.php
@@ -0,0 +1,154 @@
+<?php
+
+final class PhabricatorReleaseReleaseEditEngine
+ extends PhabricatorEditEngine {
+
+ const ENGINECONST = 'release.release';
+
+ public function getEngineName() {
+ return pht('Releases');
+ }
+
+ public function isEngineConfigurable() {
+ // This hides it from the quick-create menu, because the built-in form
+ // doesn't actually do anything.
+ return false;
+ }
+
+ protected function getObjectName() {
+ return pht('Release');
+ }
+
+ public function getSummaryHeader() {
+ return pht('Configure Releases Forms');
+ }
+
+ public function getSummaryText() {
+ return pht('Configure creation and editing Releases.');
+ }
+
+ public function getEngineApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ protected function newEditableObject() {
+ $viewer = $this->getViewer();
+ return PhabricatorReleaseRelease::initializeNewRelease($viewer);
+ }
+
+ protected function newObjectQuery() {
+ return id(new PhabricatorReleaseReleaseQuery());
+ }
+
+ protected function getObjectCreateTitleText($object) {
+ return pht('Create New Release');
+ }
+
+ protected function getObjectEditTitleText($object) {
+ return pht('Edit Release %s', $object->getName());
+ }
+
+ protected function getEditorURI() {
+ return '/release/edit/';
+ }
+
+ protected function getObjectEditShortText($object) {
+ return $object->getName();
+ }
+
+ protected function getObjectCreateShortText() {
+ return pht('New Release');
+ }
+
+ protected function getCreateNewObjectPolicy() {
+ return $this->getApplication()->getPolicy(
+ PhabricatorReleaseReleaseDefaultEditCapability::CAPABILITY);
+ }
+
+ protected function getCommentViewHeaderText($object) {
+ return pht('Discuss Release');
+ }
+
+ protected function getCommentViewButtonText($object) {
+ return pht('Comment');
+ }
+
+ protected function getObjectViewURI($object) {
+ return $object->getURI();
+ }
+
+ protected function buildCustomEditFields($object) {
+ $templates = PhabricatorReleaseTemplate::getTemplatesMap();
+ $used_template = $object->getReleaseTemplateKey();
+ if (strlen($used_template) && idx($templates, $used_template) == null) {
+ // make sure current value is allowed.
+ $templates[$used_template] = $used_template;
+ }
+
+ $states = PhabricatorReleaseRelease::getStatesMap();
+
+ $is_edit_form = !$this->getIsCreate();
+
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setKey('name')
+ ->setLabel(pht('Name'))
+ ->setIsRequired(true)
+ ->setTransactionType(
+ PhabricatorReleaseReleaseNameTransaction::TRANSACTIONTYPE)
+ ->setDescription(pht('name'))
+ ->setConduitDescription(pht('Rename the Release.'))
+ ->setConduitTypeDescription(pht('New Release title.'))
+ ->setValue($object->getName()),
+ id(new PhabricatorSelectEditField())
+ ->setKey('type')
+ ->setLabel(pht('Release Type'))
+ ->setTransactionType(
+ PhabricatorReleaseReleaseTemplateTransaction::TRANSACTIONTYPE)
+ ->setIsCopyable(true)
+ ->setOptions($templates)
+ ->setIsLocked($is_edit_form)
+ ->setDescription('Libraries / Apps / Manage /...')
+ ->setConduitTypeDescription(pht('Release Type.'))
+ ->setValue($object->getReleaseTemplateKey()),
+ id(new PhabricatorSelectEditField())
+ ->setKey('state')
+ ->setLabel(pht('State'))
+ ->setTransactionType(
+ PhabricatorReleaseReleaseStateTransaction::TRANSACTIONTYPE)
+ ->setIsCopyable(true)
+ ->setOptions($states)
+ ->setValue($object->getState()),
+ id(new PhabricatorRemarkupEditField())
+ ->setKey('description')
+ ->setLabel(pht('Description'))
+ ->setTransactionType(
+ PhabricatorReleaseReleaseDescribeTransaction::TRANSACTIONTYPE)
+ ->setDescription(pht('Free-form text.'))
+ ->setConduitDescription(pht('Description.'))
+ ->setConduitTypeDescription(pht('Remarkup'))
+ ->setValue($object->getDescription()),
+ id(new PhabricatorConduitEditField())
+ ->setKey('cutpoints')
+ ->setLabel(pht('Cut Points'))
+ ->setTransactionType(
+ PhabricatorReleaseReleaseCutpointTransaction::TRANSACTIONTYPE)
+ ->setDescription(pht('Commits where the revision was cut from'))
+ ->setConduitTypeDescription('Map repo phid -> commit')
+ ->setIsConduitOnly(true)
+ ->setValue($object->getDetail(
+ PhabricatorReleaseRelease::DETAIL_CUTPOINTS)),
+ id(new PhabricatorConduitEditField())
+ ->setKey('currentrefs')
+ ->setLabel(pht('Current References'))
+ ->setTransactionType(
+ PhabricatorReleaseReleaseCurrentRefTransaction::TRANSACTIONTYPE)
+ ->setDescription('Commits/branches where the revision currently is')
+ ->setConduitTypeDescription('Map repo phid -> commit/branch')
+ ->setIsConduitOnly(true)
+ ->setValue($object->getDetail(
+ PhabricatorReleaseRelease::DETAIL_CURRENTREF)),
+ );
+ }
+
+}
diff --git a/src/applications/release/editor/PhabricatorReleaseReleaseEditor.php b/src/applications/release/editor/PhabricatorReleaseReleaseEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/editor/PhabricatorReleaseReleaseEditor.php
@@ -0,0 +1,126 @@
+<?php
+
+final class PhabricatorReleaseReleaseEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ private $modularTypes;
+
+ public function getEditorApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ public function getEditorObjectsDescription() {
+ return pht('Releases');
+ }
+
+ public function getCreateObjectTitle($author, $object) {
+ return pht('%s created this release.', $author);
+ }
+
+ public function getCreateObjectTitleForFeed($author, $object) {
+ return pht('%s created %s.', $author, $object);
+ }
+
+ protected function applyInitialEffects(
+ PhabricatorLiskDAO $object,
+ array $xactions) {}
+
+ public function getTransactionTypes() {
+ $types = parent::getTransactionTypes();
+
+ $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
+ $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+ $types[] = PhabricatorTransactions::TYPE_COMMENT;
+
+ return $types;
+ }
+
+ protected function shouldApplyInitialEffects(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+ return true;
+ }
+
+ protected function shouldSendMail(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ return true;
+ }
+
+ protected function getMailSubjectPrefix() {
+ return '[Release]';
+ }
+
+ public function getMailTagsMap() {
+ return array(
+ PhabricatorReleaseReleaseTransaction::MAILTAG_STATE =>
+ pht('A Release state is updated.'),
+ PhabricatorReleaseReleaseTransaction::MAILTAG_OTHER =>
+ pht('Other Release activity not listed above occurs.'),
+ );
+ }
+
+ protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+ return id(new PhabricatorReleaseReleaseReplyHandler())
+ ->setMailReceiver($object);
+ }
+
+
+ protected function buildMailTemplate(PhabricatorLiskDAO $object) {
+ $id = $object->getID();
+ $name = $object->getName();
+
+ return id(new PhabricatorMetaMTAMail())
+ ->setSubject("X{$id} {$name}")
+ ->addHeader('Thread-Topic', "Release {$id}");
+ }
+
+ protected function shouldPublishFeedStory(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ return true;
+ }
+
+ protected function supportsSearch() {
+ return false;
+ }
+
+ protected function expandTransaction(
+ PhabricatorLiskDAO $release,
+ PhabricatorApplicationTransaction $xaction) {
+ $xactions = parent::expandTransaction($release, $xaction);
+
+ // TODO move this to PhabricatorApplicationTransactionEditor, or get rid of.
+ if ($xaction instanceof PhabricatorReleaseReleaseTransaction) {
+ $xactions = array_merge($xactions, $xaction->expandTransaction($release));
+ }
+
+ return $xactions;
+ }
+
+ protected function getMailTo(PhabricatorLiskDAO $object) {
+ $tos = array();
+ return $tos;
+ }
+
+ protected function buildMailBody(
+ PhabricatorLiskDAO $object,
+ array $xactions) {
+
+ $body = parent::buildMailBody($object, $xactions);
+
+ foreach ($xactions as $xaction) {
+ $type = $xaction->getTransactionType();
+ $new = $xaction->getNewValue();
+ }
+
+ $body->addLinkSection(
+ pht('RELEASE DETAIL'),
+ PhabricatorEnv::getProductionURI($object->getURI()));
+
+ return $body;
+ }
+
+}
diff --git a/src/applications/release/mail/PhabricatorReleaseReleaseReplyHandler.php b/src/applications/release/mail/PhabricatorReleaseReleaseReplyHandler.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/mail/PhabricatorReleaseReleaseReplyHandler.php
@@ -0,0 +1,20 @@
+<?php
+
+final class PhabricatorReleaseReleaseReplyHandler
+ extends PhabricatorApplicationTransactionReplyHandler {
+
+ public function validateMailReceiver($mail_receiver) {
+ if (!($mail_receiver instanceof PhabricatorReleaseRelease)) {
+ throw new Exception(
+ pht('Mail receiver is not a %s.', 'PhabricatorReleaseRelease'));
+ }
+ }
+
+ public function getObjectPrefix() {
+ return PhabricatorReleaseReleasePHIDType::TYPECONST;
+ }
+
+ protected function shouldCreateCommentFromMailBody() {
+ return false;
+ }
+}
diff --git a/src/applications/release/phid/PhabricatorReleaseReleasePHIDType.php b/src/applications/release/phid/PhabricatorReleaseReleasePHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/phid/PhabricatorReleaseReleasePHIDType.php
@@ -0,0 +1,81 @@
+<?php
+
+final class PhabricatorReleaseReleasePHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'RELS';
+
+ public function getTypeName() {
+ return pht('Release');
+ }
+
+ public function newObject() {
+ return new PhabricatorReleaseRelease();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PhabricatorReleaseReleaseQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function getTypeIcon() {
+ return 'fa-code-fork';
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $release = $objects[$phid];
+
+ $id = $release->getID();
+ $name = $release->getName();
+
+ $handle->setName("X{$id}");
+ $handle->setURI("/X{$id}");
+ $handle->setFullName("X{$id}: {$name}");
+
+ if ($release->isClosed()) {
+ $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
+ }
+ }
+ }
+
+ public function canLoadNamedObject($name) {
+ return preg_match('/^X[1-9]\d*$/i', $name);
+ }
+
+ public function loadNamedObjects(
+ PhabricatorObjectQuery $query,
+ array $names) {
+
+ $id_map = array();
+ foreach ($names as $name) {
+ $id = (int)substr($name, 1);
+ $id_map[$id][] = $name;
+ }
+
+ $objects = id(new PhabricatorReleaseReleaseQuery())
+ ->setViewer($query->getViewer())
+ ->withIDs(array_keys($id_map))
+ ->execute();
+
+ $results = array();
+ foreach ($objects as $id => $object) {
+ foreach (idx($id_map, $id, array()) as $name) {
+ $results[$name] = $object;
+ }
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/release/query/PhabricatorReleaseReleaseQuery.php b/src/applications/release/query/PhabricatorReleaseReleaseQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/PhabricatorReleaseReleaseQuery.php
@@ -0,0 +1,173 @@
+<?php
+
+final class PhabricatorReleaseReleaseQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $names;
+ private $states;
+ private $templates;
+ private $datasourceQuery;
+
+ private $groupBy = 'group-none';
+ const GROUP_NONE = 'group-none';
+ const GROUP_TEMPLATE = 'group-template';
+ const GROUP_STATE = 'group-state';
+
+ public function withIDs(array $ids) {
+ $this->ids = $ids;
+ return $this;
+ }
+
+ public function withPHIDs(array $phids) {
+ $this->phids = $phids;
+ return $this;
+ }
+
+ public function withNames(array $names) {
+ $this->names = $names;
+ return $this;
+ }
+
+ public function withStates(array $states) {
+ $this->states = $states;
+ return $this;
+ }
+
+ public function withTemplateKeys(array $templates) {
+ $this->templates = $templates;
+ return $this;
+ }
+
+ public function withDatasourceQuery($query) {
+ $this->datasourceQuery = $query;
+ return $this;
+ }
+
+ public function newResultObject() {
+ return new PhabricatorReleaseRelease();
+ }
+
+ protected function loadPage() {
+ return $this->loadStandardPage($this->newResultObject());
+ }
+
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
+
+ if ($this->ids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'id IN (%Ld)',
+ $this->ids);
+ }
+
+ if ($this->phids !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'phid IN (%Ls)',
+ $this->phids);
+ }
+
+ if ($this->names !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'name IN (%Ls)',
+ $this->names);
+ }
+
+ if ($this->states !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'state IN (%Ls)',
+ $this->states);
+ }
+
+ if ($this->templates !== null) {
+ $where[] = qsprintf(
+ $conn,
+ 'releaseTemplateKey IN (%Ls)',
+ $this->templates);
+ }
+
+ if (strlen($this->datasourceQuery)) {
+ $where[] = qsprintf(
+ $conn,
+ 'name LIKE %~',
+ $this->datasourceQuery);
+ }
+
+ return $where;
+ }
+
+ protected function getDefaultOrderVector() {
+ return array('name');
+ }
+
+ public function getBuiltinOrders() {
+ return array(
+ 'name' => array(
+ 'vector' => array('name'),
+ 'name' => pht('Name'),
+ ),
+ 'select' => array(
+ 'vector' => array('state', 'name'),
+ 'name' => pht('Name'),
+ ),
+ ) + parent::getBuiltinOrders();
+ }
+
+ public function setGroupBy($group) {
+ $this->groupBy = $group;
+ $vector = array();
+
+ switch ($this->groupBy) {
+ case self::GROUP_NONE:
+ $vector = array();
+ break;
+ case self::GROUP_TEMPLATE:
+ $vector = array('releaseTemplateKey');
+ break;
+ case self::GROUP_STATE:
+ $vector = array('state');
+ break;
+ }
+
+ $this->setGroupVector($vector);
+
+ return $this;
+ }
+
+ public function getOrderableColumns() {
+ return parent::getOrderableColumns() + array(
+ 'name' => array(
+ 'table' => $this->getPrimaryTableAlias(),
+ 'column' => 'name',
+ 'reverse' => true,
+ 'type' => 'string',
+ 'unique' => true,
+ ),
+ 'releaseTemplateKey' => array(
+ 'table' => $this->getPrimaryTableAlias(),
+ 'column' => 'releaseTemplateKey',
+ 'reverse' => true,
+ 'type' => 'string',
+ 'unique' => false,
+ ),
+ 'state' => array(
+ 'table' => $this->getPrimaryTableAlias(),
+ 'column' => 'state',
+ 'reverse' => false,
+ 'type' => 'string',
+ 'unique' => false,
+ ),
+ );
+ }
+
+
+ public function getQueryApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+}
diff --git a/src/applications/release/query/PhabricatorReleaseReleaseSearchEngine.php b/src/applications/release/query/PhabricatorReleaseReleaseSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/PhabricatorReleaseReleaseSearchEngine.php
@@ -0,0 +1,169 @@
+<?php
+
+final class PhabricatorReleaseReleaseSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Releases');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ public function newQuery() {
+ return id(new PhabricatorReleaseReleaseQuery());
+ }
+
+ protected function buildCustomSearchFields() {
+ return array(
+ id(new PhabricatorSearchStringListField())
+ ->setLabel(pht('Name'))
+ ->setKey('name'),
+ id(new PhabricatorSearchSelectField())
+ ->setLabel('Release Type')
+ ->setKey('template')
+ ->setOptions($this->getTemplateOptions())
+ ->setDefault('all'),
+ id(new PhabricatorSearchSelectField())
+ ->setLabel('State')
+ ->setKey('state')
+ ->setOptions($this->getStateOptions())
+ ->setDefault('all'),
+ id(new PhabricatorSearchSelectField())
+ ->setLabel(pht('Group By'))
+ ->setKey('group')
+ ->setOptions($this->getGroupOptions()),
+ );
+ }
+
+ protected function buildQueryFromParameters(array $map) {
+ $query = $this->newQuery();
+
+ if (idx($map, 'name')) {
+ $query->withNames($map['name']);
+ }
+
+ $state = idx($map, 'state', 'all');
+ if ($state != 'all') {
+ if ($state == 'active') {
+ $state = PhabricatorReleaseRelease::getOpenStates();
+ } else if (!is_array($state)) {
+ $state = array($state);
+ }
+ $query->withStates($state);
+ }
+
+ $template = idx($map, 'template', 'all');
+ if ($template != 'all') {
+ $query->withTemplateKeys(array($template));
+ }
+
+ $group = idx($map, 'group');
+ $group = idx($this->getGroupValues(), $group);
+ if ($group) {
+ $query->setGroupBy($group);
+ }
+
+ return $query;
+ }
+
+ protected function getURI($path) {
+ return '/release/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array();
+
+ $names['open'] = pht('Open Releases');
+ $names['prod'] = pht('In Production');
+ $names['all'] = pht('All');
+
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ $viewer_phid = $this->requireViewer()->getPHID();
+
+ switch ($query_key) {
+ case 'all':
+ return $query;
+ case 'open':
+ return $query
+ ->setParameter('state', 'active');
+ case 'prod':
+ return $query
+ ->setParameter(
+ 'state',
+ array(PhabricatorReleaseRelease::STATE_PRODUCTION));
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+ private function getGroupOptions() {
+ return array(
+ 'none' => pht('None'),
+ 'template' => pht('Release Type'),
+ 'status' => pht('State'),
+ );
+ }
+
+ private function getGroupValues() {
+ return array(
+ 'none' => PhabricatorReleaseReleaseQuery::GROUP_NONE,
+ 'template' => PhabricatorReleaseReleaseQuery::GROUP_TEMPLATE,
+ 'status' => PhabricatorReleaseReleaseQuery::GROUP_STATE,
+ );
+ }
+
+ private function getTemplateOptions() {
+ return array('all' => 'All Types') +
+ PhabricatorReleaseTemplate::getTemplatesMap();
+ }
+
+ private function getStateOptions() {
+ return array(
+ 'all' => 'Any state',
+ 'active' => 'Active states',
+ ) + PhabricatorReleaseRelease::getStatesMap();
+ }
+
+ protected function getRequiredHandlePHIDsForResultList(
+ array $releases,
+ PhabricatorSavedQuery $query) {
+
+ return array();
+ }
+
+ protected function renderResultList(
+ array $releases,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+
+ assert_instances_of($releases, 'PhabricatorReleaseRelease');
+ $viewer = $this->requireViewer();
+
+
+ $list = new PHUIObjectItemListView();
+
+ foreach ($releases as $release) {
+ $name = $release->getName();
+
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($name)
+ ->setHref($release->getURI())
+ ->addAttribute($release->getReleaseTemplateName())
+ ->addAttribute($release->getStateName());
+
+ $list->addItem($item);
+ }
+
+ return id(new PhabricatorApplicationSearchResultView())
+ ->setObjectList($list)
+ ->setNoDataString(pht('No releases found.'));
+ }
+}
diff --git a/src/applications/release/query/PhabricatorReleaseReleaseTransactionQuery.php b/src/applications/release/query/PhabricatorReleaseReleaseTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/PhabricatorReleaseReleaseTransactionQuery.php
@@ -0,0 +1,53 @@
+<?php
+
+final class PhabricatorReleaseReleaseTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ private $newOrOldValues = null;
+ private $newValues = null;
+
+ public function withNewOrOldValues($values) {
+ $this->newOrOldValues = $values;
+ return $this;
+ }
+
+ public function withNewValues($values) {
+ $this->newValues = $values;
+ return $this;
+ }
+
+ public function getTemplateApplicationTransaction() {
+ return new PhabricatorReleaseReleaseTransaction();
+ }
+
+ protected function buildMoreWhereClauses(
+ AphrontDatabaseConnection $conn_r) {
+ $where = array();
+
+ if ($this->newOrOldValues) {
+ $jsonized = array();
+ foreach ($this->newOrOldValues as $value) {
+ $jsonized[] = phutil_json_encode($value);
+ }
+ $where[] = qsprintf(
+ $conn_r,
+ '(newValue in (%Ls) OR oldValue in (%Ls))',
+ $jsonized,
+ $jsonized);
+ }
+
+ if ($this->newValues) {
+ $jsonized = array();
+ foreach ($this->newValues as $value) {
+ $jsonized[] = phutil_json_encode($value);
+ }
+ $where[] = qsprintf(
+ $conn_r,
+ 'newValue in (%Ls)',
+ $jsonized);
+ }
+
+ return $where;
+ }
+
+}
diff --git a/src/applications/release/remarkup/PhabricatorReleaseRemarkupRule.php b/src/applications/release/remarkup/PhabricatorReleaseRemarkupRule.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/remarkup/PhabricatorReleaseRemarkupRule.php
@@ -0,0 +1,18 @@
+<?php
+
+final class PhabricatorReleaseRemarkupRule
+ extends PhabricatorObjectRemarkupRule {
+
+ protected function getObjectNamePrefix() {
+ return 'X';
+ }
+
+ protected function loadObjects(array $ids) {
+ $viewer = $this->getEngine()->getConfig('viewer');
+
+ return id(new PhabricatorReleaseReleaseQuery())
+ ->setViewer($viewer)
+ ->withIDs($ids)
+ ->execute();
+ }
+}
diff --git a/src/applications/release/storage/PhabricatorReleaseDAO.php b/src/applications/release/storage/PhabricatorReleaseDAO.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/PhabricatorReleaseDAO.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class PhabricatorReleaseDAO extends PhabricatorLiskDAO {
+
+ public function getApplicationName() {
+ return 'release';
+ }
+
+}
diff --git a/src/applications/release/storage/PhabricatorReleaseRelease.php b/src/applications/release/storage/PhabricatorReleaseRelease.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/PhabricatorReleaseRelease.php
@@ -0,0 +1,293 @@
+<?php
+
+final class PhabricatorReleaseRelease extends PhabricatorReleaseDAO
+ implements
+ PhabricatorApplicationTransactionInterface,
+ PhabricatorSubscribableInterface,
+ PhabricatorFlaggableInterface,
+ PhabricatorMentionableInterface,
+ PhabricatorConduitResultInterface,
+ PhabricatorCustomFieldInterface,
+ PhabricatorPolicyInterface {
+
+ const STATE_PLANNED = 'planned';
+ const STATE_TESTING = 'testing';
+ const STATE_PRODUCTION = 'production';
+ const STATE_OBSOLETE = 'obsolete';
+ const STATE_INVALID = 'invalid';
+
+ const DETAIL_CUTPOINTS = 'cutpoints';
+ const DETAIL_CURRENTREF = 'currentref';
+
+ protected $name;
+ protected $state = self::STATE_PLANNED;
+ protected $description;
+ protected $releaseTemplateKey;
+ protected $details = array();
+
+ protected $viewPolicy;
+ protected $editPolicy;
+
+ protected $mailKey;
+
+ public static function initializeNewRelease(PhabricatorUser $actor) {
+ $release_application = id(new PhabricatorApplicationQuery())
+ ->setViewer($actor)
+ ->withClasses(array('PhabricatorReleaseApplication'))
+ ->executeOne();
+
+ $view_policy = $release_application->getPolicy(
+ PhabricatorReleaseReleaseDefaultViewCapability::CAPABILITY);
+
+ $edit_policy = $release_application->getPolicy(
+ PhabricatorReleaseReleaseDefaultEditCapability::CAPABILITY);
+
+ return id(new self())
+ ->setViewPolicy($view_policy)
+ ->setEditPolicy($edit_policy);
+ }
+
+ protected function getConfiguration() {
+ return array(
+ self::CONFIG_AUX_PHID => true,
+ self::CONFIG_SERIALIZATION => array(
+ 'details' => self::SERIALIZATION_JSON,
+ ),
+ self::CONFIG_COLUMN_SCHEMA => array(
+ 'name' => 'sort128',
+ 'state' => 'text12',
+ 'description' => 'text',
+ 'releaseTemplateKey' => 'text12',
+ 'mailKey' => 'bytes20',
+ ),
+ self::CONFIG_KEY_SCHEMA => array(
+ 'key_name' => array(
+ 'columns' => array('name'),
+ 'unique' => false,
+ ),
+ 'key_state' => array(
+ 'columns' => array('state'),
+ 'unique' => false,
+ ),
+ ),
+ ) + parent::getConfiguration();
+ }
+
+ public function generatePHID() {
+ return PhabricatorPHID::generateNewPHID(
+ PhabricatorReleaseReleasePHIDType::TYPECONST);
+ }
+
+ public function save() {
+ if (!$this->getMailKey()) {
+ $this->setMailKey(Filesystem::readRandomCharacters(20));
+ }
+ return parent::save();
+ }
+
+ public function getURI() {
+ return '/X'.$this->getID();
+ }
+
+ public function getMonogram() {
+ return 'X'.$this->getID();
+ }
+
+ public function getStateName() {
+ return idx(self::getStatesMap(), $this->state, $this->state);
+ }
+
+ public function getReleaseTemplateName() {
+ return idx(
+ PhabricatorReleaseTemplate::getTemplatesMap(),
+ $this->releaseTemplateKey,
+ $this->releaseTemplateKey);
+ }
+
+ public function getReleaseTemplate() {
+ return PhabricatorReleaseTemplate::getTemplatesInstance(
+ $this->releaseTemplateKey);
+ }
+
+ /**
+ * closed, deleted, canceled, whatever.
+ */
+ public function isClosed() {
+ switch ($this->state) {
+ case self::STATE_OBSOLETE:
+ case self::STATE_INVALID:
+ return true;
+ }
+ return false;
+ }
+
+ public function getDetail($key, $default = null) {
+ return idx($this->details, $key, $default);
+ }
+
+ public function setDetail($key, $value) {
+ $this->details[$key] = $value;
+ return $this;
+ }
+
+ public static function getStatesMap() {
+ return array(
+ self::STATE_PLANNED => pht('Planned'),
+ self::STATE_TESTING => pht('In Testing'),
+ self::STATE_PRODUCTION => pht('In Production'),
+ self::STATE_OBSOLETE => pht('Obsolete'),
+ self::STATE_INVALID => pht('Invalid'),
+ );
+ }
+ public static function getOpenStates() {
+ return array(
+ self::STATE_PLANNED,
+ self::STATE_TESTING,
+ self::STATE_PRODUCTION,
+ );
+ }
+
+ public function getCurrentRefs() {
+ return $this->getDetail(self::DETAIL_CURRENTREF, array());
+ }
+
+ public function getCutpoints() {
+ return $this->getDetail(self::DETAIL_CUTPOINTS, array());
+ }
+
+ public function isRefMutable($ref) {
+ return !preg_match('/[a-f0-9]{40}/', $ref);
+ }
+
+ public function getSimpleRefName($ref) {
+ if (!$this->isRefMutable($ref)) {
+ return $ref;
+ }
+
+ $count = 0;
+ $branch = preg_replace('/^refs\/heads\//', '', $ref, 1, $count);
+
+ return $branch;
+ }
+
+ public function getNormalizedName() {
+ return preg_replace('/[^0-9a-zA-Z\.-]+|\.\./', '_', $this->getName());
+ }
+
+/* -( PhabricatorApplicationTransactionInterface )------------------------- */
+
+ public function getApplicationTransactionEditor() {
+ return new PhabricatorReleaseReleaseEditor();
+ }
+
+ public function getApplicationTransactionObject() {
+ return $this;
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new PhabricatorReleaseReleaseTransaction();
+ }
+
+ public function willRenderTimeline(
+ PhabricatorApplicationTransactionView $timeline,
+ AphrontRequest $request) {
+
+ return $timeline;
+ }
+
+
+/* -( PhabricatorSubscribableInterface )----------------------------------- */
+
+ public function isAutomaticallySubscribed($phid) {
+ return false;
+ }
+
+/* -( PhabricatorConduitResultInterface )---------------------------------- */
+
+ public function getFieldSpecificationsForConduit() {
+ return array(
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('name')
+ ->setType('string')
+ ->setDescription(pht('The name of the release.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('state')
+ ->setType('string')
+ ->setDescription(pht('Status of the release.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('type')
+ ->setType('string')
+ ->setDescription(pht('Type of the release.')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('cut_points')
+ ->setType('map<phid, hash>')
+ ->setDescription(pht('Hashes where this release was cut from')),
+ id(new PhabricatorConduitSearchFieldSpecification())
+ ->setKey('current_refs')
+ ->setType('map<phid, hash>')
+ ->setDescription(pht('Current hashes/branches of this release')),
+ );
+ }
+
+ public function getFieldValuesForConduit() {
+ return array(
+ 'name' => $this->getName(),
+ 'state' => $this->getState(),
+ 'type' => $this->getReleaseTemplateKey(),
+ 'cut_points' => $this->getCutpoints(),
+ 'current_refs' => $this->getCurrentRefs(),
+ );
+ }
+
+ public function getConduitSearchAttachments() {
+ return array();
+ }
+
+/* -( PhabricatorCustomFieldInterface )------------------------------------ */
+
+ private $customFields = self::ATTACHABLE;
+
+ public function getCustomFieldSpecificationForRole($role) {
+ return array(); // TODO ?
+ }
+
+ public function getCustomFieldBaseClass() {
+ return 'ReleaseCustomField';
+ }
+
+ public function getCustomFields() {
+ return $this->assertAttached($this->customFields);
+ }
+
+ public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
+ $this->customFields = $fields;
+ return $this;
+ }
+
+
+/* -( PhabricatorPolicyInterface )----------------------------------------- */
+
+ public function getCapabilities() {
+ return array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ );
+ }
+
+ public function getPolicy($capability) {
+ switch ($capability) {
+ case PhabricatorPolicyCapability::CAN_VIEW:
+ return $this->getViewPolicy();
+ case PhabricatorPolicyCapability::CAN_EDIT:
+ return $this->getEditPolicy();
+ }
+ }
+
+ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
+ return false;
+ }
+
+ public function describeAutomaticCapability($capability) {
+ return null;
+ }
+}
diff --git a/src/applications/release/storage/PhabricatorReleaseReleaseTransaction.php b/src/applications/release/storage/PhabricatorReleaseReleaseTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/PhabricatorReleaseReleaseTransaction.php
@@ -0,0 +1,47 @@
+<?php
+
+final class PhabricatorReleaseReleaseTransaction
+ extends PhabricatorModularTransaction {
+
+ const MAILTAG_STATE = 'release-state';
+ const MAILTAG_OTHER = 'release-content';
+
+ public function getApplicationName() {
+ return 'release';
+ }
+
+ // TODO move to parent class
+ public function expandTransaction($object) {
+ $impl = $this->getTransactionImplementation();
+ if ($impl instanceof PhabricatorReleaseReleaseTransactionType) {
+ return $impl->expandTransaction($object);
+ }
+ return array();
+ }
+
+ public function getApplicationTransactionType() {
+ return PhabricatorReleaseReleasePHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'PhabricatorReleaseReleaseTransactionType';
+ }
+
+ public function getApplicationTransactionCommentObject() {
+ return new PhabricatorReleaseReleaseTransactionComment();
+ }
+
+ public function getMailTags() {
+ $tags = parent::getMailTags();
+
+ switch ($this->getTransactionType()) {
+ case PhabricatorReleaseReleaseStateTransaction::TRANSACTIONTYPE:
+ $tags[] = self::MAILTAG_STATE;
+ break;
+ default:
+ $tags[] = self::MAILTAG_OTHER;
+ break;
+ }
+ return $tags;
+ }
+}
diff --git a/src/applications/release/storage/PhabricatorReleaseReleaseTransactionComment.php b/src/applications/release/storage/PhabricatorReleaseReleaseTransactionComment.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/PhabricatorReleaseReleaseTransactionComment.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorReleaseReleaseTransactionComment
+ extends PhabricatorApplicationTransactionComment {
+
+ public function getApplicationTransactionObject() {
+ return new PhabricatorReleaseReleaseTransaction();
+ }
+
+}
diff --git a/src/applications/release/storage/PhabricatorReleaseSchemaSpec.php b/src/applications/release/storage/PhabricatorReleaseSchemaSpec.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/PhabricatorReleaseSchemaSpec.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PhabricatorReleaseSchemaSpec
+ extends PhabricatorConfigSchemaSpec {
+
+ public function buildSchemata() {
+ $this->buildEdgeSchemata(new PhabricatorReleaseRelease());
+ }
+
+}
diff --git a/src/applications/release/storage/ReleaseCustomFieldNumericIndex.php b/src/applications/release/storage/ReleaseCustomFieldNumericIndex.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseCustomFieldNumericIndex.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ReleaseCustomFieldNumericIndex // TODO db create tables
+ extends PhabricatorCustomFieldNumericIndexStorage {
+
+ public function getApplicationName() {
+ return 'release';
+ }
+
+}
diff --git a/src/applications/release/storage/ReleaseCustomFieldStorage.php b/src/applications/release/storage/ReleaseCustomFieldStorage.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseCustomFieldStorage.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ReleaseCustomFieldStorage
+ extends PhabricatorCustomFieldStorage {
+
+ public function getApplicationName() {
+ return 'release';
+ }
+
+}
diff --git a/src/applications/release/storage/ReleaseCustomFieldStringIndex.php b/src/applications/release/storage/ReleaseCustomFieldStringIndex.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseCustomFieldStringIndex.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ReleaseCustomFieldStringIndex
+ extends PhabricatorCustomFieldStringIndexStorage {
+
+ public function getApplicationName() {
+ return 'release';
+ }
+
+}
diff --git a/src/applications/release/template/PhabricatorReleaseTemplate.php b/src/applications/release/template/PhabricatorReleaseTemplate.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/template/PhabricatorReleaseTemplate.php
@@ -0,0 +1,49 @@
+<?php
+
+// TODO This will be replaced with the Release Plan object, which will look
+// more-or-less like HarbormasterBuildPlan.
+abstract class PhabricatorReleaseTemplate extends Phobject {
+
+ abstract public function getTemplateReadableName();
+
+ final public function getTemplateKey() {
+ return $this->getPhobjectClassConstant('TEMPLATE_KEY');
+ }
+
+ abstract public function validateRepositories(array $repositories);
+
+ private static function getAllTemplates() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setUniqueMethod('getTemplateKey')
+ ->setSortMethod('getTemplateReadableName')
+ ->execute();
+ }
+
+ public static function getTemplatesForRepository(
+ PhabricatorRepository $repository) {
+
+ $template_classes = self::getAllTemplates();
+ $valid = array();
+ $test = array($repository);
+ foreach ($template_classes as $template) {
+ if (!$template->validateRepositories($test)) {
+ $valid[] = $template;
+ }
+ }
+
+ return $valid;
+ }
+
+ public static function getTemplatesMap() {
+ $template_classes = self::getAllTemplates();
+
+ return mpull($template_classes, 'getTemplateReadableName');
+ }
+
+ public static function getTemplatesInstance($key) {
+ $template_classes = self::getAllTemplates();
+
+ return idx($template_classes, $key);
+ }
+}
diff --git a/src/applications/release/typeahead/PhabricatorReleaseReleaseDatasource.php b/src/applications/release/typeahead/PhabricatorReleaseReleaseDatasource.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/typeahead/PhabricatorReleaseReleaseDatasource.php
@@ -0,0 +1,65 @@
+<?php
+
+final class PhabricatorReleaseReleaseDatasource
+ extends PhabricatorTypeaheadDatasource {
+
+ public function getBrowseTitle() {
+ return pht('Releases');
+ }
+
+ public function getPlaceholderText() {
+ return pht('Type a release name...');
+ }
+
+ public function getDatasourceApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ public function withTemplateKeys($template_keys) {
+ $params = $this->getParameters();
+ $params['template_keys'] = $template_keys;
+ $this->setParameters($params);
+ return $this;
+ }
+
+ public function loadResults() {
+ $viewer = $this->getViewer();
+ $raw_query = $this->getRawQuery();
+
+ $query = id(new PhabricatorReleaseReleaseQuery())
+ ->setOrder('select')
+ ->withDatasourceQuery($raw_query);
+
+ $template_keys = $this->getParameter('template_keys');
+ if ($template_keys) {
+ $query->withTemplateKeys($template_keys);
+ }
+
+ $releases = $this->executeQuery($query);
+
+ $results = array();
+ foreach ($releases as $release) {
+ $name = $release->getName();
+ $display_name = $release->getMonogram().' '.$name;
+
+ $token = id(new PhabricatorTypeaheadResult())
+ ->setName($name)
+ ->setDisplayName($display_name)
+ ->setURI($release->getURI())
+ ->addAttribute($release->getReleaseTemplateName())
+ ->addAttribute($release->getStateName())
+ ->setPHID($release->getPHID());
+
+ if ($release->isClosed()) {
+ $token
+ ->setName('zz '.$name)
+ ->setClosed('old');
+ }
+
+ $results[] = $token;
+ }
+
+ return $results;
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseCurrentRefTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseCurrentRefTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseCurrentRefTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorReleaseReleaseCurrentRefTransaction
+ extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:currentref';
+
+ public function generateOldValue($object) {
+ return $object->getDetail(PhabricatorReleaseRelease::DETAIL_CURRENTREF);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDetail(PhabricatorReleaseRelease::DETAIL_CURRENTREF, $value);
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s edited the current references for this release.',
+ $this->renderAuthor());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s edited the current references for release %s',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseCutpointTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseCutpointTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseCutpointTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorReleaseReleaseCutpointTransaction
+ extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:cutpoint';
+
+ public function generateOldValue($object) {
+ return $object->getDetail(PhabricatorReleaseRelease::DETAIL_CUTPOINTS);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDetail(PhabricatorReleaseRelease::DETAIL_CUTPOINTS, $value);
+ }
+
+ public function hasChangeDetailView() {
+ return true;
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s edited the cutpoints for this release.',
+ $this->renderAuthor());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s edited the cutpoints for release %s',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseDescribeTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseDescribeTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseDescribeTransaction.php
@@ -0,0 +1,41 @@
+<?php
+
+final class PhabricatorReleaseReleaseDescribeTransaction
+ extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:describe';
+
+ public function generateOldValue($object) {
+ return $object->getDescription();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDescription($value);
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s edited the release description.',
+ $this->renderAuthor());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s edited the description of %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+ public function hasChangeDetailView() {
+ return ($this->getOldValue() !== null);
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($this->getOldValue())
+ ->setNewText($this->getNewValue());
+ }
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseNameTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseNameTransaction.php
@@ -0,0 +1,44 @@
+<?php
+
+final class PhabricatorReleaseReleaseNameTransaction
+extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:name';
+
+ public function generateOldValue($object) {
+ return $object->getName();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setName($value);
+ }
+
+ public function shouldHide() {
+ $old = $this->getOldValue();
+ if (is_array($old) && !$old) {
+ return true;
+ }
+
+ if (!is_array($old) && !strlen($old)) {
+ return true;
+ }
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s renamed the release from "%s" to "%s".',
+ $this->renderAuthor(),
+ $this->getOldValue(),
+ $this->getNewValue());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s renamed the release for %s from "%s" to "%s".',
+ $this->renderAuthor(),
+ $this->renderObject(),
+ $this->getOldValue(),
+ $this->getNewValue());
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseStateTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseStateTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseStateTransaction.php
@@ -0,0 +1,45 @@
+<?php
+
+final class PhabricatorReleaseReleaseStateTransaction
+ extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:state';
+
+ public function generateOldValue($object) {
+ return $object->getState();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setState($value);
+ }
+
+ public function getColor() {
+ return 'orange';
+ }
+
+ public function getTitle() {
+ $states = PhabricatorReleaseRelease::getStatesMap();
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ return pht(
+ '%s changed the status of this release from "%s" to "%s"',
+ $this->renderAuthor(),
+ idx($states, $old, $old),
+ idx($states, $new, $new));
+ }
+
+ public function getTitleForFeed() {
+ $states = PhabricatorReleaseRelease::getStatesMap();
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ return pht(
+ '%s changed the status of release %s from "%s" to "%s"',
+ $this->renderAuthor(),
+ $this->renderObject(),
+ idx($states, $old, $old),
+ idx($states, $new, $new));
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseTemplateTransaction.php b/src/applications/release/xaction/PhabricatorReleaseReleaseTemplateTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseTemplateTransaction.php
@@ -0,0 +1,44 @@
+<?php
+
+final class PhabricatorReleaseReleaseTemplateTransaction
+ extends PhabricatorReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:templatekey';
+
+ public function generateOldValue($object) {
+ return $object->getReleaseTemplateKey();
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setReleaseTemplateKey($value);
+ }
+
+
+ public function validateTransactions($object, array $xactions) {
+ foreach ($xactions as $xaction) {
+ $existing = $object->getReleaseTemplateKey();
+ if ($existing && $existing != $xaction->getNewValue()) {
+ $error = $this->newError(
+ pht('Locked'),
+ pht('Cannot change template after Release is created'));
+
+ return array($error);
+ }
+ }
+ return array();
+ }
+
+ public function getTitle() {
+ return pht(
+ '%s Did the impossible.',
+ $this->renderAuthor());
+ }
+
+ public function getTitleForFeed() {
+ return pht(
+ '%s Did the impossible to %s.',
+ $this->renderAuthor(),
+ $this->renderObject());
+ }
+
+}
diff --git a/src/applications/release/xaction/PhabricatorReleaseReleaseTransactionType.php b/src/applications/release/xaction/PhabricatorReleaseReleaseTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/PhabricatorReleaseReleaseTransactionType.php
@@ -0,0 +1,28 @@
+<?php
+
+abstract class PhabricatorReleaseReleaseTransactionType
+ extends PhabricatorModularTransactionType {
+
+ public function getIcon() {
+ return 'fa-css3';
+ }
+
+ // TODO move to parent class
+ public function expandTransaction($object) {
+ return array();
+ }
+
+ public function newChangeDetailView() {
+ $viewer = $this->getViewer();
+
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $json = new PhutilJSON();
+
+ return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setViewer($viewer)
+ ->setOldText($old ? $json->encodeFormatted($old) : null)
+ ->setNewText($new ? $json->encodeFormatted($new) : null);
+ }
+}
diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -112,6 +112,7 @@
'db.phurl' => array(),
'db.badges' => array(),
'db.packages' => array(),
+ 'db.release' => array(),
'0000.legacy.sql' => array(
'legacy' => 0,
),

File Metadata

Mime Type
text/plain
Expires
Wed, Jan 22, 4:04 PM (11 h, 48 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7034873
Default Alt Text
D16981.id40860.diff (74 KB)

Event Timeline