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