@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Sort of make Harbormaster build logs page properly

Summary: Depends on D19139. Ref T13088. This doesn't actually work, but is close enough that a skilled attacker might be able to briefly deceive a small child.

Test Plan:
- Viewed some very small logs under very controlled conditions, saw content.
- Larger logs vaguely do something resembling working correctly.

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13088

Differential Revision: https://secure.phabricator.com/D19141

+721 -7
+7 -2
resources/celerity/map.php
··· 78 78 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 79 79 'rsrc/css/application/files/global-drag-and-drop.css' => 'b556a948', 80 80 'rsrc/css/application/flag/flag.css' => 'bba8f811', 81 - 'rsrc/css/application/harbormaster/harbormaster.css' => 'f491c9f4', 81 + 'rsrc/css/application/harbormaster/harbormaster.css' => 'fecac64f', 82 82 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', 83 83 'rsrc/css/application/herald/herald.css' => 'cd8d0134', 84 84 'rsrc/css/application/maniphest/report.css' => '9b9580b7', ··· 416 416 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '901935ef', 417 417 'rsrc/js/application/files/behavior-icon-composer.js' => '8499b6ab', 418 418 'rsrc/js/application/files/behavior-launch-icon-composer.js' => '48086888', 419 + 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => '0844f3c1', 419 420 'rsrc/js/application/herald/HeraldRuleEditor.js' => 'dca75c0e', 420 421 'rsrc/js/application/herald/PathTypeahead.js' => 'f7fc67ec', 421 422 'rsrc/js/application/herald/herald-rule-editor.js' => '7ebaeed3', ··· 578 579 'font-fontawesome' => 'e838e088', 579 580 'font-lato' => 'c7ccd872', 580 581 'global-drag-and-drop-css' => 'b556a948', 581 - 'harbormaster-css' => 'f491c9f4', 582 + 'harbormaster-css' => 'fecac64f', 582 583 'herald-css' => 'cd8d0134', 583 584 'herald-rule-editor' => 'dca75c0e', 584 585 'herald-test-css' => 'a52e323e', ··· 635 636 'javelin-behavior-event-all-day' => 'b41537c9', 636 637 'javelin-behavior-fancy-datepicker' => 'ecf4e799', 637 638 'javelin-behavior-global-drag-and-drop' => '960f6a39', 639 + 'javelin-behavior-harbormaster-log' => '0844f3c1', 638 640 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 639 641 'javelin-behavior-high-security-warning' => 'a464fe03', 640 642 'javelin-behavior-history-install' => '7ee2b591', ··· 959 961 'javelin-dom', 960 962 'javelin-stratcom', 961 963 'javelin-workflow', 964 + ), 965 + '0844f3c1' => array( 966 + 'javelin-behavior', 962 967 ), 963 968 '08f4ccc3' => array( 964 969 'phui-oi-list-view-css',
+2
src/__phutil_library_map__.php
··· 1230 1230 'HarbormasterBuildLogDownloadController' => 'applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php', 1231 1231 'HarbormasterBuildLogPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildLogPHIDType.php', 1232 1232 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php', 1233 + 'HarbormasterBuildLogRenderController' => 'applications/harbormaster/controller/HarbormasterBuildLogRenderController.php', 1233 1234 'HarbormasterBuildLogTestCase' => 'applications/harbormaster/__tests__/HarbormasterBuildLogTestCase.php', 1234 1235 'HarbormasterBuildLogView' => 'applications/harbormaster/view/HarbormasterBuildLogView.php', 1235 1236 'HarbormasterBuildLogViewController' => 'applications/harbormaster/controller/HarbormasterBuildLogViewController.php', ··· 6519 6520 'HarbormasterBuildLogDownloadController' => 'HarbormasterController', 6520 6521 'HarbormasterBuildLogPHIDType' => 'PhabricatorPHIDType', 6521 6522 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6523 + 'HarbormasterBuildLogRenderController' => 'HarbormasterController', 6522 6524 'HarbormasterBuildLogTestCase' => 'PhabricatorTestCase', 6523 6525 'HarbormasterBuildLogView' => 'AphrontView', 6524 6526 'HarbormasterBuildLogViewController' => 'HarbormasterController',
+4 -1
src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php
··· 97 97 'buildkite/' => 'HarbormasterBuildkiteHookController', 98 98 ), 99 99 'log/' => array( 100 - 'view/(?P<id>\d+)/' => 'HarbormasterBuildLogViewController', 100 + 'view/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?' 101 + => 'HarbormasterBuildLogViewController', 102 + 'render/(?P<id>\d+)/(?:\$(?P<lines>\d+(?:-\d+)?))?' 103 + => 'HarbormasterBuildLogRenderController', 101 104 'download/(?P<id>\d+)/' => 'HarbormasterBuildLogDownloadController', 102 105 ), 103 106 ),
+562
src/applications/harbormaster/controller/HarbormasterBuildLogRenderController.php
··· 1 + <?php 2 + 3 + final class HarbormasterBuildLogRenderController 4 + extends HarbormasterController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + 9 + $id = $request->getURIData('id'); 10 + 11 + $log = id(new HarbormasterBuildLogQuery()) 12 + ->setViewer($viewer) 13 + ->withIDs(array($id)) 14 + ->executeOne(); 15 + if (!$log) { 16 + return new Aphront404Response(); 17 + } 18 + 19 + $log_size = $this->getTotalByteLength($log); 20 + 21 + $head_lines = $request->getInt('head'); 22 + if ($head_lines === null) { 23 + $head_lines = 8; 24 + } 25 + $head_lines = min($head_lines, 100); 26 + $head_lines = max($head_lines, 0); 27 + 28 + $tail_lines = $request->getInt('tail'); 29 + if ($tail_lines === null) { 30 + $tail_lines = 16; 31 + } 32 + $tail_lines = min($tail_lines, 100); 33 + $tail_lines = max($tail_lines, 0); 34 + 35 + $head_offset = $request->getInt('headOffset'); 36 + if ($head_offset === null) { 37 + $head_offset = 0; 38 + } 39 + 40 + $tail_offset = $request->getInt('tailOffset'); 41 + if ($tail_offset === null) { 42 + $tail_offset = $log_size; 43 + } 44 + 45 + // Figure out which ranges we're actually going to read. We'll read either 46 + // one range (either just at the head, or just at the tail) or two ranges 47 + // (one at the head and one at the tail). 48 + 49 + // This gets a little bit tricky because: the ranges may overlap; we just 50 + // want to do one big read if there is only a little bit of text left 51 + // between the ranges; we may not know where the tail range ends; and we 52 + // can only read forward from line map markers, not from any arbitrary 53 + // position in the file. 54 + 55 + $bytes_per_line = 140; 56 + $body_lines = 8; 57 + 58 + $views = array(); 59 + if ($head_lines > 0) { 60 + $views[] = array( 61 + 'offset' => $head_offset, 62 + 'lines' => $head_lines, 63 + 'direction' => 1, 64 + ); 65 + } 66 + 67 + if ($tail_lines > 0) { 68 + $views[] = array( 69 + 'offset' => $tail_offset, 70 + 'lines' => $tail_lines, 71 + 'direction' => -1, 72 + ); 73 + } 74 + 75 + $reads = $views; 76 + foreach ($reads as $key => $read) { 77 + $offset = $read['offset']; 78 + 79 + $lines = $read['lines']; 80 + 81 + $read_length = 0; 82 + $read_length += ($lines * $bytes_per_line); 83 + $read_length += ($body_lines * $bytes_per_line); 84 + 85 + $direction = $read['direction']; 86 + if ($direction < 0) { 87 + $offset -= $read_length; 88 + if ($offset < 0) { 89 + $offset = 0; 90 + $read_length = $log_size; 91 + } 92 + } 93 + 94 + $position = $log->getReadPosition($offset); 95 + list($position_offset, $position_line) = $position; 96 + $read_length += ($offset - $position_offset); 97 + 98 + $reads[$key]['fetchOffset'] = $position_offset; 99 + $reads[$key]['fetchLength'] = $read_length; 100 + $reads[$key]['fetchLine'] = $position_line; 101 + } 102 + 103 + $reads = $this->mergeOverlappingReads($reads); 104 + 105 + foreach ($reads as $key => $read) { 106 + $data = $log->loadData($read['fetchOffset'], $read['fetchLength']); 107 + 108 + $offset = $read['fetchOffset']; 109 + $line = $read['fetchLine']; 110 + $lines = $this->getLines($data); 111 + $line_data = array(); 112 + foreach ($lines as $line_text) { 113 + $length = strlen($line_text); 114 + $line_data[] = array( 115 + 'offset' => $offset, 116 + 'length' => $length, 117 + 'line' => $line, 118 + 'data' => $line_text, 119 + ); 120 + $line += 1; 121 + $offset += $length; 122 + } 123 + 124 + $reads[$key]['data'] = $data; 125 + $reads[$key]['lines'] = $line_data; 126 + } 127 + 128 + foreach ($views as $view_key => $view) { 129 + $anchor_byte = $view['offset']; 130 + 131 + $data_key = null; 132 + foreach ($reads as $read_key => $read) { 133 + $s = $read['fetchOffset']; 134 + $e = $s + $read['fetchLength']; 135 + 136 + if (($s <= $anchor_byte) && ($e >= $anchor_byte)) { 137 + $data_key = $read_key; 138 + break; 139 + } 140 + } 141 + 142 + if ($data_key === null) { 143 + throw new Exception( 144 + pht('Unable to find fetch!')); 145 + } 146 + 147 + $anchor_key = null; 148 + foreach ($reads[$data_key]['lines'] as $line_key => $line) { 149 + $s = $line['offset']; 150 + $e = $s + $line['length']; 151 + if (($s <= $anchor_byte) && ($e >= $anchor_byte)) { 152 + $anchor_key = $line_key; 153 + break; 154 + } 155 + } 156 + 157 + if ($anchor_key === null) { 158 + throw new Exception( 159 + pht( 160 + 'Unable to find lines.')); 161 + } 162 + 163 + if ($direction > 0) { 164 + $slice_offset = $line_key; 165 + } else { 166 + $slice_offset = max(0, $line_key - ($view['lines'] - 1)); 167 + } 168 + $slice_length = $view['lines']; 169 + 170 + $views[$view_key] += array( 171 + 'sliceKey' => $data_key, 172 + 'sliceOffset' => $slice_offset, 173 + 'sliceLength' => $slice_length, 174 + ); 175 + } 176 + 177 + foreach ($views as $view_key => $view) { 178 + $slice_key = $view['sliceKey']; 179 + $lines = array_slice( 180 + $reads[$slice_key]['lines'], 181 + $view['sliceOffset'], 182 + $view['sliceLength']); 183 + 184 + $data_offset = null; 185 + $data_length = null; 186 + foreach ($lines as $line) { 187 + if ($data_offset === null) { 188 + $data_offset = $line['offset']; 189 + } 190 + $data_length += $line['length']; 191 + } 192 + 193 + // If the view cursor starts in the middle of a line, we're going to 194 + // strip part of the line. 195 + $direction = $view['direction']; 196 + if ($direction > 0) { 197 + $view_offset = $view['offset']; 198 + $view_length = $data_length; 199 + if ($data_offset < $view_offset) { 200 + $trim = ($view_offset - $data_offset); 201 + $view_length -= $trim; 202 + } 203 + } else { 204 + $view_offset = $data_offset; 205 + $view_length = $data_length; 206 + if ($data_offset + $data_length > $view['offset']) { 207 + $view_length -= (($data_offset + $data_length) - $view['offset']); 208 + } 209 + } 210 + 211 + $views[$view_key] += array( 212 + 'viewOffset' => $view_offset, 213 + 'viewLength' => $view_length, 214 + ); 215 + } 216 + 217 + $views = $this->mergeOverlappingViews($views); 218 + 219 + foreach ($views as $view_key => $view) { 220 + $slice_key = $view['sliceKey']; 221 + $lines = array_slice( 222 + $reads[$slice_key]['lines'], 223 + $view['sliceOffset'], 224 + $view['sliceLength']); 225 + 226 + $view_offset = $view['viewOffset']; 227 + foreach ($lines as $line_key => $line) { 228 + $line_offset = $line['offset']; 229 + 230 + if ($line_offset >= $view_offset) { 231 + break; 232 + } 233 + 234 + $trim = ($view_offset - $line_offset); 235 + $line_data = substr($line['data'], $trim); 236 + if (!strlen($line_data)) { 237 + unset($lines[$line_key]); 238 + continue; 239 + } 240 + 241 + $lines[$line_key]['data'] = $line_data; 242 + $lines[$line_key]['length'] = strlen($line_data); 243 + $lines[$line_key]['offset'] += $trim; 244 + break; 245 + } 246 + 247 + $view_end = $view['viewOffset'] + $view['viewLength']; 248 + foreach ($lines as $line_key => $line) { 249 + $line_end = $line['offset'] + $line['length']; 250 + if ($line_end <= $view_end) { 251 + break; 252 + } 253 + 254 + $trim = ($line_end - $view_end); 255 + $line_data = substr($line['data'], -$trim); 256 + if (!strlen($line_data)) { 257 + unset($lines[$line_key]); 258 + continue; 259 + } 260 + 261 + $lines[$line_key]['data'] = $line_data; 262 + $lines[$line_key]['length'] = strlen($line_data); 263 + } 264 + 265 + $views[$view_key]['viewData'] = $lines; 266 + } 267 + 268 + $spacer = null; 269 + $render = array(); 270 + foreach ($views as $view) { 271 + if ($spacer) { 272 + $spacer['tail'] = $view['viewOffset']; 273 + $render[] = $spacer; 274 + } 275 + 276 + $render[] = $view; 277 + 278 + $spacer = array( 279 + 'spacer' => true, 280 + 'head' => ($view['viewOffset'] + $view['viewLength']), 281 + ); 282 + } 283 + 284 + $uri = $log->getURI(); 285 + $highlight_range = $request->getURIData('lines'); 286 + 287 + $rows = array(); 288 + foreach ($render as $range) { 289 + if (isset($range['spacer'])) { 290 + $rows[] = phutil_tag( 291 + 'tr', 292 + array(), 293 + array( 294 + phutil_tag( 295 + 'th', 296 + array(), 297 + null), 298 + phutil_tag( 299 + 'td', 300 + array(), 301 + array( 302 + javelin_tag( 303 + 'a', 304 + array( 305 + 'sigil' => 'harbormaster-log-expand', 306 + 'meta' => array( 307 + 'headOffset' => $range['head'], 308 + 'tailOffset' => $range['tail'], 309 + 'head' => 4, 310 + ), 311 + ), 312 + 'Show Up ^^^^'), 313 + '... '.($range['tail'] - $range['head']).' bytes ...', 314 + javelin_tag( 315 + 'a', 316 + array( 317 + 'sigil' => 'harbormaster-log-expand', 318 + 'meta' => array( 319 + 'headOffset' => $range['head'], 320 + 'tailOffset' => $range['tail'], 321 + 'tail' => 4, 322 + ), 323 + ), 324 + 'Show Down VVVV'), 325 + )), 326 + )); 327 + continue; 328 + } 329 + 330 + $lines = $range['viewData']; 331 + foreach ($lines as $line) { 332 + $display_line = ($line['line'] + 1); 333 + $display_text = ($line['data']); 334 + 335 + $display_line = phutil_tag( 336 + 'a', 337 + array( 338 + 'href' => $uri.'$'.$display_line, 339 + ), 340 + $display_line); 341 + 342 + $line_cell = phutil_tag('th', array(), $display_line); 343 + $text_cell = phutil_tag('td', array(), $display_text); 344 + 345 + $rows[] = phutil_tag( 346 + 'tr', 347 + array(), 348 + array( 349 + $line_cell, 350 + $text_cell, 351 + )); 352 + } 353 + } 354 + 355 + $table = phutil_tag( 356 + 'table', 357 + array( 358 + 'class' => 'harbormaster-log-table PhabricatorMonospaced', 359 + ), 360 + $rows); 361 + 362 + // When this is a normal AJAX request, return the rendered log fragment 363 + // in an AJAX payload. 364 + if ($request->isAjax()) { 365 + return id(new AphrontAjaxResponse()) 366 + ->setContent( 367 + array( 368 + 'markup' => hsprintf('%s', $table), 369 + )); 370 + } 371 + 372 + // If the page is being accessed as a standalone page, present a 373 + // readable version of the fragment for debugging. 374 + 375 + require_celerity_resource('harbormaster-css'); 376 + 377 + $header = pht('Standalone Log Fragment'); 378 + 379 + $render_view = id(new PHUIObjectBoxView()) 380 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 381 + ->setHeaderText($header) 382 + ->appendChild($table); 383 + 384 + $page_view = id(new PHUITwoColumnView()) 385 + ->setFooter($render_view); 386 + 387 + $crumbs = $this->buildApplicationCrumbs() 388 + ->addTextCrumb(pht('Build Log %d', $log->getID()), $log->getURI()) 389 + ->addTextCrumb(pht('Fragment')) 390 + ->setBorder(true); 391 + 392 + return $this->newPage() 393 + ->setTitle( 394 + array( 395 + pht('Build Log %d', $log->getID()), 396 + pht('Standalone Fragment'), 397 + )) 398 + ->setCrumbs($crumbs) 399 + ->appendChild($page_view); 400 + } 401 + 402 + private function getTotalByteLength(HarbormasterBuildLog $log) { 403 + $total_bytes = $log->getByteLength(); 404 + if ($total_bytes) { 405 + return (int)$total_bytes; 406 + } 407 + 408 + // TODO: Remove this after enough time has passed for installs to run 409 + // log rebuilds or decide they don't care about older logs. 410 + 411 + // Older logs don't have this data denormalized onto the log record unless 412 + // an administrator has run `bin/harbormaster rebuild-log --all` or 413 + // similar. Try to figure it out by summing up the size of each chunk. 414 + 415 + // Note that the log may also be legitimately empty and have actual size 416 + // zero. 417 + $chunk = new HarbormasterBuildLogChunk(); 418 + $conn = $chunk->establishConnection('r'); 419 + 420 + $row = queryfx_one( 421 + $conn, 422 + 'SELECT SUM(size) total FROM %T WHERE logID = %d', 423 + $chunk->getTableName(), 424 + $log->getID()); 425 + 426 + return (int)$row['total']; 427 + } 428 + 429 + private function getLines($data) { 430 + $parts = preg_split("/(\r\n|\r|\n)/", $data, 0, PREG_SPLIT_DELIM_CAPTURE); 431 + 432 + if (last($parts) === '') { 433 + array_pop($parts); 434 + } 435 + 436 + $lines = array(); 437 + for ($ii = 0; $ii < count($parts); $ii += 2) { 438 + $line = $parts[$ii]; 439 + if (isset($parts[$ii + 1])) { 440 + $line .= $parts[$ii + 1]; 441 + } 442 + $lines[] = $line; 443 + } 444 + 445 + return $lines; 446 + } 447 + 448 + 449 + private function mergeOverlappingReads(array $reads) { 450 + // Find planned reads which will overlap and merge them into a single 451 + // larger read. 452 + 453 + $uk = array_keys($reads); 454 + $vk = array_keys($reads); 455 + 456 + foreach ($uk as $ukey) { 457 + foreach ($vk as $vkey) { 458 + // Don't merge a range into itself, even though they do technically 459 + // overlap. 460 + if ($ukey === $vkey) { 461 + continue; 462 + } 463 + 464 + $uread = idx($reads, $ukey); 465 + if ($uread === null) { 466 + continue; 467 + } 468 + 469 + $vread = idx($reads, $vkey); 470 + if ($vread === null) { 471 + continue; 472 + } 473 + 474 + $us = $uread['fetchOffset']; 475 + $ue = $us + $uread['fetchLength']; 476 + 477 + $vs = $vread['fetchOffset']; 478 + $ve = $vs + $vread['fetchLength']; 479 + 480 + if (($vs > $ue) || ($ve < $us)) { 481 + continue; 482 + } 483 + 484 + $min = min($us, $vs); 485 + $max = max($ue, $ve); 486 + 487 + $reads[$ukey]['fetchOffset'] = $min; 488 + $reads[$ukey]['fetchLength'] = ($max - $min); 489 + $reads[$ukey]['fetchLine'] = min( 490 + $uread['fetchLine'], 491 + $vread['fetchLine']); 492 + 493 + unset($reads[$vkey]); 494 + } 495 + } 496 + 497 + return $reads; 498 + } 499 + 500 + private function mergeOverlappingViews(array $views) { 501 + $uk = array_keys($views); 502 + $vk = array_keys($views); 503 + 504 + $body_lines = 8; 505 + $body_bytes = ($body_lines * 140); 506 + 507 + foreach ($uk as $ukey) { 508 + foreach ($vk as $vkey) { 509 + if ($ukey === $vkey) { 510 + continue; 511 + } 512 + 513 + $uview = idx($views, $ukey); 514 + if ($uview === null) { 515 + continue; 516 + } 517 + 518 + $vview = idx($views, $vkey); 519 + if ($vview === null) { 520 + continue; 521 + } 522 + 523 + // If these views don't use the same line data, don't try to 524 + // merge them. 525 + if ($uview['sliceKey'] != $vview['sliceKey']) { 526 + continue; 527 + } 528 + 529 + // If these views are overlapping or separated by only a few bytes, 530 + // merge them into a single view. 531 + $us = $uview['viewOffset']; 532 + $ue = $us + $uview['viewLength']; 533 + 534 + $vs = $vview['viewOffset']; 535 + $ve = $vs + $vview['viewLength']; 536 + 537 + $uss = $uview['sliceOffset']; 538 + $use = $uss + $uview['sliceLength']; 539 + 540 + $vss = $vview['sliceOffset']; 541 + $vse = $vss + $vview['sliceLength']; 542 + 543 + if ($ue <= $vs) { 544 + if (($ue + $body_bytes) >= $vs) { 545 + if (($use + $body_lines) >= $vss) { 546 + $views[$ukey] = array( 547 + 'sliceLength' => ($vse - $uss), 548 + 'viewLength' => ($ve - $us), 549 + ) + $views[$ukey]; 550 + 551 + unset($views[$vkey]); 552 + continue; 553 + } 554 + } 555 + } 556 + } 557 + } 558 + 559 + return $views; 560 + } 561 + 562 + }
+3 -3
src/applications/harbormaster/controller/HarbormasterBuildLogViewController.php
··· 4 4 extends HarbormasterController { 5 5 6 6 public function handleRequest(AphrontRequest $request) { 7 - $request = $this->getRequest(); 8 - $viewer = $request->getUser(); 7 + $viewer = $this->getViewer(); 9 8 10 9 $id = $request->getURIData('id'); 11 10 ··· 21 20 22 21 $log_view = id(new HarbormasterBuildLogView()) 23 22 ->setViewer($viewer) 24 - ->setBuildLog($log); 23 + ->setBuildLog($log) 24 + ->setHighlightedLineRange($request->getURIData('lines')); 25 25 26 26 $crumbs = $this->buildApplicationCrumbs() 27 27 ->addTextCrumb(pht('Build Logs'))
+33
src/applications/harbormaster/storage/build/HarbormasterBuildLog.php
··· 129 129 $this->getID()); 130 130 } 131 131 132 + public function loadData($offset, $length) { 133 + return substr($this->getLogText(), $offset, $length); 134 + } 135 + 136 + public function getReadPosition($read_offset) { 137 + $position = array(0, 0); 138 + 139 + $map = $this->getLineMap(); 140 + if (!$map) { 141 + throw new Exception(pht('No line map.')); 142 + } 143 + 144 + list($map) = $map; 145 + foreach ($map as $marker) { 146 + list($offset, $count) = $marker; 147 + if ($offset > $read_offset) { 148 + break; 149 + } 150 + $position = $marker; 151 + } 152 + 153 + return $position; 154 + } 155 + 132 156 public function getLogText() { 133 157 // TODO: Remove this method since it won't scale for big logs. 134 158 ··· 146 170 public function getURI() { 147 171 $id = $this->getID(); 148 172 return "/harbormaster/log/view/{$id}/"; 173 + } 174 + 175 + public function getRenderURI($lines) { 176 + if (strlen($lines)) { 177 + $lines = '$'.$lines; 178 + } 179 + 180 + $id = $this->getID(); 181 + return "/harbormaster/log/render/{$id}/{$lines}"; 149 182 } 150 183 151 184
+29 -1
src/applications/harbormaster/view/HarbormasterBuildLogView.php
··· 3 3 final class HarbormasterBuildLogView extends AphrontView { 4 4 5 5 private $log; 6 + private $highlightedLineRange; 6 7 7 8 public function setBuildLog(HarbormasterBuildLog $log) { 8 9 $this->log = $log; ··· 11 12 12 13 public function getBuildLog() { 13 14 return $this->log; 15 + } 16 + 17 + public function setHighlightedLineRange($range) { 18 + $this->highlightedLineRange = $range; 19 + return $this; 20 + } 21 + 22 + public function getHighlightedLineRange() { 23 + return $this->highlightedLineRange; 14 24 } 15 25 16 26 public function render() { ··· 34 44 35 45 $header->addActionLink($download_button); 36 46 47 + $content_id = celerity_generate_unique_node_id(); 48 + $content_div = javelin_tag( 49 + 'div', 50 + array( 51 + 'id' => $content_id, 52 + 'class' => 'harbormaster-log-view-loading', 53 + ), 54 + pht('Loading...')); 55 + 56 + require_celerity_resource('harbormaster-css'); 57 + 58 + Javelin::initBehavior( 59 + 'harbormaster-log', 60 + array( 61 + 'contentNodeID' => $content_id, 62 + 'renderURI' => $log->getRenderURI($this->getHighlightedLineRange()), 63 + )); 64 + 37 65 $box_view = id(new PHUIObjectBoxView()) 38 66 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 39 67 ->setHeader($header) 40 - ->appendChild('...'); 68 + ->appendChild($content_div); 41 69 42 70 return $box_view; 43 71 }
+38
webroot/rsrc/css/application/harbormaster/harbormaster.css
··· 30 30 text-overflow: ellipsis; 31 31 color: {$lightgreytext}; 32 32 } 33 + 34 + .harbormaster-log-view-loading { 35 + padding: 8px; 36 + text-align: center; 37 + color: {$lightgreytext}; 38 + } 39 + 40 + .harbormaster-log-table th { 41 + background-color: {$paste.highlight}; 42 + border-right: 1px solid {$paste.border}; 43 + 44 + -moz-user-select: -moz-none; 45 + -khtml-user-select: none; 46 + -webkit-user-select: none; 47 + -ms-user-select: none; 48 + user-select: none; 49 + } 50 + 51 + .harbormaster-log-table th a { 52 + display: block; 53 + color: {$darkbluetext}; 54 + text-align: right; 55 + padding: 2px 6px 1px 12px; 56 + } 57 + 58 + .harbormaster-log-table th a:hover { 59 + background: {$paste.border}; 60 + } 61 + 62 + .harbormaster-log-table td { 63 + white-space: pre-wrap; 64 + padding: 2px 8px 1px; 65 + width: 100%; 66 + } 67 + 68 + .harbormaster-log-table tr.harbormaster-log-highlighted td { 69 + background: {$paste.highlight}; 70 + }
+43
webroot/rsrc/js/application/harbormaster/behavior-harbormaster-log.js
··· 1 + /** 2 + * @provides javelin-behavior-harbormaster-log 3 + * @requires javelin-behavior 4 + */ 5 + 6 + JX.behavior('harbormaster-log', function(config) { 7 + var contentNode = JX.$(config.contentNodeID); 8 + 9 + JX.DOM.listen(contentNode, 'click', 'harbormaster-log-expand', function(e) { 10 + if (!e.isNormalClick()) { 11 + return; 12 + } 13 + 14 + e.kill(); 15 + 16 + var row = e.getNode('tag:tr'); 17 + var data = e.getNodeData('harbormaster-log-expand'); 18 + 19 + var uri = new JX.URI(config.renderURI) 20 + .addQueryParams(data); 21 + 22 + var request = new JX.Request(uri, function(r) { 23 + var result = JX.$H(r.markup).getNode(); 24 + var rows = JX.DOM.scry(result, 'tr'); 25 + 26 + JX.DOM.replace(row, rows); 27 + }); 28 + 29 + request.send(); 30 + }); 31 + 32 + function onresponse(r) { 33 + JX.DOM.alterClass(contentNode, 'harbormaster-log-view-loading', false); 34 + 35 + JX.DOM.setContent(contentNode, JX.$H(r.markup)); 36 + } 37 + 38 + var uri = new JX.URI(config.renderURI); 39 + 40 + new JX.Request(uri, onresponse) 41 + .send(); 42 + 43 + });