@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 upstream/main 1183 lines 35 kB view raw
1<?php 2 3final class DiffusionCommitController extends DiffusionController { 4 5 const CHANGES_LIMIT = 100; 6 7 private $commitParents; 8 private $commitRefs; 9 private $commitMerges; 10 private $commitErrors; 11 private $commitExists; 12 13 public function shouldAllowPublic() { 14 return true; 15 } 16 17 public function handleRequest(AphrontRequest $request) { 18 $response = $this->loadDiffusionContext(); 19 if ($response) { 20 return $response; 21 } 22 23 $drequest = $this->getDiffusionRequest(); 24 $viewer = $request->getUser(); 25 $repository = $drequest->getRepository(); 26 $commit_identifier = $drequest->getCommit(); 27 28 // If this page is being accessed via "/source/xyz/commit/...", redirect 29 // to the canonical URI. 30 $has_callsign = 31 phutil_nonempty_string($request->getURIData('repositoryCallsign')); 32 $has_id = phutil_nonempty_string($request->getURIData('repositoryID')); 33 if (!$has_callsign && !$has_id) { 34 $canonical_uri = $repository->getCommitURI($commit_identifier); 35 return id(new AphrontRedirectResponse()) 36 ->setURI($canonical_uri); 37 } 38 39 if ($request->getStr('diff')) { 40 return $this->buildRawDiffResponse($drequest); 41 } 42 43 $commits = id(new DiffusionCommitQuery()) 44 ->setViewer($viewer) 45 ->withRepository($repository) 46 ->withIdentifiers(array($commit_identifier)) 47 ->needCommitData(true) 48 ->needAuditRequests(true) 49 ->needAuditAuthority(array($viewer)) 50 ->setLimit(100) 51 ->needIdentities(true) 52 ->execute(); 53 54 $multiple_results = count($commits) > 1; 55 56 $crumbs = $this->buildCrumbs(array( 57 'commit' => !$multiple_results, 58 )); 59 $crumbs->setBorder(true); 60 61 if (!$commits) { 62 if (!$this->getCommitExists()) { 63 return new Aphront404Response(); 64 } 65 66 $error = id(new PHUIInfoView()) 67 ->setTitle(pht('Commit Still Parsing')) 68 ->appendChild( 69 pht( 70 'Failed to load the commit because the commit has not been '. 71 'parsed yet.')); 72 73 $title = pht('Commit Still Parsing'); 74 75 return $this->newPage() 76 ->setTitle($title) 77 ->setCrumbs($crumbs) 78 ->appendChild($error); 79 } else if ($multiple_results) { 80 81 $warning_message = 82 pht( 83 'The identifier %s is ambiguous and matches more than one commit.', 84 phutil_tag( 85 'strong', 86 array(), 87 $commit_identifier)); 88 89 $error = id(new PHUIInfoView()) 90 ->setTitle(pht('Ambiguous Commit')) 91 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 92 ->appendChild($warning_message); 93 94 $list = id(new DiffusionCommitGraphView()) 95 ->setViewer($viewer) 96 ->setCommits($commits); 97 98 $crumbs->addTextCrumb(pht('Ambiguous Commit')); 99 100 $matched_commits = id(new PHUITwoColumnView()) 101 ->setFooter(array( 102 $error, 103 $list, 104 )); 105 106 return $this->newPage() 107 ->setTitle(pht('Ambiguous Commit')) 108 ->setCrumbs($crumbs) 109 ->appendChild($matched_commits); 110 } else { 111 $commit = head($commits); 112 } 113 114 $audit_requests = $commit->getAudits(); 115 116 $commit_data = $commit->getCommitData(); 117 $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); 118 $error_panel = null; 119 $unpublished_panel = null; 120 121 $hard_limit = 1000; 122 123 if ($commit->isImported()) { 124 $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( 125 $drequest); 126 $change_query->setLimit($hard_limit + 1); 127 $changes = $change_query->loadChanges(); 128 } else { 129 $changes = array(); 130 } 131 132 $was_limited = (count($changes) > $hard_limit); 133 if ($was_limited) { 134 $changes = array_slice($changes, 0, $hard_limit); 135 } 136 137 $count = count($changes); 138 139 $is_unreadable = false; 140 $hint = null; 141 if (!$count || $commit->isUnreachable()) { 142 $hint = id(new DiffusionCommitHintQuery()) 143 ->setViewer($viewer) 144 ->withRepositoryPHIDs(array($repository->getPHID())) 145 ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) 146 ->executeOne(); 147 if ($hint) { 148 $is_unreadable = $hint->isUnreadable(); 149 } 150 } 151 152 if ($is_foreign) { 153 $subpath = $commit_data->getCommitDetail('svn-subpath'); 154 155 $error_panel = new PHUIInfoView(); 156 $error_panel->setTitle(pht('Commit Not Tracked')); 157 $error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING); 158 $error_panel->appendChild( 159 pht( 160 "This Diffusion repository is configured to track only one ". 161 "subdirectory of the entire Subversion repository, and this commit ". 162 "didn't affect the tracked subdirectory ('%s'), so no ". 163 "information is available.", 164 $subpath)); 165 } else { 166 $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); 167 $engine->setConfig('viewer', $viewer); 168 169 $commit_tag = $this->renderCommitHashTag($drequest); 170 $header = id(new PHUIHeaderView()) 171 ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) 172 ->setHeaderIcon('fa-code-fork') 173 ->addTag($commit_tag); 174 175 if (!$commit->isAuditStatusNoAudit()) { 176 $status = $commit->getAuditStatusObject(); 177 178 $icon = $status->getIcon(); 179 $color = $status->getColor(); 180 $status = $status->getName(); 181 182 $header->setStatus($icon, $color, $status); 183 } 184 185 $curtain = $this->buildCurtain($commit, $repository); 186 $details = $this->buildPropertyListView( 187 $commit, 188 $commit_data, 189 $audit_requests); 190 191 $message = $commit_data->getCommitMessage(); 192 193 $revision = $commit->getCommitIdentifier(); 194 $message = $this->linkBugtraq($message); 195 $message = $engine->markupText($message); 196 197 $detail_list = new PHUIPropertyListView(); 198 $detail_list->addTextContent( 199 phutil_tag( 200 'div', 201 array( 202 'class' => 'diffusion-commit-message phabricator-remarkup', 203 ), 204 $message)); 205 206 if ($commit->isUnreachable()) { 207 $did_rewrite = false; 208 if ($hint) { 209 if ($hint->isRewritten()) { 210 $rewritten = id(new DiffusionCommitQuery()) 211 ->setViewer($viewer) 212 ->withRepository($repository) 213 ->withIdentifiers(array($hint->getNewCommitIdentifier())) 214 ->executeOne(); 215 if ($rewritten) { 216 $did_rewrite = true; 217 $rewritten_uri = $rewritten->getURI(); 218 $rewritten_name = $rewritten->getLocalName(); 219 220 $rewritten_link = phutil_tag( 221 'a', 222 array( 223 'href' => $rewritten_uri, 224 ), 225 $rewritten_name); 226 227 $this->commitErrors[] = pht( 228 'This commit was rewritten after it was published, which '. 229 'changed the commit hash. This old version of the commit is '. 230 'no longer reachable from any branch, tag or ref. The new '. 231 'version of this commit is %s.', 232 $rewritten_link); 233 } 234 } 235 } 236 237 if (!$did_rewrite) { 238 $this->commitErrors[] = pht( 239 'This commit has been deleted in the repository: it is no longer '. 240 'reachable from any branch, tag, or ref.'); 241 } 242 } 243 if (!$commit->isPermanentCommit()) { 244 $nonpermanent_tag = id(new PHUITagView()) 245 ->setType(PHUITagView::TYPE_SHADE) 246 ->setName(pht('Unpublished')) 247 ->setColor(PHUITagView::COLOR_ORANGE); 248 249 $header->addTag($nonpermanent_tag); 250 251 $holds = $commit_data->newPublisherHoldReasons(); 252 253 $reasons = array(); 254 foreach ($holds as $hold) { 255 $reasons[] = array( 256 phutil_tag('strong', array(), pht('%s:', $hold->getName())), 257 ' ', 258 $hold->getSummary(), 259 ); 260 } 261 262 if (!$holds) { 263 $reasons[] = pht('No further details are available.'); 264 } 265 266 $doc_href = PhabricatorEnv::getDoclink( 267 'Diffusion User Guide: Permanent Refs'); 268 $doc_link = phutil_tag( 269 'a', 270 array( 271 'href' => $doc_href, 272 'target' => '_blank', 273 ), 274 pht('Learn More')); 275 276 $title = array( 277 pht('Unpublished Commit'), 278 pht(" \xC2\xB7 "), 279 $doc_link, 280 ); 281 282 $unpublished_panel = id(new PHUIInfoView()) 283 ->setTitle($title) 284 ->setErrors($reasons) 285 ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 286 } 287 288 289 if ($this->getCommitErrors()) { 290 $error_panel = id(new PHUIInfoView()) 291 ->appendChild($this->getCommitErrors()) 292 ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 293 } 294 } 295 296 $timeline = $this->buildComments($commit); 297 $merge_table = $this->buildMergesTable($commit); 298 299 $show_changesets = false; 300 $info_panel = null; 301 $change_list = null; 302 $change_table = null; 303 if ($is_unreadable) { 304 $info_panel = $this->renderStatusMessage( 305 pht('Unreadable Commit'), 306 pht( 307 'This commit has been marked as unreadable by an administrator. '. 308 'It may have been corrupted or created improperly by an external '. 309 'tool.')); 310 } else if ($is_foreign) { 311 // Don't render anything else. 312 } else if (!$commit->isImported()) { 313 $info_panel = $this->renderStatusMessage( 314 pht('Still Importing...'), 315 pht( 316 'This commit is still importing. Changes will be visible once '. 317 'the import finishes.')); 318 } else if (!count($changes)) { 319 $info_panel = $this->renderStatusMessage( 320 pht('Empty Commit'), 321 pht( 322 'This commit is empty and does not affect any paths.')); 323 } else if ($was_limited) { 324 $info_panel = $this->renderStatusMessage( 325 pht('Very Large Commit'), 326 pht( 327 'This commit is very large, and affects more than %d files. '. 328 'Changes are not shown.', 329 $hard_limit)); 330 } else if (!$this->getCommitExists()) { 331 $info_panel = $this->renderStatusMessage( 332 pht('Commit No Longer Exists'), 333 pht('This commit no longer exists in the repository.')); 334 } else { 335 $show_changesets = true; 336 337 // The user has clicked "Show All Changes", and we should show all the 338 // changes inline even if there are more than the soft limit. 339 $show_all_details = $request->getBool('show_all'); 340 341 $change_header = id(new PHUIHeaderView()) 342 ->setHeader(pht('Changes (%s)', new PhutilNumber($count))); 343 344 $warning_view = null; 345 if ($count > self::CHANGES_LIMIT && !$show_all_details) { 346 $button = id(new PHUIButtonView()) 347 ->setText(pht('Show All Changes')) 348 ->setHref('?show_all=true') 349 ->setTag('a') 350 ->setIcon('fa-files-o'); 351 352 $warning_view = id(new PHUIInfoView()) 353 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 354 ->setTitle(pht('Very Large Commit')) 355 ->appendChild( 356 pht('This commit is very large. Load each file individually.')); 357 358 $change_header->addActionLink($button); 359 } 360 361 $changesets = DiffusionPathChange::convertToDifferentialChangesets( 362 $viewer, 363 $changes); 364 365 // TODO: This table and panel shouldn't really be separate, but we need 366 // to clean up the "Load All Files" interaction first. 367 $change_table = $this->buildTableOfContents( 368 $changesets, 369 $change_header, 370 $warning_view); 371 372 $vcs = $repository->getVersionControlSystem(); 373 switch ($vcs) { 374 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 375 $vcs_supports_directory_changes = true; 376 break; 377 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 378 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 379 $vcs_supports_directory_changes = false; 380 break; 381 default: 382 throw new Exception(pht('Unknown VCS.')); 383 } 384 385 $references = array(); 386 foreach ($changesets as $key => $changeset) { 387 $file_type = $changeset->getFileType(); 388 if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { 389 if (!$vcs_supports_directory_changes) { 390 unset($changesets[$key]); 391 continue; 392 } 393 } 394 395 $references[$key] = $drequest->generateURI( 396 array( 397 'action' => 'rendering-ref', 398 'path' => $changeset->getFilename(), 399 )); 400 } 401 402 // TODO: Some parts of the views still rely on properties of the 403 // DifferentialChangeset. Make the objects ephemeral to make sure we don't 404 // accidentally save them, and then set their ID to the appropriate ID for 405 // this application (the path IDs). 406 $path_ids = array_flip(mpull($changes, 'getPath')); 407 foreach ($changesets as $changeset) { 408 $changeset->makeEphemeral(); 409 $changeset->setID($path_ids[$changeset->getFilename()]); 410 } 411 412 if ($count <= self::CHANGES_LIMIT || $show_all_details) { 413 $visible_changesets = $changesets; 414 } else { 415 $visible_changesets = array(); 416 417 $inlines = id(new DiffusionDiffInlineCommentQuery()) 418 ->setViewer($viewer) 419 ->withCommitPHIDs(array($commit->getPHID())) 420 ->withPublishedComments(true) 421 ->withPublishableComments(true) 422 ->execute(); 423 $inlines = mpull($inlines, 'newInlineCommentObject'); 424 425 $path_ids = mpull($inlines, null, 'getPathID'); 426 foreach ($changesets as $key => $changeset) { 427 if (array_key_exists($changeset->getID(), $path_ids)) { 428 $visible_changesets[$key] = $changeset; 429 } 430 } 431 } 432 433 $change_list_title = $commit->getDisplayName(); 434 435 $change_list = new DifferentialChangesetListView(); 436 $change_list->setTitle($change_list_title); 437 $change_list->setChangesets($changesets); 438 $change_list->setVisibleChangesets($visible_changesets); 439 $change_list->setRenderingReferences($references); 440 $change_list->setRenderURI($repository->getPathURI('diff/')); 441 $change_list->setRepository($repository); 442 $change_list->setUser($viewer); 443 $change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); 444 445 // TODO: Try to setBranch() to something reasonable here? 446 447 $change_list->setStandaloneURI( 448 $repository->getPathURI('diff/')); 449 450 $change_list->setRawFileURIs( 451 // TODO: Implement this, somewhat tricky if there's an octopus merge 452 // or whatever? 453 null, 454 $repository->getPathURI('diff/?view=r')); 455 456 $change_list->setInlineCommentControllerURI( 457 '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); 458 459 } 460 461 $add_comment = $this->renderAddCommentPanel( 462 $commit, 463 $timeline); 464 465 $filetree = id(new DifferentialFileTreeEngine()) 466 ->setViewer($viewer) 467 ->setDisabled(!$show_changesets); 468 469 if ($show_changesets) { 470 $filetree->setChangesets($changesets); 471 } 472 473 $description_box = id(new PHUIObjectBoxView()) 474 ->setHeaderText(pht('Description')) 475 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 476 ->appendChild($detail_list); 477 478 $detail_box = id(new PHUIObjectBoxView()) 479 ->setHeaderText(pht('Details')) 480 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 481 ->appendChild($details); 482 483 $view = id(new PHUITwoColumnView()) 484 ->setHeader($header) 485 ->setCurtain($curtain) 486 ->setMainColumn( 487 array( 488 $unpublished_panel, 489 $error_panel, 490 $description_box, 491 $detail_box, 492 $timeline, 493 $merge_table, 494 $info_panel, 495 )) 496 ->setFooter( 497 array( 498 $change_table, 499 $change_list, 500 $add_comment, 501 )); 502 503 $main_content = array( 504 $crumbs, 505 $view, 506 ); 507 508 $main_content = $filetree->newView($main_content); 509 if (!$filetree->getDisabled()) { 510 $change_list->setFormationView($main_content); 511 } 512 513 $page = $this->newPage() 514 ->setTitle($commit->getDisplayName()) 515 ->setPageObjectPHIDS(array($commit->getPHID())) 516 ->appendChild($main_content); 517 518 return $page; 519 520 } 521 522 private function buildPropertyListView( 523 PhabricatorRepositoryCommit $commit, 524 PhabricatorRepositoryCommitData $data, 525 array $audit_requests) { 526 527 $viewer = $this->getViewer(); 528 $commit_phid = $commit->getPHID(); 529 $drequest = $this->getDiffusionRequest(); 530 $repository = $drequest->getRepository(); 531 532 $view = id(new PHUIPropertyListView()) 533 ->setUser($this->getRequest()->getUser()) 534 ->setObject($commit); 535 536 $edge_query = id(new PhabricatorEdgeQuery()) 537 ->withSourcePHIDs(array($commit_phid)) 538 ->withEdgeTypes(array( 539 DiffusionCommitHasTaskEdgeType::EDGECONST, 540 DiffusionCommitHasRevisionEdgeType::EDGECONST, 541 DiffusionCommitRevertsCommitEdgeType::EDGECONST, 542 DiffusionCommitRevertedByCommitEdgeType::EDGECONST, 543 )); 544 545 $edges = $edge_query->execute(); 546 547 $task_phids = array_keys( 548 $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); 549 $revision_phid = key( 550 $edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]); 551 552 $reverts_phids = array_keys( 553 $edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]); 554 $reverted_by_phids = array_keys( 555 $edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]); 556 557 $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); 558 559 560 if ($data->getCommitDetail('reviewerPHID')) { 561 $phids[] = $data->getCommitDetail('reviewerPHID'); 562 } 563 564 $phids[] = $commit->getCommitterDisplayPHID(); 565 $phids[] = $commit->getAuthorDisplayPHID(); 566 567 // NOTE: We should never normally have more than a single push log, but 568 // it can occur naturally if a commit is pushed, then the branch it was 569 // on is deleted, then the commit is pushed again (or through other similar 570 // chains of events). This should be rare, but does not indicate a bug 571 // or data issue. 572 573 // NOTE: We never query push logs in SVN because the committer is always 574 // the pusher and the commit time is always the push time; the push log 575 // is redundant and we save a query by skipping it. 576 577 $push_logs = array(); 578 if ($repository->isHosted() && !$repository->isSVN()) { 579 $push_logs = id(new PhabricatorRepositoryPushLogQuery()) 580 ->setViewer($viewer) 581 ->withRepositoryPHIDs(array($repository->getPHID())) 582 ->withNewRefs(array($commit->getCommitIdentifier())) 583 ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) 584 ->execute(); 585 foreach ($push_logs as $log) { 586 $phids[] = $log->getPusherPHID(); 587 } 588 } 589 590 $handles = array(); 591 if ($phids) { 592 $handles = $this->loadViewerHandles($phids); 593 } 594 595 $props = array(); 596 597 if ($audit_requests) { 598 $user_requests = array(); 599 $other_requests = array(); 600 601 foreach ($audit_requests as $audit_request) { 602 if ($audit_request->isUser()) { 603 $user_requests[] = $audit_request; 604 } else { 605 $other_requests[] = $audit_request; 606 } 607 } 608 609 if ($user_requests) { 610 $view->addProperty( 611 pht('Auditors'), 612 $this->renderAuditStatusView($commit, $user_requests)); 613 } 614 615 if ($other_requests) { 616 $view->addProperty( 617 pht('Group Auditors'), 618 $this->renderAuditStatusView($commit, $other_requests)); 619 } 620 } 621 622 $provenance_list = new PHUIStatusListView(); 623 624 $author_view = $commit->newCommitAuthorView($viewer); 625 if ($author_view) { 626 $author_date = $data->getAuthorEpoch(); 627 628 // Add support to Subversion commits that only have one date, 629 // since in SVN you cannot prepare local commits to push them later. 630 if ($author_date === null) { 631 $author_date = $commit->getEpoch(); 632 } 633 634 $author_date = phabricator_datetime($author_date, $viewer); 635 636 $provenance_list->addItem( 637 id(new PHUIStatusItemView()) 638 ->setTarget($author_view) 639 ->setNote(pht('Authored on %s', $author_date))); 640 } 641 642 if (!$commit->isAuthorSameAsCommitter()) { 643 $committer_view = $commit->newCommitCommitterView($viewer); 644 if ($committer_view) { 645 $committer_date = $commit->getEpoch(); 646 $committer_date = phabricator_datetime($committer_date, $viewer); 647 648 $provenance_list->addItem( 649 id(new PHUIStatusItemView()) 650 ->setTarget($committer_view) 651 ->setNote(pht('Committed on %s', $committer_date))); 652 } 653 } 654 655 if ($push_logs) { 656 $pushed_list = new PHUIStatusListView(); 657 658 foreach ($push_logs as $push_log) { 659 $pusher_date = $push_log->getEpoch(); 660 $pusher_date = phabricator_datetime($pusher_date, $viewer); 661 662 $pusher_view = $handles[$push_log->getPusherPHID()]->renderLink(); 663 664 $provenance_list->addItem( 665 id(new PHUIStatusItemView()) 666 ->setTarget($pusher_view) 667 ->setNote(pht('Pushed on %s', $pusher_date))); 668 } 669 } 670 671 $view->addProperty(pht('Provenance'), $provenance_list); 672 673 $reviewer_phid = $data->getCommitDetail('reviewerPHID'); 674 if ($reviewer_phid) { 675 $view->addProperty( 676 pht('Reviewer'), 677 $handles[$reviewer_phid]->renderLink()); 678 } 679 680 if ($revision_phid) { 681 $view->addProperty( 682 pht('Differential Revision'), 683 $handles[$revision_phid]->renderLink()); 684 } 685 686 $parents = $this->getCommitParents(); 687 if ($parents) { 688 $view->addProperty( 689 pht('Parents'), 690 $viewer->renderHandleList(mpull($parents, 'getPHID'))); 691 } 692 693 if ($this->getCommitExists()) { 694 $view->addProperty( 695 pht('Branches'), 696 phutil_tag( 697 'span', 698 array( 699 'id' => 'commit-branches', 700 ), 701 pht('Unknown'))); 702 703 $view->addProperty( 704 pht('Tags'), 705 phutil_tag( 706 'span', 707 array( 708 'id' => 'commit-tags', 709 ), 710 pht('Unknown'))); 711 712 $identifier = $commit->getCommitIdentifier(); 713 $root = $repository->getPathURI("commit/{$identifier}"); 714 Javelin::initBehavior( 715 'diffusion-commit-branches', 716 array( 717 $root.'/branches/' => 'commit-branches', 718 $root.'/tags/' => 'commit-tags', 719 )); 720 } 721 722 $refs = $this->getCommitRefs(); 723 if ($refs) { 724 $ref_links = array(); 725 foreach ($refs as $ref_data) { 726 $ref_links[] = phutil_tag( 727 'a', 728 array( 729 'href' => $ref_data['href'], 730 ), 731 $ref_data['ref']); 732 } 733 $view->addProperty( 734 pht('References'), 735 phutil_implode_html(', ', $ref_links)); 736 } 737 738 if ($reverts_phids) { 739 $view->addProperty( 740 pht('Reverts'), 741 $viewer->renderHandleList($reverts_phids)); 742 } 743 744 if ($reverted_by_phids) { 745 $view->addProperty( 746 pht('Reverted By'), 747 $viewer->renderHandleList($reverted_by_phids)); 748 } 749 750 if ($task_phids) { 751 $task_list = array(); 752 foreach ($task_phids as $phid) { 753 $task_list[] = $handles[$phid]->renderLink(); 754 } 755 $task_list = phutil_implode_html(phutil_tag('br'), $task_list); 756 $view->addProperty( 757 pht('Tasks'), 758 $task_list); 759 } 760 761 return $view; 762 } 763 764 private function buildComments(PhabricatorRepositoryCommit $commit) { 765 $timeline = $this->buildTransactionTimeline( 766 $commit, 767 new PhabricatorAuditTransactionQuery()); 768 769 $timeline->setQuoteRef($commit->getMonogram()); 770 771 return $timeline; 772 } 773 774 private function renderAddCommentPanel( 775 PhabricatorRepositoryCommit $commit, 776 $timeline) { 777 778 $request = $this->getRequest(); 779 $viewer = $request->getUser(); 780 781 // TODO: This is pretty awkward, unify the CSS between Diffusion and 782 // Differential better. 783 require_celerity_resource('differential-core-view-css'); 784 785 $comment_view = id(new DiffusionCommitEditEngine()) 786 ->setViewer($viewer) 787 ->buildEditEngineCommentView($commit); 788 789 $comment_view->setTransactionTimeline($timeline); 790 791 return $comment_view; 792 } 793 794 private function buildMergesTable(PhabricatorRepositoryCommit $commit) { 795 $viewer = $this->getViewer(); 796 $drequest = $this->getDiffusionRequest(); 797 $repository = $drequest->getRepository(); 798 799 $merges = $this->getCommitMerges(); 800 if (!$merges) { 801 return null; 802 } 803 804 $limit = $this->getMergeDisplayLimit(); 805 806 $caption = null; 807 if (count($merges) > $limit) { 808 $merges = array_slice($merges, 0, $limit); 809 $caption = new PHUIInfoView(); 810 $caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE); 811 $caption->appendChild( 812 pht( 813 'This commit merges a very large number of changes. '. 814 'Only the first %s are shown.', 815 new PhutilNumber($limit))); 816 } 817 818 $commit_list = id(new DiffusionCommitGraphView()) 819 ->setViewer($viewer) 820 ->setDiffusionRequest($drequest) 821 ->setHistory($merges); 822 823 $panel = id(new PHUIObjectBoxView()) 824 ->setHeaderText(pht('Merged Changes')) 825 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 826 ->setObjectList($commit_list->newObjectItemListView()); 827 if ($caption) { 828 $panel->setInfoView($caption); 829 } 830 831 return $panel; 832 } 833 834 private function buildCurtain( 835 PhabricatorRepositoryCommit $commit, 836 PhabricatorRepository $repository) { 837 838 $request = $this->getRequest(); 839 $viewer = $this->getViewer(); 840 $curtain = $this->newCurtainView($commit); 841 842 $can_edit = PhabricatorPolicyFilter::hasCapability( 843 $viewer, 844 $commit, 845 PhabricatorPolicyCapability::CAN_EDIT); 846 847 $id = $commit->getID(); 848 $edit_uri = $this->getApplicationURI("/commit/edit/{$id}/"); 849 850 $action = id(new PhabricatorActionView()) 851 ->setName(pht('Edit Commit')) 852 ->setHref($edit_uri) 853 ->setIcon('fa-pencil') 854 ->setDisabled(!$can_edit) 855 ->setWorkflow(!$can_edit); 856 $curtain->addAction($action); 857 858 $action = id(new PhabricatorActionView()) 859 ->setName(pht('Download Raw Diff')) 860 ->setHref($request->getRequestURI()->alter('diff', true)) 861 ->setIcon('fa-download'); 862 $curtain->addAction($action); 863 864 $relationship_list = PhabricatorObjectRelationshipList::newForObject( 865 $viewer, 866 $commit); 867 868 $relationship_submenu = $relationship_list->newActionMenu(); 869 if ($relationship_submenu) { 870 $curtain->addAction($relationship_submenu); 871 } 872 873 return $curtain; 874 } 875 876 private function buildRawDiffResponse(DiffusionRequest $drequest) { 877 $diff_info = $this->callConduitWithDiffusionRequest( 878 'diffusion.rawdiffquery', 879 array( 880 'commit' => $drequest->getCommit(), 881 'path' => $drequest->getPath(), 882 )); 883 884 $file_phid = $diff_info['filePHID']; 885 886 $file = id(new PhabricatorFileQuery()) 887 ->setViewer($this->getViewer()) 888 ->withPHIDs(array($file_phid)) 889 ->executeOne(); 890 if (!$file) { 891 throw new Exception( 892 pht( 893 'Failed to load file ("%s") returned by "%s".', 894 $file_phid, 895 'diffusion.rawdiffquery')); 896 } 897 898 return $file->getRedirectResponse(); 899 } 900 901 /** 902 * @param PhabricatorRepositoryCommit $commit 903 * @param array<PhabricatorRepositoryAuditRequest> $audit_requests 904 */ 905 private function renderAuditStatusView( 906 PhabricatorRepositoryCommit $commit, 907 array $audit_requests) { 908 assert_instances_of( 909 $audit_requests, 910 PhabricatorRepositoryAuditRequest::class); 911 $viewer = $this->getViewer(); 912 913 $view = new PHUIStatusListView(); 914 foreach ($audit_requests as $request) { 915 $status = $request->getAuditRequestStatusObject(); 916 917 $item = new PHUIStatusItemView(); 918 $item->setIcon( 919 $status->getIconIcon(), 920 $status->getIconColor(), 921 $status->getStatusName()); 922 923 $auditor_phid = $request->getAuditorPHID(); 924 $target = $viewer->renderHandle($auditor_phid); 925 $item->setTarget($target); 926 927 if ($commit->hasAuditAuthority($viewer, $request)) { 928 $item->setHighlighted(true); 929 } 930 931 $view->addItem($item); 932 } 933 934 return $view; 935 } 936 937 private function linkBugtraq($corpus) { 938 $url = PhabricatorEnv::getEnvConfig('bugtraq.url'); 939 if (!phutil_nonempty_string($url)) { 940 return $corpus; 941 } 942 943 $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex'); 944 if (!$regexes) { 945 return $corpus; 946 } 947 948 $parser = id(new PhutilBugtraqParser()) 949 ->setBugtraqPattern("[[ {$url} | %BUGID% ]]") 950 ->setBugtraqCaptureExpression(array_shift($regexes)); 951 952 $select = array_shift($regexes); 953 if ($select) { 954 $parser->setBugtraqSelectExpression($select); 955 } 956 957 return $parser->processCorpus($corpus); 958 } 959 960 private function buildTableOfContents( 961 array $changesets, 962 $header, 963 $info_view) { 964 965 $drequest = $this->getDiffusionRequest(); 966 $viewer = $this->getViewer(); 967 968 $toc_view = id(new PHUIDiffTableOfContentsListView()) 969 ->setUser($viewer) 970 ->setHeader($header) 971 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); 972 973 if ($info_view) { 974 $toc_view->setInfoView($info_view); 975 } 976 977 // TODO: This is hacky, we just want access to the linkX() methods on 978 // DiffusionView. 979 $diffusion_view = id(new DiffusionEmptyResultView()) 980 ->setDiffusionRequest($drequest); 981 982 $have_owners = PhabricatorApplication::isClassInstalledForViewer( 983 PhabricatorOwnersApplication::class, 984 $viewer); 985 986 if (!$changesets) { 987 $have_owners = false; 988 } 989 990 if ($have_owners) { 991 if ($viewer->getPHID()) { 992 $packages = id(new PhabricatorOwnersPackageQuery()) 993 ->setViewer($viewer) 994 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) 995 ->withAuthorityPHIDs(array($viewer->getPHID())) 996 ->execute(); 997 $toc_view->setAuthorityPackages($packages); 998 } 999 1000 $repository = $drequest->getRepository(); 1001 $repository_phid = $repository->getPHID(); 1002 1003 $control_query = id(new PhabricatorOwnersPackageQuery()) 1004 ->setViewer($viewer) 1005 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) 1006 ->withControl($repository_phid, mpull($changesets, 'getFilename')); 1007 $control_query->execute(); 1008 } 1009 1010 foreach ($changesets as $changeset_id => $changeset) { 1011 $path = $changeset->getFilename(); 1012 $anchor = $changeset->getAnchorName(); 1013 1014 $history_link = $diffusion_view->linkHistory($path); 1015 $browse_link = $diffusion_view->linkBrowse( 1016 $path, 1017 array( 1018 'type' => $changeset->getFileType(), 1019 )); 1020 1021 $item = id(new PHUIDiffTableOfContentsItemView()) 1022 ->setChangeset($changeset) 1023 ->setAnchor($anchor) 1024 ->setContext( 1025 array( 1026 $history_link, 1027 ' ', 1028 $browse_link, 1029 )); 1030 1031 if ($have_owners) { 1032 $packages = $control_query->getControllingPackagesForPath( 1033 $repository_phid, 1034 $changeset->getFilename()); 1035 $item->setPackages($packages); 1036 } 1037 1038 $toc_view->addItem($item); 1039 } 1040 1041 return $toc_view; 1042 } 1043 1044 private function loadCommitState() { 1045 $viewer = $this->getViewer(); 1046 $drequest = $this->getDiffusionRequest(); 1047 $repository = $drequest->getRepository(); 1048 $commit = $drequest->getCommit(); 1049 1050 // TODO: We could use futures here and resolve these calls in parallel. 1051 1052 $exceptions = array(); 1053 1054 try { 1055 $parent_refs = $this->callConduitWithDiffusionRequest( 1056 'diffusion.commitparentsquery', 1057 array( 1058 'commit' => $commit, 1059 )); 1060 1061 if ($parent_refs) { 1062 $parents = id(new DiffusionCommitQuery()) 1063 ->setViewer($viewer) 1064 ->withRepository($repository) 1065 ->withIdentifiers($parent_refs) 1066 ->execute(); 1067 } else { 1068 $parents = array(); 1069 } 1070 1071 $this->commitParents = $parents; 1072 } catch (Exception $ex) { 1073 $this->commitParents = false; 1074 $exceptions[] = $ex; 1075 } 1076 1077 $merge_limit = $this->getMergeDisplayLimit(); 1078 1079 try { 1080 if ($repository->isSVN()) { 1081 $this->commitMerges = array(); 1082 } else { 1083 $merges = $this->callConduitWithDiffusionRequest( 1084 'diffusion.mergedcommitsquery', 1085 array( 1086 'commit' => $commit, 1087 'limit' => $merge_limit + 1, 1088 )); 1089 $this->commitMerges = DiffusionPathChange::newFromConduit($merges); 1090 } 1091 } catch (Exception $ex) { 1092 $this->commitMerges = false; 1093 $exceptions[] = $ex; 1094 } 1095 1096 1097 try { 1098 if ($repository->isGit()) { 1099 $refs = $this->callConduitWithDiffusionRequest( 1100 'diffusion.refsquery', 1101 array( 1102 'commit' => $commit, 1103 )); 1104 } else { 1105 $refs = array(); 1106 } 1107 1108 $this->commitRefs = $refs; 1109 } catch (Exception $ex) { 1110 $this->commitRefs = false; 1111 $exceptions[] = $ex; 1112 } 1113 1114 if ($exceptions) { 1115 $exists = $this->callConduitWithDiffusionRequest( 1116 'diffusion.existsquery', 1117 array( 1118 'commit' => $commit, 1119 )); 1120 1121 if ($exists) { 1122 $this->commitExists = true; 1123 foreach ($exceptions as $exception) { 1124 $this->commitErrors[] = $exception->getMessage(); 1125 } 1126 } else { 1127 $this->commitExists = false; 1128 $this->commitErrors[] = pht( 1129 'This commit no longer exists in the repository. It may have '. 1130 'been part of a branch which was deleted.'); 1131 } 1132 } else { 1133 $this->commitExists = true; 1134 $this->commitErrors = array(); 1135 } 1136 } 1137 1138 private function getMergeDisplayLimit() { 1139 return 50; 1140 } 1141 1142 private function getCommitExists() { 1143 if ($this->commitExists === null) { 1144 $this->loadCommitState(); 1145 } 1146 1147 return $this->commitExists; 1148 } 1149 1150 private function getCommitParents() { 1151 if ($this->commitParents === null) { 1152 $this->loadCommitState(); 1153 } 1154 1155 return $this->commitParents; 1156 } 1157 1158 private function getCommitRefs() { 1159 if ($this->commitRefs === null) { 1160 $this->loadCommitState(); 1161 } 1162 1163 return $this->commitRefs; 1164 } 1165 1166 private function getCommitMerges() { 1167 if ($this->commitMerges === null) { 1168 $this->loadCommitState(); 1169 } 1170 1171 return $this->commitMerges; 1172 } 1173 1174 private function getCommitErrors() { 1175 if ($this->commitErrors === null) { 1176 $this->loadCommitState(); 1177 } 1178 1179 return $this->commitErrors; 1180 } 1181 1182 1183}