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