@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
at recaptime-dev/main 711 lines 19 kB view raw
1<?php 2 3final class HarbormasterBuildViewController 4 extends HarbormasterController { 5 6 public function shouldAllowPublic() { 7 return true; 8 } 9 10 public function handleRequest(AphrontRequest $request) { 11 $request = $this->getRequest(); 12 $viewer = $request->getUser(); 13 14 $id = $request->getURIData('id'); 15 16 $build = id(new HarbormasterBuildQuery()) 17 ->setViewer($viewer) 18 ->withIDs(array($id)) 19 ->executeOne(); 20 if (!$build) { 21 return new Aphront404Response(); 22 } 23 24 require_celerity_resource('harbormaster-css'); 25 26 $title = pht('Build %d', $id); 27 $warnings = array(); 28 29 $page_header = id(new PHUIHeaderView()) 30 ->setHeader($title) 31 ->setUser($viewer) 32 ->setPolicyObject($build) 33 ->setHeaderIcon('fa-cubes'); 34 35 $status = $build->getBuildPendingStatusObject(); 36 37 $status_icon = $status->getIconIcon(); 38 $status_color = $status->getIconColor(); 39 $status_name = $status->getName(); 40 41 $page_header->setStatus($status_icon, $status_color, $status_name); 42 43 $max_generation = (int)$build->getBuildGeneration(); 44 if ($max_generation === 0) { 45 $min_generation = 0; 46 } else { 47 $min_generation = 1; 48 } 49 50 if ($build->isRestarting()) { 51 $max_generation = $max_generation + 1; 52 } 53 54 $generation = $request->getURIData('generation'); 55 if ($generation === null) { 56 $generation = $max_generation; 57 } else { 58 $generation = (int)$generation; 59 } 60 61 if ($generation < $min_generation || $generation > $max_generation) { 62 return new Aphront404Response(); 63 } 64 65 if ($generation < $max_generation) { 66 $warnings[] = pht( 67 'You are viewing an older run of this build. %s', 68 phutil_tag( 69 'a', 70 array( 71 'href' => $build->getURI(), 72 ), 73 pht('View Current Build'))); 74 } 75 76 $curtain = $this->buildCurtainView($build); 77 $properties = $this->buildPropertyList($build); 78 $history = $this->buildHistoryTable( 79 $build, 80 $generation, 81 $min_generation, 82 $max_generation); 83 84 $crumbs = $this->buildApplicationCrumbs(); 85 $this->addBuildableCrumb($crumbs, $build->getBuildable()); 86 $crumbs->addTextCrumb($title); 87 $crumbs->setBorder(true); 88 89 $build_targets = id(new HarbormasterBuildTargetQuery()) 90 ->setViewer($viewer) 91 ->needBuildSteps(true) 92 ->withBuildPHIDs(array($build->getPHID())) 93 ->withBuildGenerations(array($generation)) 94 ->execute(); 95 96 if ($build_targets) { 97 $messages = id(new HarbormasterBuildMessageQuery()) 98 ->setViewer($viewer) 99 ->withReceiverPHIDs(mpull($build_targets, 'getPHID')) 100 ->execute(); 101 $messages = mgroup($messages, 'getReceiverPHID'); 102 } else { 103 $messages = array(); 104 } 105 106 if ($build_targets) { 107 $artifacts = id(new HarbormasterBuildArtifactQuery()) 108 ->setViewer($viewer) 109 ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) 110 ->execute(); 111 $artifacts = msort($artifacts, 'getArtifactKey'); 112 $artifacts = mgroup($artifacts, 'getBuildTargetPHID'); 113 } else { 114 $artifacts = array(); 115 } 116 117 118 $targets = array(); 119 foreach ($build_targets as $build_target) { 120 $header = id(new PHUIHeaderView()) 121 ->setHeader($build_target->getName()) 122 ->setUser($viewer) 123 ->setHeaderIcon('fa-bullseye'); 124 125 $target_box = id(new PHUIObjectBoxView()) 126 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 127 ->setHeader($header); 128 129 $tab_group = new PHUITabGroupView(); 130 $target_box->addTabGroup($tab_group); 131 132 $property_list = new PHUIPropertyListView(); 133 134 $target_artifacts = idx($artifacts, $build_target->getPHID(), array()); 135 136 $links = array(); 137 $type_uri = HarbormasterURIArtifact::ARTIFACTCONST; 138 foreach ($target_artifacts as $artifact) { 139 if ($artifact->getArtifactType() == $type_uri) { 140 $impl = $artifact->getArtifactImplementation(); 141 if ($impl->isExternalLink()) { 142 $links[] = $impl->renderLink(); 143 } 144 } 145 } 146 147 if ($links) { 148 $links = phutil_implode_html(phutil_tag('br'), $links); 149 $property_list->addProperty( 150 pht('External Link'), 151 $links); 152 } 153 154 $status_view = new PHUIStatusListView(); 155 $item = new PHUIStatusItemView(); 156 157 $status = $build_target->getTargetStatus(); 158 $status_name = 159 HarbormasterBuildTarget::getBuildTargetStatusName($status); 160 $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); 161 $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); 162 163 $item->setTarget($status_name); 164 $item->setIcon($icon, $color); 165 $status_view->addItem($item); 166 167 $when = array(); 168 $started = $build_target->getDateStarted(); 169 $now = PhabricatorTime::getNow(); 170 if ($started) { 171 $ended = $build_target->getDateCompleted(); 172 if ($ended) { 173 $when[] = pht( 174 'Completed at %s', 175 phabricator_datetime($ended, $viewer)); 176 177 $duration = ($ended - $started); 178 if ($duration) { 179 $when[] = pht( 180 'Built for %s', 181 phutil_format_relative_time_detailed($duration)); 182 } else { 183 $when[] = pht('Built instantly'); 184 } 185 } else { 186 $when[] = pht( 187 'Started at %s', 188 phabricator_datetime($started, $viewer)); 189 $duration = ($now - $started); 190 if ($duration) { 191 $when[] = pht( 192 'Running for %s', 193 phutil_format_relative_time_detailed($duration)); 194 } 195 } 196 } else { 197 $created = $build_target->getDateCreated(); 198 $when[] = pht( 199 'Queued at %s', 200 phabricator_datetime($started, $viewer)); 201 $duration = ($now - $created); 202 if ($duration) { 203 $when[] = pht( 204 'Waiting for %s', 205 phutil_format_relative_time_detailed($duration)); 206 } 207 } 208 209 $property_list->addProperty( 210 pht('When'), 211 phutil_implode_html(" \xC2\xB7 ", $when)); 212 213 $property_list->addProperty(pht('Status'), $status_view); 214 215 $tab_group->addTab( 216 id(new PHUITabView()) 217 ->setName(pht('Overview')) 218 ->setKey('overview') 219 ->appendChild($property_list)); 220 221 $step = $build_target->getBuildStep(); 222 223 if ($step) { 224 $description = $step->getDescription(); 225 if ($description) { 226 $description = new PHUIRemarkupView($viewer, $description); 227 $property_list->addSectionHeader( 228 pht('Description'), PHUIPropertyListView::ICON_SUMMARY); 229 $property_list->addTextContent($description); 230 } 231 } else { 232 $target_box->setFormErrors( 233 array( 234 pht( 235 'This build step has since been deleted on the build plan. '. 236 'Some information may be omitted.'), 237 )); 238 } 239 240 $details = $build_target->getDetails(); 241 $property_list = new PHUIPropertyListView(); 242 foreach ($details as $key => $value) { 243 $property_list->addProperty($key, $value); 244 } 245 $tab_group->addTab( 246 id(new PHUITabView()) 247 ->setName(pht('Configuration')) 248 ->setKey('configuration') 249 ->appendChild($property_list)); 250 251 $variables = $build_target->getVariables(); 252 $variables_tab = $this->buildProperties($variables); 253 $tab_group->addTab( 254 id(new PHUITabView()) 255 ->setName(pht('Variables')) 256 ->setKey('variables') 257 ->appendChild($variables_tab)); 258 259 $artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts); 260 $tab_group->addTab( 261 id(new PHUITabView()) 262 ->setName(pht('Artifacts')) 263 ->setKey('artifacts') 264 ->appendChild($artifacts_tab)); 265 266 $build_messages = idx($messages, $build_target->getPHID(), array()); 267 $messages_tab = $this->buildMessages($build_messages); 268 $tab_group->addTab( 269 id(new PHUITabView()) 270 ->setName(pht('Messages')) 271 ->setKey('messages') 272 ->appendChild($messages_tab)); 273 274 $property_list = new PHUIPropertyListView(); 275 $property_list->addProperty( 276 pht('Build Target ID'), 277 $build_target->getID()); 278 $property_list->addProperty( 279 pht('Build Target PHID'), 280 $build_target->getPHID()); 281 282 $tab_group->addTab( 283 id(new PHUITabView()) 284 ->setName(pht('Metadata')) 285 ->setKey('metadata') 286 ->appendChild($property_list)); 287 288 $targets[] = $target_box; 289 290 $targets[] = $this->buildLog($build, $build_target, $generation); 291 } 292 293 $timeline = $this->buildTransactionTimeline( 294 $build, 295 new HarbormasterBuildTransactionQuery()); 296 $timeline->setShouldTerminate(true); 297 298 if ($warnings) { 299 $warnings = id(new PHUIInfoView()) 300 ->setErrors($warnings) 301 ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 302 } else { 303 $warnings = null; 304 } 305 306 $view = id(new PHUITwoColumnView()) 307 ->setHeader($page_header) 308 ->setCurtain($curtain) 309 ->setMainColumn( 310 array( 311 $warnings, 312 $properties, 313 $history, 314 $targets, 315 $timeline, 316 )); 317 318 return $this->newPage() 319 ->setTitle($title) 320 ->setCrumbs($crumbs) 321 ->appendChild($view); 322 323 } 324 325 private function buildArtifacts( 326 HarbormasterBuildTarget $build_target, 327 array $artifacts) { 328 $viewer = $this->getViewer(); 329 330 $rows = array(); 331 foreach ($artifacts as $artifact) { 332 $impl = $artifact->getArtifactImplementation(); 333 334 if ($impl) { 335 $summary = $impl->renderArtifactSummary($viewer); 336 $type_name = $impl->getArtifactTypeName(); 337 } else { 338 $summary = pht('<Unknown Artifact Type>'); 339 $type_name = $artifact->getType(); 340 } 341 342 $rows[] = array( 343 $artifact->getArtifactKey(), 344 $type_name, 345 $summary, 346 ); 347 } 348 349 $table = id(new AphrontTableView($rows)) 350 ->setNoDataString(pht('This target has no associated artifacts.')) 351 ->setHeaders( 352 array( 353 pht('Key'), 354 pht('Type'), 355 pht('Summary'), 356 )) 357 ->setColumnClasses( 358 array( 359 'pri', 360 '', 361 'wide', 362 )); 363 364 return $table; 365 } 366 367 private function buildLog( 368 HarbormasterBuild $build, 369 HarbormasterBuildTarget $build_target, 370 $generation) { 371 372 $request = $this->getRequest(); 373 $viewer = $request->getUser(); 374 $limit = $request->getInt('l', 25); 375 376 $logs = id(new HarbormasterBuildLogQuery()) 377 ->setViewer($viewer) 378 ->withBuildTargetPHIDs(array($build_target->getPHID())) 379 ->execute(); 380 381 $empty_logs = array(); 382 383 $log_boxes = array(); 384 foreach ($logs as $log) { 385 $start = 1; 386 $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); 387 if ($limit !== 0) { 388 $start = count($lines) - $limit; 389 if ($start >= 1) { 390 $lines = array_slice($lines, -$limit, $limit); 391 } else { 392 $start = 1; 393 } 394 } 395 396 $id = null; 397 $is_empty = false; 398 if (count($lines) === 1 && trim($lines[0]) === '') { 399 // Prevent Harbormaster from showing empty build logs. 400 $id = celerity_generate_unique_node_id(); 401 $empty_logs[] = $id; 402 $is_empty = true; 403 } 404 405 $log_view = new ShellLogView(); 406 $log_view->setLines($lines); 407 $log_view->setStart($start); 408 409 $subheader = $this->createLogHeader($build, $log, $limit, $generation); 410 411 $prototype_view = id(new PHUIButtonView()) 412 ->setTag('a') 413 ->setHref($log->getURI()) 414 ->setIcon('fa-file-text-o') 415 ->setText(pht('New View (Prototype)')); 416 417 $header = id(new PHUIHeaderView()) 418 ->setHeader(pht( 419 'Build Log %d (%s - %s)', 420 $log->getID(), 421 $log->getLogSource(), 422 $log->getLogType())) 423 ->addActionLink($prototype_view) 424 ->setSubheader($subheader) 425 ->setUser($viewer); 426 427 $log_box = id(new PHUIObjectBoxView()) 428 ->setHeader($header) 429 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 430 ->setForm($log_view); 431 432 if ($is_empty) { 433 $log_box = phutil_tag( 434 'div', 435 array( 436 'style' => 'display: none', 437 'id' => $id, 438 ), 439 $log_box); 440 } 441 442 $log_boxes[] = $log_box; 443 } 444 445 if ($empty_logs) { 446 $hide_id = celerity_generate_unique_node_id(); 447 448 Javelin::initBehavior('phabricator-reveal-content'); 449 450 $expand = phutil_tag( 451 'div', 452 array( 453 'id' => $hide_id, 454 'class' => 'harbormaster-empty-logs-are-hidden', 455 ), 456 array( 457 pht( 458 '%s empty logs are hidden.', 459 phutil_count($empty_logs)), 460 ' ', 461 javelin_tag( 462 'a', 463 array( 464 'href' => '#', 465 'sigil' => 'reveal-content', 466 'meta' => array( 467 'showIDs' => $empty_logs, 468 'hideIDs' => array($hide_id), 469 ), 470 ), 471 pht('Show all logs.')), 472 )); 473 474 array_unshift($log_boxes, $expand); 475 } 476 477 return $log_boxes; 478 } 479 480 private function createLogHeader($build, $log, $limit, $generation) { 481 $options = array( 482 array( 483 'n' => 25, 484 ), 485 array( 486 'n' => 50, 487 ), 488 array( 489 'n' => 100, 490 ), 491 array( 492 'n' => 0, 493 'label' => pht('Unlimited'), 494 ), 495 ); 496 497 $base_uri = new PhutilURI($build->getURI().$generation.'/'); 498 499 $links = array(); 500 foreach ($options as $option) { 501 $n = $option['n']; 502 $label = idx($option, 'label', $n); 503 504 $is_selected = ($limit == $n); 505 if ($is_selected) { 506 $links[] = phutil_tag( 507 'strong', 508 array(), 509 $label); 510 } else { 511 $links[] = phutil_tag( 512 'a', 513 array( 514 'href' => (string)$base_uri->alter('l', $n), 515 ), 516 $label); 517 } 518 } 519 520 return phutil_tag( 521 'span', 522 array(), 523 array( 524 phutil_implode_html(' - ', $links), 525 ' ', 526 pht('Lines'), 527 )); 528 } 529 530 private function buildCurtainView(HarbormasterBuild $build) { 531 $viewer = $this->getViewer(); 532 $id = $build->getID(); 533 534 $curtain = $this->newCurtainView($build); 535 536 $messages = array( 537 new HarbormasterBuildMessageRestartTransaction(), 538 new HarbormasterBuildMessagePauseTransaction(), 539 new HarbormasterBuildMessageResumeTransaction(), 540 new HarbormasterBuildMessageAbortTransaction(), 541 ); 542 543 foreach ($messages as $message) { 544 $can_send = $message->canSendMessage($viewer, $build); 545 546 $message_uri = urisprintf( 547 '/build/%s/%d/', 548 $message->getHarbormasterBuildMessageType(), 549 $id); 550 $message_uri = $this->getApplicationURI($message_uri); 551 552 $action = id(new PhabricatorActionView()) 553 ->setName($message->getHarbormasterBuildMessageName()) 554 ->setIcon($message->getIcon()) 555 ->setHref($message_uri) 556 ->setDisabled(!$can_send) 557 ->setWorkflow(true); 558 559 $curtain->addAction($action); 560 } 561 562 return $curtain; 563 } 564 565 private function buildPropertyList(HarbormasterBuild $build) { 566 $viewer = $this->getViewer(); 567 568 $properties = id(new PHUIPropertyListView()) 569 ->setUser($viewer); 570 571 $handles = id(new PhabricatorHandleQuery()) 572 ->setViewer($viewer) 573 ->withPHIDs(array( 574 $build->getBuildablePHID(), 575 $build->getBuildPlanPHID(), 576 )) 577 ->execute(); 578 579 $properties->addProperty( 580 pht('Buildable'), 581 $handles[$build->getBuildablePHID()]->renderLink()); 582 583 $properties->addProperty( 584 pht('Build Plan'), 585 $handles[$build->getBuildPlanPHID()]->renderLink()); 586 587 return id(new PHUIObjectBoxView()) 588 ->setHeaderText(pht('Properties')) 589 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 590 ->appendChild($properties); 591 592 } 593 594 private function buildHistoryTable( 595 HarbormasterBuild $build, 596 $generation, 597 $min_generation, 598 $max_generation) { 599 600 if ($max_generation === $min_generation) { 601 return null; 602 } 603 604 $viewer = $this->getViewer(); 605 606 $uri = $build->getURI(); 607 608 $rows = array(); 609 $rowc = array(); 610 for ($ii = $max_generation; $ii >= $min_generation; $ii--) { 611 if ($generation == $ii) { 612 $rowc[] = 'highlighted'; 613 } else { 614 $rowc[] = null; 615 } 616 617 $rows[] = array( 618 phutil_tag( 619 'a', 620 array( 621 'href' => $uri.$ii.'/', 622 ), 623 pht('Run %d', $ii)), 624 ); 625 } 626 627 $table = id(new AphrontTableView($rows)) 628 ->setColumnClasses( 629 array( 630 'pri wide', 631 )) 632 ->setRowClasses($rowc); 633 634 return id(new PHUIObjectBoxView()) 635 ->setHeaderText(pht('History')) 636 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 637 ->setTable($table); 638 } 639 640 private function buildMessages(array $messages) { 641 $viewer = $this->getRequest()->getUser(); 642 643 if ($messages) { 644 $handles = id(new PhabricatorHandleQuery()) 645 ->setViewer($viewer) 646 ->withPHIDs(mpull($messages, 'getAuthorPHID')) 647 ->execute(); 648 } else { 649 $handles = array(); 650 } 651 652 $rows = array(); 653 foreach ($messages as $message) { 654 $rows[] = array( 655 $message->getID(), 656 $handles[$message->getAuthorPHID()]->renderLink(), 657 $message->getType(), 658 $message->getIsConsumed() ? pht('Consumed') : null, 659 phabricator_datetime($message->getDateCreated(), $viewer), 660 ); 661 } 662 663 $table = new AphrontTableView($rows); 664 $table->setNoDataString(pht('No messages for this build target.')); 665 $table->setHeaders( 666 array( 667 pht('ID'), 668 pht('From'), 669 pht('Type'), 670 pht('Consumed'), 671 pht('Received'), 672 )); 673 $table->setColumnClasses( 674 array( 675 '', 676 '', 677 'wide', 678 '', 679 'date', 680 )); 681 682 return $table; 683 } 684 685 private function buildProperties(array $properties) { 686 ksort($properties); 687 688 $rows = array(); 689 foreach ($properties as $key => $value) { 690 $rows[] = array( 691 $key, 692 $value, 693 ); 694 } 695 696 $table = id(new AphrontTableView($rows)) 697 ->setHeaders( 698 array( 699 pht('Key'), 700 pht('Value'), 701 )) 702 ->setColumnClasses( 703 array( 704 'pri right', 705 'wide', 706 )); 707 708 return $table; 709 } 710 711}