@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<?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}