@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 1516 lines 45 kB view raw
1<?php 2 3final class DifferentialRevisionViewController 4 extends DifferentialController { 5 6 private $revisionID; 7 private $changesetCount; 8 private $hiddenChangesets; 9 private $warnings = array(); 10 11 public function shouldAllowPublic() { 12 return true; 13 } 14 15 public function isLargeDiff() { 16 return ($this->getChangesetCount() > $this->getLargeDiffLimit()); 17 } 18 19 public function isVeryLargeDiff() { 20 return ($this->getChangesetCount() > $this->getVeryLargeDiffLimit()); 21 } 22 23 public function getLargeDiffLimit() { 24 return 100; 25 } 26 27 public function getVeryLargeDiffLimit() { 28 return 1000; 29 } 30 31 public function getChangesetCount() { 32 if ($this->changesetCount === null) { 33 throw new PhutilInvalidStateException('setChangesetCount'); 34 } 35 return $this->changesetCount; 36 } 37 38 public function setChangesetCount($count) { 39 $this->changesetCount = $count; 40 return $this; 41 } 42 43 44 private function newMentionsTab( 45 DifferentialRevision $revision) { 46 47 $phid = $revision->getPHID(); 48 49 $edge_types = array( 50 PhabricatorObjectMentionedByObjectEdgeType::EDGECONST, 51 PhabricatorObjectMentionsObjectEdgeType::EDGECONST, 52 ); 53 54 $edge_query = id(new PhabricatorEdgeQuery()) 55 ->withSourcePHIDs(array($phid)) 56 ->withEdgeTypes($edge_types); 57 58 $edge_query->execute(); 59 60 $view = (new PhorgeApplicationMentionsListView()) 61 ->setEdgeQuery($edge_query) 62 ->setViewer($this->getViewer()) 63 ->getMentionsView(); 64 65 if (!$view ) { 66 return null; 67 } 68 69 return id(new PHUITabView()) 70 ->setName(pht('Mentions')) 71 ->setKey('mentions') 72 ->appendChild($view); 73 } 74 75 76 public function handleRequest(AphrontRequest $request) { 77 $viewer = $this->getViewer(); 78 $this->revisionID = $request->getURIData('id'); 79 80 $viewer_is_anonymous = !$viewer->isLoggedIn(); 81 82 $revision = id(new DifferentialRevisionQuery()) 83 ->withIDs(array($this->revisionID)) 84 ->setViewer($viewer) 85 ->needReviewers(true) 86 ->needReviewerAuthority(true) 87 ->needCommitPHIDs(true) 88 ->executeOne(); 89 if (!$revision) { 90 return new Aphront404Response(); 91 } 92 93 $diffs = id(new DifferentialDiffQuery()) 94 ->setViewer($viewer) 95 ->withRevisionIDs(array($this->revisionID)) 96 ->execute(); 97 $diffs = array_reverse($diffs, $preserve_keys = true); 98 99 if (!$diffs) { 100 throw new Exception( 101 pht('This revision has no diffs. Something has gone quite wrong.')); 102 } 103 104 $revision->attachActiveDiff(last($diffs)); 105 106 $diff_vs = $this->getOldDiffID($revision, $diffs); 107 if ($diff_vs instanceof AphrontResponse) { 108 return $diff_vs; 109 } 110 111 $target_id = $this->getNewDiffID($revision, $diffs); 112 if ($target_id instanceof AphrontResponse) { 113 return $target_id; 114 } 115 116 $target = $diffs[$target_id]; 117 118 $target_manual = $target; 119 if (!$target_id) { 120 foreach ($diffs as $diff) { 121 if ($diff->getCreationMethod() != 'commit') { 122 $target_manual = $diff; 123 } 124 } 125 } 126 127 $repository = null; 128 $repository_phid = $target->getRepositoryPHID(); 129 if ($repository_phid) { 130 if ($repository_phid == $revision->getRepositoryPHID()) { 131 $repository = $revision->getRepository(); 132 } else { 133 $repository = id(new PhabricatorRepositoryQuery()) 134 ->setViewer($viewer) 135 ->withPHIDs(array($repository_phid)) 136 ->executeOne(); 137 } 138 } 139 140 list($changesets, $vs_map, $vs_changesets, $rendering_references) = 141 $this->loadChangesetsAndVsMap( 142 $target, 143 idx($diffs, $diff_vs), 144 $repository); 145 146 $this->setChangesetCount(count($rendering_references)); 147 148 if ($request->getExists('download')) { 149 return $this->buildRawDiffResponse( 150 $revision, 151 $changesets, 152 $vs_changesets, 153 $vs_map, 154 $repository); 155 } 156 157 $map = $vs_map; 158 if (!$map) { 159 $map = array_fill_keys(array_keys($changesets), 0); 160 } 161 162 $old_ids = array(); 163 $new_ids = array(); 164 foreach ($map as $id => $vs) { 165 if ($vs <= 0) { 166 $old_ids[] = $id; 167 $new_ids[] = $id; 168 } else { 169 $new_ids[] = $id; 170 $new_ids[] = $vs; 171 } 172 } 173 174 $this->loadDiffProperties($diffs); 175 $props = $target_manual->getDiffProperties(); 176 177 $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( 178 $revision->getPHID()); 179 180 $object_phids = array_merge( 181 $revision->getReviewerPHIDs(), 182 $subscriber_phids, 183 $revision->getCommitPHIDs(), 184 array( 185 $revision->getAuthorPHID(), 186 $viewer->getPHID(), 187 )); 188 189 foreach ($revision->getAttached() as $type => $phids) { 190 foreach ($phids as $phid => $info) { 191 $object_phids[] = $phid; 192 } 193 } 194 195 $field_list = PhabricatorCustomField::getObjectFields( 196 $revision, 197 PhabricatorCustomField::ROLE_VIEW); 198 199 $field_list->setViewer($viewer); 200 $field_list->readFieldsFromStorage($revision); 201 202 $warning_handle_map = array(); 203 foreach ($field_list->getFields() as $key => $field) { 204 $req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings(); 205 foreach ($req as $phid) { 206 $warning_handle_map[$key][] = $phid; 207 $object_phids[] = $phid; 208 } 209 } 210 211 $handles = $this->loadViewerHandles($object_phids); 212 $warnings = $this->warnings; 213 214 $request_uri = $request->getRequestURI(); 215 216 $large = $request->getStr('large'); 217 218 $large_warning = 219 ($this->isLargeDiff()) && 220 (!$this->isVeryLargeDiff()) && 221 (!$large); 222 223 if ($large_warning) { 224 $count = $this->getChangesetCount(); 225 226 $expand_uri = $request_uri 227 ->alter('large', 'true') 228 ->setFragment('toc'); 229 230 $message = array( 231 pht( 232 'This large diff affects %s files. Files without inline '. 233 'comments have been collapsed.', 234 new PhutilNumber($count)), 235 ' ', 236 phutil_tag( 237 'strong', 238 array(), 239 phutil_tag( 240 'a', 241 array( 242 'href' => $expand_uri, 243 ), 244 pht('Expand All Files'))), 245 ); 246 247 $warnings[] = id(new PHUIInfoView()) 248 ->setTitle(pht('Large Diff')) 249 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 250 ->appendChild($message); 251 252 $folded_changesets = $changesets; 253 } else { 254 $folded_changesets = array(); 255 } 256 257 // Don't hide or fold changesets which have inline comments. 258 $hidden_changesets = $this->hiddenChangesets; 259 if ($hidden_changesets || $folded_changesets) { 260 $old = array_select_keys($changesets, $old_ids); 261 $new = array_select_keys($changesets, $new_ids); 262 263 $inlines = id(new DifferentialDiffInlineCommentQuery()) 264 ->setViewer($viewer) 265 ->withRevisionPHIDs(array($revision->getPHID())) 266 ->withPublishableComments(true) 267 ->withPublishedComments(true) 268 ->execute(); 269 270 $inlines = mpull($inlines, 'newInlineCommentObject'); 271 272 $inlines = id(new PhabricatorInlineCommentAdjustmentEngine()) 273 ->setViewer($viewer) 274 ->setRevision($revision) 275 ->setOldChangesets($old) 276 ->setNewChangesets($new) 277 ->setInlines($inlines) 278 ->execute(); 279 280 foreach ($inlines as $inline) { 281 $changeset_id = $inline->getChangesetID(); 282 if (!isset($changesets[$changeset_id])) { 283 continue; 284 } 285 286 unset($hidden_changesets[$changeset_id]); 287 unset($folded_changesets[$changeset_id]); 288 } 289 } 290 291 // If we would hide only one changeset, don't hide anything. The notice 292 // we'd render about it is about the same size as the changeset. 293 if (count($hidden_changesets) < 2) { 294 $hidden_changesets = array(); 295 } 296 297 // Update the set of hidden changesets, since we may have just un-hidden 298 // some of them. 299 if ($hidden_changesets) { 300 $warnings[] = id(new PHUIInfoView()) 301 ->setTitle(pht('Showing Only Differences')) 302 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 303 ->appendChild( 304 pht( 305 'This revision modifies %s more files that are hidden because '. 306 'they were not modified between selected diffs and they have no '. 307 'inline comments.', 308 phutil_count($hidden_changesets))); 309 } 310 311 // Compute the unfolded changesets. By default, everything is unfolded. 312 $unfolded_changesets = $changesets; 313 foreach ($folded_changesets as $changeset_id => $changeset) { 314 unset($unfolded_changesets[$changeset_id]); 315 } 316 317 // Throw away any hidden changesets. 318 foreach ($hidden_changesets as $changeset_id => $changeset) { 319 unset($changesets[$changeset_id]); 320 unset($unfolded_changesets[$changeset_id]); 321 } 322 323 $commit_hashes = mpull($diffs, 'getSourceControlBaseRevision'); 324 $local_commits = idx($props, 'local:commits', array()); 325 foreach ($local_commits as $local_commit) { 326 $commit_hashes[] = idx($local_commit, 'tree'); 327 $commit_hashes[] = idx($local_commit, 'local'); 328 } 329 $commit_hashes = array_unique(array_filter($commit_hashes)); 330 if ($commit_hashes) { 331 $commits_for_links = id(new DiffusionCommitQuery()) 332 ->setViewer($viewer) 333 ->withIdentifiers($commit_hashes) 334 ->execute(); 335 $commits_for_links = mpull( 336 $commits_for_links, 337 null, 338 'getCommitIdentifier'); 339 } else { 340 $commits_for_links = array(); 341 } 342 343 $header = $this->buildHeader($revision); 344 $subheader = $this->buildSubheaderView($revision); 345 $details = $this->buildDetails($revision, $field_list); 346 $curtain = $this->buildCurtain($revision); 347 348 $repository = $revision->getRepository(); 349 if ($repository) { 350 $symbol_indexes = $this->buildSymbolIndexes( 351 $repository, 352 $unfolded_changesets); 353 } else { 354 $symbol_indexes = array(); 355 } 356 357 $revision_warnings = $this->buildRevisionWarnings( 358 $revision, 359 $field_list, 360 $warning_handle_map, 361 $handles); 362 $info_view = null; 363 if ($revision_warnings) { 364 $info_view = id(new PHUIInfoView()) 365 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 366 ->setErrors($revision_warnings); 367 } 368 369 if ($diff_vs === null) { 370 $diff_keys = array($target->getID()); 371 } else { 372 $diff_keys = array($diff_vs, $target->getID()); 373 } 374 $detail_diffs = array_select_keys( 375 $diffs, 376 $diff_keys); 377 $detail_diffs = mpull($detail_diffs, null, 'getPHID'); 378 379 $this->loadHarbormasterData($detail_diffs); 380 381 $diff_detail_box = $this->buildDiffDetailView( 382 $detail_diffs, 383 $revision, 384 $field_list); 385 386 $unit_box = $this->buildUnitMessagesView( 387 $target, 388 $revision); 389 390 $timeline = $this->buildTransactions( 391 $revision, 392 $diff_vs ? $diffs[$diff_vs] : $target, 393 $target, 394 $old_ids, 395 $new_ids); 396 397 $timeline->setQuoteRef($revision->getMonogram()); 398 399 if ($this->isVeryLargeDiff()) { 400 $messages = array(); 401 402 $messages[] = pht( 403 'This very large diff affects more than %s files. Use the %s to '. 404 'browse changes.', 405 new PhutilNumber($this->getVeryLargeDiffLimit()), 406 phutil_tag( 407 'a', 408 array( 409 'href' => '/differential/diff/'.$target->getID().'/changesets/', 410 ), 411 phutil_tag('strong', array(), pht('Changeset List')))); 412 413 $changeset_view = id(new PHUIInfoView()) 414 ->setErrors($messages); 415 } else { 416 $changeset_view = id(new DifferentialChangesetListView()) 417 ->setChangesets($changesets) 418 ->setVisibleChangesets($unfolded_changesets) 419 ->setStandaloneURI('/differential/changeset/') 420 ->setRawFileURIs( 421 '/differential/changeset/?view=old', 422 '/differential/changeset/?view=new') 423 ->setViewer($viewer) 424 ->setDiff($target) 425 ->setRenderingReferences($rendering_references) 426 ->setVsMap($vs_map) 427 ->setSymbolIndexes($symbol_indexes) 428 ->setTitle(pht('Diff %s', $target->getID())) 429 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); 430 431 $revision_id = $revision->getID(); 432 $inline_list_uri = "/revision/inlines/{$revision_id}/"; 433 $inline_list_uri = $this->getApplicationURI($inline_list_uri); 434 $changeset_view->setInlineListURI($inline_list_uri); 435 436 if ($repository) { 437 $changeset_view->setRepository($repository); 438 } 439 440 if (!$viewer_is_anonymous) { 441 $changeset_view->setInlineCommentControllerURI( 442 '/differential/comment/inline/edit/'.$revision->getID().'/'); 443 } 444 } 445 446 $broken_diffs = $this->loadHistoryDiffStatus($diffs); 447 448 $history = id(new DifferentialRevisionUpdateHistoryView()) 449 ->setViewer($viewer) 450 ->setDiffs($diffs) 451 ->setDiffUnitStatuses($broken_diffs) 452 ->setSelectedVersusDiffID($diff_vs) 453 ->setSelectedDiffID($target->getID()) 454 ->setCommitsForLinks($commits_for_links); 455 456 $local_table = id(new DifferentialLocalCommitsView()) 457 ->setViewer($viewer) 458 ->setLocalCommits(idx($props, 'local:commits')) 459 ->setCommitsForLinks($commits_for_links); 460 461 if ($repository && !$this->isVeryLargeDiff()) { 462 $other_revisions = $this->loadOtherRevisions( 463 $changesets, 464 $target, 465 $repository); 466 } else { 467 $other_revisions = array(); 468 } 469 470 $other_view = null; 471 if ($other_revisions) { 472 $other_view = $this->renderOtherRevisions($other_revisions); 473 } 474 475 if ($this->isVeryLargeDiff()) { 476 $toc_view = null; 477 478 // When rendering a "very large" diff, we skip computation of owners 479 // that own no files because it is significantly expensive and not very 480 // valuable. 481 foreach ($revision->getReviewers() as $reviewer) { 482 // Give each reviewer a dummy nonempty value so the UI does not render 483 // the "(Owns No Changed Paths)" note. If that behavior becomes more 484 // sophisticated in the future, this behavior might also need to. 485 $reviewer->attachChangesets($changesets); 486 } 487 } else { 488 $this->buildPackageMaps($changesets); 489 490 $toc_view = $this->buildTableOfContents( 491 $changesets, 492 $unfolded_changesets, 493 $target->loadCoverageMap($viewer)); 494 495 // Attach changesets to each reviewer so we can show which Owners package 496 // reviewers own no files. 497 foreach ($revision->getReviewers() as $reviewer) { 498 $reviewer_phid = $reviewer->getReviewerPHID(); 499 $reviewer_changesets = $this->getPackageChangesets($reviewer_phid); 500 $reviewer->attachChangesets($reviewer_changesets); 501 } 502 503 $authority_packages = $this->getAuthorityPackages(); 504 foreach ($changesets as $changeset) { 505 $changeset_packages = $this->getChangesetPackages($changeset); 506 507 $changeset 508 ->setAuthorityPackages($authority_packages) 509 ->setChangesetPackages($changeset_packages); 510 } 511 } 512 513 $tab_group = new PHUITabGroupView(); 514 515 if ($toc_view) { 516 $tab_group->addTab( 517 id(new PHUITabView()) 518 ->setName(pht('Files')) 519 ->setKey('files') 520 ->appendChild($toc_view)); 521 } 522 523 $tab_group->addTab( 524 id(new PHUITabView()) 525 ->setName(pht('History')) 526 ->setKey('history') 527 ->appendChild($history)); 528 529 $mentions_tab = $this->newMentionsTab($revision); 530 531 if ($mentions_tab) { 532 $tab_group->addTab($mentions_tab); 533 } 534 535 $filetree = id(new DifferentialFileTreeEngine()) 536 ->setViewer($viewer); 537 $filetree_collapsed = !$filetree->getIsVisible(); 538 539 // See PHI811. If the viewer has the file tree on, the files tab with the 540 // table of contents is redundant, so default to the "History" tab instead. 541 if (!$filetree_collapsed) { 542 $tab_group->selectTab('history'); 543 } 544 545 $tab_group->addTab( 546 id(new PHUITabView()) 547 ->setName(pht('Commits')) 548 ->setKey('commits') 549 ->appendChild($local_table)); 550 551 $stack_graph = id(new DifferentialRevisionGraph()) 552 ->setViewer($viewer) 553 ->setSeedPHID($revision->getPHID()) 554 ->setLoadEntireGraph(true) 555 ->loadGraph(); 556 if (!$stack_graph->isEmpty()) { 557 // See PHI1900. The graph UI element now tries to figure out the correct 558 // height automatically, but currently can't in this case because the 559 // element is not visible when the page loads. Set an explicit height. 560 $stack_graph->setHeight(34); 561 562 $stack_table = $stack_graph->newGraphTable(); 563 564 $parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; 565 $reachable = $stack_graph->getReachableObjects($parent_type); 566 567 foreach ($reachable as $key => $reachable_revision) { 568 if ($reachable_revision->isClosed()) { 569 unset($reachable[$key]); 570 } 571 } 572 573 if ($reachable) { 574 $stack_name = pht('Stack (%s Open)', phutil_count($reachable)); 575 $stack_color = PHUIListItemView::STATUS_FAIL; 576 } else { 577 $stack_name = pht('Stack'); 578 $stack_color = null; 579 } 580 581 $tab_group->addTab( 582 id(new PHUITabView()) 583 ->setName($stack_name) 584 ->setKey('stack') 585 ->setColor($stack_color) 586 ->appendChild($stack_table)); 587 } 588 589 if ($other_view) { 590 $tab_group->addTab( 591 id(new PHUITabView()) 592 ->setName(pht('Similar')) 593 ->setKey('similar') 594 ->appendChild($other_view)); 595 } 596 597 $view_button = id(new PHUIButtonView()) 598 ->setTag('a') 599 ->setText(pht('Changeset List')) 600 ->setHref('/differential/diff/'.$target->getID().'/changesets/') 601 ->setIcon('fa-align-left'); 602 603 $tab_header = id(new PHUIHeaderView()) 604 ->setHeader(pht('Revision Contents')) 605 ->addActionLink($view_button); 606 607 $tab_view = id(new PHUIObjectBoxView()) 608 ->setHeader($tab_header) 609 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 610 ->addTabGroup($tab_group); 611 612 $signatures = DifferentialRequiredSignaturesField::loadForRevision( 613 $revision); 614 $missing_signatures = false; 615 foreach ($signatures as $phid => $signed) { 616 if (!$signed) { 617 $missing_signatures = true; 618 } 619 } 620 621 $footer = array(); 622 $signature_message = null; 623 if ($missing_signatures) { 624 $signature_message = id(new PHUIInfoView()) 625 ->setTitle(pht('Content Hidden')) 626 ->appendChild( 627 pht( 628 'The content of this revision is hidden until the author has '. 629 'signed all of the required legal agreements.')); 630 } else { 631 $anchor = id(new PhabricatorAnchorView()) 632 ->setAnchorName('toc') 633 ->setNavigationMarker(true); 634 635 $footer[] = array( 636 $anchor, 637 $warnings, 638 $tab_view, 639 $changeset_view, 640 ); 641 } 642 643 $comment_view = id(new DifferentialRevisionEditEngine()) 644 ->setViewer($viewer) 645 ->buildEditEngineCommentView($revision); 646 647 $comment_view->setTransactionTimeline($timeline); 648 649 $review_warnings = array(); 650 foreach ($field_list->getFields() as $field) { 651 $review_warnings[] = $field->getWarningsForDetailView(); 652 } 653 $review_warnings = array_mergev($review_warnings); 654 655 if ($review_warnings) { 656 $warnings_view = id(new PHUIInfoView()) 657 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 658 ->setErrors($review_warnings); 659 660 $comment_view->setInfoView($warnings_view); 661 } 662 663 $footer[] = $comment_view; 664 665 $monogram = $revision->getMonogram(); 666 $operations_box = $this->buildOperationsBox($revision); 667 668 $crumbs = $this->buildApplicationCrumbs(); 669 $crumbs->addTextCrumb($monogram); 670 $crumbs->setBorder(true); 671 672 $filetree 673 ->setChangesets($changesets) 674 ->setDisabled($this->isVeryLargeDiff()); 675 676 $view = id(new PHUITwoColumnView()) 677 ->setHeader($header) 678 ->setSubheader($subheader) 679 ->setCurtain($curtain) 680 ->setMainColumn( 681 array( 682 $operations_box, 683 $info_view, 684 $details, 685 $diff_detail_box, 686 $unit_box, 687 $timeline, 688 $signature_message, 689 )) 690 ->setFooter($footer); 691 692 $main_content = array( 693 $crumbs, 694 $view, 695 ); 696 697 $main_content = $filetree->newView($main_content); 698 699 if (!$filetree->getDisabled()) { 700 $changeset_view->setFormationView($main_content); 701 } 702 703 $page = $this->newPage() 704 ->setTitle($monogram.' '.$revision->getTitle()) 705 ->setPageObjectPHIDs(array($revision->getPHID())) 706 ->appendChild($main_content); 707 708 return $page; 709 } 710 711 private function buildHeader(DifferentialRevision $revision) { 712 $view = id(new PHUIHeaderView()) 713 ->setHeader($revision->getTitle($revision)) 714 ->setViewer($this->getViewer()) 715 ->setPolicyObject($revision) 716 ->setHeaderIcon('fa-cog'); 717 718 $status_tag = id(new PHUITagView()) 719 ->setName($revision->getStatusDisplayName()) 720 ->setIcon($revision->getStatusIcon()) 721 ->setColor($revision->getStatusTagColor()) 722 ->setType(PHUITagView::TYPE_SHADE); 723 724 $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag); 725 726 // If the revision is in a status other than "Draft", but not broadcasting, 727 // add an additional "Draft" tag to the header to make it clear that this 728 // revision hasn't promoted yet. 729 if (!$revision->getShouldBroadcast() && !$revision->isDraft()) { 730 $draft_status = DifferentialRevisionStatus::newForStatus( 731 DifferentialRevisionStatus::DRAFT); 732 733 $draft_tag = id(new PHUITagView()) 734 ->setName($draft_status->getDisplayName()) 735 ->setIcon($draft_status->getIcon()) 736 ->setColor($draft_status->getTagColor()) 737 ->setType(PHUITagView::TYPE_SHADE); 738 739 $view->addTag($draft_tag); 740 } 741 742 return $view; 743 } 744 745 private function buildSubheaderView(DifferentialRevision $revision) { 746 $viewer = $this->getViewer(); 747 748 $author_phid = $revision->getAuthorPHID(); 749 750 $author = $viewer->renderHandle($author_phid)->render(); 751 $date = phabricator_datetime($revision->getDateCreated(), $viewer); 752 $author = phutil_tag('strong', array(), $author); 753 754 $handles = $viewer->loadHandles(array($author_phid)); 755 $image_uri = $handles[$author_phid]->getImageURI(); 756 $image_href = $handles[$author_phid]->getURI(); 757 758 $content = pht('Authored by %s on %s.', $author, $date); 759 760 return id(new PHUIHeadThingView()) 761 ->setImage($image_uri) 762 ->setImageHref($image_href) 763 ->setContent($content); 764 } 765 766 private function buildDetails( 767 DifferentialRevision $revision, 768 $custom_fields) { 769 $viewer = $this->getViewer(); 770 $properties = id(new PHUIPropertyListView()) 771 ->setViewer($viewer); 772 773 if ($custom_fields) { 774 $custom_fields->appendFieldsToPropertyList( 775 $revision, 776 $viewer, 777 $properties); 778 } 779 780 $header = id(new PHUIHeaderView()) 781 ->setHeader(pht('Details')); 782 783 return id(new PHUIObjectBoxView()) 784 ->setHeader($header) 785 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 786 ->appendChild($properties); 787 } 788 789 private function buildCurtain(DifferentialRevision $revision) { 790 $viewer = $this->getViewer(); 791 $revision_id = $revision->getID(); 792 $revision_phid = $revision->getPHID(); 793 $curtain = $this->newCurtainView($revision); 794 795 $can_edit = PhabricatorPolicyFilter::hasCapability( 796 $viewer, 797 $revision, 798 PhabricatorPolicyCapability::CAN_EDIT); 799 800 $curtain->addAction( 801 id(new PhabricatorActionView()) 802 ->setIcon('fa-pencil') 803 ->setHref("/differential/revision/edit/{$revision_id}/") 804 ->setName(pht('Edit Revision')) 805 ->setDisabled(!$can_edit) 806 ->setWorkflow(!$can_edit)); 807 808 $curtain->addAction( 809 id(new PhabricatorActionView()) 810 ->setIcon('fa-upload') 811 ->setHref("/differential/revision/update/{$revision_id}/") 812 ->setName(pht('Update Diff')) 813 ->setDisabled(!$can_edit) 814 ->setWorkflow(!$can_edit)); 815 816 $request_uri = $this->getRequest()->getRequestURI(); 817 $curtain->addAction( 818 id(new PhabricatorActionView()) 819 ->setIcon('fa-download') 820 ->setName(pht('Download Raw Diff')) 821 ->setHref($request_uri->alter('download', 'true'))); 822 823 $relationship_list = PhabricatorObjectRelationshipList::newForObject( 824 $viewer, 825 $revision); 826 827 $revision_actions = array( 828 DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY, 829 DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY, 830 ); 831 832 $revision_submenu = $relationship_list->newActionSubmenu($revision_actions) 833 ->setName(pht('Edit Related Revisions...')) 834 ->setIcon('fa-cog'); 835 836 $curtain->addAction($revision_submenu); 837 838 $relationship_submenu = $relationship_list->newActionMenu(); 839 if ($relationship_submenu) { 840 $curtain->addAction($relationship_submenu); 841 } 842 843 $repository = $revision->getRepository(); 844 if ($repository && $repository->canPerformAutomation()) { 845 $revision_id = $revision->getID(); 846 847 $op = new DrydockLandRepositoryOperation(); 848 $barrier = $op->getBarrierToLanding($viewer, $revision); 849 850 if ($barrier) { 851 $can_land = false; 852 } else { 853 $can_land = true; 854 } 855 856 $action = id(new PhabricatorActionView()) 857 ->setName(pht('Land Revision')) 858 ->setIcon('fa-fighter-jet') 859 ->setHref("/differential/revision/operation/{$revision_id}/") 860 ->setWorkflow(true) 861 ->setDisabled(!$can_land); 862 863 $curtain->addAction($action); 864 } 865 866 return $curtain; 867 } 868 869 /** 870 * @param array<DifferentialDiff> $diffs 871 */ 872 private function loadHistoryDiffStatus(array $diffs) { 873 assert_instances_of($diffs, DifferentialDiff::class); 874 875 $diff_phids = mpull($diffs, 'getPHID'); 876 $bad_unit_status = array( 877 ArcanistUnitTestResult::RESULT_FAIL, 878 ArcanistUnitTestResult::RESULT_BROKEN, 879 ); 880 881 $message = new HarbormasterBuildUnitMessage(); 882 $target = new HarbormasterBuildTarget(); 883 $build = new HarbormasterBuild(); 884 $buildable = new HarbormasterBuildable(); 885 886 $broken_diffs = queryfx_all( 887 $message->establishConnection('r'), 888 'SELECT distinct a.buildablePHID 889 FROM %T m 890 JOIN %T t ON m.buildTargetPHID = t.phid 891 JOIN %T b ON t.buildPHID = b.phid 892 JOIN %T a ON b.buildablePHID = a.phid 893 WHERE a.buildablePHID IN (%Ls) 894 AND m.result in (%Ls)', 895 $message->getTableName(), 896 $target->getTableName(), 897 $build->getTableName(), 898 $buildable->getTableName(), 899 $diff_phids, 900 $bad_unit_status); 901 902 $unit_status = array(); 903 foreach ($broken_diffs as $broken) { 904 $phid = $broken['buildablePHID']; 905 $unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL; 906 } 907 908 return $unit_status; 909 } 910 911 private function loadChangesetsAndVsMap( 912 DifferentialDiff $target, 913 ?DifferentialDiff $diff_vs = null, 914 ?PhabricatorRepository $repository = null) { 915 $viewer = $this->getViewer(); 916 917 $load_diffs = array($target); 918 if ($diff_vs) { 919 $load_diffs[] = $diff_vs; 920 } 921 922 $raw_changesets = id(new DifferentialChangesetQuery()) 923 ->setViewer($viewer) 924 ->withDiffs($load_diffs) 925 ->execute(); 926 $changeset_groups = mgroup($raw_changesets, 'getDiffID'); 927 928 $changesets = idx($changeset_groups, $target->getID(), array()); 929 $changesets = mpull($changesets, null, 'getID'); 930 931 $refs = array(); 932 $vs_map = array(); 933 $vs_changesets = array(); 934 $must_compare = array(); 935 if ($diff_vs) { 936 $vs_id = $diff_vs->getID(); 937 $vs_changesets_path_map = array(); 938 foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { 939 $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); 940 $vs_changesets_path_map[$path] = $changeset; 941 $vs_changesets[$changeset->getID()] = $changeset; 942 } 943 944 foreach ($changesets as $key => $changeset) { 945 $path = $changeset->getAbsoluteRepositoryPath($repository, $target); 946 if (isset($vs_changesets_path_map[$path])) { 947 $vs_map[$changeset->getID()] = 948 $vs_changesets_path_map[$path]->getID(); 949 $refs[$changeset->getID()] = 950 $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); 951 unset($vs_changesets_path_map[$path]); 952 953 $must_compare[] = $changeset->getID(); 954 955 } else { 956 $refs[$changeset->getID()] = $changeset->getID(); 957 } 958 } 959 960 foreach ($vs_changesets_path_map as $path => $changeset) { 961 $changesets[$changeset->getID()] = $changeset; 962 $vs_map[$changeset->getID()] = -1; 963 $refs[$changeset->getID()] = $changeset->getID().'/-1'; 964 } 965 966 } else { 967 foreach ($changesets as $changeset) { 968 $refs[$changeset->getID()] = $changeset->getID(); 969 } 970 } 971 972 $changesets = msort($changesets, 'getSortKey'); 973 974 // See T13137. When displaying the diff between two updates, hide any 975 // changesets which haven't actually changed. 976 $this->hiddenChangesets = array(); 977 foreach ($must_compare as $changeset_id) { 978 $changeset = $changesets[$changeset_id]; 979 $vs_changeset = $vs_changesets[$vs_map[$changeset_id]]; 980 981 if ($changeset->hasSameEffectAs($vs_changeset)) { 982 $this->hiddenChangesets[$changeset_id] = $changesets[$changeset_id]; 983 } 984 } 985 986 return array($changesets, $vs_map, $vs_changesets, $refs); 987 } 988 989 /** 990 * @param PhabricatorRepository $repository 991 * @param array<DifferentialChangeset> $unfolded_changesets 992 */ 993 private function buildSymbolIndexes( 994 PhabricatorRepository $repository, 995 array $unfolded_changesets) { 996 assert_instances_of($unfolded_changesets, DifferentialChangeset::class); 997 998 $engine = PhabricatorSyntaxHighlighter::newEngine(); 999 1000 $langs = $repository->getSymbolLanguages(); 1001 $langs = nonempty($langs, array()); 1002 1003 $sources = $repository->getSymbolSources(); 1004 $sources = nonempty($sources, array()); 1005 1006 $symbol_indexes = array(); 1007 1008 if ($langs && $sources) { 1009 $have_symbols = id(new DiffusionSymbolQuery()) 1010 ->existsSymbolsInRepository($repository->getPHID()); 1011 if (!$have_symbols) { 1012 return $symbol_indexes; 1013 } 1014 } 1015 1016 $repository_phids = array_merge( 1017 array($repository->getPHID()), 1018 $sources); 1019 1020 $indexed_langs = array_fill_keys($langs, true); 1021 foreach ($unfolded_changesets as $key => $changeset) { 1022 $lang = $engine->getLanguageFromFilename($changeset->getFilename()); 1023 if (empty($indexed_langs) || isset($indexed_langs[$lang])) { 1024 $symbol_indexes[$key] = array( 1025 'lang' => $lang, 1026 'repositories' => $repository_phids, 1027 ); 1028 } 1029 } 1030 1031 return $symbol_indexes; 1032 } 1033 1034 /** 1035 * @param array<DifferentialChangeset> $changesets 1036 * @param DifferentialDiff $target 1037 * @param PhabricatorRepository $repository 1038 */ 1039 private function loadOtherRevisions( 1040 array $changesets, 1041 DifferentialDiff $target, 1042 PhabricatorRepository $repository) { 1043 assert_instances_of($changesets, DifferentialChangeset::class); 1044 1045 $viewer = $this->getViewer(); 1046 1047 $paths = array(); 1048 foreach ($changesets as $changeset) { 1049 $paths[] = $changeset->getAbsoluteRepositoryPath( 1050 $repository, 1051 $target); 1052 } 1053 1054 if (!$paths) { 1055 return array(); 1056 } 1057 1058 $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); 1059 1060 $query = id(new DifferentialRevisionQuery()) 1061 ->setViewer($viewer) 1062 ->withIsOpen(true) 1063 ->withUpdatedEpochBetween($recent, null) 1064 ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) 1065 ->setLimit(10) 1066 ->needFlags(true) 1067 ->needDrafts(true) 1068 ->needReviewers(true) 1069 ->withRepositoryPHIDs( 1070 array( 1071 $repository->getPHID(), 1072 )) 1073 ->withPaths($paths); 1074 1075 $results = $query->execute(); 1076 1077 // Strip out *this* revision. 1078 foreach ($results as $key => $result) { 1079 if ($result->getID() == $this->revisionID) { 1080 unset($results[$key]); 1081 break; 1082 } 1083 } 1084 1085 return $results; 1086 } 1087 1088 /** 1089 * @param array<DifferentialRevision> $revisions 1090 */ 1091 private function renderOtherRevisions(array $revisions) { 1092 assert_instances_of($revisions, DifferentialRevision::class); 1093 $viewer = $this->getViewer(); 1094 1095 $header = id(new PHUIHeaderView()) 1096 ->setHeader(pht('Recent Similar Revisions')); 1097 1098 return id(new DifferentialRevisionListView()) 1099 ->setViewer($viewer) 1100 ->setRevisions($revisions) 1101 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 1102 ->setNoBox(true); 1103 } 1104 1105 /** 1106 * @param DifferentialRevision $revision 1107 * @param array<DifferentialChangeset> $changesets 1108 * @param array<DifferentialChangeset> $vs_changesets 1109 * @param array $vs_map 1110 * @param ?PhabricatorRepository $repository 1111 */ 1112 private function buildRawDiffResponse( 1113 DifferentialRevision $revision, 1114 array $changesets, 1115 array $vs_changesets, 1116 array $vs_map, 1117 ?PhabricatorRepository $repository = null) { 1118 1119 assert_instances_of($changesets, DifferentialChangeset::class); 1120 assert_instances_of($vs_changesets, DifferentialChangeset::class); 1121 1122 $viewer = $this->getViewer(); 1123 1124 id(new DifferentialHunkQuery()) 1125 ->setViewer($viewer) 1126 ->withChangesets($changesets) 1127 ->needAttachToChangesets(true) 1128 ->execute(); 1129 1130 $diff = new DifferentialDiff(); 1131 $diff->attachChangesets($changesets); 1132 $raw_changes = $diff->buildChangesList(); 1133 $changes = array(); 1134 foreach ($raw_changes as $changedict) { 1135 $changes[] = ArcanistDiffChange::newFromDictionary($changedict); 1136 } 1137 1138 $loader = id(new PhabricatorFileBundleLoader()) 1139 ->setViewer($viewer); 1140 1141 $bundle = ArcanistBundle::newFromChanges($changes); 1142 $bundle->setLoadFileDataCallback(array($loader, 'loadFileData')); 1143 1144 $vcs = $repository ? $repository->getVersionControlSystem() : null; 1145 switch ($vcs) { 1146 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 1147 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 1148 $raw_diff = $bundle->toGitPatch(); 1149 break; 1150 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 1151 default: 1152 $raw_diff = $bundle->toUnifiedDiff(); 1153 break; 1154 } 1155 1156 $request_uri = $this->getRequest()->getRequestURI(); 1157 1158 // Filename ends up being something like D123.1692295858.diff 1159 // This discards some options in the query string that may affect the diff 1160 // response, but is intentional to avoid spammy titles from bot requests. 1161 $timestamp = 1162 PhabricatorTime::getNow() + 1163 phutil_units('24 hours in seconds'); 1164 $file_name = ltrim($request_uri->getPath(), '/').'.'.$timestamp.'.diff'; 1165 1166 $iterator = new ArrayIterator(array($raw_diff)); 1167 1168 $source = id(new PhabricatorIteratorFileUploadSource()) 1169 ->setName($file_name) 1170 ->setMIMEType('text/plain') 1171 ->setRelativeTTL(phutil_units('24 hours in seconds')) 1172 ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) 1173 ->setIterator($iterator); 1174 1175 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 1176 $file = $source->uploadFile(); 1177 $file->attachToObject($revision->getPHID()); 1178 unset($unguarded); 1179 1180 return $file->getRedirectResponse(); 1181 } 1182 1183 private function buildTransactions( 1184 DifferentialRevision $revision, 1185 DifferentialDiff $left_diff, 1186 DifferentialDiff $right_diff, 1187 array $old_ids, 1188 array $new_ids) { 1189 1190 $timeline = $this->buildTransactionTimeline( 1191 $revision, 1192 new DifferentialTransactionQuery(), 1193 $engine = null, 1194 array( 1195 'left' => $left_diff->getID(), 1196 'right' => $right_diff->getID(), 1197 'old' => implode(',', $old_ids), 1198 'new' => implode(',', $new_ids), 1199 )); 1200 1201 return $timeline; 1202 } 1203 1204 private function buildRevisionWarnings( 1205 DifferentialRevision $revision, 1206 PhabricatorCustomFieldList $field_list, 1207 array $warning_handle_map, 1208 array $handles) { 1209 1210 $warnings = array(); 1211 foreach ($field_list->getFields() as $key => $field) { 1212 $phids = idx($warning_handle_map, $key, array()); 1213 $field_handles = array_select_keys($handles, $phids); 1214 $field_warnings = $field->getWarningsForRevisionHeader($field_handles); 1215 foreach ($field_warnings as $warning) { 1216 $warnings[] = $warning; 1217 } 1218 } 1219 1220 return $warnings; 1221 } 1222 1223 private function buildDiffDetailView( 1224 array $diffs, 1225 DifferentialRevision $revision, 1226 PhabricatorCustomFieldList $field_list) { 1227 $viewer = $this->getViewer(); 1228 1229 $fields = array(); 1230 foreach ($field_list->getFields() as $field) { 1231 if ($field->shouldAppearInDiffPropertyView()) { 1232 $fields[] = $field; 1233 } 1234 } 1235 1236 if (!$fields) { 1237 return null; 1238 } 1239 1240 $property_lists = array(); 1241 foreach ($this->getDiffTabLabels($diffs) as $tab) { 1242 list($label, $diff) = $tab; 1243 1244 $property_lists[] = array( 1245 $label, 1246 $this->buildDiffPropertyList($diff, $revision, $fields), 1247 ); 1248 } 1249 1250 $tab_group = id(new PHUITabGroupView()) 1251 ->setHideSingleTab(true); 1252 1253 foreach ($property_lists as $key => $property_list) { 1254 list($tab_name, $list_view) = $property_list; 1255 1256 $tab = id(new PHUITabView()) 1257 ->setKey($key) 1258 ->setName($tab_name) 1259 ->appendChild($list_view); 1260 1261 $tab_group->addTab($tab); 1262 $tab_group->selectTab($key); 1263 } 1264 1265 return id(new PHUIObjectBoxView()) 1266 ->setHeaderText(pht('Diff Detail')) 1267 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 1268 ->setViewer($viewer) 1269 ->addTabGroup($tab_group); 1270 } 1271 1272 private function buildDiffPropertyList( 1273 DifferentialDiff $diff, 1274 DifferentialRevision $revision, 1275 array $fields) { 1276 $viewer = $this->getViewer(); 1277 1278 $view = id(new PHUIPropertyListView()) 1279 ->setViewer($viewer) 1280 ->setObject($diff); 1281 1282 foreach ($fields as $field) { 1283 $label = $field->renderDiffPropertyViewLabel($diff); 1284 $value = $field->renderDiffPropertyViewValue($diff); 1285 if ($value !== null) { 1286 $view->addProperty($label, $value); 1287 } 1288 } 1289 1290 return $view; 1291 } 1292 1293 private function buildOperationsBox(DifferentialRevision $revision) { 1294 $viewer = $this->getViewer(); 1295 1296 // Save a query if we can't possibly have pending operations. 1297 $repository = $revision->getRepository(); 1298 if (!$repository || !$repository->canPerformAutomation()) { 1299 return null; 1300 } 1301 1302 $operations = id(new DrydockRepositoryOperationQuery()) 1303 ->setViewer($viewer) 1304 ->withObjectPHIDs(array($revision->getPHID())) 1305 ->withIsDismissed(false) 1306 ->withOperationTypes( 1307 array( 1308 DrydockLandRepositoryOperation::OPCONST, 1309 )) 1310 ->execute(); 1311 if (!$operations) { 1312 return null; 1313 } 1314 1315 $state_fail = DrydockRepositoryOperation::STATE_FAIL; 1316 1317 // We're going to show the oldest operation which hasn't failed, or the 1318 // most recent failure if they're all failures. 1319 $operations = msort($operations, 'getID'); 1320 foreach ($operations as $operation) { 1321 if ($operation->getOperationState() != $state_fail) { 1322 break; 1323 } 1324 } 1325 1326 // If we found a completed operation, don't render anything. We don't want 1327 // to show an older error after the thing worked properly. 1328 if ($operation->isDone()) { 1329 return null; 1330 } 1331 1332 $box_view = id(new PHUIObjectBoxView()) 1333 ->setHeaderText(pht('Active Operations')) 1334 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); 1335 1336 return id(new DrydockRepositoryOperationStatusView()) 1337 ->setUser($viewer) 1338 ->setBoxView($box_view) 1339 ->setOperation($operation); 1340 } 1341 1342 private function buildUnitMessagesView( 1343 DifferentialDiff $diff, 1344 DifferentialRevision $revision) { 1345 $viewer = $this->getViewer(); 1346 1347 if (!$diff->getBuildable()) { 1348 return null; 1349 } 1350 1351 if (!$diff->getUnitMessages()) { 1352 return null; 1353 } 1354 1355 $interesting_messages = array(); 1356 foreach ($diff->getUnitMessages() as $message) { 1357 switch ($message->getResult()) { 1358 case ArcanistUnitTestResult::RESULT_PASS: 1359 case ArcanistUnitTestResult::RESULT_SKIP: 1360 break; 1361 default: 1362 $interesting_messages[] = $message; 1363 break; 1364 } 1365 } 1366 1367 if (!$interesting_messages) { 1368 return null; 1369 } 1370 1371 return id(new HarbormasterUnitSummaryView()) 1372 ->setViewer($viewer) 1373 ->setBuildable($diff->getBuildable()) 1374 ->setUnitMessages($diff->getUnitMessages()) 1375 ->setLimit(5) 1376 ->setShowViewAll(true); 1377 } 1378 1379 /** 1380 * @param DifferentialRevision $revision 1381 * @param array<DifferentialDiff> $diffs 1382 */ 1383 private function getOldDiffID(DifferentialRevision $revision, array $diffs) { 1384 assert_instances_of($diffs, DifferentialDiff::class); 1385 $request = $this->getRequest(); 1386 1387 $diffs = mpull($diffs, null, 'getID'); 1388 1389 $is_new = ($request->getURIData('filter') === 'new'); 1390 $old_id = $request->getInt('vs'); 1391 1392 // This is ambiguous, so just 404 rather than trying to figure out what 1393 // the user expects. 1394 if ($is_new && $old_id) { 1395 return new Aphront404Response(); 1396 } 1397 1398 if ($is_new) { 1399 $viewer = $this->getViewer(); 1400 1401 $xactions = id(new DifferentialTransactionQuery()) 1402 ->setViewer($viewer) 1403 ->withObjectPHIDs(array($revision->getPHID())) 1404 ->withAuthorPHIDs(array($viewer->getPHID())) 1405 ->setOrder('newest') 1406 ->setLimit(1) 1407 ->execute(); 1408 1409 if (!$xactions) { 1410 $this->warnings[] = id(new PHUIInfoView()) 1411 ->setTitle(pht('No Actions')) 1412 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 1413 ->appendChild( 1414 pht( 1415 'Showing all changes because you have never taken an '. 1416 'action on this revision.')); 1417 } else { 1418 $xaction = head($xactions); 1419 1420 // Find the transactions which updated this revision. We want to 1421 // figure out which diff was active when you last took an action. 1422 $updates = id(new DifferentialTransactionQuery()) 1423 ->setViewer($viewer) 1424 ->withObjectPHIDs(array($revision->getPHID())) 1425 ->withTransactionTypes( 1426 array( 1427 DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE, 1428 )) 1429 ->setOrder('oldest') 1430 ->execute(); 1431 1432 // Sort the diffs into two buckets: those older than your last action 1433 // and those newer than your last action. 1434 $older = array(); 1435 $newer = array(); 1436 foreach ($updates as $update) { 1437 // If you updated the revision with "arc diff", try to count that 1438 // update as "before your last action". 1439 if ($update->getDateCreated() <= $xaction->getDateCreated()) { 1440 $older[] = $update->getNewValue(); 1441 } else { 1442 $newer[] = $update->getNewValue(); 1443 } 1444 } 1445 1446 if (!$newer) { 1447 $this->warnings[] = id(new PHUIInfoView()) 1448 ->setTitle(pht('No Recent Updates')) 1449 ->setSeverity(PHUIInfoView::SEVERITY_WARNING) 1450 ->appendChild( 1451 pht( 1452 'Showing all changes because the diff for this revision '. 1453 'has not been updated since your last action.')); 1454 } else { 1455 $older = array_fuse($older); 1456 1457 // Find the most recent diff from before the last action. 1458 $old = null; 1459 foreach ($diffs as $diff) { 1460 if (!isset($older[$diff->getPHID()])) { 1461 break; 1462 } 1463 1464 $old = $diff; 1465 } 1466 1467 // It's possible we may not find such a diff: transactions may have 1468 // been removed from the database, for example. If we miss, just 1469 // fail into some reasonable state since 404'ing would be perplexing. 1470 if ($old) { 1471 $this->warnings[] = id(new PHUIInfoView()) 1472 ->setTitle(pht('New Changes Shown')) 1473 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) 1474 ->appendChild( 1475 pht( 1476 'Showing changes since the last action you took on this '. 1477 'revision.')); 1478 1479 $old_id = $old->getID(); 1480 } 1481 } 1482 } 1483 } 1484 1485 if ($old_id && isset($diffs[$old_id])) { 1486 return $old_id; 1487 } 1488 1489 return null; 1490 } 1491 1492 /** 1493 * @param DifferentialRevision $revision 1494 * @param array<DifferentialDiff> $diffs 1495 */ 1496 private function getNewDiffID(DifferentialRevision $revision, array $diffs) { 1497 assert_instances_of($diffs, DifferentialDiff::class); 1498 $request = $this->getRequest(); 1499 1500 $diffs = mpull($diffs, null, 'getID'); 1501 1502 $is_new = ($request->getURIData('filter') === 'new'); 1503 $new_id = $request->getInt('id'); 1504 1505 if ($is_new && $new_id) { 1506 return new Aphront404Response(); 1507 } 1508 1509 if ($new_id && isset($diffs[$new_id])) { 1510 return $new_id; 1511 } 1512 1513 return (int)last_key($diffs); 1514 } 1515 1516}