Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / diffusion / controller / DiffusionCommitController.php
blob7b893b1e52704dd6012e49676a1fef878cccd754
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 $has_callsign = strlen($request->getURIData('repositoryCallsign'));
31 $has_id = strlen($request->getURIData('repositoryID'));
32 if (!$has_callsign && !$has_id) {
33 $canonical_uri = $repository->getCommitURI($commit_identifier);
34 return id(new AphrontRedirectResponse())
35 ->setURI($canonical_uri);
38 if ($request->getStr('diff')) {
39 return $this->buildRawDiffResponse($drequest);
42 $commits = id(new DiffusionCommitQuery())
43 ->setViewer($viewer)
44 ->withRepository($repository)
45 ->withIdentifiers(array($commit_identifier))
46 ->needCommitData(true)
47 ->needAuditRequests(true)
48 ->needAuditAuthority(array($viewer))
49 ->setLimit(100)
50 ->needIdentities(true)
51 ->execute();
53 $multiple_results = count($commits) > 1;
55 $crumbs = $this->buildCrumbs(array(
56 'commit' => !$multiple_results,
57 ));
58 $crumbs->setBorder(true);
60 if (!$commits) {
61 if (!$this->getCommitExists()) {
62 return new Aphront404Response();
65 $error = id(new PHUIInfoView())
66 ->setTitle(pht('Commit Still Parsing'))
67 ->appendChild(
68 pht(
69 'Failed to load the commit because the commit has not been '.
70 'parsed yet.'));
72 $title = pht('Commit Still Parsing');
74 return $this->newPage()
75 ->setTitle($title)
76 ->setCrumbs($crumbs)
77 ->appendChild($error);
78 } else if ($multiple_results) {
80 $warning_message =
81 pht(
82 'The identifier %s is ambiguous and matches more than one commit.',
83 phutil_tag(
84 'strong',
85 array(),
86 $commit_identifier));
88 $error = id(new PHUIInfoView())
89 ->setTitle(pht('Ambiguous Commit'))
90 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
91 ->appendChild($warning_message);
93 $list = id(new DiffusionCommitGraphView())
94 ->setViewer($viewer)
95 ->setCommits($commits);
97 $crumbs->addTextCrumb(pht('Ambiguous Commit'));
99 $matched_commits = id(new PHUITwoColumnView())
100 ->setFooter(array(
101 $error,
102 $list,
105 return $this->newPage()
106 ->setTitle(pht('Ambiguous Commit'))
107 ->setCrumbs($crumbs)
108 ->appendChild($matched_commits);
109 } else {
110 $commit = head($commits);
113 $audit_requests = $commit->getAudits();
115 $commit_data = $commit->getCommitData();
116 $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub');
117 $error_panel = null;
118 $unpublished_panel = null;
120 $hard_limit = 1000;
122 if ($commit->isImported()) {
123 $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
124 $drequest);
125 $change_query->setLimit($hard_limit + 1);
126 $changes = $change_query->loadChanges();
127 } else {
128 $changes = array();
131 $was_limited = (count($changes) > $hard_limit);
132 if ($was_limited) {
133 $changes = array_slice($changes, 0, $hard_limit);
136 $count = count($changes);
138 $is_unreadable = false;
139 $hint = null;
140 if (!$count || $commit->isUnreachable()) {
141 $hint = id(new DiffusionCommitHintQuery())
142 ->setViewer($viewer)
143 ->withRepositoryPHIDs(array($repository->getPHID()))
144 ->withOldCommitIdentifiers(array($commit->getCommitIdentifier()))
145 ->executeOne();
146 if ($hint) {
147 $is_unreadable = $hint->isUnreadable();
151 if ($is_foreign) {
152 $subpath = $commit_data->getCommitDetail('svn-subpath');
154 $error_panel = new PHUIInfoView();
155 $error_panel->setTitle(pht('Commit Not Tracked'));
156 $error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING);
157 $error_panel->appendChild(
158 pht(
159 "This Diffusion repository is configured to track only one ".
160 "subdirectory of the entire Subversion repository, and this commit ".
161 "didn't affect the tracked subdirectory ('%s'), so no ".
162 "information is available.",
163 $subpath));
164 } else {
165 $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine();
166 $engine->setConfig('viewer', $viewer);
168 $commit_tag = $this->renderCommitHashTag($drequest);
169 $header = id(new PHUIHeaderView())
170 ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail')))
171 ->setHeaderIcon('fa-code-fork')
172 ->addTag($commit_tag);
174 if (!$commit->isAuditStatusNoAudit()) {
175 $status = $commit->getAuditStatusObject();
177 $icon = $status->getIcon();
178 $color = $status->getColor();
179 $status = $status->getName();
181 $header->setStatus($icon, $color, $status);
184 $curtain = $this->buildCurtain($commit, $repository);
185 $details = $this->buildPropertyListView(
186 $commit,
187 $commit_data,
188 $audit_requests);
190 $message = $commit_data->getCommitMessage();
192 $revision = $commit->getCommitIdentifier();
193 $message = $this->linkBugtraq($message);
194 $message = $engine->markupText($message);
196 $detail_list = new PHUIPropertyListView();
197 $detail_list->addTextContent(
198 phutil_tag(
199 'div',
200 array(
201 'class' => 'diffusion-commit-message phabricator-remarkup',
203 $message));
205 if ($commit->isUnreachable()) {
206 $did_rewrite = false;
207 if ($hint) {
208 if ($hint->isRewritten()) {
209 $rewritten = id(new DiffusionCommitQuery())
210 ->setViewer($viewer)
211 ->withRepository($repository)
212 ->withIdentifiers(array($hint->getNewCommitIdentifier()))
213 ->executeOne();
214 if ($rewritten) {
215 $did_rewrite = true;
216 $rewritten_uri = $rewritten->getURI();
217 $rewritten_name = $rewritten->getLocalName();
219 $rewritten_link = phutil_tag(
220 'a',
221 array(
222 'href' => $rewritten_uri,
224 $rewritten_name);
226 $this->commitErrors[] = pht(
227 'This commit was rewritten after it was published, which '.
228 'changed the commit hash. This old version of the commit is '.
229 'no longer reachable from any branch, tag or ref. The new '.
230 'version of this commit is %s.',
231 $rewritten_link);
236 if (!$did_rewrite) {
237 $this->commitErrors[] = pht(
238 'This commit has been deleted in the repository: it is no longer '.
239 'reachable from any branch, tag, or ref.');
242 if (!$commit->isPermanentCommit()) {
243 $nonpermanent_tag = id(new PHUITagView())
244 ->setType(PHUITagView::TYPE_SHADE)
245 ->setName(pht('Unpublished'))
246 ->setColor(PHUITagView::COLOR_ORANGE);
248 $header->addTag($nonpermanent_tag);
250 $holds = $commit_data->newPublisherHoldReasons();
252 $reasons = array();
253 foreach ($holds as $hold) {
254 $reasons[] = array(
255 phutil_tag('strong', array(), pht('%s:', $hold->getName())),
256 ' ',
257 $hold->getSummary(),
261 if (!$holds) {
262 $reasons[] = pht('No further details are available.');
265 $doc_href = PhabricatorEnv::getDoclink(
266 'Diffusion User Guide: Permanent Refs');
267 $doc_link = phutil_tag(
268 'a',
269 array(
270 'href' => $doc_href,
271 'target' => '_blank',
273 pht('Learn More'));
275 $title = array(
276 pht('Unpublished Commit'),
277 pht(" \xC2\xB7 "),
278 $doc_link,
281 $unpublished_panel = id(new PHUIInfoView())
282 ->setTitle($title)
283 ->setErrors($reasons)
284 ->setSeverity(PHUIInfoView::SEVERITY_WARNING);
288 if ($this->getCommitErrors()) {
289 $error_panel = id(new PHUIInfoView())
290 ->appendChild($this->getCommitErrors())
291 ->setSeverity(PHUIInfoView::SEVERITY_WARNING);
295 $timeline = $this->buildComments($commit);
296 $merge_table = $this->buildMergesTable($commit);
298 $show_changesets = false;
299 $info_panel = null;
300 $change_list = null;
301 $change_table = null;
302 if ($is_unreadable) {
303 $info_panel = $this->renderStatusMessage(
304 pht('Unreadable Commit'),
305 pht(
306 'This commit has been marked as unreadable by an administrator. '.
307 'It may have been corrupted or created improperly by an external '.
308 'tool.'));
309 } else if ($is_foreign) {
310 // Don't render anything else.
311 } else if (!$commit->isImported()) {
312 $info_panel = $this->renderStatusMessage(
313 pht('Still Importing...'),
314 pht(
315 'This commit is still importing. Changes will be visible once '.
316 'the import finishes.'));
317 } else if (!count($changes)) {
318 $info_panel = $this->renderStatusMessage(
319 pht('Empty Commit'),
320 pht(
321 'This commit is empty and does not affect any paths.'));
322 } else if ($was_limited) {
323 $info_panel = $this->renderStatusMessage(
324 pht('Very Large Commit'),
325 pht(
326 'This commit is very large, and affects more than %d files. '.
327 'Changes are not shown.',
328 $hard_limit));
329 } else if (!$this->getCommitExists()) {
330 $info_panel = $this->renderStatusMessage(
331 pht('Commit No Longer Exists'),
332 pht('This commit no longer exists in the repository.'));
333 } else {
334 $show_changesets = true;
336 // The user has clicked "Show All Changes", and we should show all the
337 // changes inline even if there are more than the soft limit.
338 $show_all_details = $request->getBool('show_all');
340 $change_header = id(new PHUIHeaderView())
341 ->setHeader(pht('Changes (%s)', new PhutilNumber($count)));
343 $warning_view = null;
344 if ($count > self::CHANGES_LIMIT && !$show_all_details) {
345 $button = id(new PHUIButtonView())
346 ->setText(pht('Show All Changes'))
347 ->setHref('?show_all=true')
348 ->setTag('a')
349 ->setIcon('fa-files-o');
351 $warning_view = id(new PHUIInfoView())
352 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
353 ->setTitle(pht('Very Large Commit'))
354 ->appendChild(
355 pht('This commit is very large. Load each file individually.'));
357 $change_header->addActionLink($button);
360 $changesets = DiffusionPathChange::convertToDifferentialChangesets(
361 $viewer,
362 $changes);
364 // TODO: This table and panel shouldn't really be separate, but we need
365 // to clean up the "Load All Files" interaction first.
366 $change_table = $this->buildTableOfContents(
367 $changesets,
368 $change_header,
369 $warning_view);
371 $vcs = $repository->getVersionControlSystem();
372 switch ($vcs) {
373 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
374 $vcs_supports_directory_changes = true;
375 break;
376 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
377 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
378 $vcs_supports_directory_changes = false;
379 break;
380 default:
381 throw new Exception(pht('Unknown VCS.'));
384 $references = array();
385 foreach ($changesets as $key => $changeset) {
386 $file_type = $changeset->getFileType();
387 if ($file_type == DifferentialChangeType::FILE_DIRECTORY) {
388 if (!$vcs_supports_directory_changes) {
389 unset($changesets[$key]);
390 continue;
394 $references[$key] = $drequest->generateURI(
395 array(
396 'action' => 'rendering-ref',
397 'path' => $changeset->getFilename(),
401 // TODO: Some parts of the views still rely on properties of the
402 // DifferentialChangeset. Make the objects ephemeral to make sure we don't
403 // accidentally save them, and then set their ID to the appropriate ID for
404 // this application (the path IDs).
405 $path_ids = array_flip(mpull($changes, 'getPath'));
406 foreach ($changesets as $changeset) {
407 $changeset->makeEphemeral();
408 $changeset->setID($path_ids[$changeset->getFilename()]);
411 if ($count <= self::CHANGES_LIMIT || $show_all_details) {
412 $visible_changesets = $changesets;
413 } else {
414 $visible_changesets = array();
416 $inlines = id(new DiffusionDiffInlineCommentQuery())
417 ->setViewer($viewer)
418 ->withCommitPHIDs(array($commit->getPHID()))
419 ->withPublishedComments(true)
420 ->withPublishableComments(true)
421 ->execute();
422 $inlines = mpull($inlines, 'newInlineCommentObject');
424 $path_ids = mpull($inlines, null, 'getPathID');
425 foreach ($changesets as $key => $changeset) {
426 if (array_key_exists($changeset->getID(), $path_ids)) {
427 $visible_changesets[$key] = $changeset;
432 $change_list_title = $commit->getDisplayName();
434 $change_list = new DifferentialChangesetListView();
435 $change_list->setTitle($change_list_title);
436 $change_list->setChangesets($changesets);
437 $change_list->setVisibleChangesets($visible_changesets);
438 $change_list->setRenderingReferences($references);
439 $change_list->setRenderURI($repository->getPathURI('diff/'));
440 $change_list->setRepository($repository);
441 $change_list->setUser($viewer);
442 $change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
444 // TODO: Try to setBranch() to something reasonable here?
446 $change_list->setStandaloneURI(
447 $repository->getPathURI('diff/'));
449 $change_list->setRawFileURIs(
450 // TODO: Implement this, somewhat tricky if there's an octopus merge
451 // or whatever?
452 null,
453 $repository->getPathURI('diff/?view=r'));
455 $change_list->setInlineCommentControllerURI(
456 '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/');
460 $add_comment = $this->renderAddCommentPanel(
461 $commit,
462 $timeline);
464 $filetree = id(new DifferentialFileTreeEngine())
465 ->setViewer($viewer)
466 ->setDisabled(!$show_changesets);
468 if ($show_changesets) {
469 $filetree->setChangesets($changesets);
472 $description_box = id(new PHUIObjectBoxView())
473 ->setHeaderText(pht('Description'))
474 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
475 ->appendChild($detail_list);
477 $detail_box = id(new PHUIObjectBoxView())
478 ->setHeaderText(pht('Details'))
479 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
480 ->appendChild($details);
482 $view = id(new PHUITwoColumnView())
483 ->setHeader($header)
484 ->setCurtain($curtain)
485 ->setMainColumn(
486 array(
487 $unpublished_panel,
488 $error_panel,
489 $description_box,
490 $detail_box,
491 $timeline,
492 $merge_table,
493 $info_panel,
495 ->setFooter(
496 array(
497 $change_table,
498 $change_list,
499 $add_comment,
502 $main_content = array(
503 $crumbs,
504 $view,
507 $main_content = $filetree->newView($main_content);
508 if (!$filetree->getDisabled()) {
509 $change_list->setFormationView($main_content);
512 $page = $this->newPage()
513 ->setTitle($commit->getDisplayName())
514 ->setPageObjectPHIDS(array($commit->getPHID()))
515 ->appendChild($main_content);
517 return $page;
521 private function buildPropertyListView(
522 PhabricatorRepositoryCommit $commit,
523 PhabricatorRepositoryCommitData $data,
524 array $audit_requests) {
526 $viewer = $this->getViewer();
527 $commit_phid = $commit->getPHID();
528 $drequest = $this->getDiffusionRequest();
529 $repository = $drequest->getRepository();
531 $view = id(new PHUIPropertyListView())
532 ->setUser($this->getRequest()->getUser())
533 ->setObject($commit);
535 $edge_query = id(new PhabricatorEdgeQuery())
536 ->withSourcePHIDs(array($commit_phid))
537 ->withEdgeTypes(array(
538 DiffusionCommitHasTaskEdgeType::EDGECONST,
539 DiffusionCommitHasRevisionEdgeType::EDGECONST,
540 DiffusionCommitRevertsCommitEdgeType::EDGECONST,
541 DiffusionCommitRevertedByCommitEdgeType::EDGECONST,
544 $edges = $edge_query->execute();
546 $task_phids = array_keys(
547 $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]);
548 $revision_phid = key(
549 $edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]);
551 $reverts_phids = array_keys(
552 $edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]);
553 $reverted_by_phids = array_keys(
554 $edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]);
556 $phids = $edge_query->getDestinationPHIDs(array($commit_phid));
559 if ($data->getCommitDetail('reviewerPHID')) {
560 $phids[] = $data->getCommitDetail('reviewerPHID');
563 $phids[] = $commit->getCommitterDisplayPHID();
564 $phids[] = $commit->getAuthorDisplayPHID();
566 // NOTE: We should never normally have more than a single push log, but
567 // it can occur naturally if a commit is pushed, then the branch it was
568 // on is deleted, then the commit is pushed again (or through other similar
569 // chains of events). This should be rare, but does not indicate a bug
570 // or data issue.
572 // NOTE: We never query push logs in SVN because the committer is always
573 // the pusher and the commit time is always the push time; the push log
574 // is redundant and we save a query by skipping it.
576 $push_logs = array();
577 if ($repository->isHosted() && !$repository->isSVN()) {
578 $push_logs = id(new PhabricatorRepositoryPushLogQuery())
579 ->setViewer($viewer)
580 ->withRepositoryPHIDs(array($repository->getPHID()))
581 ->withNewRefs(array($commit->getCommitIdentifier()))
582 ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT))
583 ->execute();
584 foreach ($push_logs as $log) {
585 $phids[] = $log->getPusherPHID();
589 $handles = array();
590 if ($phids) {
591 $handles = $this->loadViewerHandles($phids);
594 $props = array();
596 if ($audit_requests) {
597 $user_requests = array();
598 $other_requests = array();
600 foreach ($audit_requests as $audit_request) {
601 if ($audit_request->isUser()) {
602 $user_requests[] = $audit_request;
603 } else {
604 $other_requests[] = $audit_request;
608 if ($user_requests) {
609 $view->addProperty(
610 pht('Auditors'),
611 $this->renderAuditStatusView($commit, $user_requests));
614 if ($other_requests) {
615 $view->addProperty(
616 pht('Group Auditors'),
617 $this->renderAuditStatusView($commit, $other_requests));
621 $provenance_list = new PHUIStatusListView();
623 $author_view = $commit->newCommitAuthorView($viewer);
624 if ($author_view) {
625 $author_date = $data->getAuthorEpoch();
626 $author_date = phabricator_datetime($author_date, $viewer);
628 $provenance_list->addItem(
629 id(new PHUIStatusItemView())
630 ->setTarget($author_view)
631 ->setNote(pht('Authored on %s', $author_date)));
634 if (!$commit->isAuthorSameAsCommitter()) {
635 $committer_view = $commit->newCommitCommitterView($viewer);
636 if ($committer_view) {
637 $committer_date = $commit->getEpoch();
638 $committer_date = phabricator_datetime($committer_date, $viewer);
640 $provenance_list->addItem(
641 id(new PHUIStatusItemView())
642 ->setTarget($committer_view)
643 ->setNote(pht('Committed on %s', $committer_date)));
647 if ($push_logs) {
648 $pushed_list = new PHUIStatusListView();
650 foreach ($push_logs as $push_log) {
651 $pusher_date = $push_log->getEpoch();
652 $pusher_date = phabricator_datetime($pusher_date, $viewer);
654 $pusher_view = $handles[$push_log->getPusherPHID()]->renderLink();
656 $provenance_list->addItem(
657 id(new PHUIStatusItemView())
658 ->setTarget($pusher_view)
659 ->setNote(pht('Pushed on %s', $pusher_date)));
663 $view->addProperty(pht('Provenance'), $provenance_list);
665 $reviewer_phid = $data->getCommitDetail('reviewerPHID');
666 if ($reviewer_phid) {
667 $view->addProperty(
668 pht('Reviewer'),
669 $handles[$reviewer_phid]->renderLink());
672 if ($revision_phid) {
673 $view->addProperty(
674 pht('Differential Revision'),
675 $handles[$revision_phid]->renderLink());
678 $parents = $this->getCommitParents();
679 if ($parents) {
680 $view->addProperty(
681 pht('Parents'),
682 $viewer->renderHandleList(mpull($parents, 'getPHID')));
685 if ($this->getCommitExists()) {
686 $view->addProperty(
687 pht('Branches'),
688 phutil_tag(
689 'span',
690 array(
691 'id' => 'commit-branches',
693 pht('Unknown')));
695 $view->addProperty(
696 pht('Tags'),
697 phutil_tag(
698 'span',
699 array(
700 'id' => 'commit-tags',
702 pht('Unknown')));
704 $identifier = $commit->getCommitIdentifier();
705 $root = $repository->getPathURI("commit/{$identifier}");
706 Javelin::initBehavior(
707 'diffusion-commit-branches',
708 array(
709 $root.'/branches/' => 'commit-branches',
710 $root.'/tags/' => 'commit-tags',
714 $refs = $this->getCommitRefs();
715 if ($refs) {
716 $ref_links = array();
717 foreach ($refs as $ref_data) {
718 $ref_links[] = phutil_tag(
719 'a',
720 array(
721 'href' => $ref_data['href'],
723 $ref_data['ref']);
725 $view->addProperty(
726 pht('References'),
727 phutil_implode_html(', ', $ref_links));
730 if ($reverts_phids) {
731 $view->addProperty(
732 pht('Reverts'),
733 $viewer->renderHandleList($reverts_phids));
736 if ($reverted_by_phids) {
737 $view->addProperty(
738 pht('Reverted By'),
739 $viewer->renderHandleList($reverted_by_phids));
742 if ($task_phids) {
743 $task_list = array();
744 foreach ($task_phids as $phid) {
745 $task_list[] = $handles[$phid]->renderLink();
747 $task_list = phutil_implode_html(phutil_tag('br'), $task_list);
748 $view->addProperty(
749 pht('Tasks'),
750 $task_list);
753 return $view;
756 private function buildComments(PhabricatorRepositoryCommit $commit) {
757 $timeline = $this->buildTransactionTimeline(
758 $commit,
759 new PhabricatorAuditTransactionQuery());
761 $timeline->setQuoteRef($commit->getMonogram());
763 return $timeline;
766 private function renderAddCommentPanel(
767 PhabricatorRepositoryCommit $commit,
768 $timeline) {
770 $request = $this->getRequest();
771 $viewer = $request->getUser();
773 // TODO: This is pretty awkward, unify the CSS between Diffusion and
774 // Differential better.
775 require_celerity_resource('differential-core-view-css');
777 $comment_view = id(new DiffusionCommitEditEngine())
778 ->setViewer($viewer)
779 ->buildEditEngineCommentView($commit);
781 $comment_view->setTransactionTimeline($timeline);
783 return $comment_view;
786 private function buildMergesTable(PhabricatorRepositoryCommit $commit) {
787 $viewer = $this->getViewer();
788 $drequest = $this->getDiffusionRequest();
789 $repository = $drequest->getRepository();
791 $merges = $this->getCommitMerges();
792 if (!$merges) {
793 return null;
796 $limit = $this->getMergeDisplayLimit();
798 $caption = null;
799 if (count($merges) > $limit) {
800 $merges = array_slice($merges, 0, $limit);
801 $caption = new PHUIInfoView();
802 $caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
803 $caption->appendChild(
804 pht(
805 'This commit merges a very large number of changes. '.
806 'Only the first %s are shown.',
807 new PhutilNumber($limit)));
810 $commit_list = id(new DiffusionCommitGraphView())
811 ->setViewer($viewer)
812 ->setDiffusionRequest($drequest)
813 ->setHistory($merges);
815 $panel = id(new PHUIObjectBoxView())
816 ->setHeaderText(pht('Merged Changes'))
817 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
818 ->setObjectList($commit_list->newObjectItemListView());
819 if ($caption) {
820 $panel->setInfoView($caption);
823 return $panel;
826 private function buildCurtain(
827 PhabricatorRepositoryCommit $commit,
828 PhabricatorRepository $repository) {
830 $request = $this->getRequest();
831 $viewer = $this->getViewer();
832 $curtain = $this->newCurtainView($commit);
834 $can_edit = PhabricatorPolicyFilter::hasCapability(
835 $viewer,
836 $commit,
837 PhabricatorPolicyCapability::CAN_EDIT);
839 $id = $commit->getID();
840 $edit_uri = $this->getApplicationURI("/commit/edit/{$id}/");
842 $action = id(new PhabricatorActionView())
843 ->setName(pht('Edit Commit'))
844 ->setHref($edit_uri)
845 ->setIcon('fa-pencil')
846 ->setDisabled(!$can_edit)
847 ->setWorkflow(!$can_edit);
848 $curtain->addAction($action);
850 $action = id(new PhabricatorActionView())
851 ->setName(pht('Download Raw Diff'))
852 ->setHref($request->getRequestURI()->alter('diff', true))
853 ->setIcon('fa-download');
854 $curtain->addAction($action);
856 $relationship_list = PhabricatorObjectRelationshipList::newForObject(
857 $viewer,
858 $commit);
860 $relationship_submenu = $relationship_list->newActionMenu();
861 if ($relationship_submenu) {
862 $curtain->addAction($relationship_submenu);
865 return $curtain;
868 private function buildRawDiffResponse(DiffusionRequest $drequest) {
869 $diff_info = $this->callConduitWithDiffusionRequest(
870 'diffusion.rawdiffquery',
871 array(
872 'commit' => $drequest->getCommit(),
873 'path' => $drequest->getPath(),
876 $file_phid = $diff_info['filePHID'];
878 $file = id(new PhabricatorFileQuery())
879 ->setViewer($this->getViewer())
880 ->withPHIDs(array($file_phid))
881 ->executeOne();
882 if (!$file) {
883 throw new Exception(
884 pht(
885 'Failed to load file ("%s") returned by "%s".',
886 $file_phid,
887 'diffusion.rawdiffquery'));
890 return $file->getRedirectResponse();
893 private function renderAuditStatusView(
894 PhabricatorRepositoryCommit $commit,
895 array $audit_requests) {
896 assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest');
897 $viewer = $this->getViewer();
899 $view = new PHUIStatusListView();
900 foreach ($audit_requests as $request) {
901 $status = $request->getAuditRequestStatusObject();
903 $item = new PHUIStatusItemView();
904 $item->setIcon(
905 $status->getIconIcon(),
906 $status->getIconColor(),
907 $status->getStatusName());
909 $auditor_phid = $request->getAuditorPHID();
910 $target = $viewer->renderHandle($auditor_phid);
911 $item->setTarget($target);
913 if ($commit->hasAuditAuthority($viewer, $request)) {
914 $item->setHighlighted(true);
917 $view->addItem($item);
920 return $view;
923 private function linkBugtraq($corpus) {
924 $url = PhabricatorEnv::getEnvConfig('bugtraq.url');
925 if (!strlen($url)) {
926 return $corpus;
929 $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex');
930 if (!$regexes) {
931 return $corpus;
934 $parser = id(new PhutilBugtraqParser())
935 ->setBugtraqPattern("[[ {$url} | %BUGID% ]]")
936 ->setBugtraqCaptureExpression(array_shift($regexes));
938 $select = array_shift($regexes);
939 if ($select) {
940 $parser->setBugtraqSelectExpression($select);
943 return $parser->processCorpus($corpus);
946 private function buildTableOfContents(
947 array $changesets,
948 $header,
949 $info_view) {
951 $drequest = $this->getDiffusionRequest();
952 $viewer = $this->getViewer();
954 $toc_view = id(new PHUIDiffTableOfContentsListView())
955 ->setUser($viewer)
956 ->setHeader($header)
957 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
959 if ($info_view) {
960 $toc_view->setInfoView($info_view);
963 // TODO: This is hacky, we just want access to the linkX() methods on
964 // DiffusionView.
965 $diffusion_view = id(new DiffusionEmptyResultView())
966 ->setDiffusionRequest($drequest);
968 $have_owners = PhabricatorApplication::isClassInstalledForViewer(
969 'PhabricatorOwnersApplication',
970 $viewer);
972 if (!$changesets) {
973 $have_owners = false;
976 if ($have_owners) {
977 if ($viewer->getPHID()) {
978 $packages = id(new PhabricatorOwnersPackageQuery())
979 ->setViewer($viewer)
980 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
981 ->withAuthorityPHIDs(array($viewer->getPHID()))
982 ->execute();
983 $toc_view->setAuthorityPackages($packages);
986 $repository = $drequest->getRepository();
987 $repository_phid = $repository->getPHID();
989 $control_query = id(new PhabricatorOwnersPackageQuery())
990 ->setViewer($viewer)
991 ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE))
992 ->withControl($repository_phid, mpull($changesets, 'getFilename'));
993 $control_query->execute();
996 foreach ($changesets as $changeset_id => $changeset) {
997 $path = $changeset->getFilename();
998 $anchor = $changeset->getAnchorName();
1000 $history_link = $diffusion_view->linkHistory($path);
1001 $browse_link = $diffusion_view->linkBrowse(
1002 $path,
1003 array(
1004 'type' => $changeset->getFileType(),
1007 $item = id(new PHUIDiffTableOfContentsItemView())
1008 ->setChangeset($changeset)
1009 ->setAnchor($anchor)
1010 ->setContext(
1011 array(
1012 $history_link,
1013 ' ',
1014 $browse_link,
1017 if ($have_owners) {
1018 $packages = $control_query->getControllingPackagesForPath(
1019 $repository_phid,
1020 $changeset->getFilename());
1021 $item->setPackages($packages);
1024 $toc_view->addItem($item);
1027 return $toc_view;
1030 private function loadCommitState() {
1031 $viewer = $this->getViewer();
1032 $drequest = $this->getDiffusionRequest();
1033 $repository = $drequest->getRepository();
1034 $commit = $drequest->getCommit();
1036 // TODO: We could use futures here and resolve these calls in parallel.
1038 $exceptions = array();
1040 try {
1041 $parent_refs = $this->callConduitWithDiffusionRequest(
1042 'diffusion.commitparentsquery',
1043 array(
1044 'commit' => $commit,
1047 if ($parent_refs) {
1048 $parents = id(new DiffusionCommitQuery())
1049 ->setViewer($viewer)
1050 ->withRepository($repository)
1051 ->withIdentifiers($parent_refs)
1052 ->execute();
1053 } else {
1054 $parents = array();
1057 $this->commitParents = $parents;
1058 } catch (Exception $ex) {
1059 $this->commitParents = false;
1060 $exceptions[] = $ex;
1063 $merge_limit = $this->getMergeDisplayLimit();
1065 try {
1066 if ($repository->isSVN()) {
1067 $this->commitMerges = array();
1068 } else {
1069 $merges = $this->callConduitWithDiffusionRequest(
1070 'diffusion.mergedcommitsquery',
1071 array(
1072 'commit' => $commit,
1073 'limit' => $merge_limit + 1,
1075 $this->commitMerges = DiffusionPathChange::newFromConduit($merges);
1077 } catch (Exception $ex) {
1078 $this->commitMerges = false;
1079 $exceptions[] = $ex;
1083 try {
1084 if ($repository->isGit()) {
1085 $refs = $this->callConduitWithDiffusionRequest(
1086 'diffusion.refsquery',
1087 array(
1088 'commit' => $commit,
1090 } else {
1091 $refs = array();
1094 $this->commitRefs = $refs;
1095 } catch (Exception $ex) {
1096 $this->commitRefs = false;
1097 $exceptions[] = $ex;
1100 if ($exceptions) {
1101 $exists = $this->callConduitWithDiffusionRequest(
1102 'diffusion.existsquery',
1103 array(
1104 'commit' => $commit,
1107 if ($exists) {
1108 $this->commitExists = true;
1109 foreach ($exceptions as $exception) {
1110 $this->commitErrors[] = $exception->getMessage();
1112 } else {
1113 $this->commitExists = false;
1114 $this->commitErrors[] = pht(
1115 'This commit no longer exists in the repository. It may have '.
1116 'been part of a branch which was deleted.');
1118 } else {
1119 $this->commitExists = true;
1120 $this->commitErrors = array();
1124 private function getMergeDisplayLimit() {
1125 return 50;
1128 private function getCommitExists() {
1129 if ($this->commitExists === null) {
1130 $this->loadCommitState();
1133 return $this->commitExists;
1136 private function getCommitParents() {
1137 if ($this->commitParents === null) {
1138 $this->loadCommitState();
1141 return $this->commitParents;
1144 private function getCommitRefs() {
1145 if ($this->commitRefs === null) {
1146 $this->loadCommitState();
1149 return $this->commitRefs;
1152 private function getCommitMerges() {
1153 if ($this->commitMerges === null) {
1154 $this->loadCommitState();
1157 return $this->commitMerges;
1160 private function getCommitErrors() {
1161 if ($this->commitErrors === null) {
1162 $this->loadCommitState();
1165 return $this->commitErrors;