@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class 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}