Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F19911614
D16981.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
77 KB
Referenced Files
None
Subscribers
None
D16981.diff
View Options
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
@@ -3565,6 +3565,12 @@
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php',
'PhabricatorRegistrationProfile' => 'applications/people/storage/PhabricatorRegistrationProfile.php',
+ 'PhabricatorReleaseApplication' => 'applications/release/application/PhabricatorReleaseApplication.php',
+ 'PhabricatorReleaseConfigOptions' => 'applications/release/config/PhabricatorReleaseConfigOptions.php',
+ 'PhabricatorReleaseDAO' => 'applications/release/storage/PhabricatorReleaseDAO.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',
@@ -4491,6 +4497,37 @@
'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php',
'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php',
'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
+ 'ReleaseConfiguredCustomField' => 'applications/release/customfield/ReleaseConfiguredCustomField.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',
+ 'ReleaseRelease' => 'applications/release/storage/ReleaseRelease.php',
+ 'ReleaseReleaseCreateReleaseCapability' => 'applications/release/capability/ReleaseReleaseCreateReleaseCapability.php',
+ 'ReleaseReleaseCurrentRefTransaction' => 'applications/release/xaction/ReleaseReleaseCurrentRefTransaction.php',
+ 'ReleaseReleaseCutpointTransaction' => 'applications/release/xaction/ReleaseReleaseCutpointTransaction.php',
+ 'ReleaseReleaseDatasource' => 'applications/release/typeahead/ReleaseReleaseDatasource.php',
+ 'ReleaseReleaseDefaultEditCapability' => 'applications/release/capability/ReleaseReleaseDefaultEditCapability.php',
+ 'ReleaseReleaseDefaultViewCapability' => 'applications/release/capability/ReleaseReleaseDefaultViewCapability.php',
+ 'ReleaseReleaseDescribeTransaction' => 'applications/release/xaction/ReleaseReleaseDescribeTransaction.php',
+ 'ReleaseReleaseDetailsController' => 'applications/release/controller/ReleaseReleaseDetailsController.php',
+ 'ReleaseReleaseEditConduitAPIMethod' => 'applications/release/conduit/ReleaseReleaseEditConduitAPIMethod.php',
+ 'ReleaseReleaseEditController' => 'applications/release/controller/ReleaseReleaseEditController.php',
+ 'ReleaseReleaseEditEngine' => 'applications/release/editor/ReleaseReleaseEditEngine.php',
+ 'ReleaseReleaseEditor' => 'applications/release/editor/ReleaseReleaseEditor.php',
+ 'ReleaseReleaseListController' => 'applications/release/controller/ReleaseReleaseListController.php',
+ 'ReleaseReleaseNameTransaction' => 'applications/release/xaction/ReleaseReleaseNameTransaction.php',
+ 'ReleaseReleasePHIDType' => 'applications/release/phid/ReleaseReleasePHIDType.php',
+ 'ReleaseReleaseQuery' => 'applications/release/query/ReleaseReleaseQuery.php',
+ 'ReleaseReleaseReplyHandler' => 'applications/release/mail/ReleaseReleaseReplyHandler.php',
+ 'ReleaseReleaseSearchConduitAPIMethod' => 'applications/release/conduit/ReleaseReleaseSearchConduitAPIMethod.php',
+ 'ReleaseReleaseSearchEngine' => 'applications/release/query/ReleaseReleaseSearchEngine.php',
+ 'ReleaseReleaseStateTransaction' => 'applications/release/xaction/ReleaseReleaseStateTransaction.php',
+ 'ReleaseReleaseTemplateTransaction' => 'applications/release/xaction/ReleaseReleaseTemplateTransaction.php',
+ 'ReleaseReleaseTransaction' => 'applications/release/storage/ReleaseReleaseTransaction.php',
+ 'ReleaseReleaseTransactionComment' => 'applications/release/storage/ReleaseReleaseTransactionComment.php',
+ 'ReleaseReleaseTransactionQuery' => 'applications/release/query/ReleaseReleaseTransactionQuery.php',
+ 'ReleaseReleaseTransactionType' => 'applications/release/xaction/ReleaseReleaseTransactionType.php',
'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php',
'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php',
'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php',
@@ -8747,6 +8784,12 @@
'PhabricatorRedirectController' => 'PhabricatorController',
'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController',
'PhabricatorRegistrationProfile' => 'Phobject',
+ 'PhabricatorReleaseApplication' => 'PhabricatorApplication',
+ 'PhabricatorReleaseConfigOptions' => 'PhabricatorApplicationConfigOptions',
+ 'PhabricatorReleaseDAO' => 'PhabricatorLiskDAO',
+ 'PhabricatorReleaseRemarkupRule' => 'PhabricatorObjectRemarkupRule',
+ 'PhabricatorReleaseSchemaSpec' => 'PhabricatorConfigSchemaSpec',
+ 'PhabricatorReleaseTemplate' => 'Phobject',
'PhabricatorReleephApplication' => 'PhabricatorApplication',
'PhabricatorReleephApplicationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl',
@@ -9913,6 +9956,49 @@
'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
'QueryFormattingTestCase' => 'PhabricatorTestCase',
+ 'ReleaseConfiguredCustomField' => array(
+ 'ReleaseCustomField',
+ 'PhabricatorStandardCustomFieldInterface',
+ ),
+ 'ReleaseCustomField' => 'PhabricatorCustomField',
+ 'ReleaseCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage',
+ 'ReleaseCustomFieldStorage' => 'PhabricatorCustomFieldStorage',
+ 'ReleaseCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
+ 'ReleaseRelease' => array(
+ 'PhabricatorReleaseDAO',
+ 'PhabricatorApplicationTransactionInterface',
+ 'PhabricatorSubscribableInterface',
+ 'PhabricatorFlaggableInterface',
+ 'PhabricatorMentionableInterface',
+ 'PhabricatorConduitResultInterface',
+ 'PhabricatorCustomFieldInterface',
+ 'PhabricatorPolicyInterface',
+ ),
+ 'ReleaseReleaseCreateReleaseCapability' => 'PhabricatorPolicyCapability',
+ 'ReleaseReleaseCurrentRefTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleaseCutpointTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleaseDatasource' => 'PhabricatorTypeaheadDatasource',
+ 'ReleaseReleaseDefaultEditCapability' => 'PhabricatorPolicyCapability',
+ 'ReleaseReleaseDefaultViewCapability' => 'PhabricatorPolicyCapability',
+ 'ReleaseReleaseDescribeTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleaseDetailsController' => 'PhabricatorController',
+ 'ReleaseReleaseEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod',
+ 'ReleaseReleaseEditController' => 'PhabricatorController',
+ 'ReleaseReleaseEditEngine' => 'PhabricatorEditEngine',
+ 'ReleaseReleaseEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'ReleaseReleaseListController' => 'PhabricatorController',
+ 'ReleaseReleaseNameTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleasePHIDType' => 'PhabricatorPHIDType',
+ 'ReleaseReleaseQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'ReleaseReleaseReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
+ 'ReleaseReleaseSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod',
+ 'ReleaseReleaseSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'ReleaseReleaseStateTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleaseTemplateTransaction' => 'ReleaseReleaseTransactionType',
+ 'ReleaseReleaseTransaction' => 'PhabricatorModularTransaction',
+ 'ReleaseReleaseTransactionComment' => 'PhabricatorApplicationTransactionComment',
+ 'ReleaseReleaseTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+ 'ReleaseReleaseTransactionType' => 'PhabricatorModularTransactionType',
'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification',
'ReleephBranch' => array(
'ReleephDAO',
diff --git a/src/applications/release/OPEN_QUESTIONS b/src/applications/release/OPEN_QUESTIONS
new file mode 100644
--- /dev/null
+++ b/src/applications/release/OPEN_QUESTIONS
@@ -0,0 +1,34 @@
+Some questions I have that will effect this diff (Also remember to delete this
+file when done):
+
+- expandTransaction: Because I decided to implement "custom actions" using
+ transactions (P2009), I ended up having xactions that are shown on the
+ timeline ("avive has frozen this release"), but their actual work is been
+ handled by another xaction ("CloseBranchTransaction"). expandTransaction is
+ how the old transaction code handled this, but it didn't make it to the
+ modular code.
+ Should I just find a new way to do it?
+- What kind of Edges do we need? RepositoryInRelease, ?
+
+
+
+General questions:
+- EditEngine::buildCustomEditFields(): Why don't we move the field definitions
+ into the *TransactionType classes, and then use reflection to implement this
+ method? It looks like there's a 1-1 relation between fields and xaction types.
+
+
+
+More technical debt (For after initial dump):
+
+- UI for create release: like in Add Build Step, first select template, then
+ fill in the rest of the stuff. Right now, I need custom create forms because I
+ don't yet have Template objects.
+- None of this staff is abstracted over VCS yet - it's all git. Although this
+ mostly effects terminology, because there's very little code that actually
+ does things...
+- expand transaction: in a couple of cases I need to (Mostly because of the
+ custom actions thing; Either move to base ApplicationEditor, or find another
+ solution on my end.
+- Release Statuses are a hard-coded list right now, but should probably be
+ customizable at some point.
diff --git a/src/applications/release/TODO b/src/applications/release/TODO
new file mode 100644
--- /dev/null
+++ b/src/applications/release/TODO
@@ -0,0 +1,2 @@
+Things to complete before D16981 makes sense:
+- show a workflow for creating a release
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,67 @@
+<?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-hand-scissors-o';
+ }
+
+ public function getApplicationGroup() {
+ return self::GROUP_UTILITIES;
+ }
+
+ public function isPrototype() {
+ return true;
+ }
+
+ public function getRemarkupRules() {
+ return array(
+ new PhabricatorReleaseRemarkupRule(),
+ );
+ }
+
+ protected function getCustomCapabilities() {
+ return array(
+ ReleaseReleaseCreateReleaseCapability::CAPABILITY => array(),
+ ReleaseReleaseDefaultViewCapability::CAPABILITY => array(
+ 'caption' => pht('Default edit policy for newly created releases.'),
+ 'template' => ReleaseReleasePHIDType::TYPECONST,
+ 'default' => PhabricatorPolicies::POLICY_PUBLIC,
+ ),
+ ReleaseReleaseDefaultEditCapability::CAPABILITY => array(
+ 'caption' => pht('Default view policy for newly created releases.'),
+ 'template' => ReleaseReleasePHIDType::TYPECONST,
+ 'default' => PhabricatorPolicies::POLICY_USER,
+ ),
+ );
+ }
+
+ public function getRoutes() {
+ return array(
+ '/X(?P<id>[1-9]\d*)' => 'ReleaseReleaseDetailsController',
+ '/release/' => array(
+ $this->getEditRoutePattern('edit/') => 'ReleaseReleaseEditController',
+ '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleaseReleaseListController',
+ // TODO new release
+
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/release/capability/ReleaseReleaseCreateReleaseCapability.php b/src/applications/release/capability/ReleaseReleaseCreateReleaseCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/capability/ReleaseReleaseCreateReleaseCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class ReleaseReleaseCreateReleaseCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'release.release.create';
+
+ public function getCapabilityName() {
+ return pht('Can Create Releases');
+ }
+
+ public function describeCapabilityRejection() {
+ return pht('You do not have permission to create new Releases.');
+ }
+
+}
diff --git a/src/applications/release/capability/ReleaseReleaseDefaultEditCapability.php b/src/applications/release/capability/ReleaseReleaseDefaultEditCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/capability/ReleaseReleaseDefaultEditCapability.php
@@ -0,0 +1,12 @@
+<?php
+
+final class ReleaseReleaseDefaultEditCapability
+ extends PhabricatorPolicyCapability {
+
+ const CAPABILITY = 'release.default.edit';
+
+ public function getCapabilityName() {
+ return pht('Default Release Edit Policy');
+ }
+
+}
diff --git a/src/applications/release/capability/ReleaseReleaseDefaultViewCapability.php b/src/applications/release/capability/ReleaseReleaseDefaultViewCapability.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/capability/ReleaseReleaseDefaultViewCapability.php
@@ -0,0 +1,16 @@
+<?php
+
+final class ReleaseReleaseDefaultViewCapability
+ 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/ReleaseReleaseEditConduitAPIMethod.php b/src/applications/release/conduit/ReleaseReleaseEditConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/conduit/ReleaseReleaseEditConduitAPIMethod.php
@@ -0,0 +1,19 @@
+<?php
+
+final class ReleaseReleaseEditConduitAPIMethod
+ extends PhabricatorEditEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'release.edit';
+ }
+
+ public function newEditEngine() {
+ return new ReleaseReleaseEditEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht(
+ 'Apply transactions to create a new Release or edit an existing one.');
+ }
+
+}
diff --git a/src/applications/release/conduit/ReleaseReleaseSearchConduitAPIMethod.php b/src/applications/release/conduit/ReleaseReleaseSearchConduitAPIMethod.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/conduit/ReleaseReleaseSearchConduitAPIMethod.php
@@ -0,0 +1,18 @@
+<?php
+
+final class ReleaseReleaseSearchConduitAPIMethod
+ extends PhabricatorSearchEngineAPIMethod {
+
+ public function getAPIMethodName() {
+ return 'release.search';
+ }
+
+ public function newSearchEngine() {
+ return new ReleaseReleaseSearchEngine();
+ }
+
+ public function getMethodSummary() {
+ return pht('Read information about releases.');
+ }
+
+}
diff --git a/src/applications/release/config/PhabricatorReleaseConfigOptions.php b/src/applications/release/config/PhabricatorReleaseConfigOptions.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/config/PhabricatorReleaseConfigOptions.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorReleaseConfigOptions
+ extends PhabricatorApplicationConfigOptions {
+
+ public function getName() {
+ return pht('Release');
+ }
+
+ public function getDescription() {
+ return pht('Options for the Release app.');
+ }
+
+ public function getGroup() {
+ return 'apps';
+ }
+
+ public function getIcon() {
+ return 'fa-hand-scissors-o';
+ }
+
+ public function getOptions() {
+ $custom_fields_href = PhabricatorEnv::getDoclink(
+ 'Configuring Custom Fields');
+
+ return array(
+ $this->newOption(
+ 'release.release.customFields',
+ 'wild',
+ array())
+ ->setSummary(pht('Custom fields for Releases.'))
+ ->setDescription(pht(
+ 'Array of custom fields for Release objects. For details, see '.
+ '**[[ %s | Configuring Custom Fields ]]** in the documentation.',
+ $custom_fields_href)),
+ $this->newOption(
+ 'release.show-debug-tools',
+ 'bool',
+ true)
+ ->setDescription(pht(
+ 'Show some actions and options that are only useful for developing '.
+ 'this prototype application.')),
+ );
+ }
+
+}
diff --git a/src/applications/release/controller/ReleaseReleaseDetailsController.php b/src/applications/release/controller/ReleaseReleaseDetailsController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/controller/ReleaseReleaseDetailsController.php
@@ -0,0 +1,217 @@
+<?php
+
+// TODO rename to DetailsController
+final class ReleaseReleaseDetailsController extends PhabricatorController {
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->getViewer();
+
+ $id = $request->getURIData('id');
+
+ $crumbs = $this->buildApplicationCrumbs();
+
+ $release = id(new ReleaseReleaseQuery())
+ ->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);
+
+ $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 ReleaseReleaseTransactionQuery());
+ $timeline->setQuoteRef($release->getMonogram());
+
+ $comment_view = id(new ReleaseReleaseEditEngine())
+ ->setViewer($viewer)
+ ->buildEditEngineCommentView($release);
+ $comment_view->setTransactionTimeline($timeline);
+
+ $repositories = $this->buildRepositoriesSection($viewer, $release);
+
+ $view = id(new PHUITwoColumnView())
+ ->setHeader($header)
+ ->setCurtain($curtain)
+ ->setMainColumn(array(
+ $timeline,
+ $comment_view,
+ ))
+ ->addPropertySection(pht('Details'), $properties)
+ ->addPropertySection(pht('Repositories'), $repositories);
+
+ return $this->newPage()
+ ->setTitle('X'.$release->getID().' '.$release_name)
+ ->setCrumbs($crumbs)
+ ->setPageObjectPHIDs(
+ array(
+ $release->getPHID(),
+ ))
+ ->appendChild(
+ array(
+ $view,
+ ));
+ }
+
+ private function buildRepositoriesSection($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 {
+ // TODO futurize
+ list($cp) = $repo->execxLocalCommand(
+ 'rev-list --count %s..%s',
+ $cut,
+ $current);
+
+ if ($cp == 0) {
+ $cp = '--';
+ } else {
+ // $cut is always a commit hash.
+ $short = $release->getSimpleRefName($current);
+ $href = hsprintf(
+ '/compare/?head=%s&against=%s',
+ $short,
+ $cut);
+ $href = $repo->getPathURI($href);
+
+ $cp = hsprintf('+%d cherry-picks', $cp);
+ $cp = phutil_tag(
+ 'a',
+ array('href' => $href, 'target' => '_blank'),
+ $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/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 ReleaseReleaseEditEngine())
+ ->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,16 @@
+<?php
+
+final class ReleaseReleaseListController extends PhabricatorController {
+
+ public function handleRequest(AphrontRequest $request) {
+ return id(new ReleaseReleaseSearchEngine())
+ ->setController($this)
+ ->buildResponse();
+ }
+
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+}
diff --git a/src/applications/release/customfield/ReleaseConfiguredCustomField.php b/src/applications/release/customfield/ReleaseConfiguredCustomField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/customfield/ReleaseConfiguredCustomField.php
@@ -0,0 +1,22 @@
+<?php
+
+final class ReleaseConfiguredCustomField
+ extends ReleaseCustomField
+ implements PhabricatorStandardCustomFieldInterface {
+
+ public function getStandardCustomFieldNamespace() {
+ return 'release';
+ }
+
+ public function createFields($object) {
+ $config = PhabricatorEnv::getEnvConfig(
+ 'release.release.customFields',
+ array());
+ $fields = PhabricatorStandardCustomField::buildStandardFields(
+ $this,
+ $config);
+
+ return $fields;
+ }
+
+}
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/ReleaseReleaseEditEngine.php b/src/applications/release/editor/ReleaseReleaseEditEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/editor/ReleaseReleaseEditEngine.php
@@ -0,0 +1,153 @@
+<?php
+
+final class ReleaseReleaseEditEngine
+ 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 ReleaseRelease::initializeNewRelease($viewer);
+ }
+
+ protected function newObjectQuery() {
+ return id(new ReleaseReleaseQuery());
+ }
+
+ 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(
+ ReleaseReleaseCreateReleaseCapability::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 available in UI.
+ $templates[$used_template] = $used_template;
+ }
+
+ $states = ReleaseRelease::getStatesMap();
+
+ $is_edit_form = !$this->getIsCreate();
+
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setKey('name')
+ ->setLabel(pht('Name'))
+ ->setIsRequired(true)
+ ->setTransactionType(
+ ReleaseReleaseNameTransaction::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(
+ ReleaseReleaseTemplateTransaction::TRANSACTIONTYPE)
+ ->setIsCopyable(true)
+ ->setOptions($templates)
+ ->setIsLocked($is_edit_form)
+ ->setDescription('Template key to use. ')
+ ->setValue($object->getReleaseTemplateKey()),
+ id(new PhabricatorSelectEditField())
+ ->setKey('state')
+ ->setLabel(pht('State'))
+ ->setTransactionType(
+ ReleaseReleaseStateTransaction::TRANSACTIONTYPE)
+ ->setIsCopyable(true)
+ ->setOptions($states)
+ ->setValue($object->getState()),
+ id(new PhabricatorRemarkupEditField())
+ ->setKey('description')
+ ->setLabel(pht('Description'))
+ ->setTransactionType(
+ ReleaseReleaseDescribeTransaction::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(
+ ReleaseReleaseCutpointTransaction::TRANSACTIONTYPE)
+ ->setDescription(pht('Commits where the revision was cut from'))
+ ->setConduitTypeDescription('Map repo phid -> commit')
+ ->setIsConduitOnly(true)
+ ->setValue($object->getDetail(
+ ReleaseRelease::DETAIL_CUTPOINTS)),
+ id(new PhabricatorConduitEditField())
+ ->setKey('currentrefs')
+ ->setLabel(pht('Current References'))
+ ->setTransactionType(
+ ReleaseReleaseCurrentRefTransaction::TRANSACTIONTYPE)
+ ->setDescription('Commits/branches where the revision currently is')
+ ->setConduitTypeDescription('Map repo phid -> commit/branch')
+ ->setIsConduitOnly(true)
+ ->setValue($object->getDetail(
+ ReleaseRelease::DETAIL_CURRENTREF)),
+ );
+ }
+
+}
diff --git a/src/applications/release/editor/ReleaseReleaseEditor.php b/src/applications/release/editor/ReleaseReleaseEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/editor/ReleaseReleaseEditor.php
@@ -0,0 +1,126 @@
+<?php
+
+final class ReleaseReleaseEditor
+ 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(
+ ReleaseReleaseTransaction::MAILTAG_STATE =>
+ pht('A Release state is updated.'),
+ ReleaseReleaseTransaction::MAILTAG_OTHER =>
+ pht('Other Release activity not listed above occurs.'),
+ );
+ }
+
+ protected function buildReplyHandler(PhabricatorLiskDAO $object) {
+ return id(new ReleaseReleaseReplyHandler())
+ ->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 ReleaseReleaseTransaction) {
+ $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/ReleaseReleaseReplyHandler.php b/src/applications/release/mail/ReleaseReleaseReplyHandler.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/mail/ReleaseReleaseReplyHandler.php
@@ -0,0 +1,20 @@
+<?php
+
+final class ReleaseReleaseReplyHandler
+ extends PhabricatorApplicationTransactionReplyHandler {
+
+ public function validateMailReceiver($mail_receiver) {
+ if (!($mail_receiver instanceof ReleaseRelease)) {
+ throw new Exception(
+ pht('Mail receiver is not a %s.', 'ReleaseRelease'));
+ }
+ }
+
+ public function getObjectPrefix() {
+ return ReleaseReleasePHIDType::TYPECONST;
+ }
+
+ protected function shouldCreateCommentFromMailBody() {
+ return false;
+ }
+}
diff --git a/src/applications/release/phid/ReleaseReleasePHIDType.php b/src/applications/release/phid/ReleaseReleasePHIDType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/phid/ReleaseReleasePHIDType.php
@@ -0,0 +1,81 @@
+<?php
+
+final class ReleaseReleasePHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'RELS';
+
+ public function getTypeName() {
+ return pht('Release');
+ }
+
+ public function newObject() {
+ return new ReleaseRelease();
+ }
+
+ public function getPHIDTypeApplicationClass() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new ReleaseReleaseQuery())
+ ->withPHIDs($phids);
+ }
+
+ public function getTypeIcon() {
+ return 'fa-hand-scissors-o';
+ }
+
+ 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 ReleaseReleaseQuery())
+ ->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/ReleaseReleaseQuery.php b/src/applications/release/query/ReleaseReleaseQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/ReleaseReleaseQuery.php
@@ -0,0 +1,173 @@
+<?php
+
+final class ReleaseReleaseQuery
+ 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 ReleaseRelease();
+ }
+
+ 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/ReleaseReleaseSearchEngine.php b/src/applications/release/query/ReleaseReleaseSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/ReleaseReleaseSearchEngine.php
@@ -0,0 +1,170 @@
+<?php
+
+final class ReleaseReleaseSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Releases');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorReleaseApplication';
+ }
+
+ public function newQuery() {
+ return id(new ReleaseReleaseQuery());
+ }
+
+ 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 = ReleaseRelease::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(ReleaseRelease::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' => ReleaseReleaseQuery::GROUP_NONE,
+ 'template' => ReleaseReleaseQuery::GROUP_TEMPLATE,
+ 'status' => ReleaseReleaseQuery::GROUP_STATE,
+ );
+ }
+
+ private function getTemplateOptions() {
+ return array('all' => 'All Types') +
+ PhabricatorReleaseTemplate::getTemplatesMap();
+ }
+
+ private function getStateOptions() {
+ return array(
+ 'all' => 'Any state',
+ 'active' => 'Active states',
+ ) + ReleaseRelease::getStatesMap();
+ }
+
+ protected function getRequiredHandlePHIDsForResultList(
+ array $releases,
+ PhabricatorSavedQuery $query) {
+
+ return array();
+ }
+
+ protected function renderResultList(
+ array $releases,
+ PhabricatorSavedQuery $query,
+ array $handles) {
+
+ assert_instances_of($releases, 'ReleaseRelease');
+ $viewer = $this->requireViewer();
+
+
+ $list = new PHUIObjectItemListView();
+
+ foreach ($releases as $release) {
+ $name = $release->getName();
+
+ $item = id(new PHUIObjectItemView())
+ ->setObjectName($release->getMonogram())
+ ->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/ReleaseReleaseTransactionQuery.php b/src/applications/release/query/ReleaseReleaseTransactionQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/query/ReleaseReleaseTransactionQuery.php
@@ -0,0 +1,53 @@
+<?php
+
+final class ReleaseReleaseTransactionQuery
+ 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 ReleaseReleaseTransaction();
+ }
+
+ 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 ReleaseReleaseQuery())
+ ->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/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 ReleaseRelease());
+ }
+
+}
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
+ 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/storage/ReleaseRelease.php b/src/applications/release/storage/ReleaseRelease.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseRelease.php
@@ -0,0 +1,293 @@
+<?php
+
+final class ReleaseRelease 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(
+ ReleaseReleaseDefaultViewCapability::CAPABILITY);
+
+ $edit_policy = $release_application->getPolicy(
+ ReleaseReleaseDefaultEditCapability::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(
+ ReleaseReleasePHIDType::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 ReleaseReleaseEditor();
+ }
+
+ public function getApplicationTransactionObject() {
+ return $this;
+ }
+
+ public function getApplicationTransactionTemplate() {
+ return new ReleaseReleaseTransaction();
+ }
+
+ 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/ReleaseReleaseTransaction.php b/src/applications/release/storage/ReleaseReleaseTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseReleaseTransaction.php
@@ -0,0 +1,47 @@
+<?php
+
+final class ReleaseReleaseTransaction
+ 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 ReleaseReleaseTransactionType) {
+ return $impl->expandTransaction($object);
+ }
+ return array();
+ }
+
+ public function getApplicationTransactionType() {
+ return ReleaseReleasePHIDType::TYPECONST;
+ }
+
+ public function getBaseTransactionClass() {
+ return 'ReleaseReleaseTransactionType';
+ }
+
+ public function getApplicationTransactionCommentObject() {
+ return new ReleaseReleaseTransactionComment();
+ }
+
+ public function getMailTags() {
+ $tags = parent::getMailTags();
+
+ switch ($this->getTransactionType()) {
+ case ReleaseReleaseStateTransaction::TRANSACTIONTYPE:
+ $tags[] = self::MAILTAG_STATE;
+ break;
+ default:
+ $tags[] = self::MAILTAG_OTHER;
+ break;
+ }
+ return $tags;
+ }
+}
diff --git a/src/applications/release/storage/ReleaseReleaseTransactionComment.php b/src/applications/release/storage/ReleaseReleaseTransactionComment.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/storage/ReleaseReleaseTransactionComment.php
@@ -0,0 +1,10 @@
+<?php
+
+final class ReleaseReleaseTransactionComment
+ extends PhabricatorApplicationTransactionComment {
+
+ public function getApplicationTransactionObject() {
+ return new ReleaseReleaseTransaction();
+ }
+
+}
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/ReleaseReleaseDatasource.php b/src/applications/release/typeahead/ReleaseReleaseDatasource.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/typeahead/ReleaseReleaseDatasource.php
@@ -0,0 +1,65 @@
+<?php
+
+final class ReleaseReleaseDatasource
+ 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 ReleaseReleaseQuery())
+ ->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/ReleaseReleaseCurrentRefTransaction.php b/src/applications/release/xaction/ReleaseReleaseCurrentRefTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseCurrentRefTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class ReleaseReleaseCurrentRefTransaction
+ extends ReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:currentref';
+
+ public function generateOldValue($object) {
+ return $object->getDetail(ReleaseRelease::DETAIL_CURRENTREF);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDetail(ReleaseRelease::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/ReleaseReleaseCutpointTransaction.php b/src/applications/release/xaction/ReleaseReleaseCutpointTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseCutpointTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class ReleaseReleaseCutpointTransaction
+ extends ReleaseReleaseTransactionType {
+
+ const TRANSACTIONTYPE = 'release:cutpoint';
+
+ public function generateOldValue($object) {
+ return $object->getDetail(ReleaseRelease::DETAIL_CUTPOINTS);
+ }
+
+ public function applyInternalEffects($object, $value) {
+ $object->setDetail(ReleaseRelease::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/ReleaseReleaseDescribeTransaction.php b/src/applications/release/xaction/ReleaseReleaseDescribeTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseDescribeTransaction.php
@@ -0,0 +1,41 @@
+<?php
+
+final class ReleaseReleaseDescribeTransaction
+ extends ReleaseReleaseTransactionType {
+
+ 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/ReleaseReleaseNameTransaction.php b/src/applications/release/xaction/ReleaseReleaseNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseNameTransaction.php
@@ -0,0 +1,44 @@
+<?php
+
+final class ReleaseReleaseNameTransaction
+extends ReleaseReleaseTransactionType {
+
+ 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/ReleaseReleaseStateTransaction.php b/src/applications/release/xaction/ReleaseReleaseStateTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseStateTransaction.php
@@ -0,0 +1,45 @@
+<?php
+
+final class ReleaseReleaseStateTransaction
+ extends ReleaseReleaseTransactionType {
+
+ 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 = ReleaseRelease::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 = ReleaseRelease::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/ReleaseReleaseTemplateTransaction.php b/src/applications/release/xaction/ReleaseReleaseTemplateTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseTemplateTransaction.php
@@ -0,0 +1,44 @@
+<?php
+
+final class ReleaseReleaseTemplateTransaction
+ extends ReleaseReleaseTransactionType {
+
+ 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/ReleaseReleaseTransactionType.php b/src/applications/release/xaction/ReleaseReleaseTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/release/xaction/ReleaseReleaseTransactionType.php
@@ -0,0 +1,28 @@
+<?php
+
+abstract class ReleaseReleaseTransactionType
+ 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
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 1:43 PM (1 w, 5 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/4w/7r/5kqfetk3nhx575j3
Default Alt Text
D16981.diff (77 KB)
Attached To
Mode
D16981: Initial code-dump of Release
Attached
Detach File
Event Timeline
Log In to Comment