diff --git a/resources/sql/autopatches/20160822.buildable.details.1.sql b/resources/sql/autopatches/20160822.buildable.details.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160822.buildable.details.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildable +ADD details LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160822.buildable.details.2.sql b/resources/sql/autopatches/20160822.buildable.details.2.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160822.buildable.details.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildable +SET details = '{}' WHERE details = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1186,6 +1186,7 @@ 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', + 'HarbormasterLintStatus' => 'applications/harbormaster/constants/HarbormasterLintStatus.php', 'HarbormasterManagementArchiveLogsWorkflow' => 'applications/harbormaster/management/HarbormasterManagementArchiveLogsWorkflow.php', 'HarbormasterManagementBuildWorkflow' => 'applications/harbormaster/management/HarbormasterManagementBuildWorkflow.php', 'HarbormasterManagementUpdateWorkflow' => 'applications/harbormaster/management/HarbormasterManagementUpdateWorkflow.php', @@ -5773,6 +5774,7 @@ 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', + 'HarbormasterLintStatus' => 'Phobject', 'HarbormasterManagementArchiveLogsWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementBuildWorkflow' => 'HarbormasterManagementWorkflow', 'HarbormasterManagementUpdateWorkflow' => 'HarbormasterManagementWorkflow', diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php --- a/src/applications/differential/controller/DifferentialChangesetViewController.php +++ b/src/applications/differential/controller/DifferentialChangesetViewController.php @@ -403,6 +403,8 @@ } private function loadCoverage(DifferentialChangeset $changeset) { + // TODO contrast with DifferentialDiff::loadCoverageMap + // TODO load from buildTargets directly. $target_phids = $changeset->getDiff()->getBuildTargetPHIDs(); if (!$target_phids) { return null; diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -108,8 +108,7 @@ } } - - protected function loadHarbormasterData(array $diffs) { + protected function loadHarbormasterBuilds(array $diffs) { $viewer = $this->getViewer(); $diffs = mpull($diffs, null, 'getPHID'); @@ -127,6 +126,11 @@ $diff->attachBuildable(idx($buildables, $phid)); } + } + + protected function loadHarbormasterData(array $diffs) { + $diffs = mpull($diffs, null, 'getPHID'); + $target_map = array(); foreach ($diffs as $phid => $diff) { $target_map[$phid] = $diff->getBuildTargetPHIDs(); diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -239,6 +239,8 @@ ->setErrors($revision_warnings); } + $this->loadHarbormasterBuilds($diffs); + $detail_diffs = array_select_keys( $diffs, array($diff_vs, $target->getID())); diff --git a/src/applications/differential/customfield/DifferentialLintField.php b/src/applications/differential/customfield/DifferentialLintField.php --- a/src/applications/differential/customfield/DifferentialLintField.php +++ b/src/applications/differential/customfield/DifferentialLintField.php @@ -3,6 +3,8 @@ final class DifferentialLintField extends DifferentialHarbormasterField { + // For lint, we're loading all the messages anyway, to display them inline. + public function getFieldKey() { return 'differential:lint'; } @@ -84,6 +86,7 @@ DifferentialDiff $diff, array $messages) { + $status = DifferentialRevisionUpdateHistoryView::getDiffLintStatus($diff); $colors = array( DifferentialLintStatus::LINT_NONE => 'grey', DifferentialLintStatus::LINT_OKAY => 'green', @@ -92,9 +95,9 @@ DifferentialLintStatus::LINT_SKIP => 'blue', DifferentialLintStatus::LINT_AUTO_SKIP => 'blue', ); - $icon_color = idx($colors, $diff->getLintStatus(), 'grey'); + $icon_color = idx($colors, $status, 'grey'); // TODO fetch from build/buildtarget - $message = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff); + $message = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($status); $excuse = $diff->getProperty('arc:lint-excuse'); if (strlen($excuse)) { diff --git a/src/applications/differential/customfield/DifferentialUnitField.php b/src/applications/differential/customfield/DifferentialUnitField.php --- a/src/applications/differential/customfield/DifferentialUnitField.php +++ b/src/applications/differential/customfield/DifferentialUnitField.php @@ -50,6 +50,14 @@ } public function renderDiffPropertyViewValue(DifferentialDiff $diff) { + $buildable = $diff->getBuildable(); + + $builds_unit_status = null; + if ($buildable) { + $builds_unit_status = $buildable->getDetail(HarbormasterBuildable::DETAIL_UNIT_STATUS); + $builds_unit_status = HarbormasterUnitStatus::getDifferentialUnitStatus($builds_unit_status); + } + var_dump($builds_unit_status); $colors = array( DifferentialUnitStatus::UNIT_NONE => 'grey', @@ -59,17 +67,29 @@ DifferentialUnitStatus::UNIT_SKIP => 'blue', DifferentialUnitStatus::UNIT_AUTO_SKIP => 'blue', ); - $icon_color = idx($colors, $diff->getUnitStatus(), 'grey'); - $message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff); + $view = id(new PHUIStatusListView()); - $status = id(new PHUIStatusListView()) - ->addItem( + if ($builds_unit_status) { + $icon_color = idx($colors, $builds_unit_status, 'grey'); + $message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($builds_unit_status); + $view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_STAR, $icon_color) ->setTarget($message)); + } - return $status; + $manual_tests_status = $diff->getUnitStatus(); // try to pull off the autoplan first. + // TODO this is actually only interesting if the local is "skip"; OTherwise, it should be included with the builds. + if ($manual_tests_status !== null) { + $icon_color = idx($colors, $manual_tests_status, 'grey'); + $message = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($manual_tests_status); + $view->addItem( + id(new PHUIStatusItemView()) + ->setIcon(PHUIStatusItemView::ICON_STAR, $icon_color) + ->setTarget($message)); + } + return $view; } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -360,20 +360,21 @@ } public function getBuildTargetPHIDs() { + // TODO since this function would have broken anyway if the targets were not + // loaded, we probably always want to use getBuildTargets() anyway? + $targets = $this->getBuildTargets(); + return mpull($targets, 'getPHID'); + } + + public function getBuildTargets() { $buildable = $this->getBuildable(); if (!$buildable) { return array(); } - $target_phids = array(); - foreach ($buildable->getBuilds() as $build) { - foreach ($build->getBuildTargets() as $target) { - $target_phids[] = $target->getPHID(); - } - } - - return $target_phids; + $targets = mpull($buildable->getBuilds(), 'getBuildTargets'); + return array_mergev($targets); } public function loadCoverageMap(PhabricatorUser $viewer) { @@ -381,7 +382,7 @@ if (!$target_phids) { return array(); } - + // TODO load from targets directly. $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); @@ -414,6 +415,7 @@ public function getUnitMessages() { + // TODO practically no callsite of this method should want ALL messages. return $this->assertAttached($this->unitMessages); } diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -139,21 +139,24 @@ } if ($diff) { - $lint = self::renderDiffLintStar($row['obj']); + $lint_status = self::getDiffLintStatus($diff); + $unit_status = self::getDiffUnitStatus($diff); + + $lint = self::renderDiffLintStar($lint_status); $lint = phutil_tag( 'div', array( 'class' => 'lintunit-star', - 'title' => self::getDiffLintMessage($diff), + 'title' => self::getDiffLintMessage($lint_status), ), $lint); - $unit = self::renderDiffUnitStar($row['obj']); + $unit = self::renderDiffUnitStar($unit_status); $unit = phutil_tag( 'div', array( 'class' => 'lintunit-star', - 'title' => self::getDiffUnitMessage($diff), + 'title' => self::getDiffUnitMessage($unit_status), ), $unit); @@ -312,7 +315,40 @@ const STAR_FAIL = 'fail'; const STAR_SKIP = 'skip'; - public static function renderDiffLintStar(DifferentialDiff $diff) { + public static function getDiffLintStatus(DifferentialDiff $diff) { + // TODO for lint, "no information" might actually mean "lint ok", but it + // looks just like "linters not configured". + // This might require more magic in the hm.sendmessage. + + $buildable = $diff->getBuildable(); + if ($buildable) { + $builds_lint_status = $buildable->getDetail(HarbormasterBuildable::DETAIL_LINT_STATUS); + $builds_lint_status = HarbormasterLintStatus::getDifferentialLintStatus($builds_lint_status); + } else { + $builds_lint_status = null; + } + + // TODO now we need to actually join these two values, not just coalesce: + // there might not be an appropriate message that will be baked into the + // buildable object with the right values. + return coalesce($builds_lint_status, $diff->getLintStatus()); + } + + public static function getDiffUnitStatus(DifferentialDiff $diff) { + $buildable = $diff->getBuildable(); + $builds_unit_status = $buildable ? + $buildable->getDetail(HarbormasterBuildable::DETAIL_UNIT_STATUS) : + null; + + $builds_unit_status = HarbormasterUnitStatus::getDifferentialUnitStatus($builds_unit_status); + + // TODO now we need to actually join these two values, not just coalesce: + // there might not be an appropriate message that will be baked into the + // buildable object with the right values. + return coalesce($builds_unit_status, $diff->getUnitStatus()); + } + + public static function renderDiffLintStar($status) { static $map = array( DifferentialLintStatus::LINT_NONE => self::STAR_NONE, DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY, @@ -322,12 +358,12 @@ DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP, ); - $star = idx($map, $diff->getLintStatus(), self::STAR_FAIL); + $star = idx($map, $status, self::STAR_FAIL); return self::renderDiffStar($star); } - public static function renderDiffUnitStar(DifferentialDiff $diff) { + public static function renderDiffUnitStar($unit_status) { static $map = array( DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE, DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY, @@ -337,13 +373,14 @@ DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP, ); - $star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL); + + $star = idx($map, $unit_status, self::STAR_FAIL); return self::renderDiffStar($star); } - public static function getDiffLintMessage(DifferentialDiff $diff) { - switch ($diff->getLintStatus()) { + public static function getDiffLintMessage($status) { + switch ($status) { case DifferentialLintStatus::LINT_NONE: return pht('No Linters Available'); case DifferentialLintStatus::LINT_OKAY: @@ -360,8 +397,8 @@ return pht('Unknown'); } - public static function getDiffUnitMessage(DifferentialDiff $diff) { - switch ($diff->getUnitStatus()) { + public static function getDiffUnitMessage($status) { + switch ($status) { case DifferentialUnitStatus::UNIT_NONE: return pht('No Unit Test Coverage'); case DifferentialUnitStatus::UNIT_OKAY: diff --git a/src/applications/harbormaster/constants/HarbormasterLintStatus.php b/src/applications/harbormaster/constants/HarbormasterLintStatus.php new file mode 100644 --- /dev/null +++ b/src/applications/harbormaster/constants/HarbormasterLintStatus.php @@ -0,0 +1,67 @@ + array( + 'differential_value' => DifferentialLintStatus::LINT_FAIL, + 'label' => pht('Error'), + 'sort' => 'A', + ), + ArcanistLintSeverity::SEVERITY_WARNING => array( + 'differential_value' => DifferentialLintStatus::LINT_WARN, + 'label' => pht('Warning'), + 'sort' => 'B', + ), + ArcanistLintSeverity::SEVERITY_ADVICE => array( + 'differential_value' => DifferentialLintStatus::LINT_OKAY, + 'label' => pht('Advice'), + 'sort' => 'Y', + ), + ); + } + +} diff --git a/src/applications/harbormaster/constants/HarbormasterUnitStatus.php b/src/applications/harbormaster/constants/HarbormasterUnitStatus.php --- a/src/applications/harbormaster/constants/HarbormasterUnitStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterUnitStatus.php @@ -27,6 +27,30 @@ return idx($map, 'sort', $default); } + public static function getWorstStatus(array $statuses) { + // TODO we sort PASS before SKIP, so maybe call this "summarizeStatuses" instead? + if (!$statuses) { + return null; + } + $map = self::getUnitStatusMap(); + $default = 'Z'; + $worst = head($statuses); + $w_index = idx($map, $worst, $default); + foreach ($statuses as $status) { + $r = idx($map, $status, $default); + if ($r < $w_index) { + $worst = $status; + $w_index = $r; + } + } + return $worst; + } + + public static function getDifferentialUnitStatus($status) { + $map = self::getUnitStatusDictionary($status); + return idx($map, 'differantial_result', $status); + } + private static function getUnitStatusDictionary($status) { $map = self::getUnitStatusMap(); $default = array(); @@ -55,30 +79,35 @@ private static function getUnitStatusMap() { return array( ArcanistUnitTestResult::RESULT_FAIL => array( + 'differantial_result' => DifferentialUnitStatus::UNIT_FAIL, // SOMEWHERER, we already making this translation. 'label' => pht('Failed'), 'icon' => 'fa-times', 'color' => 'red', 'sort' => 'A', ), ArcanistUnitTestResult::RESULT_BROKEN => array( + 'differantial_result' => DifferentialUnitStatus::UNIT_WARN, 'label' => pht('Broken'), 'icon' => 'fa-bomb', 'color' => 'indigo', 'sort' => 'B', ), ArcanistUnitTestResult::RESULT_UNSOUND => array( + 'differantial_result' => DifferentialUnitStatus::UNIT_WARN, 'label' => pht('Unsound'), 'icon' => 'fa-exclamation-triangle', 'color' => 'yellow', 'sort' => 'C', ), ArcanistUnitTestResult::RESULT_PASS => array( + 'differantial_result' => DifferentialUnitStatus::UNIT_OKAY, 'label' => pht('Passed'), 'icon' => 'fa-check', 'color' => 'green', 'sort' => 'D', ), ArcanistUnitTestResult::RESULT_SKIP => array( + 'differantial_result' => DifferentialUnitStatus::UNIT_SKIP, 'label' => pht('Skipped'), 'icon' => 'fa-fast-forward', 'color' => 'blue', diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -12,6 +12,8 @@ private $artifactReleaseQueue = array(); private $forceBuildableUpdate; + private $consumedNewMessages = false; + public function setForceBuildableUpdate($force_buildable_update) { $this->forceBuildableUpdate = $force_buildable_update; return $this; @@ -91,7 +93,10 @@ // If the build changed status, we might need to update the overall status // on the buildable. $new_status = $build->getBuildStatus(); - if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) { + if ($new_status != $old_status || + $this->consumedNewMessages || + $this->shouldForceBuildableUpdate()) { + $this->updateBuildable($build->getBuildable()); } @@ -375,7 +380,6 @@ $waiting_targets[$target->getPHID()] = $target; } } - if (!$waiting_targets) { return; } @@ -386,6 +390,7 @@ ->withConsumed(false) ->execute(); + $updated_targets = array(); foreach ($messages as $message) { $target = $waiting_targets[$message->getBuildTargetPHID()]; @@ -403,21 +408,22 @@ } if ($new_status !== null) { - $message->setIsConsumed(true); - $message->save(); - $target->setTargetStatus($new_status); + if ($target->isComplete()) { $target->setDateCompleted(PhabricatorTime::getNow()); } $target->save(); } + + $this->consumedNewMessages = true; + $message->setIsConsumed(true); + $message->save(); } } - /** * Update the overall status of the buildable this build is attached to. * @@ -438,8 +444,12 @@ ->setViewer($viewer) ->withIDs(array($buildable->getID())) ->needBuilds(true) + ->needTargets(true) ->executeOne(); + // This doesn't effect the `should_publish` state. + $this->updateLintAndUnitStatus($buildable); + $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { @@ -535,6 +545,84 @@ array($template)); } + /** + * Update lint and unit aggregated information. + * Invoked in the context of the Buildable lock. + */ + private function updateLintAndUnitStatus(HarbormasterBuildable $buildable) { + // var_dump(mpull($buildable->getBuilds(), 'getBuildTargets')); + // return; + $targets = array_mergev(mpull($buildable->getBuilds(), 'getBuildTargets')); + $target_phids = mpull($targets, 'getPHID'); + + // TODO HarbormasterBuildLintMessageQuery ? + $buildable_lints = id(new HarbormasterBuildLintMessage())->loadAllWhere( + 'buildTargetPHID IN (%Ls)', + $target_phids); + + $buildable_units = id(new HarbormasterBuildUnitMessage())->loadAllWhere( + 'buildTargetPHID IN (%Ls)', + $target_phids); + + if ($buildable_lints) { + $worst = head(msort($buildable_lints, 'getSortKey')); + $lint_severity = $worst->getSeverity(); + } else { + $lint_severity = null; + } + + // TODO also count lint by severity? We don't currently do that + + if ($buildable_units) { + // TODO rename `worst` to something else... + $worst = head(msort($buildable_units, 'getSortKey')); + $unit_result = $worst->getResult(); + + $coverage_map = array(); + foreach ($buildable_units as $unit) { + $coverage = $unit->getProperty('coverage', array()); + foreach ($coverage as $path => $coverage_data) { + $coverage_map[$path][] = $coverage_data; + } + } + + $groups = mgroup($buildable_units, 'getResult'); + $unit_counts = array(); + foreach ($groups as $status => $group) { + $unit_counts[$status] = count($group); + } + + foreach ($coverage_map as $path => $coverage_items) { + $coverage_map[$path] = ArcanistUnitTestResult::mergeCoverage( + $coverage_items); + } + } else { + $unit_result = null; + $coverage_map = null; + $unit_counts = null; + } + + // evil magic: `ORDER BY FIELD(result, 'fail', 'broken', 'skip', 'fail')`, although god knows which versions of mysql allow this. + + $buildable->setDetail( + HarbormasterBuildable::DETAIL_LINT_STATUS, + $lint_severity); + + $buildable->setDetail( + HarbormasterBuildable::DETAIL_UNIT_STATUS, + $unit_result); + + $buildable->setDetail( + HarbormasterBuildable::DETAIL_UNIT_COUNTS, + $unit_counts); + + $buildable->setDetail( + HarbormasterBuildable::DETAIL_COVERAGE_MAP, + $coverage_map); + + $buildable->save(); + } + private function releaseAllArtifacts(HarbormasterBuild $build) { $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -9,6 +9,7 @@ protected $buildablePHID; protected $containerPHID; protected $buildableStatus; + protected $details; protected $isManualBuildable; private $buildableObject = self::ATTACHABLE; @@ -19,6 +20,11 @@ const STATUS_PASSED = 'passed'; const STATUS_FAILED = 'failed'; + const DETAIL_LINT_STATUS = 'lint:status'; + const DETAIL_UNIT_STATUS = 'unit:status'; + const DETAIL_UNIT_COUNTS = 'unit:counts'; + const DETAIL_COVERAGE_MAP = 'unit:coverage'; + public static function getBuildableStatusName($status) { $map = self::getBuildStatusMap(); return idx($map, $status, pht('Unknown ("%s")', $status)); @@ -197,6 +203,9 @@ protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'details' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', @@ -249,6 +258,14 @@ return $this->assertAttached($this->builds); } + public function getDetail($key, $default = null) { + return idx($this->details, $key, $default); + } + + public function setDetail($key, $value) { + $this->details[$key] = $value; + return $this; + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php b/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php --- a/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php +++ b/src/applications/harbormaster/view/HarbormasterUnitPropertyView.php @@ -103,7 +103,7 @@ if ($full_uri && (count($messages) > $limit)) { $counts = array(); - $groups = mgroup($messages, 'getResult'); + $groups = mgroup($messages, 'getResult'); // TODO use aggregated info foreach ($groups as $status => $group) { $counts[] = HarbormasterUnitStatus::getUnitStatusCountLabel( $status, diff --git a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php --- a/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php +++ b/src/applications/harbormaster/view/HarbormasterUnitSummaryView.php @@ -40,7 +40,7 @@ $id = $buildable->getID(); $full_uri = "/harbormaster/unit/{$id}/"; - $messages = msort($messages, 'getSortKey'); + $messages = msort($messages, 'getSortKey'); // TODO avoid msort again, use saved information $head_unit = head($messages); if ($head_unit) { $status = $head_unit->getResult();