diff --git a/resources/sql/autopatches/20161220.release.changes.sql b/resources/sql/autopatches/20161220.release.changes.sql new file mode 100755 --- /dev/null +++ b/resources/sql/autopatches/20161220.release.changes.sql @@ -0,0 +1,63 @@ +CREATE TABLE {$NAMESPACE}_release.release_changerequest ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + requestedObjectPHID VARBINARY(64) NOT NULL, + description longtext COLLATE {$COLLATE_TEXT} NOT NULL, + status VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + requestorPHID VARBINARY(64) NOT NULL, + releasePHID VARBINARY(64) NOT NULL, + details longtext COLLATE {$COLLATE_TEXT} NOT NULL, + implementationKey VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT(10) UNSIGNED NOT NULL, + dateModified INT(10) UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (`phid`), + KEY `key_release` (`releasePHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; + +CREATE TABLE {$NAMESPACE}_release.release_changerequesttransaction ( + 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_changerequestcustomfieldstorage ( + 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_changerequestcustomfieldstringindex ( + 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_changerequestcustomfieldnumericindex ( + 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 @@ -4497,12 +4497,46 @@ 'ProjectReplyHandler' => 'applications/project/mail/ProjectReplyHandler.php', 'ProjectSearchConduitAPIMethod' => 'applications/project/conduit/ProjectSearchConduitAPIMethod.php', 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', + 'ReleaseChangeRequest' => 'applications/release/storage/ReleaseChangeRequest.php', + 'ReleaseChangeRequestAction' => 'applications/release/changes/actions/ReleaseChangeRequestAction.php', + 'ReleaseChangeRequestActionController' => 'applications/release/controller/ReleaseChangeRequestActionController.php', + 'ReleaseChangeRequestCommitImplementation' => 'applications/release/changes/ReleaseChangeRequestCommitImplementation.php', + 'ReleaseChangeRequestConfiguredCustomField' => 'applications/release/customfield/ReleaseChangeRequestConfiguredCustomField.php', + 'ReleaseChangeRequestCustomField' => 'applications/release/customfield/ReleaseChangeRequestCustomField.php', + 'ReleaseChangeRequestCustomFieldNumericIndex' => 'applications/release/storage/ReleaseChangeRequestCustomFieldNumericIndex.php', + 'ReleaseChangeRequestCustomFieldStorage' => 'applications/release/storage/ReleaseChangeRequestCustomFieldStorage.php', + 'ReleaseChangeRequestCustomFieldStringIndex' => 'applications/release/storage/ReleaseChangeRequestCustomFieldStringIndex.php', + 'ReleaseChangeRequestDescribeTransaction' => 'applications/release/xaction/ReleaseChangeRequestDescribeTransaction.php', + 'ReleaseChangeRequestDetailsController' => 'applications/release/controller/ReleaseChangeRequestDetailsController.php', + 'ReleaseChangeRequestEditConduitAPIMethod' => 'applications/release/conduit/ReleaseChangeRequestEditConduitAPIMethod.php', + 'ReleaseChangeRequestEditController' => 'applications/release/controller/ReleaseChangeRequestEditController.php', + 'ReleaseChangeRequestEditEngine' => 'applications/release/editor/ReleaseChangeRequestEditEngine.php', + 'ReleaseChangeRequestEditor' => 'applications/release/editor/ReleaseChangeRequestEditor.php', + 'ReleaseChangeRequestFromRevisionController' => 'applications/release/controller/ReleaseChangeRequestFromRevisionController.php', + 'ReleaseChangeRequestImplementation' => 'applications/release/changes/ReleaseChangeRequestImplementation.php', + 'ReleaseChangeRequestImplementationTransaction' => 'applications/release/xaction/ReleaseChangeRequestImplementationTransaction.php', + 'ReleaseChangeRequestListController' => 'applications/release/controller/ReleaseChangeRequestListController.php', + 'ReleaseChangeRequestMergeAction' => 'applications/release/changes/actions/ReleaseChangeRequestMergeAction.php', + 'ReleaseChangeRequestPHIDType' => 'applications/release/phid/ReleaseChangeRequestPHIDType.php', + 'ReleaseChangeRequestQuery' => 'applications/release/query/ReleaseChangeRequestQuery.php', + 'ReleaseChangeRequestRejectAction' => 'applications/release/changes/actions/ReleaseChangeRequestRejectAction.php', + 'ReleaseChangeRequestReleaseTransaction' => 'applications/release/xaction/ReleaseChangeRequestReleaseTransaction.php', + 'ReleaseChangeRequestRequestedObjectTransaction' => 'applications/release/xaction/ReleaseChangeRequestRequestedObjectTransaction.php', + 'ReleaseChangeRequestRequestorTransaction' => 'applications/release/xaction/ReleaseChangeRequestRequestorTransaction.php', + 'ReleaseChangeRequestRevisionImplementation' => 'applications/release/changes/ReleaseChangeRequestRevisionImplementation.php', + 'ReleaseChangeRequestSearchConduitAPIMethod' => 'applications/release/conduit/ReleaseChangeRequestSearchConduitAPIMethod.php', + 'ReleaseChangeRequestSearchEngine' => 'applications/release/query/ReleaseChangeRequestSearchEngine.php', + 'ReleaseChangeRequestStateTransaction' => 'applications/release/xaction/ReleaseChangeRequestStateTransaction.php', + 'ReleaseChangeRequestTransaction' => 'applications/release/storage/ReleaseChangeRequestTransaction.php', + 'ReleaseChangeRequestTransactionQuery' => 'applications/release/query/ReleaseChangeRequestTransactionQuery.php', + 'ReleaseChangeRequestTransactionType' => 'applications/release/xaction/ReleaseChangeRequestTransactionType.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', + 'ReleaseReleaseChangeRequestAddedTransaction' => 'applications/release/xaction/ReleaseReleaseChangeRequestAddedTransaction.php', 'ReleaseReleaseCreateReleaseCapability' => 'applications/release/capability/ReleaseReleaseCreateReleaseCapability.php', 'ReleaseReleaseCurrentRefTransaction' => 'applications/release/xaction/ReleaseReleaseCurrentRefTransaction.php', 'ReleaseReleaseCutpointTransaction' => 'applications/release/xaction/ReleaseReleaseCutpointTransaction.php', @@ -4528,6 +4562,7 @@ 'ReleaseReleaseTransactionComment' => 'applications/release/storage/ReleaseReleaseTransactionComment.php', 'ReleaseReleaseTransactionQuery' => 'applications/release/query/ReleaseReleaseTransactionQuery.php', 'ReleaseReleaseTransactionType' => 'applications/release/xaction/ReleaseReleaseTransactionType.php', + 'ReleaseRenderEventListener' => 'applications/release/ReleaseRenderEventListener.php', 'ReleephAuthorFieldSpecification' => 'applications/releeph/field/specification/ReleephAuthorFieldSpecification.php', 'ReleephBranch' => 'applications/releeph/storage/ReleephBranch.php', 'ReleephBranchAccessController' => 'applications/releeph/controller/branch/ReleephBranchAccessController.php', @@ -9956,6 +9991,47 @@ 'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'QueryFormattingTestCase' => 'PhabricatorTestCase', + 'ReleaseChangeRequest' => array( + 'PhabricatorReleaseDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorCustomFieldInterface', + ), + 'ReleaseChangeRequestAction' => 'Phobject', + 'ReleaseChangeRequestActionController' => 'PhabricatorController', + 'ReleaseChangeRequestCommitImplementation' => 'Phobject', + 'ReleaseChangeRequestConfiguredCustomField' => array( + 'ReleaseChangeRequestCustomField', + 'PhabricatorStandardCustomFieldInterface', + ), + 'ReleaseChangeRequestCustomField' => 'PhabricatorCustomField', + 'ReleaseChangeRequestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', + 'ReleaseChangeRequestCustomFieldStorage' => 'PhabricatorCustomFieldStorage', + 'ReleaseChangeRequestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', + 'ReleaseChangeRequestDescribeTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestDetailsController' => 'PhabricatorController', + 'ReleaseChangeRequestEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', + 'ReleaseChangeRequestEditController' => 'PhabricatorController', + 'ReleaseChangeRequestEditEngine' => 'PhabricatorEditEngine', + 'ReleaseChangeRequestEditor' => 'PhabricatorApplicationTransactionEditor', + 'ReleaseChangeRequestFromRevisionController' => 'PhabricatorController', + 'ReleaseChangeRequestImplementation' => 'Phobject', + 'ReleaseChangeRequestImplementationTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestListController' => 'PhabricatorController', + 'ReleaseChangeRequestMergeAction' => 'ReleaseChangeRequestAction', + 'ReleaseChangeRequestPHIDType' => 'PhabricatorPHIDType', + 'ReleaseChangeRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'ReleaseChangeRequestRejectAction' => 'ReleaseChangeRequestAction', + 'ReleaseChangeRequestReleaseTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestRequestedObjectTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestRequestorTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestRevisionImplementation' => 'ReleaseChangeRequestImplementation', + 'ReleaseChangeRequestSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'ReleaseChangeRequestSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'ReleaseChangeRequestStateTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestTransaction' => 'PhabricatorModularTransaction', + 'ReleaseChangeRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'ReleaseChangeRequestTransactionType' => 'PhabricatorModularTransactionType', 'ReleaseConfiguredCustomField' => array( 'ReleaseCustomField', 'PhabricatorStandardCustomFieldInterface', @@ -9974,6 +10050,7 @@ 'PhabricatorCustomFieldInterface', 'PhabricatorPolicyInterface', ), + 'ReleaseReleaseChangeRequestAddedTransaction' => 'ReleaseReleaseTransactionType', 'ReleaseReleaseCreateReleaseCapability' => 'PhabricatorPolicyCapability', 'ReleaseReleaseCurrentRefTransaction' => 'ReleaseReleaseTransactionType', 'ReleaseReleaseCutpointTransaction' => 'ReleaseReleaseTransactionType', @@ -9999,6 +10076,7 @@ 'ReleaseReleaseTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ReleaseReleaseTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'ReleaseReleaseTransactionType' => 'PhabricatorModularTransactionType', + 'ReleaseRenderEventListener' => 'PhabricatorEventListener', 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 'ReleephBranch' => array( 'ReleephDAO', diff --git a/src/applications/release/OPEN_QUESTIONS b/src/applications/release/OPEN_QUESTIONS --- a/src/applications/release/OPEN_QUESTIONS +++ b/src/applications/release/OPEN_QUESTIONS @@ -8,8 +8,16 @@ 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, ? - +- What kind of Edges do we need? RepositoryInRelease, ObjectRequestedAsChangeForRelease, + ChangeRequestInRelease, ? +- Should Change Request have their own policy, subscribers, comments, etc.? +- Transactions Metadata: That one didn't make the cut for modular-transactions + either. For the case of "Updates on CR show up on the Release page", they + would be very useful. Do we hate it? +- In Releeph, there's a "want" status, that's corresponds to Differential's + Accept, and distinct from "Pull" status. Is that an important enough feature + to include as first-class? Is there a way to include something like that as + a Custom Field? General questions: diff --git a/src/applications/release/ReleaseRenderEventListener.php b/src/applications/release/ReleaseRenderEventListener.php new file mode 100644 --- /dev/null +++ b/src/applications/release/ReleaseRenderEventListener.php @@ -0,0 +1,51 @@ +listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: + $this->handleActionsEvent($event); + break; + } + } + + private function handleActionsEvent(PhutilEvent $event) { + if (!$this->canUseApplication($event->getUser())) { + return; + } + + $object = $event->getValue('object'); + if ($object instanceof DifferentialRevision) { + $this->addRevisionAction($event, $object); + } + // if ($object instanceof PhabricatorRepository) { + // $this->addRepositoryActions($event); + // } + // if ($object instanceof PhabricatorRepositoryCommit) { + // $this->addCommitActions($event); + // } + } + + private function addRevisionAction( + PhutilEvent $event, + DifferentialRevision $revision) { + + $repository = $revision->getRepository(); + if (!$repository) { + return; + } + $revision_phid = $revision->getPHID(); + $actions[] = id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setName('Request Pick To Release') + ->setIcon('fa-steam') + ->setHref("/release/request/revision/{$revision_phid}/"); + + $this->addActionMenuItems($event, $actions); + } +} diff --git a/src/applications/release/TODO b/src/applications/release/TODO --- a/src/applications/release/TODO +++ b/src/applications/release/TODO @@ -1,2 +1,10 @@ +Things to complete before D17020 makes sense: + +- Maybe CR from a Commit ? +- wire in some CC/TO mail addresses +- At least a local hacked version of "merge action" that really merges and + marks the CR. +- Implementation selector shows keys, should show nice names. + 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 --- a/src/applications/release/application/PhabricatorReleaseApplication.php +++ b/src/applications/release/application/PhabricatorReleaseApplication.php @@ -30,6 +30,12 @@ return true; } + public function getEventListeners() { + return array( + new ReleaseRenderEventListener(), + ); + } + public function getRemarkupRules() { return array( new PhabricatorReleaseRemarkupRule(), @@ -54,12 +60,24 @@ public function getRoutes() { return array( - '/X(?P[1-9]\d*)' => 'ReleaseReleaseDetailsController', + '/X(?P[1-9]\d*)/?' => 'ReleaseReleaseDetailsController', + '/Y(?P[1-9]\d*)' => 'ReleaseChangeRequestDetailsController', '/release/' => array( $this->getEditRoutePattern('edit/') => 'ReleaseReleaseEditController', '(?:query/(?P[^/]+)/)?' => 'ReleaseReleaseListController', // TODO new release - + 'request/' => array( + 'revision/(?P[^/]*)/' => + 'ReleaseChangeRequestFromRevisionController', + ), + 'changerequest/' => array( + '(?:query/(?P[^/]+)/)?' => + 'ReleaseChangeRequestListController', + $this->getEditRoutePattern('edit/') => + 'ReleaseChangeRequestEditController', + 'action/(?P[^/]*)/(?P[^/]*)/' => + 'ReleaseChangeRequestActionController', + ), ), ); } diff --git a/src/applications/release/changes/ReleaseChangeRequestCommitImplementation.php b/src/applications/release/changes/ReleaseChangeRequestCommitImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/release/changes/ReleaseChangeRequestCommitImplementation.php @@ -0,0 +1,7 @@ +getPhobjectClassConstant('IMPLEMENTATION_KEY'); + } + + public static function getImplementationByKey($key) { + $all = self::getAllImplementations(); + $implementation = idx($all, $key); + if (!$implementation) { + throw new Exception(pht('Implementation not found for key %s', $key)); + } + return $implementation; + } + + public static function getAllImplementationKeys() { + $all = self::getAllImplementations(); + return mpull($all, 'getImplementationKey'); + } + + // TODO does this need caching if we load lots of requests? + private static function getAllImplementations() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getImplementationKey') + ->execute(); + } +} diff --git a/src/applications/release/changes/ReleaseChangeRequestRevisionImplementation.php b/src/applications/release/changes/ReleaseChangeRequestRevisionImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/release/changes/ReleaseChangeRequestRevisionImplementation.php @@ -0,0 +1,22 @@ +getRequestedObject()->getTitle(); + } + + public function getChangeAuthorPHID(ReleaseChangeRequest $request) { + return $request->getRequestedObject()->getAuthorPHID(); + } +} diff --git a/src/applications/release/changes/actions/ReleaseChangeRequestAction.php b/src/applications/release/changes/actions/ReleaseChangeRequestAction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/changes/actions/ReleaseChangeRequestAction.php @@ -0,0 +1,62 @@ +getPHID(); + $key = $this->getActionKey(); + return "/release/changerequest/action/{$phid}/{$key}/"; + } + + public function generateActions() { + return array($this); + } + + public static function getAllActions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setExpandMethod('generateActions') + ->setUniqueMethod('getActionKey') + ->execute(); + } + + public static function getActionByKey($action_key) { + $action = idx(self::getAllActions(), $action_key); + if ($action) { + return $action; + } + throw new Exception(pht( + 'Change Request Action not found for action key "%s".', + $action_key)); + } +} diff --git a/src/applications/release/changes/actions/ReleaseChangeRequestMergeAction.php b/src/applications/release/changes/actions/ReleaseChangeRequestMergeAction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/changes/actions/ReleaseChangeRequestMergeAction.php @@ -0,0 +1,74 @@ +getStatus() == ReleaseChangeRequest::STATUS_PENDING; + } + + public function act(ReleaseChangeRequest $change, AphrontRequest $request) { + $viewer = $request->getViewer(); + $xaction_type = ReleaseChangeRequestStateTransaction::TRANSACTIONTYPE; + $status = ReleaseChangeRequest::STATUS_INCLUDED; + + $xaction = id(new ReleaseChangeRequestTransaction()) + ->setTransactionType($xaction_type) + ->setNewValue($status); + + $editor = id(new ReleaseChangeRequestEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($change, array($xaction)); + + return $change->getURI(); + } + + public function getActionName() { + return 'Mark Merged'; + } + public function getActionIcon() { + return 'fa-plane'; + } + public function getActionKey() { + return 'markmerged'; + } + + public function generateActions() { + // Ideally, we should detect the commit in Diffusion, like with Revisions, + // but I'm not sure how complex that would be (And how scalable across + // Implmentations). + + // Or maybe just out-right remove it and let users download it from the docs + if (PhabricatorEnv::getEnvConfig('release.show-debug-tools')) { + return array($this); + } else { + return array(); + } + } + +} diff --git a/src/applications/release/changes/actions/ReleaseChangeRequestRejectAction.php b/src/applications/release/changes/actions/ReleaseChangeRequestRejectAction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/changes/actions/ReleaseChangeRequestRejectAction.php @@ -0,0 +1,45 @@ +getStatus() == ReleaseChangeRequest::STATUS_PENDING; + } + + public function act(ReleaseChangeRequest $change, AphrontRequest $request) { + $viewer = $request->getViewer(); + $xaction_type = ReleaseChangeRequestStateTransaction::TRANSACTIONTYPE; + $status = ReleaseChangeRequest::STATUS_REJECTED; + + $xaction = id(new ReleaseChangeRequestTransaction()) + ->setTransactionType($xaction_type) + ->setNewValue($status); + + $editor = id(new ReleaseChangeRequestEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($change, array($xaction)); + + return $change->getURI(); + } + + public function getActionName() { + return pht('Reject'); + } + public function getActionIcon() { + return 'fa-times'; + } + public function getActionKey() { + return 'reject'; + } +} diff --git a/src/applications/release/conduit/ReleaseChangeRequestEditConduitAPIMethod.php b/src/applications/release/conduit/ReleaseChangeRequestEditConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/release/conduit/ReleaseChangeRequestEditConduitAPIMethod.php @@ -0,0 +1,20 @@ +newOption( + 'release.changerequest.customFields', + 'wild', + array()) + ->setSummary(pht('Custom fields for Change Requests.')) + ->setDescription(pht( + 'Array of custom fields for Change Requests. For details, see '. + '**[[ %s | Configuring Custom Fields ]]** in the documentation.', + $custom_fields_href)), + $this->newOption( 'release.show-debug-tools', 'bool', true) diff --git a/src/applications/release/controller/ReleaseChangeRequestActionController.php b/src/applications/release/controller/ReleaseChangeRequestActionController.php new file mode 100644 --- /dev/null +++ b/src/applications/release/controller/ReleaseChangeRequestActionController.php @@ -0,0 +1,60 @@ +getViewer(); + + $change_phid = $request->getURIData('phid'); + + $change = id(new ReleaseChangeRequestQuery()) + ->setViewer($viewer) + ->withPHIDs(array($change_phid)) + ->needReleases(true) + ->needRequestObjects(true) + ->executeOne(); + if (!$change) { + return new Aphront404Response(); + } + $release = $change->getRelease(); + + $action_key = $request->getURIData('action'); + $action = ReleaseChangeRequestAction::getActionByKey($action_key); + + $action->assertPolicy($viewer, $change); + + $errors = array(); + + if (!$action->isEnabledForRequest($change)) { + $errors[] = pht( + 'This action can not be applied to this Change Request at this time.'); + } + + if ($request->isDialogFormPost()) { + + if (!$errors) { + try { + $redirect_uri = $action->act($change, $request); + return id(new AphrontRedirectResponse())->setURI($redirect_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $errors[] = 'Failed to invoke action! '.$ex->getMessage(); + } + } + } + + $prompt = $action->getPrompt($change); + + $dialog = $this->newDialog() + ->setSubmitURI($request->getRequestURI()) + ->setTitle($action->getFormTitle($change)) + ->appendChild($prompt) + ->setErrors($errors) + ->addCancelButton('#'); + + if ($action->isEnabledForRequest($change)) { + $dialog->addSubmitButton(pht('Submit')); + } + + return $dialog; + } +} diff --git a/src/applications/release/controller/ReleaseChangeRequestDetailsController.php b/src/applications/release/controller/ReleaseChangeRequestDetailsController.php new file mode 100644 --- /dev/null +++ b/src/applications/release/controller/ReleaseChangeRequestDetailsController.php @@ -0,0 +1,145 @@ +getViewer(); + $id = $request->getURIData('id'); + + $change = id(new ReleaseChangeRequestQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->needReleases(true) + ->needRequestObjects(true) + ->executeOne(); + if (!$change) { + return new Aphront404Response(); + } + $release = $change->getRelease(); + + $header = id(new PHUIHeaderView()) + ->setHeader( + hsprintf('%s %s', $change->getMonogram(), $change->getTitle())) + ->setUser($viewer) + ->setPolicyObject($release); + $curtain = $this->buildCurtain($change); + + $release_properties = $this->buildReleaseProperties($release); + $change_properties = $this->buildChangeProperties($change); + + $timeline = $this->buildTransactionTimeline( + $change, + new ReleaseChangeRequestTransactionQuery()); + $timeline->setQuoteRef($change->getMonogram()); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $timeline, + )) + ->addPropertySection($release->getName(), $release_properties) + ->addPropertySection(pht('Change Request'), $change_properties); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($release->getMonogram(), $release->getURI()); + $crumbs->addTextCrumb($change->getMonogram()); + + $title = $change->getTitle(); + return $this->newPage() + ->setTitle($change->getMonogram().' '.$title) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $release->getPHID(), + $change->getPHID(), + )) + ->appendChild( + array( + $view, + )); + + } + + + private function buildChangeProperties($change) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->addProperty( + pht('Change Type'), + $change->getImplementation()->getImplementationName()) + ->addProperty( + pht('Requested By'), + $viewer->renderHandle($change->getRequestorPHID())) + ->addProperty( + pht('Content'), + $viewer->renderHandle($change->getRequestedObjectPHID())) + ->addProperty( + pht('Author'), + $viewer->renderHandle($change->getChangeAuthorPHID())) + ->addProperty(pht('Status'), $change->getStatusName()); + + $description = $change->getDescription(); + if (strlen($description)) { + $properties + ->addSectionHeader('Description') + ->addTextContent( + new PHUIRemarkupView($this->getViewer(), $description)); + } + + $field_list = PhabricatorCustomField::getObjectFields( + $change, + PhabricatorCustomField::ROLE_VIEW); + $field_list->appendFieldsToPropertyList( + $change, + $viewer, + $properties); + + return $properties; + } + + private function buildReleaseProperties($release) { + $properties = id(new PHUIPropertyListView()) + ->addProperty(pht('Release Type'), $release->getReleaseTemplateName()) + ->addProperty(pht('Status'), $release->getStateName()); + + return $properties; + } + + private function buildCurtain($change) { + $curtain = $this->newCurtainView($change); + $viewer = $this->getViewer(); + + $change_phid = $change->getPHID(); + + $id = $change->getID(); + $edit_uri = $this->getApplicationURI("changerequest/edit/{$id}/"); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $change, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain->addAction(id(new PhabricatorActionView()) + ->setName(pht('Edit Change Request')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit)); + + $actions = ReleaseChangeRequestAction::getAllActions(); + foreach ($actions as $action) { + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($action->getActionName()) + ->setWorkflow(true) + ->setDisabled(!$action->isEnabledForRequest($change)) + ->setIcon($action->getActionIcon()) + ->setHref($action->getActionHref($change))); + } + + return $curtain; + } + +} diff --git a/src/applications/release/controller/ReleaseChangeRequestEditController.php b/src/applications/release/controller/ReleaseChangeRequestEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/release/controller/ReleaseChangeRequestEditController.php @@ -0,0 +1,14 @@ +getViewer(); + + $engine = id(new ReleaseChangeRequestEditEngine()) + ->setController($this); + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/release/controller/ReleaseChangeRequestFromRevisionController.php b/src/applications/release/controller/ReleaseChangeRequestFromRevisionController.php new file mode 100644 --- /dev/null +++ b/src/applications/release/controller/ReleaseChangeRequestFromRevisionController.php @@ -0,0 +1,128 @@ +getViewer(); + + $revision_phid = $request->getURIData('revision'); + + $revision = id(new DifferentialRevisionQuery()) + ->withPHIDs(array($revision_phid)) + ->setViewer($viewer) + ->executeOne(); + if (!$revision) { + return new Aphront404Response(); + } + + $repository = $revision->getRepository(); + $errors = array(); + + // TODO requireCapability + + $v_release = null; + $e_release = true; + + $v_message = null; + + if ($request->isDialogFormPost()) { + + $v_release = $request->getArr('release'); + if (!$v_release) { + $e_release = pht('Required'); + $errors[] = 'Specify target release'; + } else { + $release = id(new ReleaseReleaseQuery()) + ->setViewer($viewer) + ->withPHIDs($v_release) + ->executeOne(); + + if (!$release->canAcceptChangeRequests()) { + $e_release = pht('Invalid'); + $errors[] = pht( + 'This release can not accept any change requests at this time.'); + } + } + + $v_message = $request->getStr('message'); + + $actor_phid = $viewer->getPHID(); + + if (!$errors) { + + $change_request = new ReleaseChangeRequest(); + + $xactions = array( + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE), + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(ReleaseChangeRequestRequestorTransaction::TRANSACTIONTYPE) + ->setNewValue($actor_phid), + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(ReleaseChangeRequestReleaseTransaction::TRANSACTIONTYPE) + ->setNewValue($release->getPHID()), + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(ReleaseChangeRequestImplementationTransaction::TRANSACTIONTYPE) + ->setNewValue(ReleaseChangeRequestRevisionImplementation::IMPLEMENTATION_KEY), + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(ReleaseChangeRequestRequestedObjectTransaction::TRANSACTIONTYPE) + ->setNewValue($revision_phid), + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(ReleaseChangeRequestDescribeTransaction::TRANSACTIONTYPE) + ->setNewValue($v_message), + ); + $editor = id(new ReleaseChangeRequestEditor()) + ->setActor($viewer) + ->setContentSource( + PhabricatorContentSource::newFromRequest($request)) + ->setContinueOnNoEffect(true); + $editor->applyTransactions($change_request, $xactions); + + // maybe add some xactions to the Release here. + + return id(new AphrontRedirectResponse())->setURI($change_request->getURI()); + } + } + + $prompt = hsprintf( + 'This will request the Release Managers to include this Revision in the '. + 'selected Release.'); + + // TODO filter only to relevant releases + $datasource = id(new ReleaseReleaseDatasource()); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setDatasource($datasource) + ->setLimit(1) + ->setName('release') + ->setLabel(pht('Release')) + ->setValue($v_release) + ->setError($e_release)) + ->appendControl( + id(new PhabricatorRemarkupControl()) + ->setLabel('Reason') + ->setValue($v_message) + ->setName('message')); + + $errors_view = null; + if ($errors) { + $errors_view = id(new PHUIInfoView()) + ->setErrors($errors); + } + + $dialog = $this->newDialog() + ->setTitle(pht('Pick Revision to Release?')) + ->appendChild($prompt) + ->appendChild($errors_view) + ->appendForm($form) + ->addSubmitButton(pht('Please')) + ->addCancelButton('#'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/release/controller/ReleaseChangeRequestListController.php b/src/applications/release/controller/ReleaseChangeRequestListController.php new file mode 100644 --- /dev/null +++ b/src/applications/release/controller/ReleaseChangeRequestListController.php @@ -0,0 +1,25 @@ +setController($this) + ->buildResponse(); + } + + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addTextCrumb(pht('Change Requests')); + + return $crumbs; + } + + + public function shouldAllowPublic() { + return true; + } + +} diff --git a/src/applications/release/controller/ReleaseReleaseDetailsController.php b/src/applications/release/controller/ReleaseReleaseDetailsController.php --- a/src/applications/release/controller/ReleaseReleaseDetailsController.php +++ b/src/applications/release/controller/ReleaseReleaseDetailsController.php @@ -1,6 +1,5 @@ getViewer(); @@ -62,6 +61,18 @@ $comment_view->setTransactionTimeline($timeline); $repositories = $this->buildRepositoriesSection($viewer, $release); + $changes = $this->buildChangesSection($viewer, $release); + + $phid = $release->getPHID(); + $button = id(new PHUIButtonView()) + ->setText(pht('See All')) + ->setHref($this->getApplicationURI("changerequest/?release={$phid}")) + ->setTag('a') + ->setIcon('fa-list-ul'); + + $changes_header = id(new PHUIHeaderView()) + ->setHeader(pht('Pending Changes')) + ->addActionLink($button); $view = id(new PHUITwoColumnView()) ->setHeader($header) @@ -71,7 +82,8 @@ $comment_view, )) ->addPropertySection(pht('Details'), $properties) - ->addPropertySection(pht('Repositories'), $repositories); + ->addPropertySection(pht('Repositories'), $repositories) + ->addPropertySection($changes_header, $changes); return $this->newPage() ->setTitle('X'.$release->getID().' '.$release_name) @@ -86,6 +98,47 @@ )); } + private function buildChangesSection($viewer, $release) { + // TODO see all + $changes = id(new ReleaseChangeRequestQuery()) + ->setViewer($viewer) + ->setLimit(5) + ->withReleasePHIDs(array($release->getPHID())) + ->withStatuses(array(ReleaseChangeRequest::STATUS_PENDING)) + ->needRequestObjects(true) + ->execute(); + + if (!$changes) { + return null; + } + + $icon = id(new PHUIIconView()) + ->setIcon('fa-list-alt'); + + + $list = id(new PHUIObjectItemListView()); + + foreach ($changes as $change) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($change->getMonogram()) + ->setHref($change->getURI()) + ->setHeader($change->getTitle()) + ->addAttribute(pht('(impl: %s)', $change->getImplementationKey())); + +/* D17004 + $field_list = PhabricatorCustomField::getObjectFields( + $change, + PhabricatorCustomField::ROLE_LIST); + $field_list + ->appendFieldsToListItem($change, $this->getViewer(), $item); +*/ + $list->addItem($item); + } + + return $list; + } + + private function buildRepositoriesSection($viewer, $release) { $cutpoints = $release->getCutpoints(); $currentrefs = $release->getCurrentRefs(); diff --git a/src/applications/release/controller/ReleaseReleaseEditController.php b/src/applications/release/controller/ReleaseReleaseEditController.php --- a/src/applications/release/controller/ReleaseReleaseEditController.php +++ b/src/applications/release/controller/ReleaseReleaseEditController.php @@ -8,11 +8,6 @@ $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 --- a/src/applications/release/controller/ReleaseReleaseListController.php +++ b/src/applications/release/controller/ReleaseReleaseListController.php @@ -3,8 +3,19 @@ final class ReleaseReleaseListController extends PhabricatorController { public function handleRequest(AphrontRequest $request) { + $nav_items = array( + id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Change Requests')), + id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LINK) + ->setName(pht('List Change Requests')) + ->setHref($this->getApplicationURI('changerequest/')), + ); + return id(new ReleaseReleaseSearchEngine()) ->setController($this) + ->setNavigationItems($nav_items) ->buildResponse(); } diff --git a/src/applications/release/customfield/ReleaseChangeRequestConfiguredCustomField.php b/src/applications/release/customfield/ReleaseChangeRequestConfiguredCustomField.php new file mode 100644 --- /dev/null +++ b/src/applications/release/customfield/ReleaseChangeRequestConfiguredCustomField.php @@ -0,0 +1,22 @@ +needRequestObjects(true); + } + + protected function getObjectCreateTitleText($object) { + return pht('Request a new Change to a Release'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Change Request %s', $object->getTitle()); + } + + protected function getEditorURI() { + return '/release/change/edit/'; + } + + protected function getObjectEditShortText($object) { + return $object->getTitle(); + } + + protected function getObjectCreateShortText() { + return pht('New Change Request'); + } + + protected function getCreateNewObjectPolicy() { + // TODO new capability. + return $this->getApplication()->getPolicy( + ReleaseReleaseDefaultEditCapability::CAPABILITY); + } + + protected function getCommentViewHeaderText($object) { + return pht('Discuss Change'); + } + + protected function getCommentViewButtonText($object) { + return pht('Comment'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + + $states = ReleaseChangeRequest::getStatusMap(); + + $implementation_keys = + ReleaseChangeRequestImplementation::getAllImplementationKeys(); + + $is_edit_form = !$this->getIsCreate(); + + return array( + id(new PhabricatorUsersEditField()) + ->setKey('requestor') + ->setLabel(pht('Requested By')) + ->setDescription(pht('The user who asks this Change to be included.')) + ->setTransactionType( + ReleaseChangeRequestRequestorTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + ->setSingleValue($object->getRequestorPHID()), + id(new PhabricatorSelectEditField()) + ->setKey('state') + ->setLabel(pht('State')) + ->setTransactionType( + ReleaseChangeRequestStateTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + ->setOptions($states) + ->setValue($object->getStatus()), + id(new PhabricatorHandlesEditField()) + ->setKey('release') + ->setLabel('Release') + ->setDescription('Release to request the change for.') + ->setTransactionType( + ReleaseChangeRequestReleaseTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + ->setSingleValue($object->getReleasePHID()), + id(new PhabricatorHandlesEditField()) + ->setKey('targetobject') + ->setLabel(pht('Requested Object')) + ->setDescription(pht('Object that contains the change itself.')) + ->setTransactionType( + ReleaseChangeRequestRequestedObjectTransaction::TRANSACTIONTYPE) + ->setIsLockable(false) + ->setIsLocked($is_edit_form) + ->setSingleValue($object->getRequestedObjectPHID()), + id(new PhabricatorSelectEditField()) + ->setKey('implkey') + ->setLabel(pht('Implementation')) + ->setDescription( + pht('An Implementation Key of a valid implementation.')) + ->setTransactionType( + ReleaseChangeRequestImplementationTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + // is there a way to do this for conduit? + ->setOptions($implementation_keys) + ->setIsLockable(false) + ->setIsLocked($is_edit_form) + ->setValue($object->getImplementationKey()), + id(new PhabricatorRemarkupEditField()) + ->setKey('description') + ->setLabel(pht('Description')) + ->setTransactionType( + ReleaseChangeRequestDescribeTransaction::TRANSACTIONTYPE) + ->setDescription(pht('ISO-9001 Change Request explanation.')) + ->setConduitDescription(pht('Description.')) + ->setConduitTypeDescription(pht('Remarkup')) + ->setValue($object->getDescription()), + ); + } + +} diff --git a/src/applications/release/editor/ReleaseChangeRequestEditor.php b/src/applications/release/editor/ReleaseChangeRequestEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/release/editor/ReleaseChangeRequestEditor.php @@ -0,0 +1,125 @@ + + pht('Other activity not listed above occurs.'), + ); + } + + // protected function buildReplyHandler(PhabricatorLiskDAO $object) { + // return id(new ReleaseReleaseReplyHandler()) // TODO + // ->setMailReceiver($object); + // } + + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $id = $object->getID(); + $name = $object->getName(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("Y{$id} {$name}") + ->addHeader('Thread-Topic', "Change Request {$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); + + 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('REQUEST DETAILS'), + PhabricatorEnv::getProductionURI($object->getURI())); + + return $body; + } + +} diff --git a/src/applications/release/phid/ReleaseChangeRequestPHIDType.php b/src/applications/release/phid/ReleaseChangeRequestPHIDType.php new file mode 100755 --- /dev/null +++ b/src/applications/release/phid/ReleaseChangeRequestPHIDType.php @@ -0,0 +1,73 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { /// modernize this? support lookup by monogram. + $change = $objects[$phid]; + + $id = $change->getID(); + $title = $change->getTitle(); + + $handle->setURI($change->getURI()); + $handle->setName($title); + $handle->setFullName("Y{$id}: {$title}"); + } + } + + public function canLoadNamedObject($name) { + return preg_match('/^Y[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 ReleaseChangeRequestQuery()) + ->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/ReleaseChangeRequestQuery.php b/src/applications/release/query/ReleaseChangeRequestQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/release/query/ReleaseChangeRequestQuery.php @@ -0,0 +1,204 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withReleasePHIDs(array $release_phids) { + $this->releasePHIDs = $release_phids; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function withImplementationKeys(array $implementation_keys) { + $this->implementationKeys = $implementation_keys; + return $this; + } + + public function withDatasourceQuery($query) { + $this->datasourceQuery = $query; + return $this; + } + + public function needReleases($need_releases) { + // $this->needReleases = $need_releases; + // TODO drop method - always take them. + return $this; + } + + public function needRequestObjects($need_request_objects) { + // TODO drop method - always take them. + return $this; + } + + public function newResultObject() { + return new ReleaseChangeRequest(); + } + + 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->releasePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'releasePHID IN (%Ls)', + $this->releasePHIDs); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + + if ($this->implementationKeys !== null) { + $where[] = qsprintf( + $conn, + 'implementationKey IN (%Ls)', + $this->implementationKeys); + } + + if (strlen($this->datasourceQuery)) { + // TODO + // $where[] = qsprintf( + // $conn, + // 'name LIKE %~', + // $this->datasourceQuery); + } + + return $where; + } + + protected function willFilterPage(array $changes) { + $viewer = $this->getViewer(); + + $release_phids = mpull($changes, 'getReleasePHID'); + $releases = id(new ReleaseReleaseQuery()) + ->setViewer($viewer) + ->setParentQuery($this) + ->withPHIDs($release_phids) + ->execute(); + $releases = mpull($releases, null, 'getPHID'); + + foreach ($changes as $key => $change) { + $change->attachRelease(idx($releases, $change->getReleasePHID())); + // TODO if !$release + // $this->didRejectResult($change); + // unset($changes[$key]); + // continue; + } + + $object_phids = mpull($changes, 'getRequestedObjectPHID'); + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->setParentQuery($this) + ->withPHIDs($object_phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + foreach ($changes as $change) { + $change->attachRequestedObject(idx($objects, $change->getRequestedObjectPHID())); + } + + return $changes; + } + + // protected function getDefaultOrderVector() { + // return array('name'); + // } + + public function getBuiltinOrders() { + return array( + 'select' => array( + 'vector' => array('status', '-id'), + 'name' => pht('Status'), + ), + ) + parent::getBuiltinOrders(); + } + + public function setGroupBy($group) { + $this->groupBy = $group; + $vector = array(); + + switch ($this->groupBy) { + case self::GROUP_NONE: + $vector = array(); + break; + case self::GROUP_STATE: + $vector = array('status'); + break; + } + + // $this->setGroupVector($vector); + + return $this; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'release' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'releasePHID', + 'reverse' => false, + 'type' => 'phid', + 'unique' => false, + ), + 'status' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'status', + 'reverse' => false, + 'type' => 'string', + 'unique' => false, + ), + ); + } + + public function getQueryApplicationClass() { + return 'PhabricatorReleaseApplication'; + } + +} diff --git a/src/applications/release/query/ReleaseChangeRequestSearchEngine.php b/src/applications/release/query/ReleaseChangeRequestSearchEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/release/query/ReleaseChangeRequestSearchEngine.php @@ -0,0 +1,125 @@ +needRequestObjects(true); + } + + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchDatasourceField()) + ->setLabel('Release') + ->setKey('release') + ->setDatasource(new ReleaseReleaseDatasource()), + id(new PhabricatorSearchSelectField()) + ->setLabel('Status') + ->setKey('status') + ->setOptions($this->getStateOptions()) + ->setDefault('all'), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + $release = idx($map, 'release'); + if ($release) { + $query->withReleasePHIDs($release); + } + + $status = idx($map, 'status', 'all'); + if ($status != 'all') { + $query->withStatuses(array($status)); + } + + + return $query; + } + + protected function getURI($path) { + return '/release/changerequest/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All'), + 'pending' => pht('Pending'), + ); + } + + 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 'pending': + return $query + ->setParameter('status', 'pending'); // TODO use const + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + private function getTemplateOptions() { + return array('all' => 'All Types') + + PhabricatorReleaseTemplate::getTemplatesMap(); + } + + private function getStateOptions() { + return array( + 'all' => 'Any status', + ) + ReleaseChangeRequest::getStatusMap(); + } + + protected function getRequiredHandlePHIDsForResultList( + array $changes, + PhabricatorSavedQuery $query) { + + return mpull($changes, 'getReleasePHID'); + } + + protected function renderResultList( + array $changes, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($changes, 'ReleaseChangeRequest'); + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + + foreach ($changes as $change) { + $name = $change->getTitle(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($change->getMonogram()) + ->setHeader($name) + ->setHref($change->getURI()) + ->addAttribute($change->getStatusName()) + ->addAttribute(pht( + 'Release: %s', + $viewer->renderHandle($change->getReleasePHID()))); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No change requests found.')); + } +} diff --git a/src/applications/release/query/ReleaseChangeRequestTransactionQuery.php b/src/applications/release/query/ReleaseChangeRequestTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/release/query/ReleaseChangeRequestTransactionQuery.php @@ -0,0 +1,9 @@ +requestedObject == self::ATTACHABLE) { + return 'TBD'; // TODO just copy this when creating? + } + return $this->getImplementation()->getTitle($this); + } + + public function getChangeAuthorPHID() { + if ($this->requestedObject == self::ATTACHABLE) { + return 'TBD'; // TODO + } + return $this->getImplementation()->getChangeAuthorPHID($this); + } + + public function getDetail($key, $default = null) { + return idx($this->getDetails(), $key, $default); + } + + public function setDetail($key, $value) { + $this->details[$key] = $value; + return $this; + } + + public function getURI() { + return '/Y'.$this->getID(); + } + + public function getMonogram() { + return 'Y'.$this->getID(); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'description' => 'text', + 'requestorPHID' => 'phid', + 'releasePHID' => 'phid', + 'requestedObjectPHID' => 'phid', + 'status' => 'text32', + 'implementationKey' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_release' => array( + 'columns' => array('releasePHID'), + 'unique' => false, + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return ReleaseChangeRequestPHIDType::TYPECONST; + } + + public function attachRelease(ReleaseRelease $release) { + $this->release = $release; + return $this; + } + public function getRelease() { + return $this->assertAttached($this->release); + } + + public function attachRequestedObject($object) { + $this->requestedObject = $object; + return $this; + } + public function getRequestedObject() { + return $this->assertAttached($this->requestedObject); + } + + public function getImplementation() { + if (!$this->implementation) { + $this->implementation = + ReleaseChangeRequestImplementation::getImplementationByKey( + $this->getImplementationKey()); + } + return $this->implementation; + } + + public static function getStatusMap() { + return array( + self::STATUS_PENDING => pht('Pending'), + self::STATUS_REJECTED => pht('Rejected'), + self::STATUS_INCLUDED => pht('Merged'), + ); + } + + public static function translateStatusName($status) { + $names = self::getStatusMap(); + return idx($names, $status, $status); + } + + public function getStatusName() { + return self::translateStatusName($this->getStatus()); + } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + public function getApplicationTransactionEditor() { + return new ReleaseChangeRequestEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new ReleaseChangeRequestTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + + return $timeline; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + // TODO need to see both release and target object. + public function getPolicy($capability) { + return $this->getRelease()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRelease()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Change Requests inherit the policies of the release they belong to.'); + } + + +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + private $customFields = self::ATTACHABLE; + + public function getCustomFieldSpecificationForRole($role) { + return array(); + } + + public function getCustomFieldBaseClass() { + return 'ReleaseChangeRequestCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + +} diff --git a/src/applications/release/storage/ReleaseChangeRequestCustomFieldNumericIndex.php b/src/applications/release/storage/ReleaseChangeRequestCustomFieldNumericIndex.php new file mode 100644 --- /dev/null +++ b/src/applications/release/storage/ReleaseChangeRequestCustomFieldNumericIndex.php @@ -0,0 +1,10 @@ +getTransactionType()) { + default: + $tags[] = self::MAILTAG_OTHER; + break; + } + return $tags; + } +} diff --git a/src/applications/release/storage/ReleaseRelease.php b/src/applications/release/storage/ReleaseRelease.php --- a/src/applications/release/storage/ReleaseRelease.php +++ b/src/applications/release/storage/ReleaseRelease.php @@ -147,6 +147,14 @@ ); } + public function canAcceptChangeRequests() { + $states = array( + self::STATE_PLANNED => true, + self::STATE_TESTING => true, + ); + return idx($states, $this->getState(), false); + } + public function getCurrentRefs() { return $this->getDetail(self::DETAIL_CURRENTREF, array()); } diff --git a/src/applications/release/xaction/ReleaseChangeRequestDescribeTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestDescribeTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestDescribeTransaction.php @@ -0,0 +1,41 @@ +getDescription(); + } + + public function applyInternalEffects($object, $value) { + $object->setDescription($value); + } + + public function getTitle() { + return pht( + '%s edited the Request\'s 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/ReleaseChangeRequestImplementationTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestImplementationTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestImplementationTransaction.php @@ -0,0 +1,28 @@ +getImplementationKey(); + } + + public function applyInternalEffects($object, $value) { + $object->setImplementationKey($value); + } + + public function shouldHide() { + return true; + } + + public function validateTransactions($object, array $xactions) { + $errors = $this->validateRequiredFieldTransactions( + $object, + $xactions, + pht('Implementation')); + + return $errors; + } +} diff --git a/src/applications/release/xaction/ReleaseChangeRequestReleaseTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestReleaseTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestReleaseTransaction.php @@ -0,0 +1,77 @@ +getReleasePHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setReleasePHID($value); + } + + + public function applyExternalEffects($object, $value) { + $release_phid = $value; + $actor = $this->getActor(); + + $release = id(new ReleaseReleaseQuery()) + ->setViewer($actor) + ->withPHIDs(array($release_phid)) + ->executeOne(); + $sub_xactions = array( + id(new ReleaseReleaseTransaction()) + ->setTransactionType( + ReleaseReleaseChangeRequestAddedTransaction::TRANSACTIONTYPE) + ->setNewValue($object->getPHID()), + ); + + $sub_editor = id(new ReleaseReleaseEditor()) + ->setActor($actor) + ->setContentSource($this->getEditor()->getContentSource()) + ->setContinueOnNoEffect(true); + $sub_editor->applyTransactions($release, $sub_xactions); + + } + + public function shouldHide() { + return true; + } + + public function validateTransactions($object, array $xactions) { + $errors = $this->validateRequiredFieldTransactions( + $object, + $xactions, + pht('Release')); + + $viewer = $this->getActor(); + foreach ($xactions as $xaction) { + $release_phid = $xaction->getNewValue(); + + $target_object = id(new ReleaseReleaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($release_phid)) + ->setRaisePolicyExceptions(false) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); + + if (!$target_object) { + $errors[] = $this->newInvalidError( + pht( + 'Release "%s" is invalid: the release must exist and you '. + 'must have permission to view it in order to create a new Change'. + 'Request.', + $release_phid), + $xaction); + } + } + + return $errors; + } +} diff --git a/src/applications/release/xaction/ReleaseChangeRequestRequestedObjectTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestRequestedObjectTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestRequestedObjectTransaction.php @@ -0,0 +1,54 @@ +getRequestedObjectPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setRequestedObjectPHID($value); + } + + public function shouldHide() { + return true; + } + + public function validateTransactions($object, array $xactions) { + $errors = $this->validateRequiredFieldTransactions( + $object, + $xactions, + pht('Requested Object')); + + $viewer = $this->getActor(); + foreach ($xactions as $xaction) { + $object_phid = $xaction->getNewValue(); + + $target_object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->setRaisePolicyExceptions(false) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); + + if (!$target_object) { + $errors[] = $this->newInvalidError( + pht( + 'Requested object "%s" is invalid: the object must exist and you '. + 'must have permission to view it in order to create a new Change'. + 'Request.', + $object_phid), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/release/xaction/ReleaseChangeRequestRequestorTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestRequestorTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestRequestorTransaction.php @@ -0,0 +1,24 @@ +getRequestorPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setRequestorPHID($value); + } + + public function validateTransactions($object, array $xactions) { + $errors = $this->validateRequiredFieldTransactions( + $object, + $xactions, + pht('Requestor')); + + return $errors; + } +} diff --git a/src/applications/release/xaction/ReleaseChangeRequestStateTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestStateTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestStateTransaction.php @@ -0,0 +1,61 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + + /* TODO This was supposed to create a xaction on the Release, but w/o + metadata in modular-transactions, showing anything useful there is too + hacky. Rethink later. + public function applyExternalEffects($object, $value) { + $release_phid = $object->getReleasePHID(); + $actor = $this->getActor(); + + $release = id(new ReleaseReleaseQuery()) + ->setViewer($actor) + ->withPHIDs(array($release_phid)) + ->executeOne(); + $sub_xactions = array( + id(new ReleaseReleaseTransaction()) + ->setTransactionType( + ReleaseReleaseChangeRequestStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($value) + ->setMetadataValue('changerequest_phid', $object->getPHID()) + ->setMetadataValue('changerequest:old_status', $object->getStatus()), + ); + + $sub_editor = id(new ReleaseReleaseEditor()) + ->setActor($actor) + ->setContentSource($this->getEditor()->getContentSource()) + ->setContinueOnNoEffect(true); + $sub_editor->applyTransactions($release, $sub_xactions); + } */ + + public function getTitle() { + $status = ReleaseChangeRequest::translateStatusName($this->getNewValue()); + return pht( + '%s updated the status of this change to %s.', + $this->renderAuthor(), + $this->renderValue($status)); + } + + public function getTitleForFeed() { + $status = ReleaseChangeRequest::translateStatusName($this->getNewValue()); + return pht( + '%s updated the status of %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderValue($status)); + } + +} diff --git a/src/applications/release/xaction/ReleaseChangeRequestTransactionType.php b/src/applications/release/xaction/ReleaseChangeRequestTransactionType.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestTransactionType.php @@ -0,0 +1,50 @@ +generateOldValue($object); + + if ($old === null && !$xactions) { + $errors[] = $this->newRequiredError(pht('%s is required.', $field_name)); + } + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newRequiredError( + pht('%s is required.', $field_name)); + } + } + + return $errors; + } + + 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/applications/release/xaction/ReleaseReleaseChangeRequestAddedTransaction.php b/src/applications/release/xaction/ReleaseReleaseChangeRequestAddedTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/release/xaction/ReleaseReleaseChangeRequestAddedTransaction.php @@ -0,0 +1,28 @@ +getViewer(); + return pht( + '%s requested a new change to this release: %s', + $this->renderAuthor(), + $viewer->renderNewHandle()); + } + + public function getTitleForFeed() { + $viewer = $this->getViewer(); + return pht( + '%s requested changes to release %s: %s', + $this->renderAuthor(), + $this->renderObject(), + $viewer->renderNewHandle()); + } +} diff --git a/src/applications/release/xaction/ReleaseReleaseNameTransaction.php b/src/applications/release/xaction/ReleaseReleaseNameTransaction.php --- a/src/applications/release/xaction/ReleaseReleaseNameTransaction.php +++ b/src/applications/release/xaction/ReleaseReleaseNameTransaction.php @@ -1,6 +1,6 @@