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 @@ + 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[1-9]\d*)' => 'ReleaseReleaseDetailsController', + '/release/' => array( + $this->getEditRoutePattern('edit/') => 'ReleaseReleaseEditController', + '(?:query/(?P[^/]+)/)?' => '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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + + 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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') + ->setDescription(pht('Hashes where this release was cut from')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('current_refs') + ->setType('map') + ->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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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, ),