@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 617 lines 17 kB view raw
1<?php 2 3final class HarbormasterPlanViewController extends HarbormasterPlanController { 4 5 public function shouldAllowPublic() { 6 return true; 7 } 8 9 public function handleRequest(AphrontRequest $request) { 10 $viewer = $this->getViewer(); 11 $id = $request->getURIData('id'); 12 13 $plan = id(new HarbormasterBuildPlanQuery()) 14 ->setViewer($viewer) 15 ->withIDs(array($id)) 16 ->executeOne(); 17 if (!$plan) { 18 return new Aphront404Response(); 19 } 20 21 $title = $plan->getName(); 22 23 $header = id(new PHUIHeaderView()) 24 ->setHeader($plan->getName()) 25 ->setUser($viewer) 26 ->setPolicyObject($plan) 27 ->setHeaderIcon('fa-ship'); 28 29 $curtain = $this->buildCurtainView($plan); 30 31 $crumbs = $this->buildApplicationCrumbs() 32 ->addTextCrumb($plan->getObjectName()) 33 ->setBorder(true); 34 35 list($step_list, $has_any_conflicts, $would_deadlock, $steps) = 36 $this->buildStepList($plan); 37 38 $error = null; 39 if (!$steps) { 40 $error = pht( 41 'This build plan does not have any build steps yet, so it will '. 42 'not do anything when run.'); 43 } else if ($would_deadlock) { 44 $error = pht( 45 'This build plan will deadlock when executed, due to circular '. 46 'dependencies present in the build plan. Examine the step list '. 47 'and resolve the deadlock.'); 48 } else if ($has_any_conflicts) { 49 // A deadlocking build will also cause all the artifacts to be 50 // invalid, so we just skip showing this message if that's the 51 // case. 52 $error = pht( 53 'This build plan has conflicts in one or more build steps. '. 54 'Examine the step list and resolve the listed errors.'); 55 } 56 57 if ($error) { 58 $error = id(new PHUIInfoView()) 59 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 60 ->appendChild($error); 61 } 62 63 $builds_view = $this->newBuildsView($plan); 64 $options_view = $this->newOptionsView($plan); 65 $rules_view = $this->newRulesView($plan); 66 67 $timeline = $this->buildTransactionTimeline( 68 $plan, 69 new HarbormasterBuildPlanTransactionQuery()); 70 $timeline->setShouldTerminate(true); 71 72 $view = id(new PHUITwoColumnView()) 73 ->setHeader($header) 74 ->setCurtain($curtain) 75 ->setMainColumn( 76 array( 77 $error, 78 $step_list, 79 $options_view, 80 $rules_view, 81 $builds_view, 82 $timeline, 83 )); 84 85 return $this->newPage() 86 ->setTitle($title) 87 ->setCrumbs($crumbs) 88 ->setPageObjectPHIDs(array($plan->getPHID())) 89 ->appendChild($view); 90 } 91 92 private function buildStepList(HarbormasterBuildPlan $plan) { 93 $viewer = $this->getViewer(); 94 95 $run_order = HarbormasterBuildGraph::determineDependencyExecution($plan); 96 97 $steps = id(new HarbormasterBuildStepQuery()) 98 ->setViewer($viewer) 99 ->withBuildPlanPHIDs(array($plan->getPHID())) 100 ->execute(); 101 $steps = mpull($steps, null, 'getPHID'); 102 103 $can_edit = PhabricatorPolicyFilter::hasCapability( 104 $viewer, 105 $plan, 106 PhabricatorPolicyCapability::CAN_EDIT); 107 108 $step_list = id(new PHUIObjectItemListView()) 109 ->setUser($viewer) 110 ->setNoDataString( 111 pht('This build plan does not have any build steps yet.')); 112 113 $i = 1; 114 $last_depth = 0; 115 $has_any_conflicts = false; 116 $is_deadlocking = false; 117 foreach ($run_order as $run_ref) { 118 $step = $steps[$run_ref['node']->getPHID()]; 119 $depth = $run_ref['depth'] + 1; 120 if ($last_depth !== $depth) { 121 $last_depth = $depth; 122 $i = 1; 123 } else { 124 $i++; 125 } 126 127 $step_id = $step->getID(); 128 $view_uri = $this->getApplicationURI("step/view/{$step_id}/"); 129 130 $item = id(new PHUIObjectItemView()) 131 ->setObjectName(pht('Step %d.%d', $depth, $i)) 132 ->setHeader($step->getName()) 133 ->setHref($view_uri); 134 135 $step_list->addItem($item); 136 137 $implementation = null; 138 try { 139 $implementation = $step->getStepImplementation(); 140 } catch (Exception $ex) { 141 // We can't initialize the implementation. This might be because 142 // it's been renamed or no longer exists. 143 $item 144 ->setStatusIcon('fa-warning red') 145 ->addAttribute(pht( 146 'This step has an invalid implementation (%s).', 147 $step->getClassName())); 148 continue; 149 } 150 151 $item->addAttribute($implementation->getDescription()); 152 $item->setHref($view_uri); 153 154 $depends = $step->getStepImplementation()->getDependencies($step); 155 $inputs = $step->getStepImplementation()->getArtifactInputs(); 156 $outputs = $step->getStepImplementation()->getArtifactOutputs(); 157 158 $has_conflicts = false; 159 if ($depends || $inputs || $outputs) { 160 $available_artifacts = 161 HarbormasterBuildStepImplementation::getAvailableArtifacts( 162 $plan, 163 $step, 164 null); 165 $available_artifacts = ipull($available_artifacts, 'type'); 166 167 list($depends_ui, $has_conflicts) = $this->buildDependsOnList( 168 $depends, 169 pht('Depends On'), 170 $steps); 171 172 list($inputs_ui, $has_conflicts) = $this->buildArtifactList( 173 $inputs, 174 'in', 175 pht('Input Artifacts'), 176 $available_artifacts); 177 178 list($outputs_ui) = $this->buildArtifactList( 179 $outputs, 180 'out', 181 pht('Output Artifacts'), 182 array()); 183 184 $item->appendChild( 185 phutil_tag( 186 'div', 187 array( 188 'class' => 'harbormaster-artifact-io', 189 ), 190 array( 191 $depends_ui, 192 $inputs_ui, 193 $outputs_ui, 194 ))); 195 } 196 197 if ($has_conflicts) { 198 $has_any_conflicts = true; 199 $item->setStatusIcon('fa-warning red'); 200 } 201 202 if ($run_ref['cycle']) { 203 $is_deadlocking = true; 204 } 205 206 if ($is_deadlocking) { 207 $item->setStatusIcon('fa-warning red'); 208 } 209 } 210 211 $step_list->setFlush(true); 212 213 $plan_id = $plan->getID(); 214 215 $header = id(new PHUIHeaderView()) 216 ->setHeader(pht('Build Steps')) 217 ->addActionLink( 218 id(new PHUIButtonView()) 219 ->setText(pht('Add Build Step')) 220 ->setHref($this->getApplicationURI("step/add/{$plan_id}/")) 221 ->setTag('a') 222 ->setIcon('fa-plus') 223 ->setDisabled(!$can_edit) 224 ->setWorkflow(!$can_edit)); 225 226 $step_box = id(new PHUIObjectBoxView()) 227 ->setHeader($header) 228 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 229 ->appendChild($step_list); 230 231 return array($step_box, $has_any_conflicts, $is_deadlocking, $steps); 232 } 233 234 private function buildCurtainView(HarbormasterBuildPlan $plan) { 235 $viewer = $this->getViewer(); 236 $id = $plan->getID(); 237 238 $curtain = $this->newCurtainView($plan); 239 240 $can_edit = PhabricatorPolicyFilter::hasCapability( 241 $viewer, 242 $plan, 243 PhabricatorPolicyCapability::CAN_EDIT); 244 245 $curtain->addAction( 246 id(new PhabricatorActionView()) 247 ->setName(pht('Edit Plan')) 248 ->setHref($this->getApplicationURI("plan/edit/{$id}/")) 249 ->setWorkflow(!$can_edit) 250 ->setDisabled(!$can_edit) 251 ->setIcon('fa-pencil')); 252 253 if ($plan->isDisabled()) { 254 $curtain->addAction( 255 id(new PhabricatorActionView()) 256 ->setName(pht('Enable Plan')) 257 ->setHref($this->getApplicationURI("plan/disable/{$id}/")) 258 ->setWorkflow(true) 259 ->setDisabled(!$can_edit) 260 ->setIcon('fa-check')); 261 } else { 262 $curtain->addAction( 263 id(new PhabricatorActionView()) 264 ->setName(pht('Disable Plan')) 265 ->setHref($this->getApplicationURI("plan/disable/{$id}/")) 266 ->setWorkflow(true) 267 ->setDisabled(!$can_edit) 268 ->setIcon('fa-ban')); 269 } 270 271 $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually()); 272 273 $curtain->addAction( 274 id(new PhabricatorActionView()) 275 ->setName(pht('Run Plan Manually')) 276 ->setHref($this->getApplicationURI("plan/run/{$id}/")) 277 ->setWorkflow(true) 278 ->setDisabled(!$can_run) 279 ->setIcon('fa-play-circle')); 280 281 return $curtain; 282 } 283 284 private function buildArtifactList( 285 array $artifacts, 286 $kind, 287 $name, 288 array $available_artifacts) { 289 $has_conflicts = false; 290 291 if (!$artifacts) { 292 return array(null, $has_conflicts); 293 } 294 295 $this->requireResource('harbormaster-css'); 296 297 $header = phutil_tag( 298 'div', 299 array( 300 'class' => 'harbormaster-artifact-summary-header', 301 ), 302 $name); 303 304 $is_input = ($kind == 'in'); 305 306 $list = new PHUIStatusListView(); 307 foreach ($artifacts as $artifact) { 308 $error = null; 309 310 $key = idx($artifact, 'key'); 311 if (!strlen($key)) { 312 $bound = phutil_tag('em', array(), pht('(null)')); 313 if ($is_input) { 314 // This is an unbound input. For now, all inputs are always required. 315 $icon = PHUIStatusItemView::ICON_WARNING; 316 $color = 'red'; 317 $icon_label = pht('Required Input'); 318 $has_conflicts = true; 319 $error = pht('This input is required, but not configured.'); 320 } else { 321 // This is an unnamed output. Outputs do not necessarily need to be 322 // named. 323 $icon = PHUIStatusItemView::ICON_OPEN; 324 $color = 'bluegrey'; 325 $icon_label = pht('Unused Output'); 326 } 327 } else { 328 $bound = phutil_tag('strong', array(), $key); 329 if ($is_input) { 330 if (isset($available_artifacts[$key])) { 331 if ($available_artifacts[$key] == idx($artifact, 'type')) { 332 $icon = PHUIStatusItemView::ICON_ACCEPT; 333 $color = 'green'; 334 $icon_label = pht('Valid Input'); 335 } else { 336 $icon = PHUIStatusItemView::ICON_WARNING; 337 $color = 'red'; 338 $icon_label = pht('Bad Input Type'); 339 $has_conflicts = true; 340 $error = pht( 341 'This input is bound to the wrong artifact type. It is bound '. 342 'to a "%s" artifact, but should be bound to a "%s" artifact.', 343 $available_artifacts[$key], 344 idx($artifact, 'type')); 345 } 346 } else { 347 $icon = PHUIStatusItemView::ICON_QUESTION; 348 $color = 'red'; 349 $icon_label = pht('Unknown Input'); 350 $has_conflicts = true; 351 $error = pht( 352 'This input is bound to an artifact ("%s") which does not exist '. 353 'at this stage in the build process.', 354 $key); 355 } 356 } else { 357 $icon = PHUIStatusItemView::ICON_DOWN; 358 $color = 'green'; 359 $icon_label = pht('Valid Output'); 360 } 361 } 362 363 if ($error) { 364 $note = array( 365 phutil_tag('strong', array(), pht('ERROR:')), 366 ' ', 367 $error, 368 ); 369 } else { 370 $note = $bound; 371 } 372 373 $list->addItem( 374 id(new PHUIStatusItemView()) 375 ->setIcon($icon, $color, $icon_label) 376 ->setTarget($artifact['name']) 377 ->setNote($note)); 378 } 379 380 $ui = array( 381 $header, 382 $list, 383 ); 384 385 return array($ui, $has_conflicts); 386 } 387 388 private function buildDependsOnList( 389 array $step_phids, 390 $name, 391 array $steps) { 392 $has_conflicts = false; 393 394 if (!$step_phids) { 395 return null; 396 } 397 398 $this->requireResource('harbormaster-css'); 399 400 $steps = mpull($steps, null, 'getPHID'); 401 402 $header = phutil_tag( 403 'div', 404 array( 405 'class' => 'harbormaster-artifact-summary-header', 406 ), 407 $name); 408 409 $list = new PHUIStatusListView(); 410 foreach ($step_phids as $step_phid) { 411 $error = null; 412 413 if (idx($steps, $step_phid) === null) { 414 $icon = PHUIStatusItemView::ICON_WARNING; 415 $color = 'red'; 416 $icon_label = pht('Missing Dependency'); 417 $has_conflicts = true; 418 $error = pht( 419 "This dependency specifies a build step which doesn't exist."); 420 } else { 421 $bound = phutil_tag( 422 'strong', 423 array(), 424 idx($steps, $step_phid)->getName()); 425 $icon = PHUIStatusItemView::ICON_ACCEPT; 426 $color = 'green'; 427 $icon_label = pht('Valid Input'); 428 } 429 430 if ($error) { 431 $note = array( 432 phutil_tag('strong', array(), pht('ERROR:')), 433 ' ', 434 $error, 435 ); 436 } else { 437 $note = $bound; 438 } 439 440 $list->addItem( 441 id(new PHUIStatusItemView()) 442 ->setIcon($icon, $color, $icon_label) 443 ->setTarget(pht('Build Step')) 444 ->setNote($note)); 445 } 446 447 $ui = array( 448 $header, 449 $list, 450 ); 451 452 return array($ui, $has_conflicts); 453 } 454 455 private function newBuildsView(HarbormasterBuildPlan $plan) { 456 $viewer = $this->getViewer(); 457 458 $limit = 10; 459 $builds = id(new HarbormasterBuildQuery()) 460 ->setViewer($viewer) 461 ->withBuildPlanPHIDs(array($plan->getPHID())) 462 ->setLimit($limit + 1) 463 ->execute(); 464 465 $more_results = (count($builds) > $limit); 466 $builds = array_slice($builds, 0, $limit); 467 468 $list = id(new HarbormasterBuildView()) 469 ->setViewer($viewer) 470 ->setBuilds($builds) 471 ->newObjectList(); 472 473 $list->setNoDataString(pht('No recent builds.')); 474 475 $more_href = new PhutilURI( 476 $this->getApplicationURI('/build/'), 477 array('plan' => $plan->getPHID())); 478 479 if ($more_results) { 480 $list->newTailButton() 481 ->setHref($more_href); 482 } 483 484 $more_link = id(new PHUIButtonView()) 485 ->setTag('a') 486 ->setIcon('fa-list-ul') 487 ->setText(pht('View All Builds')) 488 ->setHref($more_href); 489 490 $header = id(new PHUIHeaderView()) 491 ->setHeader(pht('Recent Builds')) 492 ->addActionLink($more_link); 493 494 return id(new PHUIObjectBoxView()) 495 ->setHeader($header) 496 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 497 ->appendChild($list); 498 } 499 500 private function newRulesView(HarbormasterBuildPlan $plan) { 501 $viewer = $this->getViewer(); 502 503 $limit = 10; 504 $rules = id(new HeraldRuleQuery()) 505 ->setViewer($viewer) 506 ->withDisabled(false) 507 ->withAffectedObjectPHIDs(array($plan->getPHID())) 508 ->needValidateAuthors(true) 509 ->setLimit($limit + 1) 510 ->execute(); 511 512 $more_results = (count($rules) > $limit); 513 $rules = array_slice($rules, 0, $limit); 514 515 $list = id(new HeraldRuleListView()) 516 ->setViewer($viewer) 517 ->setRules($rules) 518 ->newObjectList(); 519 520 $list->setNoDataString(pht('No active Herald rules trigger this build.')); 521 522 $more_href = new PhutilURI( 523 '/herald/', 524 array('affectedPHID' => $plan->getPHID())); 525 526 if ($more_results) { 527 $list->newTailButton() 528 ->setHref($more_href); 529 } 530 531 $more_link = id(new PHUIButtonView()) 532 ->setTag('a') 533 ->setIcon('fa-list-ul') 534 ->setText(pht('View All Rules')) 535 ->setHref($more_href); 536 537 $header = id(new PHUIHeaderView()) 538 ->setHeader(pht('Run By Herald Rules')) 539 ->addActionLink($more_link); 540 541 return id(new PHUIObjectBoxView()) 542 ->setHeader($header) 543 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 544 ->appendChild($list); 545 } 546 547 private function newOptionsView(HarbormasterBuildPlan $plan) { 548 $viewer = $this->getViewer(); 549 550 $can_edit = PhabricatorPolicyFilter::hasCapability( 551 $viewer, 552 $plan, 553 PhabricatorPolicyCapability::CAN_EDIT); 554 555 $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); 556 557 $rows = array(); 558 foreach ($behaviors as $behavior) { 559 $option = $behavior->getPlanOption($plan); 560 561 $icon = $option->getIcon(); 562 $icon = id(new PHUIIconView())->setIcon($icon); 563 564 $edit_uri = new PhutilURI( 565 $this->getApplicationURI( 566 urisprintf( 567 'plan/behavior/%d/%s/', 568 $plan->getID(), 569 $behavior->getKey()))); 570 571 $edit_button = id(new PHUIButtonView()) 572 ->setTag('a') 573 ->setColor(PHUIButtonView::GREY) 574 ->setSize(PHUIButtonView::SMALL) 575 ->setDisabled(!$can_edit) 576 ->setWorkflow(true) 577 ->setText(pht('Edit')) 578 ->setHref($edit_uri); 579 580 $rows[] = array( 581 $icon, 582 $behavior->getName(), 583 $option->getName(), 584 $option->getDescription(), 585 $edit_button, 586 ); 587 } 588 589 $table = id(new AphrontTableView($rows)) 590 ->setHeaders( 591 array( 592 null, 593 pht('Name'), 594 pht('Behavior'), 595 pht('Details'), 596 null, 597 )) 598 ->setColumnClasses( 599 array( 600 null, 601 'pri', 602 null, 603 'wide', 604 null, 605 )); 606 607 608 $header = id(new PHUIHeaderView()) 609 ->setHeader(pht('Plan Behaviors')); 610 611 return id(new PHUIObjectBoxView()) 612 ->setHeader($header) 613 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 614 ->setTable($table); 615 } 616 617}