diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -221,8 +221,8 @@ ), self::STATUS_PAUSED => array( 'name' => pht('Paused'), - 'icon' => 'fa-minus-circle', - 'color' => 'dark', + 'icon' => 'fa-pause', + 'color' => 'yellow', 'color.ansi' => 'yellow', 'isBuilding' => false, 'isComplete' => false, diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -22,24 +22,13 @@ return new Aphront404Response(); } - switch ($action) { - case HarbormasterBuildCommand::COMMAND_RESTART: - $can_issue = $build->canRestartBuild(); - break; - case HarbormasterBuildCommand::COMMAND_PAUSE: - $can_issue = $build->canPauseBuild(); - break; - case HarbormasterBuildCommand::COMMAND_RESUME: - $can_issue = $build->canResumeBuild(); - break; - case HarbormasterBuildCommand::COMMAND_ABORT: - $can_issue = $build->canAbortBuild(); - break; - default: - return new Aphront400Response(); - } + $xaction = + HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType( + $action); - $build->assertCanIssueCommand($viewer, $action); + if (!$xaction) { + return new Aphront404Response(); + } switch ($via) { case 'buildable': @@ -50,100 +39,29 @@ break; } - if ($request->isDialogFormPost() && $can_issue) { - $build->sendMessage($viewer, $action); - return id(new AphrontRedirectResponse())->setURI($return_uri); + try { + $xaction->assertCanSendMessage($viewer, $build); + } catch (HarbormasterRestartException $ex) { + return $this->newDialog() + ->setTitle($ex->getTitle()) + ->appendChild($ex->getBody()) + ->addCancelButton($return_uri); } - switch ($action) { - case HarbormasterBuildCommand::COMMAND_RESTART: - if ($can_issue) { - $title = pht('Really restart build?'); - $body = pht( - 'Progress on this build will be discarded and the build will '. - 'restart. Side effects of the build will occur again. Really '. - 'restart build?'); - $submit = pht('Restart Build'); - } else { - try { - $build->assertCanRestartBuild(); - throw new Exception(pht('Expected to be unable to restart build.')); - } catch (HarbormasterRestartException $ex) { - $title = $ex->getTitle(); - $body = $ex->getBody(); - } - } - break; - case HarbormasterBuildCommand::COMMAND_ABORT: - if ($can_issue) { - $title = pht('Really abort build?'); - $body = pht( - 'Progress on this build will be discarded. Really '. - 'abort build?'); - $submit = pht('Abort Build'); - } else { - $title = pht('Unable to Abort Build'); - $body = pht('You can not abort this build.'); - } - break; - case HarbormasterBuildCommand::COMMAND_PAUSE: - if ($can_issue) { - $title = pht('Really pause build?'); - $body = pht( - 'If you pause this build, work will halt once the current steps '. - 'complete. You can resume the build later.'); - $submit = pht('Pause Build'); - } else { - $title = pht('Unable to Pause Build'); - if ($build->isComplete()) { - $body = pht( - 'This build is already complete. You can not pause a completed '. - 'build.'); - } else if ($build->isPaused()) { - $body = pht( - 'This build is already paused. You can not pause a build which '. - 'has already been paused.'); - } else if ($build->isPausing()) { - $body = pht( - 'This build is already pausing. You can not reissue a pause '. - 'command to a pausing build.'); - } else { - $body = pht( - 'This build can not be paused.'); - } - } - break; - case HarbormasterBuildCommand::COMMAND_RESUME: - if ($can_issue) { - $title = pht('Really resume build?'); - $body = pht( - 'Work will continue on the build. Really resume?'); - $submit = pht('Resume Build'); - } else { - $title = pht('Unable to Resume Build'); - if ($build->isResuming()) { - $body = pht( - 'This build is already resuming. You can not reissue a resume '. - 'command to a resuming build.'); - } else if (!$build->isPaused()) { - $body = pht( - 'This build is not paused. You can only resume a paused '. - 'build.'); - } - } - break; + if ($request->isDialogFormPost()) { + $build->sendMessage($viewer, $xaction->getHarbormasterBuildMessageType()); + return id(new AphrontRedirectResponse())->setURI($return_uri); } - $dialog = $this->newDialog() + $title = $xaction->newConfirmPromptTitle(); + $body = $xaction->newConfirmPromptBody(); + $submit = $xaction->getHarbormasterBuildMessageName(); + + return $this->newDialog() ->setTitle($title) ->appendChild($body) - ->addCancelButton($return_uri); - - if ($can_issue) { - $dialog->addSubmitButton($submit); - } - - return $dialog; + ->addCancelButton($return_uri) + ->addSubmitButton($submit); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -533,63 +533,31 @@ $curtain = $this->newCurtainView($build); - $can_restart = - $build->canRestartBuild() && - $build->canIssueCommand( - $viewer, - HarbormasterBuildCommand::COMMAND_RESTART); - - $can_pause = - $build->canPauseBuild() && - $build->canIssueCommand( - $viewer, - HarbormasterBuildCommand::COMMAND_PAUSE); - - $can_resume = - $build->canResumeBuild() && - $build->canIssueCommand( - $viewer, - HarbormasterBuildCommand::COMMAND_RESUME); - - $can_abort = - $build->canAbortBuild() && - $build->canIssueCommand( - $viewer, - HarbormasterBuildCommand::COMMAND_ABORT); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Restart Build')) - ->setIcon('fa-repeat') - ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) - ->setDisabled(!$can_restart) - ->setWorkflow(true)); - - if ($build->canResumeBuild()) { - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Resume Build')) - ->setIcon('fa-play') - ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) - ->setDisabled(!$can_resume) - ->setWorkflow(true)); - } else { - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Pause Build')) - ->setIcon('fa-pause') - ->setHref($this->getApplicationURI('/build/pause/'.$id.'/')) - ->setDisabled(!$can_pause) - ->setWorkflow(true)); - } + $messages = array( + new HarbormasterBuildMessageRestartTransaction(), + new HarbormasterBuildMessagePauseTransaction(), + new HarbormasterBuildMessageResumeTransaction(), + new HarbormasterBuildMessageAbortTransaction(), + ); - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Abort Build')) - ->setIcon('fa-exclamation-triangle') - ->setHref($this->getApplicationURI('/build/abort/'.$id.'/')) - ->setDisabled(!$can_abort) - ->setWorkflow(true)); + foreach ($messages as $message) { + $can_send = $message->canSendMessage($viewer, $build); + + $message_uri = urisprintf( + '/build/%s/%d/', + $message->getHarbormasterBuildMessageType(), + $id); + $message_uri = $this->getApplicationURI($message_uri); + + $action = id(new PhabricatorActionView()) + ->setName($message->getHarbormasterBuildMessageName()) + ->setIcon($message->getIcon()) + ->setHref($message_uri) + ->setDisabled(!$can_send) + ->setWorkflow(true); + + $curtain->addAction($action); + } return $curtain; } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -22,54 +22,89 @@ return new Aphront404Response(); } - $issuable = array(); + $message = + HarbormasterBuildMessageTransaction::getTransactionObjectForMessageType( + $action); + if (!$message) { + return new Aphront404Response(); + } + + $return_uri = '/'.$buildable->getMonogram(); + + // See T13348. Actions may apply to only a subset of builds, so give the + // user a preview of what will happen. + + $can_send = array(); + $rows = array(); $builds = $buildable->getBuilds(); foreach ($builds as $key => $build) { - switch ($action) { - case HarbormasterBuildCommand::COMMAND_RESTART: - if ($build->canRestartBuild()) { - $issuable[$key] = $build; - } - break; - case HarbormasterBuildCommand::COMMAND_PAUSE: - if ($build->canPauseBuild()) { - $issuable[$key] = $build; - } - break; - case HarbormasterBuildCommand::COMMAND_RESUME: - if ($build->canResumeBuild()) { - $issuable[$key] = $build; - } - break; - case HarbormasterBuildCommand::COMMAND_ABORT: - if ($build->canAbortBuild()) { - $issuable[$key] = $build; - } - break; - default: - return new Aphront400Response(); + $exception = null; + try { + $message->assertCanSendMessage($viewer, $build); + $can_send[$key] = $build; + } catch (HarbormasterRestartException $ex) { + $exception = $ex; } - } - $restricted = false; - foreach ($issuable as $key => $build) { - if (!$build->canIssueCommand($viewer, $action)) { - $restricted = true; - unset($issuable[$key]); - } - } + if (!$exception) { + $icon_icon = $message->getIcon(); + $icon_color = 'green'; - $building = false; - foreach ($issuable as $key => $build) { - if ($build->isBuilding()) { - $building = true; - break; + $title = $message->getHarbormasterBuildMessageName(); + $body = $message->getHarbormasterBuildableMessageEffect(); + } else { + $icon_icon = 'fa-times'; + $icon_color = 'red'; + + $title = $ex->getTitle(); + $body = $ex->getBody(); } + + $icon = id(new PHUIIconView()) + ->setIcon($icon_icon) + ->setColor($icon_color); + + $build_name = phutil_tag( + 'a', + array( + 'href' => $build->getURI(), + 'target' => '_blank', + ), + pht('%s %s', $build->getObjectName(), $build->getName())); + + $rows[] = array( + $icon, + $build_name, + $title, + $body, + ); } - $return_uri = '/'.$buildable->getMonogram(); - if ($request->isDialogFormPost() && $issuable) { + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Build'), + pht('Action'), + pht('Details'), + )) + ->setColumnClasses( + array( + null, + null, + 'pri', + 'wide', + )); + + $table = phutil_tag( + 'div', + array( + 'class' => 'mlt mlb', + ), + $table); + + if ($request->isDialogFormPost() && $can_send) { $editor = id(new HarbormasterBuildableTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) @@ -88,233 +123,65 @@ ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); - foreach ($issuable as $build) { - $xaction = id(new HarbormasterBuildTransaction()) - ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) - ->setNewValue($action); - $build_editor->applyTransactions($build, array($xaction)); + foreach ($can_send as $build) { + $build->sendMessage( + $viewer, + $message->getHarbormasterBuildMessageType()); } return id(new AphrontRedirectResponse())->setURI($return_uri); } - $width = AphrontDialogView::WIDTH_DEFAULT; - - switch ($action) { - case HarbormasterBuildCommand::COMMAND_RESTART: - // See T13348. The "Restart Builds" action may restart only a subset - // of builds, so show the user a preview of which builds will actually - // restart. - - $body = array(); - - if ($issuable) { - $title = pht('Restart Builds'); - $submit = pht('Restart Builds'); - } else { - $title = pht('Unable to Restart Builds'); - } - - if ($builds) { - $width = AphrontDialogView::WIDTH_FORM; - - $body[] = pht('Builds for this buildable:'); - - $rows = array(); - foreach ($builds as $key => $build) { - if (isset($issuable[$key])) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-repeat green'); - $build_note = pht('Will Restart'); - } else { - $icon = null; - - try { - $build->assertCanRestartBuild(); - } catch (HarbormasterRestartException $ex) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-times red'); - $build_note = pht( - '%s: %s', - phutil_tag('strong', array(), pht('Not Restartable')), - $ex->getTitle()); - } - - if (!$icon) { - try { - $build->assertCanIssueCommand($viewer, $action); - } catch (PhabricatorPolicyException $ex) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-lock red'); - $build_note = pht( - '%s: %s', - phutil_tag('strong', array(), pht('Not Restartable')), - pht('You do not have permission to restart this build.')); - } - } - - if (!$icon) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-times red'); - $build_note = pht('Will Not Restart'); - } - } - - $build_name = phutil_tag( - 'a', - array( - 'href' => $build->getURI(), - 'target' => '_blank', - ), - pht('%s %s', $build->getObjectName(), $build->getName())); - - $rows[] = array( - $icon, - $build_name, - $build_note, - ); - } - - $table = id(new AphrontTableView($rows)) - ->setHeaders( - array( - null, - pht('Build'), - pht('Action'), - )) - ->setColumnClasses( - array( - null, - 'pri', - 'wide', - )); - - $table = phutil_tag( - 'div', - array( - 'class' => 'mlt mlb', - ), - $table); - - $body[] = $table; - } - - if ($issuable) { - $warnings = array(); - - if ($restricted) { - $warnings[] = pht( - 'You only have permission to restart some builds.'); - } - - if ($building) { - $warnings[] = pht( - 'Progress on running builds will be discarded.'); - } - - $warnings[] = pht( - 'When a build is restarted, side effects associated with '. - 'the build may occur again.'); - - $body[] = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setErrors($warnings); - - $body[] = pht('Really restart builds?'); - } else { - if ($restricted) { - $body[] = pht('You do not have permission to restart any builds.'); - } else { - $body[] = pht('No builds can be restarted.'); - } - } - - break; - case HarbormasterBuildCommand::COMMAND_PAUSE: - if ($issuable) { - $title = pht('Really pause builds?'); - - if ($restricted) { - $body = pht( - 'You only have permission to pause some builds. Once the '. - 'current steps complete, work will halt on builds you have '. - 'permission to pause. You can resume the builds later.'); - } else { - $body = pht( - 'If you pause all builds, work will halt once the current steps '. - 'complete. You can resume the builds later.'); - } - $submit = pht('Pause Builds'); - } else { - $title = pht('Unable to Pause Builds'); - - if ($restricted) { - $body = pht('You do not have permission to pause any builds.'); - } else { - $body = pht('No builds can be paused.'); - } - } - break; - case HarbormasterBuildCommand::COMMAND_ABORT: - if ($issuable) { - $title = pht('Really abort builds?'); - if ($restricted) { - $body = pht( - 'You only have permission to abort some builds. Work will '. - 'halt immediately on builds you have permission to abort. '. - 'Progress will be discarded, and builds must be completely '. - 'restarted if you want them to complete.'); - } else { - $body = pht( - 'If you abort all builds, work will halt immediately. Work '. - 'will be discarded, and builds must be completely restarted.'); - } - $submit = pht('Abort Builds'); - } else { - $title = pht('Unable to Abort Builds'); - - if ($restricted) { - $body = pht('You do not have permission to abort any builds.'); - } else { - $body = pht('No builds can be aborted.'); - } - } - break; - case HarbormasterBuildCommand::COMMAND_RESUME: - if ($issuable) { - $title = pht('Really resume builds?'); - if ($restricted) { - $body = pht( - 'You only have permission to resume some builds. Work will '. - 'continue on builds you have permission to resume.'); - } else { - $body = pht('Work will continue on all builds. Really resume?'); - } - - $submit = pht('Resume Builds'); - } else { - $title = pht('Unable to Resume Builds'); - if ($restricted) { - $body = pht('You do not have permission to resume any builds.'); - } else { - $body = pht('No builds can be resumed.'); - } - } - break; + if (!$builds) { + $title = pht('No Builds'); + $body = pht( + 'This buildable has no builds, so you can not issue any commands.'); + } else { + if ($can_send) { + $title = $message->newBuildableConfirmPromptTitle( + $builds, + $can_send); + + $body = $message->newBuildableConfirmPromptBody( + $builds, + $can_send); + } else { + $title = pht('Unable to Send Command'); + $body = pht( + 'You can not send this command to any of the current builds '. + 'for this buildable.'); + } + + $body = array( + pht('Builds for this buildable:'), + $table, + $body, + ); } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setWidth($width) + $warnings = $message->newBuildableConfirmPromptWarnings( + $builds, + $can_send); + + if ($warnings) { + $body[] = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($warnings); + } + + $submit = $message->getHarbormasterBuildableMessageName(); + + $dialog = $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); - if ($issuable) { + if ($can_send) { $dialog->addSubmitButton($submit); } - return id(new AphrontDialogResponse())->setDialog($dialog); + return $dialog; } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -87,75 +87,39 @@ $buildable, PhabricatorPolicyCapability::CAN_EDIT); - $can_restart = false; - $can_resume = false; - $can_pause = false; - $can_abort = false; - - $command_restart = HarbormasterBuildCommand::COMMAND_RESTART; - $command_resume = HarbormasterBuildCommand::COMMAND_RESUME; - $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE; - $command_abort = HarbormasterBuildCommand::COMMAND_ABORT; - - foreach ($buildable->getBuilds() as $build) { - if ($build->canRestartBuild()) { - if ($build->canIssueCommand($viewer, $command_restart)) { - $can_restart = true; - } - } - if ($build->canResumeBuild()) { - if ($build->canIssueCommand($viewer, $command_resume)) { - $can_resume = true; - } - } - if ($build->canPauseBuild()) { - if ($build->canIssueCommand($viewer, $command_pause)) { - $can_pause = true; - } - } - if ($build->canAbortBuild()) { - if ($build->canIssueCommand($viewer, $command_abort)) { - $can_abort = true; + $messages = array( + new HarbormasterBuildMessageRestartTransaction(), + new HarbormasterBuildMessagePauseTransaction(), + new HarbormasterBuildMessageResumeTransaction(), + new HarbormasterBuildMessageAbortTransaction(), + ); + + foreach ($messages as $message) { + + // Messages are enabled if they can be sent to at least one build. + $can_send = false; + foreach ($buildable->getBuilds() as $build) { + $can_send = $message->canSendMessage($viewer, $build); + if ($can_send) { + break; } } - } - $restart_uri = "buildable/{$id}/restart/"; - $pause_uri = "buildable/{$id}/pause/"; - $resume_uri = "buildable/{$id}/resume/"; - $abort_uri = "buildable/{$id}/abort/"; - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-repeat') - ->setName(pht('Restart Builds')) - ->setHref($this->getApplicationURI($restart_uri)) - ->setWorkflow(true) - ->setDisabled(!$can_restart || !$can_edit)); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-pause') - ->setName(pht('Pause Builds')) - ->setHref($this->getApplicationURI($pause_uri)) - ->setWorkflow(true) - ->setDisabled(!$can_pause || !$can_edit)); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-play') - ->setName(pht('Resume Builds')) - ->setHref($this->getApplicationURI($resume_uri)) - ->setWorkflow(true) - ->setDisabled(!$can_resume || !$can_edit)); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-exclamation-triangle') - ->setName(pht('Abort Builds')) - ->setHref($this->getApplicationURI($abort_uri)) - ->setWorkflow(true) - ->setDisabled(!$can_abort || !$can_edit)); + $message_uri = urisprintf( + '/buildable/%d/%s/', + $id, + $message->getHarbormasterBuildMessageType()); + $message_uri = $this->getApplicationURI($message_uri); + + $action = id(new PhabricatorActionView()) + ->setName($message->getHarbormasterBuildableMessageName()) + ->setIcon($message->getIcon()) + ->setHref($message_uri) + ->setDisabled(!$can_send || !$can_edit) + ->setWorkflow(true); + + $curtain->addAction($action); + } return $curtain; } @@ -198,56 +162,17 @@ ->setUser($viewer); foreach ($buildable->getBuilds() as $build) { $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); + $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Build %d', $build->getID())) ->setHeader($build->getName()) ->setHref($view_uri); - $status = $build->getBuildStatus(); - $status_color = HarbormasterBuildStatus::getBuildStatusColor($status); - $status_name = HarbormasterBuildStatus::getBuildStatusName($status); - $item->setStatusIcon('fa-dot-circle-o '.$status_color, $status_name); - $item->addAttribute($status_name); - - if ($build->isRestarting()) { - $item->addIcon('fa-repeat', pht('Restarting')); - } else if ($build->isPausing()) { - $item->addIcon('fa-pause', pht('Pausing')); - } else if ($build->isResuming()) { - $item->addIcon('fa-play', pht('Resuming')); - } + $status = $build->getBuildPendingStatusObject(); - $build_id = $build->getID(); - - $restart_uri = "build/restart/{$build_id}/buildable/"; - $resume_uri = "build/resume/{$build_id}/buildable/"; - $pause_uri = "build/pause/{$build_id}/buildable/"; - $abort_uri = "build/abort/{$build_id}/buildable/"; - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-repeat') - ->setName(pht('Restart')) - ->setHref($this->getApplicationURI($restart_uri)) - ->setWorkflow(true) - ->setDisabled(!$build->canRestartBuild())); - - if ($build->canResumeBuild()) { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-play') - ->setName(pht('Resume')) - ->setHref($this->getApplicationURI($resume_uri)) - ->setWorkflow(true)); - } else { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-pause') - ->setName(pht('Pause')) - ->setHref($this->getApplicationURI($pause_uri)) - ->setWorkflow(true) - ->setDisabled(!$build->canPauseBuild())); - } + $item->setStatusIcon( + $status->getIconIcon().' '.$status->getIconColor(), + $status->getName()); $targets = $build->getBuildTargets(); diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php --- a/src/applications/harbormaster/exception/HarbormasterRestartException.php +++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php @@ -30,4 +30,13 @@ return $this->body; } + public function newDisplayString() { + $title = $this->getTitle(); + + $body = $this->getBody(); + $body = implode("\n\n", $body); + + return pht('%s: %s', $title, $body); + } + } diff --git a/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php b/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php --- a/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php +++ b/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php @@ -61,21 +61,24 @@ throw new ArcanistUserAbortException(); } + $message = new HarbormasterBuildMessageRestartTransaction(); + foreach ($builds as $build) { $this->logInfo( pht('RESTARTING'), pht('Build %d: %s', $build->getID(), $build->getName())); - if (!$build->canRestartBuild()) { + try { + $message->assertCanSendMessage($viewer, $build); + } catch (HarbormasterRestartException $ex) { $this->logWarn( pht('INVALID'), - pht('Build can not be restarted.')); - continue; + $ex->newDisplayString()); } $build->sendMessage( $viewer, - HarbormasterBuildCommand::COMMAND_RESTART); + $message->getHarbormasterBuildMessageType()); $this->logOkay( pht('QUEUED'), diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -315,107 +315,6 @@ return $this; } - public function canRestartBuild() { - try { - $this->assertCanRestartBuild(); - return true; - } catch (HarbormasterRestartException $ex) { - return false; - } - } - - public function assertCanRestartBuild() { - if ($this->isAutobuild()) { - throw new HarbormasterRestartException( - pht('Can Not Restart Autobuild'), - pht( - 'This build can not be restarted because it is an automatic '. - 'build.')); - } - - $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; - $plan = $this->getBuildPlan(); - - // See T13526. Users who can't see the "BuildPlan" can end up here with - // no object. This is highly questionable. - if (!$plan) { - throw new HarbormasterRestartException( - pht('No Build Plan Permission'), - pht( - 'You can not restart this build because you do not have '. - 'permission to access the build plan.')); - } - - $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) - ->getPlanOption($plan); - $option_key = $option->getKey(); - - $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER; - $is_never = ($option_key === $never_restartable); - if ($is_never) { - throw new HarbormasterRestartException( - pht('Build Plan Prevents Restart'), - pht( - 'This build can not be restarted because the build plan is '. - 'configured to prevent the build from restarting.')); - } - - $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED; - $is_failed = ($option_key === $failed_restartable); - if ($is_failed) { - if (!$this->isFailed()) { - throw new HarbormasterRestartException( - pht('Only Restartable if Failed'), - pht( - 'This build can not be restarted because the build plan is '. - 'configured to prevent the build from restarting unless it '. - 'has failed, and it has not failed.')); - } - } - - if ($this->isRestarting()) { - throw new HarbormasterRestartException( - pht('Already Restarting'), - pht( - 'This build is already restarting. You can not reissue a restart '. - 'command to a restarting build.')); - } - } - - public function canPauseBuild() { - if ($this->isAutobuild()) { - return false; - } - - return !$this->isComplete() && - !$this->isPaused() && - !$this->isPausing() && - !$this->isRestarting() && - !$this->isAborting(); - } - - public function canAbortBuild() { - if ($this->isAutobuild()) { - return false; - } - - return - !$this->isComplete() && - !$this->isAborting(); - } - - public function canResumeBuild() { - if ($this->isAutobuild()) { - return false; - } - - return - $this->isPaused() && - !$this->isResuming() && - !$this->isRestarting() && - !$this->isAborting(); - } - public function isPausing() { return $this->getBuildPendingStatusObject()->isPausing(); } @@ -451,51 +350,6 @@ return $this; } - public function canIssueCommand(PhabricatorUser $viewer, $command) { - try { - $this->assertCanIssueCommand($viewer, $command); - return true; - } catch (Exception $ex) { - return false; - } - } - - public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { - $plan = $this->getBuildPlan(); - - // See T13526. Users without permission to access the build plan can - // currently end up here with no "BuildPlan" object. - if (!$plan) { - return false; - } - - $need_edit = true; - switch ($command) { - case HarbormasterBuildCommand::COMMAND_RESTART: - case HarbormasterBuildCommand::COMMAND_PAUSE: - case HarbormasterBuildCommand::COMMAND_RESUME: - case HarbormasterBuildCommand::COMMAND_ABORT: - if ($plan->canRunWithoutEditCapability()) { - $need_edit = false; - } - break; - default: - throw new Exception( - pht( - 'Invalid Harbormaster build command "%s".', - $command)); - } - - // Issuing these commands requires that you be able to edit the build, to - // prevent enemy engineers from sabotaging your builds. See T9614. - if ($need_edit) { - PhabricatorPolicyFilter::requireCapability( - $viewer, - $plan, - PhabricatorPolicyCapability::CAN_EDIT); - } - } - public function sendMessage(PhabricatorUser $viewer, $message_type) { HarbormasterBuildMessage::initializeNewMessage($viewer) ->setReceiverPHID($this->getPHID()) diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php @@ -4,9 +4,51 @@ extends HarbormasterBuildMessageTransaction { const TRANSACTIONTYPE = 'message/abort'; + const MESSAGETYPE = 'abort'; - public function getMessageType() { - return 'abort'; + public function getHarbormasterBuildMessageName() { + return pht('Abort Build'); + } + + public function getHarbormasterBuildableMessageName() { + return pht('Abort Builds'); + } + + public function newConfirmPromptTitle() { + return pht('Really abort build?'); + } + + public function getHarbormasterBuildableMessageEffect() { + return pht('Build will abort.'); + } + + public function newConfirmPromptBody() { + return pht( + 'Progress on this build will be discarded. Really abort build?'); + } + + public function newBuildableConfirmPromptTitle( + array $builds, + array $sendable) { + return pht( + 'Really abort %s build(s)?', + phutil_count($builds)); + } + + public function newBuildableConfirmPromptBody( + array $builds, + array $sendable) { + + if (count($sendable) === count($builds)) { + return pht( + 'If you abort all builds, work will halt immediately. Work '. + 'will be discarded, and builds must be completely restarted.'); + } else { + return pht( + 'You can only abort some builds. Work will halt immediately on '. + 'builds you can abort. Progress will be discarded, and builds must '. + 'be completely restarted if you want them to complete.'); + } } public function getTitle() { @@ -37,5 +79,35 @@ $build->releaseAllArtifacts($actor); } + protected function newCanApplyMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isAutobuild()) { + throw new HarbormasterRestartException( + pht('Unable to Abort Build'), + pht( + 'You can not abort a build that uses an autoplan.')); + } + + if ($build->isComplete()) { + throw new HarbormasterRestartException( + pht('Unable to Abort Build'), + pht( + 'You can not abort this biuld because it is already complete.')); + } + } + + protected function newCanSendMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isAborting()) { + throw new HarbormasterRestartException( + pht('Unable to Abort Build'), + pht( + 'You can not abort this build because it is already aborting.')); + } + } } diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php @@ -4,9 +4,52 @@ extends HarbormasterBuildMessageTransaction { const TRANSACTIONTYPE = 'message/pause'; + const MESSAGETYPE = 'pause'; - public function getMessageType() { - return 'pause'; + public function getHarbormasterBuildMessageName() { + return pht('Pause Build'); + } + + public function getHarbormasterBuildableMessageName() { + return pht('Pause Builds'); + } + + public function newConfirmPromptTitle() { + return pht('Really pause build?'); + } + + public function getHarbormasterBuildableMessageEffect() { + return pht('Build will pause.'); + } + + public function newConfirmPromptBody() { + return pht( + 'If you pause this build, work will halt once the current steps '. + 'complete. You can resume the build later.'); + } + + public function newBuildableConfirmPromptTitle( + array $builds, + array $sendable) { + return pht( + 'Really pause %s build(s)?', + phutil_count($builds)); + } + + public function newBuildableConfirmPromptBody( + array $builds, + array $sendable) { + + if (count($sendable) === count($builds)) { + return pht( + 'If you pause all builds, work will halt once the current steps '. + 'complete. You can resume the builds later.'); + } else { + return pht( + 'You can only pause some builds. Once the current steps complete, '. + 'work will halt on builds you can pause. You can resume the builds '. + 'later.'); + } } public function getTitle() { @@ -30,4 +73,49 @@ $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); } + protected function newCanApplyMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isAutobuild()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause a build that uses an autoplan.')); + } + + if ($build->isPaused()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause this build because it is already paused.')); + } + + if ($build->isComplete()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause this build because it has already completed.')); + } + } + + protected function newCanSendMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isPausing()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause this build because it is already pausing.')); + } + + if ($build->isRestarting()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause this build because it is already restarting.')); + } + + if ($build->isAborting()) { + throw new HarbormasterRestartException( + pht('Unable to Pause Build'), + pht('You can not pause this build because it is already aborting.')); + } + } } diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php @@ -4,9 +4,77 @@ extends HarbormasterBuildMessageTransaction { const TRANSACTIONTYPE = 'message/restart'; + const MESSAGETYPE = 'restart'; - public function getMessageType() { - return 'restart'; + public function getHarbormasterBuildMessageName() { + return pht('Restart Build'); + } + + public function getHarbormasterBuildableMessageName() { + return pht('Restart Builds'); + } + + public function getHarbormasterBuildableMessageEffect() { + return pht('Build will restart.'); + } + + public function newConfirmPromptTitle() { + return pht('Really restart build?'); + } + + public function newConfirmPromptBody() { + return pht( + 'Progress on this build will be discarded and the build will restart. '. + 'Side effects of the build will occur again. Really restart build?'); + } + + public function newBuildableConfirmPromptTitle( + array $builds, + array $sendable) { + return pht( + 'Really restart %s build(s)?', + phutil_count($builds)); + } + + public function newBuildableConfirmPromptBody( + array $builds, + array $sendable) { + + if (count($sendable) === count($builds)) { + return pht( + 'All builds will restart.'); + } else { + return pht( + 'You can only restart some builds.'); + } + } + + public function newBuildableConfirmPromptWarnings( + array $builds, + array $sendable) { + + $building = false; + foreach ($sendable as $build) { + if ($build->isBuilding()) { + $building = true; + break; + } + } + + $warnings = array(); + + if ($building) { + $warnings[] = pht( + 'Progress on running builds will be discarded.'); + } + + if ($sendable) { + $warnings[] = pht( + 'When a build is restarted, side effects associated with '. + 'the build may occur again.'); + } + + return $warnings; } public function getTitle() { @@ -16,7 +84,7 @@ } public function getIcon() { - return 'fa-backward'; + return 'fa-repeat'; } public function applyInternalEffects($object, $value) { @@ -27,4 +95,72 @@ $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); } + protected function newCanApplyMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isAutobuild()) { + throw new HarbormasterRestartException( + pht('Can Not Restart Autobuild'), + pht( + 'This build can not be restarted because it is an automatic '. + 'build.')); + } + + $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; + $plan = $build->getBuildPlan(); + + // See T13526. Users who can't see the "BuildPlan" can end up here with + // no object. This is highly questionable. + if (!$plan) { + throw new HarbormasterRestartException( + pht('No Build Plan Permission'), + pht( + 'You can not restart this build because you do not have '. + 'permission to access the build plan.')); + } + + $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) + ->getPlanOption($plan); + $option_key = $option->getKey(); + + $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER; + $is_never = ($option_key === $never_restartable); + if ($is_never) { + throw new HarbormasterRestartException( + pht('Build Plan Prevents Restart'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting.')); + } + + $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED; + $is_failed = ($option_key === $failed_restartable); + if ($is_failed) { + if (!$this->isFailed()) { + throw new HarbormasterRestartException( + pht('Only Restartable if Failed'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting unless it '. + 'has failed, and it has not failed.')); + } + } + + } + + protected function newCanSendMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isRestarting()) { + throw new HarbormasterRestartException( + pht('Already Restarting'), + pht( + 'This build is already restarting. You can not reissue a restart '. + 'command to a restarting build.')); + } + + } + } diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php @@ -4,9 +4,49 @@ extends HarbormasterBuildMessageTransaction { const TRANSACTIONTYPE = 'message/resume'; + const MESSAGETYPE = 'resume'; - public function getMessageType() { - return 'resume'; + public function getHarbormasterBuildMessageName() { + return pht('Resume Build'); + } + + public function getHarbormasterBuildableMessageName() { + return pht('Resume Builds'); + } + + public function getHarbormasterBuildableMessageEffect() { + return pht('Build will resume.'); + } + + public function newConfirmPromptTitle() { + return pht('Really resume build?'); + } + + public function newConfirmPromptBody() { + return pht( + 'Work will continue on the build. Really resume?'); + } + + public function newBuildableConfirmPromptTitle( + array $builds, + array $sendable) { + return pht( + 'Really resume %s build(s)?', + phutil_count($builds)); + } + + public function newBuildableConfirmPromptBody( + array $builds, + array $sendable) { + + if (count($sendable) === count($builds)) { + return pht( + 'Work will continue on all builds. Really resume?'); + } else { + return pht( + 'You can only resume some builds. Work will continue on builds '. + 'you have permission to resume.'); + } } public function getTitle() { @@ -26,4 +66,50 @@ $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); } + protected function newCanApplyMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isAutobuild()) { + throw new HarbormasterRestartException( + pht('Unable to Resume Build'), + pht( + 'You can not resume a build that uses an autoplan.')); + } + + if (!$build->isPaused()) { + throw new HarbormasterRestartException( + pht('Unable to Resume Build'), + pht( + 'You can not resume this build because it is not paused. You can '. + 'only resume a paused build.')); + } + + } + + protected function newCanSendMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + if ($build->isResuming()) { + throw new HarbormasterRestartException( + pht('Unable to Resume Build'), + pht( + 'You can not resume this build beacuse it is already resuming.')); + } + + if ($build->isRestarting()) { + throw new HarbormasterRestartException( + pht('Unable to Resume Build'), + pht('You can not resume this build because it is already restarting.')); + } + + if ($build->isAborting()) { + throw new HarbormasterRestartException( + pht('Unable to Resume Build'), + pht('You can not resume this build because it is already aborting.')); + } + + } + } diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php --- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php +++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php @@ -3,6 +3,31 @@ abstract class HarbormasterBuildMessageTransaction extends HarbormasterBuildTransactionType { + final public function getHarbormasterBuildMessageType() { + return $this->getPhobjectClassConstant('MESSAGETYPE'); + } + + abstract public function getHarbormasterBuildMessageName(); + abstract public function getHarbormasterBuildableMessageName(); + abstract public function getHarbormasterBuildableMessageEffect(); + + abstract public function newConfirmPromptTitle(); + abstract public function newConfirmPromptBody(); + + abstract public function newBuildableConfirmPromptTitle( + array $builds, + array $sendable); + + abstract public function newBuildableConfirmPromptBody( + array $builds, + array $sendable); + + public function newBuildableConfirmPromptWarnings( + array $builds, + array $sendable) { + return array(); + } + final public function generateOldValue($object) { return null; } @@ -17,32 +42,110 @@ ); } - final public static function getTransactionTypeForMessageType($message_type) { + final public static function getTransactionObjectForMessageType( + $message_type) { $message_xactions = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); foreach ($message_xactions as $message_xaction) { - if ($message_xaction->getMessageType() === $message_type) { - return $message_xaction->getTransactionTypeConstant(); + $xaction_type = $message_xaction->getHarbormasterBuildMessageType(); + if ($xaction_type === $message_type) { + return $message_xaction; } } return null; } - abstract public function getMessageType(); + final public static function getTransactionTypeForMessageType($message_type) { + $message_xaction = self::getTransactionObjectForMessageType($message_type); + + if ($message_xaction) { + return $message_xaction->getTransactionTypeConstant(); + } + + return null; + } - public function validateTransactions($object, array $xactions) { - $errors = array(); + final public function getTransactionHasEffect($object, $old, $new) { + return $this->canApplyMessage($this->getActor(), $object); + } - // TODO: Restore logic that tests if the command can issue without causing - // anything to lapse into an invalid state. This should not be the same - // as the logic which powers the web UI: for example, if an "abort" is - // queued we want to disable "Abort" in the web UI, but should obviously - // process it here. + final public function canApplyMessage( + PhabricatorUser $viewer, + HarbormasterBuild $build) { - return $errors; + try { + $this->assertCanApplyMessage($viewer, $build); + return true; + } catch (HarbormasterRestartException $ex) { + return false; + } } + final public function canSendMessage( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + + try { + $this->assertCanSendMessage($viewer, $build); + return true; + } catch (HarbormasterRestartException $ex) { + return false; + } + } + + final public function assertCanApplyMessage( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + $this->newCanApplyMessageAssertion($viewer, $build); + } + + final public function assertCanSendMessage( + PhabricatorUser $viewer, + HarbormasterBuild $build) { + $plan = $build->getBuildPlan(); + + // See T13526. Users without permission to access the build plan can + // currently end up here with no "BuildPlan" object. + if (!$plan) { + throw new HarbormasterRestartException( + pht('No Build Plan Permission'), + pht( + 'You can not issue this command because you do not have '. + 'permission to access the build plan for this build.')); + } + + // Issuing these commands requires that you be able to edit the build, to + // prevent enemy engineers from sabotaging your builds. See T9614. + if (!$plan->canRunWithoutEditCapability()) { + try { + PhabricatorPolicyFilter::requireCapability( + $viewer, + $plan, + PhabricatorPolicyCapability::CAN_EDIT); + } catch (PhabricatorPolicyException $ex) { + throw new HarbormasterRestartException( + pht('Insufficent Build Plan Permission'), + pht( + 'The build plan for this build is configured to prevent '. + 'users who can not edit it from issuing commands to the '. + 'build, and you do not have permission to edit the build '. + 'plan.')); + } + } + + $this->newCanSendMessageAssertion($viewer, $build); + $this->assertCanApplyMessage($viewer, $build); + } + + abstract protected function newCanSendMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build); + + abstract protected function newCanApplyMessageAssertion( + PhabricatorUser $viewer, + HarbormasterBuild $build); + }