diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -416,7 +416,7 @@ 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', - 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '796a8803', + 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'ab1173d1', 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e', 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', @@ -636,7 +636,7 @@ 'javelin-behavior-event-all-day' => 'b41537c9', 'javelin-behavior-fancy-datepicker' => 'ecf4e799', 'javelin-behavior-global-drag-and-drop' => '960f6a39', - 'javelin-behavior-harbormaster-log' => '796a8803', + 'javelin-behavior-harbormaster-log' => 'ab1173d1', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', @@ -1526,9 +1526,6 @@ 'javelin-behavior', 'javelin-quicksand', ), - '796a8803' => array( - 'javelin-behavior', - ), '7a68dda3' => array( 'owners-path-editor', 'javelin-behavior', @@ -1768,6 +1765,9 @@ 'javelin-util', 'phabricator-prefab', ), + 'ab1173d1' => array( + 'javelin-behavior', + ), 'ab2f381b' => array( 'javelin-request', 'javelin-behavior', diff --git a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php --- a/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php @@ -16,6 +16,8 @@ return new Aphront404Response(); } + $highlight_range = $request->getURILineRange('lines', 1000); + $log_size = $this->getTotalByteLength($log); $head_lines = $request->getInt('head'); @@ -65,6 +67,16 @@ ); } + if ($highlight_range) { + $highlight_views = $this->getHighlightViews( + $log, + $highlight_range, + $log_size); + foreach ($highlight_views as $highlight_view) { + $views[] = $highlight_view; + } + } + if ($tail_lines > 0) { $views[] = array( 'offset' => $tail_offset, @@ -86,10 +98,11 @@ $direction = $read['direction']; if ($direction < 0) { - $offset -= $read_length; - if ($offset < 0) { + if ($offset > $read_length) { + $offset -= $read_length; + } else { + $read_length = $offset; $offset = 0; - $read_length = $log_size; } } @@ -215,8 +228,10 @@ } $limit = $view['limit']; - if ($limit < ($view_offset + $view_length)) { - $view_length = ($limit - $view_offset); + if ($limit !== null) { + if ($limit < ($view_offset + $view_length)) { + $view_length = ($limit - $view_offset); + } } } else { $view_offset = $data_offset; @@ -226,9 +241,11 @@ } $limit = $view['limit']; - if ($limit > $view_offset) { - $view_length -= ($limit - $view_offset); - $view_offset = $limit; + if ($limit !== null) { + if ($limit > $view_offset) { + $view_length -= ($limit - $view_offset); + $view_offset = $limit; + } } } @@ -325,7 +342,6 @@ } $uri = $log->getURI(); - $highlight_range = $request->getURIData('lines'); $rows = array(); foreach ($render as $range) { @@ -339,6 +355,16 @@ $display_line = ($line['line'] + 1); $display_text = ($line['data']); + $cell_attr = array(); + if ($highlight_range) { + if (($display_line >= $highlight_range[0]) && + ($display_line <= $highlight_range[1])) { + $cell_attr = array( + 'class' => 'phabricator-source-highlight', + ); + } + } + $display_line = phutil_tag( 'a', array( @@ -347,7 +373,7 @@ $display_line); $line_cell = phutil_tag('th', array(), $display_line); - $text_cell = phutil_tag('td', array(), $display_text); + $text_cell = phutil_tag('td', $cell_attr, $display_text); $rows[] = phutil_tag( 'tr', @@ -557,25 +583,43 @@ $vs = $vview['viewOffset']; $ve = $vs + $vview['viewLength']; + // Don't merge if one of the slices starts at a byte offset + // significantly after the other ends. + if (($vs > $ue + $body_bytes) || ($us > $ve + $body_bytes)) { + continue; + } + $uss = $uview['sliceOffset']; $use = $uss + $uview['sliceLength']; $vss = $vview['sliceOffset']; $vse = $vss + $vview['sliceLength']; - if ($ue <= $vs) { - if (($ue + $body_bytes) >= $vs) { - if (($use + $body_lines) >= $vss) { - $views[$ukey] = array( - 'sliceLength' => ($vse - $uss), - 'viewLength' => ($ve - $us), - ) + $views[$ukey]; - - unset($views[$vkey]); - continue; - } - } + // Don't merge if one of the slices starts at a line offset + // significantly after the other ends. + if ($uss > ($vse + $body_lines) || $vss > ($use + $body_lines)) { + continue; } + + // These views are overlapping or nearly overlapping, so we merge + // them. We merge views even if they aren't exactly adjacent since + // it's silly to render an "expand more" which only expands a couple + // of lines. + + $offset = min($us, $vs); + $length = max($ue, $ve) - $offset; + + $slice_offset = min($uss, $vss); + $slice_length = max($use, $vse) - $slice_offset; + + $views[$ukey] = array( + 'viewOffset' => $offset, + 'viewLength' => $length, + 'sliceOffset' => $slice_offset, + 'sliceLength' => $slice_length, + ) + $views[$ukey]; + + unset($views[$vkey]); } } @@ -603,7 +647,7 @@ 'meta' => array( 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], - 'head' => 4, + 'head' => 128, 'tail' => 0, ), ), @@ -620,8 +664,8 @@ 'meta' => array( 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], - 'head' => 2, - 'tail' => 2, + 'head' => 128, + 'tail' => 128, ), ), $mid_text); @@ -640,7 +684,7 @@ 'headOffset' => $range['head'], 'tailOffset' => $range['tail'], 'head' => 0, - 'tail' => 4, + 'tail' => 128, ), ), $down_text); @@ -727,4 +771,98 @@ return phutil_tag('tr', array(), $format_cells); } + private function getHighlightViews( + HarbormasterBuildLog $log, + array $range, + $log_size) { + // If we're highlighting a line range in the file, we first need to figure + // out the offsets for the lines we care about. + list($range_min, $range_max) = $range; + + // Read the markers to find a range we can load which includes both lines. + $read_range = $log->getLineSpanningRange($range_min, $range_max); + list($min_pos, $max_pos, $min_line) = $read_range; + + $length = ($max_pos - $min_pos); + + // Reject to do the read if it requires us to examine a huge amount of + // data. For example, the user may request lines "$1-1000" of a file where + // each line has 100MB of text. + $limit = (1024 * 1024 * 16); + if ($length > $limit) { + return array(); + } + + $data = $log->loadData($min_pos, $length); + + $offset = $min_pos; + $min_offset = null; + $max_offset = null; + + $lines = $this->getLines($data); + $number = ($min_line + 1); + + foreach ($lines as $line) { + if ($min_offset === null) { + if ($number === $range_min) { + $min_offset = $offset; + } + } + + $offset += strlen($line); + + if ($max_offset === null) { + if ($number === $range_max) { + $max_offset = $offset; + break; + } + } + + $number += 1; + } + + $context_lines = 8; + + // Build views around the beginning and ends of the respective lines. We + // expect these views to overlap significantly in normal circumstances + // and be merged later. + $views = array(); + + if ($min_offset !== null) { + $views[] = array( + 'offset' => $min_offset, + 'lines' => $context_lines + ($range_max - $range_min) - 1, + 'direction' => 1, + 'limit' => null, + ); + if ($min_offset > 0) { + $views[] = array( + 'offset' => $min_offset, + 'lines' => $context_lines, + 'direction' => -1, + 'limit' => null, + ); + } + } + + if ($max_offset !== null) { + $views[] = array( + 'offset' => $max_offset, + 'lines' => $context_lines + ($range_max - $range_min), + 'direction' => -1, + 'limit' => null, + ); + if ($max_offset < $log_size) { + $views[] = array( + 'offset' => $max_offset, + 'lines' => $context_lines, + 'direction' => 1, + 'limit' => null, + ); + } + } + + return $views; + } + } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -212,6 +212,36 @@ return $parts; } + public function getLineSpanningRange($min_line, $max_line) { + $map = $this->getLineMap(); + if (!$map) { + throw new Exception(pht('No line map.')); + } + + $min_pos = 0; + $min_line = 0; + $max_pos = $this->getByteLength(); + list($map) = $map; + foreach ($map as $marker) { + list($offset, $count) = $marker; + + if ($count < $min_line) { + if ($offset > $min_pos) { + $min_pos = $offset; + $min_line = $count; + } + } + + if ($count > $max_line) { + $max_pos = min($max_pos, $offset); + break; + } + } + + return array($min_pos, $max_pos, $min_line); + } + + public function getReadPosition($read_offset) { $position = array(0, 0); diff --git a/src/applications/harbormaster/view/HarbormasterBuildLogView.php b/src/applications/harbormaster/view/HarbormasterBuildLogView.php --- a/src/applications/harbormaster/view/HarbormasterBuildLogView.php +++ b/src/applications/harbormaster/view/HarbormasterBuildLogView.php @@ -67,7 +67,8 @@ 'harbormaster-log', array( 'contentNodeID' => $content_id, - 'renderURI' => $log->getRenderURI($this->getHighlightedLineRange()), + 'initialURI' => $log->getRenderURI($this->getHighlightedLineRange()), + 'renderURI' => $log->getRenderURI(null), )); $box_view->appendChild($content_div); diff --git a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js --- a/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js +++ b/webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js @@ -79,7 +79,7 @@ JX.DOM.setContent(contentNode, JX.$H(r.markup)); } - var uri = new JX.URI(config.renderURI); + var uri = new JX.URI(config.initialURI); new JX.Request(uri, onresponse) .send();