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 @@ -4462,6 +4462,30 @@ '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', + 'ReleaseChangeRequestCustomField' => 'applications/release/customfield/ReleaseChangeRequestCustomField.php', + 'ReleaseChangeRequestCustomFieldNumericIndex' => 'applications/release/storage/ReleaseChangeRequestCustomFieldNumericIndex.php', + 'ReleaseChangeRequestCustomFieldStorage' => 'applications/release/storage/ReleaseChangeRequestCustomFieldStorage.php', + 'ReleaseChangeRequestCustomFieldStringIndex' => 'applications/release/storage/ReleaseChangeRequestCustomFieldStringIndex.php', + 'ReleaseChangeRequestDetailsController' => 'applications/release/controller/ReleaseChangeRequestDetailsController.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', + 'ReleaseChangeRequestListController' => 'applications/release/controller/ReleaseChangeRequestListController.php', + 'ReleaseChangeRequestMarkStateAction' => 'applications/release/changes/actions/ReleaseChangeRequestMarkStateAction.php', + 'ReleaseChangeRequestPHIDType' => 'applications/release/phid/ReleaseChangeRequestPHIDType.php', + 'ReleaseChangeRequestQuery' => 'applications/release/query/ReleaseChangeRequestQuery.php', + 'ReleaseChangeRequestRevisionImplementation' => 'applications/release/changes/ReleaseChangeRequestRevisionImplementation.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', 'ReleaseCustomField' => 'applications/release/customfield/ReleaseCustomField.php', 'ReleaseCustomFieldNumericIndex' => 'applications/release/storage/ReleaseCustomFieldNumericIndex.php', 'ReleaseCustomFieldStorage' => 'applications/release/storage/ReleaseCustomFieldStorage.php', @@ -4469,6 +4493,7 @@ 'ReleaseReleaseEditController' => 'applications/release/controller/ReleaseReleaseEditController.php', 'ReleaseReleaseListController' => 'applications/release/controller/ReleaseReleaseListController.php', 'ReleaseReleaseViewController' => 'applications/release/controller/ReleaseReleaseViewController.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', @@ -4566,6 +4591,7 @@ 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', 'SubscriptionListDialogBuilder' => 'applications/subscriptions/view/SubscriptionListDialogBuilder.php', 'SubscriptionListStringBuilder' => 'applications/subscriptions/view/SubscriptionListStringBuilder.php', + 'TestTemplate' => 'applications/release/TestTemplate.php', 'TokenConduitAPIMethod' => 'applications/tokens/conduit/TokenConduitAPIMethod.php', 'TokenGiveConduitAPIMethod' => 'applications/tokens/conduit/TokenGiveConduitAPIMethod.php', 'TokenGivenConduitAPIMethod' => 'applications/tokens/conduit/TokenGivenConduitAPIMethod.php', @@ -9865,6 +9891,35 @@ 'ProjectReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'ProjectSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'QueryFormattingTestCase' => 'PhabricatorTestCase', + 'ReleaseChangeRequest' => array( + 'PhabricatorReleaseDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorCustomFieldInterface', + ), + 'ReleaseChangeRequestAction' => 'Phobject', + 'ReleaseChangeRequestActionController' => 'PhabricatorController', + 'ReleaseChangeRequestCommitImplementation' => 'Phobject', + 'ReleaseChangeRequestCustomField' => 'PhabricatorCustomField', + 'ReleaseChangeRequestCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', + 'ReleaseChangeRequestCustomFieldStorage' => 'PhabricatorCustomFieldStorage', + 'ReleaseChangeRequestCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', + 'ReleaseChangeRequestDetailsController' => 'PhabricatorController', + 'ReleaseChangeRequestEditController' => 'PhabricatorController', + 'ReleaseChangeRequestEditEngine' => 'PhabricatorEditEngine', + 'ReleaseChangeRequestEditor' => 'PhabricatorApplicationTransactionEditor', + 'ReleaseChangeRequestFromRevisionController' => 'PhabricatorController', + 'ReleaseChangeRequestImplementation' => 'Phobject', + 'ReleaseChangeRequestListController' => 'PhabricatorController', + 'ReleaseChangeRequestMarkStateAction' => 'ReleaseChangeRequestAction', + 'ReleaseChangeRequestPHIDType' => 'PhabricatorPHIDType', + 'ReleaseChangeRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'ReleaseChangeRequestRevisionImplementation' => 'ReleaseChangeRequestImplementation', + 'ReleaseChangeRequestSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'ReleaseChangeRequestStateTransaction' => 'ReleaseChangeRequestTransactionType', + 'ReleaseChangeRequestTransaction' => 'PhabricatorModularTransaction', + 'ReleaseChangeRequestTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'ReleaseChangeRequestTransactionType' => 'PhabricatorModularTransactionType', 'ReleaseCustomField' => 'PhabricatorCustomField', 'ReleaseCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'ReleaseCustomFieldStorage' => 'PhabricatorCustomFieldStorage', @@ -9872,6 +9927,7 @@ 'ReleaseReleaseEditController' => 'PhabricatorController', 'ReleaseReleaseListController' => 'PhabricatorController', 'ReleaseReleaseViewController' => 'PhabricatorController', + 'ReleaseRenderEventListener' => 'PhabricatorEventListener', 'ReleephAuthorFieldSpecification' => 'ReleephFieldSpecification', 'ReleephBranch' => array( 'ReleephDAO', @@ -9985,6 +10041,7 @@ 'SlowvoteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'SubscriptionListDialogBuilder' => 'Phobject', 'SubscriptionListStringBuilder' => 'Phobject', + 'TestTemplate' => 'PhabricatorReleaseTemplate', 'TokenConduitAPIMethod' => 'ConduitAPIMethod', 'TokenGiveConduitAPIMethod' => 'TokenConduitAPIMethod', 'TokenGivenConduitAPIMethod' => 'TokenConduitAPIMethod', diff --git a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php --- a/src/applications/meta/controller/PhabricatorApplicationUninstallController.php +++ b/src/applications/meta/controller/PhabricatorApplicationUninstallController.php @@ -44,7 +44,7 @@ phutil_tag('tt', array(), 'phabricator.show-prototypes'))); return id(new AphrontDialogResponse())->setDialog($dialog); } - +phlog($view_uri); if ($request->isDialogFormPost()) { $this->manageApplication(); return id(new AphrontRedirectResponse())->setURI($view_uri); 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/TestTemplate.php b/src/applications/release/TestTemplate.php --- a/src/applications/release/TestTemplate.php +++ b/src/applications/release/TestTemplate.php @@ -6,4 +6,4 @@ public function validateRepositories(array $repositories) { } -} \ No newline at end of file +} 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*)' => 'ReleaseReleaseViewController', + '/X(?P[1-9]\d*)/?' => 'ReleaseReleaseViewController', + '/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'); + } + + // TODO does this need caching if we load lots of requests? + public static function getImplementationByKey($key) { + $all = id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getImplementationKey') + ->execute(); + $implementation = idx($all, $key); + if (!$implementation) { + throw new Exception(pht('Implementation not found for key %s', $key)); + } + return $implementation; + } + + public function createRequest() { + return id(new ReleaseChangeRequest()) + ->setImplementationKey($this->getImplementationKey()) + ->setRequestedObjectPHID($this->getRequestReference()); + } + + + /** + * Return the value to use for the requestReference field in the Request. + */ + protected abstract function getRequestReference(); + + + +} 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,25 @@ +revisionPHID = $phid; + return $this; + } + + public function getTitle(ReleaseChangeRequest $request) { + return $request->getRequestedObject()->getTitle(); + } + + protected function getRequestReference() { + return $this->revisionPHID; + } +} diff --git a/src/applications/release/changes/actions/ReleaseChangeRequestAction.php b/src/applications/release/changes/actions/ReleaseChangeRequestAction.php new file mode 100755 --- /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/ReleaseChangeRequestMarkStateAction.php b/src/applications/release/changes/actions/ReleaseChangeRequestMarkStateAction.php new file mode 100755 --- /dev/null +++ b/src/applications/release/changes/actions/ReleaseChangeRequestMarkStateAction.php @@ -0,0 +1,98 @@ +getChangeStatus())); + } + public function getFormTitle(ReleaseChangeRequest $change) { + return pht('Mark as %s', ReleaseChangeRequest::translateStatusName($this->getChangeStatus())); + } + + public function act(ReleaseChangeRequest $change, AphrontRequest $request) { + $viewer = $request->getViewer(); + $xaction_type = ReleaseChangeRequestStateTransaction::TRANSACTIONTYPE; + $status = $this->getChangeStatus(); + + $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(); + } + + private function getChangeStatus() { + $statuses = array( + self::ACTION_REJECT => ReleaseChangeRequest::STATUS_REJECTED, + self::ACTION_MARK_MERGED => ReleaseChangeRequest::STATUS_INCLUDED, + ); + $status = idx($statuses, $this->key); + if ($status) { + return $status; + } + throw new Exception( + pht('Status not found for action key %s', $action_key)); + } + + public function setActionName($name) { + $this->name = $name; + return $this; + } + public function setActionIcon($icon) { + $this->icon = $icon; + return $this; + } + public function setActionKey($action_key) { + $this->key = $action_key; + return $this; + } + + public function getActionName() { + return $this->name; + } + public function getActionIcon() { + return $this->icon; + } + public function getActionKey() { + return $this->key; + } + + public function generateActions() { + return array( + id(new ReleaseChangeRequestMarkStateAction()) + ->setActionName(pht('Reject')) + ->setActionKey(self::ACTION_REJECT) + ->setActionIcon('fa-times'), + + + // We should try to detect commits like with Revisions, and + // automatically mark a Change as Merged, but I'm not sure how complex + // that would be (And how scalable across Implmentations). + // Until then, I'd like to hide this option locally, and use my hack for + // T182 to do the merge. + // Actually, I think I'm just going to delete this, and offer it as an + // Extension for the happy few who want to try it. Preview FTW :-) + // Also replace the rest of the class with a non-generic ...RejectAction + id(new ReleaseChangeRequestMarkStateAction()) + ->setActionName(pht('Mark as Merged')) + ->setActionKey(self::ACTION_MARK_MERGED) + ->setActionIcon('fa-check'), + ); + } + +} 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,50 @@ +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 ($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); + + return $this->newDialog() + ->setSubmitURI($request->getRequestURI()) + ->setTitle($action->getFormTitle($change)) + ->appendChild($prompt) + ->setErrors($errors) + ->addSubmitButton(pht('Submit')) + ->addCancelButton('#'); + } +} 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,149 @@ +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, + // $comment_view, // Should we have comments on each Request? + )) + ->addPropertySection($release->getName(), $release_properties) + ->addPropertySection(pht('Change Request'), $change_properties); + + + // release information (lite) + // request properties + // message + // actions + // timeline + + // crumbs = release crumbs + this change. + $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(); + + $requestor = $viewer->renderHandle($change->getRequestorPHID()) + ->setShowHovercard(true); + $status = $change->getStatus(); // houmanize + + $properties = id(new PHUIPropertyListView()) + ->addProperty(pht('Type'), $change->getImplementationKey()) // humanize + ->addProperty(pht('Requested By'), $requestor) + ->addProperty(pht('Status'), $status); +// author - implementation specific! + + + $description = $change->getDescription(); + if (strlen($description)) { + $properties + ->addSectionHeader('Description') + ->addTextContent( + new PHUIRemarkupView($this->getViewer(), $description)); + } + + // TODO custom fields + + 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(); + // TODO these actions should be extensible. + + + $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))); + } + + + // mark merged + // rejected + + 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,118 @@ +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 PhabricatorReleaseReleaseQuery()) + ->setViewer($viewer) + ->withPHIDs($v_release) + ->executeOne(); + + // if (!$release->canAcceptChangeRequests()) { // TODO + // $e_release = pht('Invalid'); + // $errors[] = + // pht('This release can not accept cany change requests at this time.'); + // } + } + + $v_message = $request->getStr('message'); + + $actor_phid = $viewer->getPHID(); + $revision_id = $revision->getID(); + + if (!$errors) { + + $implementation = id(new ReleaseChangeRequestRevisionImplementation()) + ->setRevisionPHID($revision_phid); + $change_request = $implementation->createRequest() + ->setRequestorPHID($actor_phid) + ->setReleasePHID($release->getPHID()) + ->setDescription($v_message); + + $xactions = array( + id(new ReleaseChangeRequestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE), + ); + $editor = id(new ReleaseChangeRequestEditor()) + ->setActor($viewer) + ->setContentSource( + PhabricatorContentSource::newFromRequest($request)); + $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 PhabricatorReleaseReleaseDatasource()); + + $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,40 @@ +getViewer(); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb('Change Requests'); + + $querykey = $request->getURIData('queryKey'); + + $release_id = $request->getURIData('id'); + if ($release_id) { + phlog('dd'); + } + + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($querykey) + ->setSearchEngine(new ReleaseChangeRequestSearchEngine()) + ->setNavigation($this->buildSideNavView()); + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $viewer = $this->getViewer(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new ReleaseChangeRequestSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + +} 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 PhabricatorReleaseReleaseEditEngine()) ->setController($this); - $id = $request->getURIData('id'); - if (!$id) { - // ?? - } - return $engine->buildResponse(); } diff --git a/src/applications/release/controller/ReleaseReleaseViewController.php b/src/applications/release/controller/ReleaseReleaseViewController.php --- a/src/applications/release/controller/ReleaseReleaseViewController.php +++ b/src/applications/release/controller/ReleaseReleaseViewController.php @@ -63,6 +63,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) @@ -72,7 +84,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) @@ -87,6 +100,46 @@ )); } + 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/customfield/ReleaseChangeRequestCustomField.php b/src/applications/release/customfield/ReleaseChangeRequestCustomField.php new file mode 100755 --- /dev/null +++ b/src/applications/release/customfield/ReleaseChangeRequestCustomField.php @@ -0,0 +1,24 @@ +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( + PhabricatorReleaseReleaseDefaultEditCapability::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(); + + $is_edit_form = !$this->getIsCreate(); + + return array( + id(new PhabricatorSelectEditField()) + ->setKey('state') + ->setLabel(pht('State')) + ->setTransactionType( + ReleaseChangeRequestStateTransaction::TRANSACTIONTYPE) + ->setIsCopyable(true) + ->setOptions($states) + ->setValue($object->getStatus()), + ); + } + +} 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 PhabricatorReleaseReleaseReplyHandler()) + ->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,205 @@ +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; + return $this; + } + + public function needRequestObjects($need_request_objects) { + $this->needRequestObjects = $need_request_objects; + 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 didFilterPage(array $changes) { + $viewer = $this->getViewer(); + + if ($this->needReleases) { + $release_phids = mpull($changes, 'getReleasePHID'); + $releases = id(new PhabricatorReleaseReleaseQuery()) + ->setViewer($viewer) + ->setParentQuery($this) + ->withPHIDs($release_phids) + ->execute(); + $releases = mpull($releases, null, 'getPHID'); + + foreach ($changes as $change) { + $change->attachRelease(idx($releases, $change->getReleasePHID())); + } + } + + if ($this->needRequestObjects) { + $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 PhabricatorReleaseReleaseDatasource()), + 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'; + } + return $this->getImplementation()->getTitle($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( + '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(PhabricatorReleaseRelease $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) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + case PhabricatorPolicyCapability::CAN_EDIT: // TODO + return PhabricatorPolicies::POLICY_USER; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + private $customFields = self::ATTACHABLE; + + public function getCustomFieldSpecificationForRole($role) { + return array(); // TODO + // return PhabricatorEnv::getEnvConfig(<<<'application.fields'>>>); + } + + 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/xaction/ReleaseChangeRequestStateTransaction.php b/src/applications/release/xaction/ReleaseChangeRequestStateTransaction.php new file mode 100755 --- /dev/null +++ b/src/applications/release/xaction/ReleaseChangeRequestStateTransaction.php @@ -0,0 +1,39 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function shouldHide() { + $old = $this->getOldValue(); + + if (!strlen($old)) { + return true; + } + } + + public function getTitle() { + return pht( + '%s updated the status of this change to "%s".', + $this->renderAuthor(), + ReleaseChangeRequest::translateStatusName($this->getNewValue())); + } + + public function getTitleForFeed() { + return pht( + '%s updated the status of %s to "%s".', + $this->renderAuthor(), + $this->renderObject(), + ReleaseChangeRequest::translateStatusName($this->getNewValue())); + } + +} 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,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); + } +}