Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
index 16a4aee5c7..009758078f 100644
--- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
+++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php
@@ -1,289 +1,289 @@
<?php
final class HarbormasterBuildStatus extends Phobject {
const STATUS_INACTIVE = 'inactive';
const STATUS_PENDING = 'pending';
const STATUS_BUILDING = 'building';
const STATUS_PASSED = 'passed';
const STATUS_FAILED = 'failed';
const STATUS_ABORTED = 'aborted';
const STATUS_ERROR = 'error';
const STATUS_PAUSED = 'paused';
const STATUS_DEADLOCKED = 'deadlocked';
const PENDING_PAUSING = 'x-pausing';
const PENDING_RESUMING = 'x-resuming';
const PENDING_RESTARTING = 'x-restarting';
const PENDING_ABORTING = 'x-aborting';
private $key;
private $properties;
public function __construct($key, array $properties) {
$this->key = $key;
$this->properties = $properties;
}
public static function newBuildStatusObject($status) {
$spec = self::getBuildStatusSpec($status);
return new self($status, $spec);
}
private function getProperty($key) {
if (!array_key_exists($key, $this->properties)) {
throw new Exception(
pht(
'Attempting to access unknown build status property ("%s").',
$key));
}
return $this->properties[$key];
}
public function isBuilding() {
return $this->getProperty('isBuilding');
}
public function isPaused() {
return ($this->key === self::STATUS_PAUSED);
}
public function isComplete() {
return $this->getProperty('isComplete');
}
public function isPassed() {
return ($this->key === self::STATUS_PASSED);
}
public function isFailed() {
return ($this->key === self::STATUS_FAILED);
}
public function isAborting() {
return ($this->key === self::PENDING_ABORTING);
}
public function isRestarting() {
return ($this->key === self::PENDING_RESTARTING);
}
public function isResuming() {
return ($this->key === self::PENDING_RESUMING);
}
public function isPausing() {
return ($this->key === self::PENDING_PAUSING);
}
public function isPending() {
return ($this->key === self::STATUS_PENDING);
}
public function getIconIcon() {
return $this->getProperty('icon');
}
public function getIconColor() {
return $this->getProperty('color');
}
public function getName() {
return $this->getProperty('name');
}
/**
* Get a human readable name for a build status constant.
*
* @param const Build status constant.
* @return string Human-readable name.
*/
public static function getBuildStatusName($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['name'];
}
public static function getBuildStatusMap() {
$specs = self::getBuildStatusSpecMap();
return ipull($specs, 'name');
}
public static function getBuildStatusIcon($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['icon'];
}
public static function getBuildStatusColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color'];
}
public static function getBuildStatusANSIColor($status) {
$spec = self::getBuildStatusSpec($status);
return $spec['color.ansi'];
}
public static function getWaitingStatusConstants() {
return array(
self::STATUS_INACTIVE,
self::STATUS_PENDING,
);
}
public static function getActiveStatusConstants() {
return array(
self::STATUS_BUILDING,
self::STATUS_PAUSED,
);
}
public static function getIncompleteStatusConstants() {
$map = self::getBuildStatusSpecMap();
$constants = array();
foreach ($map as $constant => $spec) {
if (!$spec['isComplete']) {
$constants[] = $constant;
}
}
return $constants;
}
public static function getCompletedStatusConstants() {
return array(
self::STATUS_PASSED,
self::STATUS_FAILED,
self::STATUS_ABORTED,
self::STATUS_ERROR,
self::STATUS_DEADLOCKED,
);
}
private static function getBuildStatusSpecMap() {
return array(
self::STATUS_INACTIVE => array(
'name' => pht('Inactive'),
'icon' => 'fa-circle-o',
'color' => 'dark',
'color.ansi' => 'yellow',
'isBuilding' => false,
'isComplete' => false,
),
self::STATUS_PENDING => array(
'name' => pht('Pending'),
'icon' => 'fa-circle-o',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_BUILDING => array(
'name' => pht('Building'),
'icon' => 'fa-chevron-circle-right',
'color' => 'blue',
'color.ansi' => 'yellow',
'isBuilding' => true,
'isComplete' => false,
),
self::STATUS_PASSED => array(
'name' => pht('Passed'),
'icon' => 'fa-check-circle',
'color' => 'green',
'color.ansi' => 'green',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_FAILED => array(
'name' => pht('Failed'),
'icon' => 'fa-times-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ABORTED => array(
'name' => pht('Aborted'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::STATUS_ERROR => array(
'name' => pht('Unexpected Error'),
'icon' => 'fa-minus-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
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,
),
self::STATUS_DEADLOCKED => array(
'name' => pht('Deadlocked'),
'icon' => 'fa-exclamation-circle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => true,
),
self::PENDING_PAUSING => array(
'name' => pht('Pausing'),
'icon' => 'fa-exclamation-triangle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => false,
),
self::PENDING_RESUMING => array(
'name' => pht('Resuming'),
'icon' => 'fa-exclamation-triangle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => false,
),
self::PENDING_RESTARTING => array(
'name' => pht('Restarting'),
'icon' => 'fa-exclamation-triangle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => false,
),
self::PENDING_ABORTING => array(
'name' => pht('Aborting'),
'icon' => 'fa-exclamation-triangle',
'color' => 'red',
'color.ansi' => 'red',
'isBuilding' => false,
'isComplete' => false,
),
);
}
private static function getBuildStatusSpec($status) {
$map = self::getBuildStatusSpecMap();
if (isset($map[$status])) {
return $map[$status];
}
return array(
'name' => pht('Unknown ("%s")', $status),
'icon' => 'fa-question-circle',
'color' => 'bluegrey',
'color.ansi' => 'magenta',
'isBuilding' => false,
'isComplete' => false,
);
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
index 6a4a2b1fee..d62fafeeb7 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php
@@ -1,149 +1,67 @@
<?php
final class HarbormasterBuildActionController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$via = $request->getURIData('via');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$build) {
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':
$return_uri = '/'.$build->getBuildable()->getMonogram();
break;
default:
$return_uri = $this->getApplicationURI('/build/'.$build->getID().'/');
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
index 970c01a564..a17948fc8d 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php
@@ -1,743 +1,711 @@
<?php
final class HarbormasterBuildViewController
extends HarbormasterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest();
$viewer = $request->getUser();
$id = $request->getURIData('id');
$build = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$build) {
return new Aphront404Response();
}
require_celerity_resource('harbormaster-css');
$title = pht('Build %d', $id);
$warnings = array();
$page_header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($build)
->setHeaderIcon('fa-cubes');
$status = $build->getBuildPendingStatusObject();
$status_icon = $status->getIconIcon();
$status_color = $status->getIconColor();
$status_name = $status->getName();
$page_header->setStatus($status_icon, $status_color, $status_name);
$max_generation = (int)$build->getBuildGeneration();
if ($max_generation === 0) {
$min_generation = 0;
} else {
$min_generation = 1;
}
if ($build->isRestarting()) {
$max_generation = $max_generation + 1;
}
$generation = $request->getURIData('generation');
if ($generation === null) {
$generation = $max_generation;
} else {
$generation = (int)$generation;
}
if ($generation < $min_generation || $generation > $max_generation) {
return new Aphront404Response();
}
if ($generation < $max_generation) {
$warnings[] = pht(
'You are viewing an older run of this build. %s',
phutil_tag(
'a',
array(
'href' => $build->getURI(),
),
pht('View Current Build')));
}
$curtain = $this->buildCurtainView($build);
$properties = $this->buildPropertyList($build);
$history = $this->buildHistoryTable(
$build,
$generation,
$min_generation,
$max_generation);
$crumbs = $this->buildApplicationCrumbs();
$this->addBuildableCrumb($crumbs, $build->getBuildable());
$crumbs->addTextCrumb($title);
$crumbs->setBorder(true);
$build_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->needBuildSteps(true)
->withBuildPHIDs(array($build->getPHID()))
->withBuildGenerations(array($generation))
->execute();
if ($build_targets) {
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(mpull($build_targets, 'getPHID'))
->execute();
$messages = mgroup($messages, 'getReceiverPHID');
} else {
$messages = array();
}
if ($build_targets) {
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(mpull($build_targets, 'getPHID'))
->execute();
$artifacts = msort($artifacts, 'getArtifactKey');
$artifacts = mgroup($artifacts, 'getBuildTargetPHID');
} else {
$artifacts = array();
}
$targets = array();
foreach ($build_targets as $build_target) {
$header = id(new PHUIHeaderView())
->setHeader($build_target->getName())
->setUser($viewer)
->setHeaderIcon('fa-bullseye');
$target_box = id(new PHUIObjectBoxView())
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setHeader($header);
$tab_group = new PHUITabGroupView();
$target_box->addTabGroup($tab_group);
$property_list = new PHUIPropertyListView();
$target_artifacts = idx($artifacts, $build_target->getPHID(), array());
$links = array();
$type_uri = HarbormasterURIArtifact::ARTIFACTCONST;
foreach ($target_artifacts as $artifact) {
if ($artifact->getArtifactType() == $type_uri) {
$impl = $artifact->getArtifactImplementation();
if ($impl->isExternalLink()) {
$links[] = $impl->renderLink();
}
}
}
if ($links) {
$links = phutil_implode_html(phutil_tag('br'), $links);
$property_list->addProperty(
pht('External Link'),
$links);
}
$status_view = new PHUIStatusListView();
$item = new PHUIStatusItemView();
$status = $build_target->getTargetStatus();
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$item->setTarget($status_name);
$item->setIcon($icon, $color);
$status_view->addItem($item);
$when = array();
$started = $build_target->getDateStarted();
$now = PhabricatorTime::getNow();
if ($started) {
$ended = $build_target->getDateCompleted();
if ($ended) {
$when[] = pht(
'Completed at %s',
phabricator_datetime($ended, $viewer));
$duration = ($ended - $started);
if ($duration) {
$when[] = pht(
'Built for %s',
phutil_format_relative_time_detailed($duration));
} else {
$when[] = pht('Built instantly');
}
} else {
$when[] = pht(
'Started at %s',
phabricator_datetime($started, $viewer));
$duration = ($now - $started);
if ($duration) {
$when[] = pht(
'Running for %s',
phutil_format_relative_time_detailed($duration));
}
}
} else {
$created = $build_target->getDateCreated();
$when[] = pht(
'Queued at %s',
phabricator_datetime($started, $viewer));
$duration = ($now - $created);
if ($duration) {
$when[] = pht(
'Waiting for %s',
phutil_format_relative_time_detailed($duration));
}
}
$property_list->addProperty(
pht('When'),
phutil_implode_html(" \xC2\xB7 ", $when));
$property_list->addProperty(pht('Status'), $status_view);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Overview'))
->setKey('overview')
->appendChild($property_list));
$step = $build_target->getBuildStep();
if ($step) {
$description = $step->getDescription();
if ($description) {
$description = new PHUIRemarkupView($viewer, $description);
$property_list->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$property_list->addTextContent($description);
}
} else {
$target_box->setFormErrors(
array(
pht(
'This build step has since been deleted on the build plan. '.
'Some information may be omitted.'),
));
}
$details = $build_target->getDetails();
$property_list = new PHUIPropertyListView();
foreach ($details as $key => $value) {
$property_list->addProperty($key, $value);
}
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Configuration'))
->setKey('configuration')
->appendChild($property_list));
$variables = $build_target->getVariables();
$variables_tab = $this->buildProperties($variables);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Variables'))
->setKey('variables')
->appendChild($variables_tab));
$artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Artifacts'))
->setKey('artifacts')
->appendChild($artifacts_tab));
$build_messages = idx($messages, $build_target->getPHID(), array());
$messages_tab = $this->buildMessages($build_messages);
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Messages'))
->setKey('messages')
->appendChild($messages_tab));
$property_list = new PHUIPropertyListView();
$property_list->addProperty(
pht('Build Target ID'),
$build_target->getID());
$property_list->addProperty(
pht('Build Target PHID'),
$build_target->getPHID());
$tab_group->addTab(
id(new PHUITabView())
->setName(pht('Metadata'))
->setKey('metadata')
->appendChild($property_list));
$targets[] = $target_box;
$targets[] = $this->buildLog($build, $build_target, $generation);
}
$timeline = $this->buildTransactionTimeline(
$build,
new HarbormasterBuildTransactionQuery());
$timeline->setShouldTerminate(true);
if ($warnings) {
$warnings = id(new PHUIInfoView())
->setErrors($warnings)
->setSeverity(PHUIInfoView::SEVERITY_WARNING);
} else {
$warnings = null;
}
$view = id(new PHUITwoColumnView())
->setHeader($page_header)
->setCurtain($curtain)
->setMainColumn(
array(
$warnings,
$properties,
$history,
$targets,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildArtifacts(
HarbormasterBuildTarget $build_target,
array $artifacts) {
$viewer = $this->getViewer();
$rows = array();
foreach ($artifacts as $artifact) {
$impl = $artifact->getArtifactImplementation();
if ($impl) {
$summary = $impl->renderArtifactSummary($viewer);
$type_name = $impl->getArtifactTypeName();
} else {
$summary = pht('<Unknown Artifact Type>');
$type_name = $artifact->getType();
}
$rows[] = array(
$artifact->getArtifactKey(),
$type_name,
$summary,
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(pht('This target has no associated artifacts.'))
->setHeaders(
array(
pht('Key'),
pht('Type'),
pht('Summary'),
))
->setColumnClasses(
array(
'pri',
'',
'wide',
));
return $table;
}
private function buildLog(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target,
$generation) {
$request = $this->getRequest();
$viewer = $request->getUser();
$limit = $request->getInt('l', 25);
$logs = id(new HarbormasterBuildLogQuery())
->setViewer($viewer)
->withBuildTargetPHIDs(array($build_target->getPHID()))
->execute();
$empty_logs = array();
$log_boxes = array();
foreach ($logs as $log) {
$start = 1;
$lines = preg_split("/\r\n|\r|\n/", $log->getLogText());
if ($limit !== 0) {
$start = count($lines) - $limit;
if ($start >= 1) {
$lines = array_slice($lines, -$limit, $limit);
} else {
$start = 1;
}
}
$id = null;
$is_empty = false;
if (count($lines) === 1 && trim($lines[0]) === '') {
// Prevent Harbormaster from showing empty build logs.
$id = celerity_generate_unique_node_id();
$empty_logs[] = $id;
$is_empty = true;
}
$log_view = new ShellLogView();
$log_view->setLines($lines);
$log_view->setStart($start);
$subheader = $this->createLogHeader($build, $log, $limit, $generation);
$prototype_view = id(new PHUIButtonView())
->setTag('a')
->setHref($log->getURI())
->setIcon('fa-file-text-o')
->setText(pht('New View (Prototype)'));
$header = id(new PHUIHeaderView())
->setHeader(pht(
'Build Log %d (%s - %s)',
$log->getID(),
$log->getLogSource(),
$log->getLogType()))
->addActionLink($prototype_view)
->setSubheader($subheader)
->setUser($viewer);
$log_box = id(new PHUIObjectBoxView())
->setHeader($header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setForm($log_view);
if ($is_empty) {
$log_box = phutil_tag(
'div',
array(
'style' => 'display: none',
'id' => $id,
),
$log_box);
}
$log_boxes[] = $log_box;
}
if ($empty_logs) {
$hide_id = celerity_generate_unique_node_id();
Javelin::initBehavior('phabricator-reveal-content');
$expand = phutil_tag(
'div',
array(
'id' => $hide_id,
'class' => 'harbormaster-empty-logs-are-hidden',
),
array(
pht(
'%s empty logs are hidden.',
phutil_count($empty_logs)),
' ',
javelin_tag(
'a',
array(
'href' => '#',
'sigil' => 'reveal-content',
'meta' => array(
'showIDs' => $empty_logs,
'hideIDs' => array($hide_id),
),
),
pht('Show all logs.')),
));
array_unshift($log_boxes, $expand);
}
return $log_boxes;
}
private function createLogHeader($build, $log, $limit, $generation) {
$options = array(
array(
'n' => 25,
),
array(
'n' => 50,
),
array(
'n' => 100,
),
array(
'n' => 0,
'label' => pht('Unlimited'),
),
);
$base_uri = id(new PhutilURI($build->getURI().$generation.'/'));
$links = array();
foreach ($options as $option) {
$n = $option['n'];
$label = idx($option, 'label', $n);
$is_selected = ($limit == $n);
if ($is_selected) {
$links[] = phutil_tag(
'strong',
array(),
$label);
} else {
$links[] = phutil_tag(
'a',
array(
'href' => (string)$base_uri->alter('l', $n),
),
$label);
}
}
return phutil_tag(
'span',
array(),
array(
phutil_implode_html(' - ', $links),
' ',
pht('Lines'),
));
}
private function buildCurtainView(HarbormasterBuild $build) {
$viewer = $this->getViewer();
$id = $build->getID();
$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;
}
private function buildPropertyList(HarbormasterBuild $build) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(array(
$build->getBuildablePHID(),
$build->getBuildPlanPHID(),
))
->execute();
$properties->addProperty(
pht('Buildable'),
$handles[$build->getBuildablePHID()]->renderLink());
$properties->addProperty(
pht('Build Plan'),
$handles[$build->getBuildPlanPHID()]->renderLink());
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildHistoryTable(
HarbormasterBuild $build,
$generation,
$min_generation,
$max_generation) {
if ($max_generation === $min_generation) {
return null;
}
$viewer = $this->getViewer();
$uri = $build->getURI();
$rows = array();
$rowc = array();
for ($ii = $max_generation; $ii >= $min_generation; $ii--) {
if ($generation == $ii) {
$rowc[] = 'highlighted';
} else {
$rowc[] = null;
}
$rows[] = array(
phutil_tag(
'a',
array(
'href' => $uri.$ii.'/',
),
pht('Run %d', $ii)),
);
}
$table = id(new AphrontTableView($rows))
->setColumnClasses(
array(
'pri wide',
))
->setRowClasses($rowc);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('History'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($table);
}
private function buildMessages(array $messages) {
$viewer = $this->getRequest()->getUser();
if ($messages) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($viewer)
->withPHIDs(mpull($messages, 'getAuthorPHID'))
->execute();
} else {
$handles = array();
}
$rows = array();
foreach ($messages as $message) {
$rows[] = array(
$message->getID(),
$handles[$message->getAuthorPHID()]->renderLink(),
$message->getType(),
$message->getIsConsumed() ? pht('Consumed') : null,
phabricator_datetime($message->getDateCreated(), $viewer),
);
}
$table = new AphrontTableView($rows);
$table->setNoDataString(pht('No messages for this build target.'));
$table->setHeaders(
array(
pht('ID'),
pht('From'),
pht('Type'),
pht('Consumed'),
pht('Received'),
));
$table->setColumnClasses(
array(
'',
'',
'wide',
'',
'date',
));
return $table;
}
private function buildProperties(array $properties) {
ksort($properties);
$rows = array();
foreach ($properties as $key => $value) {
$rows[] = array(
$key,
$value,
);
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Key'),
pht('Value'),
))
->setColumnClasses(
array(
'pri right',
'wide',
));
return $table;
}
}
diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
index b701274eb0..cfed19a6c1 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php
@@ -1,320 +1,187 @@
<?php
final class HarbormasterBuildableActionController
extends HarbormasterController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
$action = $request->getURIData('action');
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($id))
->needBuilds(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$buildable) {
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)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xaction = id(new HarbormasterBuildableTransaction())
->setTransactionType(HarbormasterBuildableTransaction::TYPE_COMMAND)
->setNewValue($action);
$editor->applyTransactions($buildable, array($xaction));
$build_editor = id(new HarbormasterBuildTransactionEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->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
index aa433be656..d8f6f2f950 100644
--- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
+++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php
@@ -1,359 +1,284 @@
<?php
final class HarbormasterBuildableViewController
extends HarbormasterController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$buildable = id(new HarbormasterBuildableQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$buildable) {
return new Aphront404Response();
}
$id = $buildable->getID();
// Pull builds and build targets.
$builds = id(new HarbormasterBuildQuery())
->setViewer($viewer)
->withBuildablePHIDs(array($buildable->getPHID()))
->needBuildTargets(true)
->execute();
list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds);
$buildable->attachBuilds($builds);
$object = $buildable->getBuildableObject();
$build_list = $this->buildBuildList($buildable);
$title = pht('Buildable %d', $id);
$header = id(new PHUIHeaderView())
->setHeader($title)
->setUser($viewer)
->setPolicyObject($buildable)
->setStatus(
$buildable->getStatusIcon(),
$buildable->getStatusColor(),
$buildable->getStatusDisplayName())
->setHeaderIcon('fa-recycle');
$timeline = $this->buildTransactionTimeline(
$buildable,
new HarbormasterBuildableTransactionQuery());
$timeline->setShouldTerminate(true);
$curtain = $this->buildCurtainView($buildable);
$properties = $this->buildPropertyList($buildable);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($buildable->getMonogram());
$crumbs->setBorder(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$properties,
$lint,
$unit,
$build_list,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($view);
}
private function buildCurtainView(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$id = $buildable->getID();
$curtain = $this->newCurtainView($buildable);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$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;
}
private function buildPropertyList(HarbormasterBuildable $buildable) {
$viewer = $this->getViewer();
$properties = id(new PHUIPropertyListView())
->setUser($viewer);
$container_phid = $buildable->getContainerPHID();
$buildable_phid = $buildable->getBuildablePHID();
if ($container_phid) {
$properties->addProperty(
pht('Container'),
$viewer->renderHandle($container_phid));
}
$properties->addProperty(
pht('Buildable'),
$viewer->renderHandle($buildable_phid));
$properties->addProperty(
pht('Origin'),
$buildable->getIsManualBuildable()
? pht('Manual Buildable')
: pht('Automatic Buildable'));
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Properties'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($properties);
}
private function buildBuildList(HarbormasterBuildable $buildable) {
$viewer = $this->getRequest()->getUser();
$build_list = id(new PHUIObjectItemListView())
->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();
if ($targets) {
$target_list = id(new PHUIStatusListView());
foreach ($targets as $target) {
$status = $target->getTargetStatus();
$icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status);
$color = HarbormasterBuildTarget::getBuildTargetStatusColor($status);
$status_name =
HarbormasterBuildTarget::getBuildTargetStatusName($status);
$name = $target->getName();
$target_list->addItem(
id(new PHUIStatusItemView())
->setIcon($icon, $color, $status_name)
->setTarget(pht('Target %d', $target->getID()))
->setNote($name));
}
$target_box = id(new PHUIBoxView())
->addPadding(PHUI::PADDING_SMALL)
->appendChild($target_list);
$item->appendChild($target_box);
}
$build_list->addItem($item);
}
$build_list->setFlush(true);
$box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Builds'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->appendChild($build_list);
return $box;
}
private function renderLintAndUnit(
HarbormasterBuildable $buildable,
array $builds) {
$viewer = $this->getViewer();
$targets = array();
foreach ($builds as $build) {
foreach ($build->getBuildTargets() as $target) {
$targets[] = $target;
}
}
if (!$targets) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere(
'buildTargetPHID IN (%Ls)',
$target_phids);
$unit_data = id(new HarbormasterBuildUnitMessageQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->execute();
if ($lint_data) {
$lint_table = id(new HarbormasterLintPropertyView())
->setViewer($viewer)
->setLimit(10)
->setLintMessages($lint_data);
$lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/');
$lint_header = id(new PHUIHeaderView())
->setHeader(pht('Lint Messages'))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setHref($lint_href)
->setIcon('fa-list-ul')
->setText('View All'));
$lint = id(new PHUIObjectBoxView())
->setHeader($lint_header)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setTable($lint_table);
} else {
$lint = null;
}
if ($unit_data) {
$unit = id(new HarbormasterUnitSummaryView())
->setViewer($viewer)
->setBuildable($buildable)
->setUnitMessages($unit_data)
->setShowViewAll(true)
->setLimit(5);
} else {
$unit = null;
}
return array($lint, $unit);
}
}
diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php
index bd0b86184a..0d9484093f 100644
--- a/src/applications/harbormaster/exception/HarbormasterRestartException.php
+++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php
@@ -1,33 +1,42 @@
<?php
final class HarbormasterRestartException extends Exception {
private $title;
private $body = array();
public function __construct($title, $body = null) {
$this->setTitle($title);
$this->appendParagraph($body);
parent::__construct($title);
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function getTitle() {
return $this->title;
}
public function appendParagraph($description) {
$this->body[] = $description;
return $this;
}
public function getBody() {
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
index 0837dd7912..1285c512d8 100644
--- a/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php
+++ b/src/applications/harbormaster/management/HarbormasterManagementRestartWorkflow.php
@@ -1,88 +1,91 @@
<?php
final class HarbormasterManagementRestartWorkflow
extends HarbormasterManagementWorkflow {
protected function didConstruct() {
$this
->setName('restart')
->setExamples(
"**restart** --active\n".
'**restart** --id id')
->setSynopsis(pht('Restart Harbormaster builds.'))
->setArguments(
array(
array(
'name' => 'id',
'param' => 'id',
'repeat' => true,
'help' => pht('Select one or more builds by ID.'),
),
array(
'name' => 'active',
'help' => pht('Select all active builds.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$ids = $args->getArg('id');
$active = $args->getArg('active');
if (!$ids && !$active) {
throw new PhutilArgumentUsageException(
pht('Use "--id" or "--active" to select builds.'));
} if ($ids && $active) {
throw new PhutilArgumentUsageException(
pht('Use one of "--id" or "--active" to select builds, but not both.'));
}
$query = id(new HarbormasterBuildQuery())
->setViewer($viewer);
if ($ids) {
$query->withIDs($ids);
} else {
$query->withBuildStatuses(
HarbormasterBuildStatus::getActiveStatusConstants());
}
$builds = $query->execute();
$count = count($builds);
if (!$count) {
$this->logSkip(
pht('SKIP'),
pht('No builds to restart.'));
return 0;
}
$prompt = pht('Restart %s build(s)?', new PhutilNumber($count));
if (!phutil_console_confirm($prompt)) {
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'),
pht('Sent a restart message to build.'));
}
return 0;
}
}
diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
index 5b059bbfcf..1eea3d75b1 100644
--- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php
+++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php
@@ -1,675 +1,529 @@
<?php
final class HarbormasterBuild extends HarbormasterDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorConduitResultInterface,
PhabricatorDestructibleInterface {
protected $buildablePHID;
protected $buildPlanPHID;
protected $buildStatus;
protected $buildGeneration;
protected $buildParameters = array();
protected $initiatorPHID;
protected $planAutoKey;
private $buildable = self::ATTACHABLE;
private $buildPlan = self::ATTACHABLE;
private $buildTargets = self::ATTACHABLE;
private $unprocessedMessages = self::ATTACHABLE;
public static function initializeNewBuild(PhabricatorUser $actor) {
return id(new HarbormasterBuild())
->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
->setBuildGeneration(0);
}
public function delete() {
$this->openTransaction();
$this->deleteUnprocessedMessages();
$result = parent::delete();
$this->saveTransaction();
return $result;
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'buildParameters' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'buildStatus' => 'text32',
'buildGeneration' => 'uint32',
'planAutoKey' => 'text32?',
'initiatorPHID' => 'phid?',
),
self::CONFIG_KEY_SCHEMA => array(
'key_buildable' => array(
'columns' => array('buildablePHID'),
),
'key_plan' => array(
'columns' => array('buildPlanPHID'),
),
'key_status' => array(
'columns' => array('buildStatus'),
),
'key_planautokey' => array(
'columns' => array('buildablePHID', 'planAutoKey'),
'unique' => true,
),
'key_initiator' => array(
'columns' => array('initiatorPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
HarbormasterBuildPHIDType::TYPECONST);
}
public function attachBuildable(HarbormasterBuildable $buildable) {
$this->buildable = $buildable;
return $this;
}
public function getBuildable() {
return $this->assertAttached($this->buildable);
}
public function getName() {
if ($this->getBuildPlan()) {
return $this->getBuildPlan()->getName();
}
return pht('Build');
}
public function attachBuildPlan(
HarbormasterBuildPlan $build_plan = null) {
$this->buildPlan = $build_plan;
return $this;
}
public function getBuildPlan() {
return $this->assertAttached($this->buildPlan);
}
public function getBuildTargets() {
return $this->assertAttached($this->buildTargets);
}
public function attachBuildTargets(array $targets) {
$this->buildTargets = $targets;
return $this;
}
public function isBuilding() {
return $this->getBuildStatusObject()->isBuilding();
}
public function isAutobuild() {
return ($this->getPlanAutoKey() !== null);
}
public function retrieveVariablesFromBuild() {
$results = array(
'buildable.diff' => null,
'buildable.revision' => null,
'buildable.commit' => null,
'repository.callsign' => null,
'repository.phid' => null,
'repository.vcs' => null,
'repository.uri' => null,
'step.timestamp' => null,
'build.id' => null,
'initiator.phid' => null,
'buildable.phid' => null,
'buildable.object.phid' => null,
'buildable.container.phid' => null,
'build.phid' => null,
);
foreach ($this->getBuildParameters() as $key => $value) {
$results['build/'.$key] = $value;
}
$buildable = $this->getBuildable();
$object = $buildable->getBuildableObject();
$object_variables = $object->getBuildVariables();
$results = $object_variables + $results;
$results['step.timestamp'] = time();
$results['build.id'] = $this->getID();
$results['initiator.phid'] = $this->getInitiatorPHID();
$results['buildable.phid'] = $buildable->getPHID();
$results['buildable.object.phid'] = $object->getPHID();
$results['buildable.container.phid'] = $buildable->getContainerPHID();
$results['build.phid'] = $this->getPHID();
return $results;
}
public static function getAvailableBuildVariables() {
$objects = id(new PhutilClassMapQuery())
->setAncestorClass('HarbormasterBuildableInterface')
->execute();
$variables = array();
$variables[] = array(
'step.timestamp' => pht('The current UNIX timestamp.'),
'build.id' => pht('The ID of the current build.'),
'target.phid' => pht('The PHID of the current build target.'),
'initiator.phid' => pht(
'The PHID of the user or Object that initiated the build, '.
'if applicable.'),
'buildable.phid' => pht(
'The object PHID of the Harbormaster Buildable being built.'),
'buildable.object.phid' => pht(
'The object PHID of the object (usually a diff or commit) '.
'being built.'),
'buildable.container.phid' => pht(
'The object PHID of the container (usually a revision or repository) '.
'for the object being built.'),
'build.phid' => pht(
'The object PHID of the Harbormaster Build being built.'),
);
foreach ($objects as $object) {
$variables[] = $object->getAvailableBuildVariables();
}
$variables = array_mergev($variables);
return $variables;
}
public function isComplete() {
return $this->getBuildStatusObject()->isComplete();
}
public function isPaused() {
return $this->getBuildStatusObject()->isPaused();
}
public function isPassed() {
return $this->getBuildStatusObject()->isPassed();
}
public function isFailed() {
return $this->getBuildStatusObject()->isFailed();
}
public function isPending() {
return $this->getBuildstatusObject()->isPending();
}
public function getURI() {
$id = $this->getID();
return "/harbormaster/build/{$id}/";
}
public function getBuildPendingStatusObject() {
list($pending_status) = $this->getUnprocessedMessageState();
if ($pending_status !== null) {
return HarbormasterBuildStatus::newBuildStatusObject($pending_status);
}
return $this->getBuildStatusObject();
}
protected function getBuildStatusObject() {
$status_key = $this->getBuildStatus();
return HarbormasterBuildStatus::newBuildStatusObject($status_key);
}
public function getObjectName() {
return pht('Build %d', $this->getID());
}
/* -( Build Messages )----------------------------------------------------- */
private function getUnprocessedMessages() {
return $this->assertAttached($this->unprocessedMessages);
}
public function getUnprocessedMessagesForApply() {
$unprocessed_state = $this->getUnprocessedMessageState();
list($pending_status, $apply_messages) = $unprocessed_state;
return $apply_messages;
}
private function getUnprocessedMessageState() {
// NOTE: If a build has multiple unprocessed messages, we'll ignore
// messages that are obsoleted by a later or stronger message.
//
// For example, if a build has both "pause" and "abort" messages in queue,
// we just ignore the "pause" message and perform an "abort", since pausing
// first wouldn't affect the final state, so we can just skip it.
//
// Likewise, if a build has both "restart" and "abort" messages, the most
// recent message is controlling: we'll take whichever action a command
// was most recently issued for.
$is_restarting = false;
$is_aborting = false;
$is_pausing = false;
$is_resuming = false;
$apply_messages = array();
foreach ($this->getUnprocessedMessages() as $message_object) {
$message_type = $message_object->getType();
switch ($message_type) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
$is_aborting = false;
$apply_messages = array($message_object);
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_aborting = true;
$is_restarting = false;
$apply_messages = array($message_object);
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_pausing = true;
$is_resuming = false;
$apply_messages = array($message_object);
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
$is_pausing = false;
$apply_messages = array($message_object);
break;
}
}
$pending_status = null;
if ($is_restarting) {
$pending_status = HarbormasterBuildStatus::PENDING_RESTARTING;
} else if ($is_aborting) {
$pending_status = HarbormasterBuildStatus::PENDING_ABORTING;
} else if ($is_pausing) {
$pending_status = HarbormasterBuildStatus::PENDING_PAUSING;
} else if ($is_resuming) {
$pending_status = HarbormasterBuildStatus::PENDING_RESUMING;
}
return array($pending_status, $apply_messages);
}
public function attachUnprocessedMessages(array $messages) {
assert_instances_of($messages, 'HarbormasterBuildMessage');
$this->unprocessedMessages = $messages;
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();
}
public function isResuming() {
return $this->getBuildPendingStatusObject()->isResuming();
}
public function isRestarting() {
return $this->getBuildPendingStatusObject()->isRestarting();
}
public function isAborting() {
return $this->getBuildPendingStatusObject()->isAborting();
}
public function markUnprocessedMessagesAsProcessed() {
foreach ($this->getUnprocessedMessages() as $key => $message_object) {
$message_object
->setIsConsumed(1)
->save();
}
return $this;
}
public function deleteUnprocessedMessages() {
foreach ($this->getUnprocessedMessages() as $key => $message_object) {
$message_object->delete();
unset($this->unprocessedMessages[$key]);
}
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())
->setType($message_type)
->save();
PhabricatorWorker::scheduleTask(
'HarbormasterBuildWorker',
array(
'buildID' => $this->getID(),
),
array(
'objectPHID' => $this->getPHID(),
'containerPHID' => $this->getBuildablePHID(),
));
}
public function releaseAllArtifacts(PhabricatorUser $viewer) {
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withBuildPHIDs(array($this->getPHID()))
->withBuildGenerations(array($this->getBuildGeneration()))
->execute();
if (!$targets) {
return;
}
$target_phids = mpull($targets, 'getPHID');
$artifacts = id(new HarbormasterBuildArtifactQuery())
->setViewer($viewer)
->withBuildTargetPHIDs($target_phids)
->withIsReleased(false)
->execute();
foreach ($artifacts as $artifact) {
$artifact->releaseArtifact();
}
}
public function restartBuild(PhabricatorUser $viewer) {
// TODO: This should become transactional.
// We're restarting the build, so release all previous artifacts.
$this->releaseAllArtifacts($viewer);
// Increment the build generation counter on the build.
$this->setBuildGeneration($this->getBuildGeneration() + 1);
// Currently running targets should periodically check their build
// generation (which won't have changed) against the build's generation.
// If it is different, they will automatically stop what they're doing
// and abort.
// Previously we used to delete targets, logs and artifacts here. Instead,
// leave them around so users can view previous generations of this build.
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new HarbormasterBuildTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new HarbormasterBuildTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getBuildable()->getPolicy($capability);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return $this->getBuildable()->hasAutomaticCapability(
$capability,
$viewer);
}
public function describeAutomaticCapability($capability) {
return pht('A build inherits policies from its buildable.');
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildablePHID')
->setType('phid')
->setDescription(pht('PHID of the object this build is building.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildPlanPHID')
->setType('phid')
->setDescription(pht('PHID of the build plan being run.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('buildStatus')
->setType('map<string, wild>')
->setDescription(pht('The current status of this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('initiatorPHID')
->setType('phid')
->setDescription(pht('The person (or thing) that started this build.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('name')
->setType('string')
->setDescription(pht('The name of this build.')),
);
}
public function getFieldValuesForConduit() {
$status = $this->getBuildStatus();
return array(
'buildablePHID' => $this->getBuildablePHID(),
'buildPlanPHID' => $this->getBuildPlanPHID(),
'buildStatus' => array(
'value' => $status,
'name' => HarbormasterBuildStatus::getBuildStatusName($status),
'color.ansi' =>
HarbormasterBuildStatus::getBuildStatusANSIColor($status),
),
'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
'name' => $this->getName(),
);
}
public function getConduitSearchAttachments() {
return array(
id(new HarbormasterQueryBuildsSearchEngineAttachment())
->setAttachmentKey('querybuilds'),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$viewer = $engine->getViewer();
$this->openTransaction();
$targets = id(new HarbormasterBuildTargetQuery())
->setViewer($viewer)
->withBuildPHIDs(array($this->getPHID()))
->execute();
foreach ($targets as $target) {
$engine->destroyObject($target);
}
$messages = id(new HarbormasterBuildMessageQuery())
->setViewer($viewer)
->withReceiverPHIDs(array($this->getPHID()))
->execute();
foreach ($messages as $message) {
$engine->destroyObject($message);
}
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php
index 7824a99216..2d5a6162a0 100644
--- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageAbortTransaction.php
@@ -1,41 +1,113 @@
<?php
final class HarbormasterBuildMessageAbortTransaction
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() {
return pht(
'%s aborted this build.',
$this->renderAuthor());
}
public function getIcon() {
return 'fa-exclamation-triangle';
}
public function getColor() {
return 'red';
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$build = $object;
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED);
}
public function applyExternalEffects($object, $value) {
$actor = $this->getActor();
$build = $object;
$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
index 76d0a6457e..8501984314 100644
--- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessagePauseTransaction.php
@@ -1,33 +1,121 @@
<?php
final class HarbormasterBuildMessagePauseTransaction
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() {
return pht(
'%s paused this build.',
$this->renderAuthor());
}
public function getIcon() {
return 'fa-pause';
}
public function getColor() {
return 'red';
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$build = $object;
$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
index a3deb269e5..4d62be31bb 100644
--- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageRestartTransaction.php
@@ -1,30 +1,166 @@
<?php
final class HarbormasterBuildMessageRestartTransaction
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() {
return pht(
'%s restarted this build.',
$this->renderAuthor());
}
public function getIcon() {
- return 'fa-backward';
+ return 'fa-repeat';
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$build = $object;
$build->restartBuild($actor);
$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
index 06269604c6..ce7f8fc964 100644
--- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageResumeTransaction.php
@@ -1,29 +1,115 @@
<?php
final class HarbormasterBuildMessageResumeTransaction
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() {
return pht(
'%s resumed this build.',
$this->renderAuthor());
}
public function getIcon() {
return 'fa-play';
}
public function applyInternalEffects($object, $value) {
$actor = $this->getActor();
$build = $object;
$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
index bd8393c23d..bfe4209812 100644
--- a/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php
+++ b/src/applications/harbormaster/xaction/build/HarbormasterBuildMessageTransaction.php
@@ -1,48 +1,151 @@
<?php
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;
}
final public function getTransactionTypeForConduit($xaction) {
return 'message';
}
final public function getFieldValuesForConduit($xaction, $data) {
return array(
'type' => $xaction->getNewValue(),
);
}
- 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);
+
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 28, 9:56 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
107915
Default Alt Text
(107 KB)

Event Timeline