3 final class DiffusionBrowseController
extends DiffusionController
{
7 private $corpusButtons = array();
9 public function shouldAllowPublic() {
13 public function handleRequest(AphrontRequest
$request) {
14 $response = $this->loadDiffusionContext();
19 $drequest = $this->getDiffusionRequest();
21 // Figure out if we're browsing a directory, a file, or a search result
24 $grep = $request->getStr('grep');
26 return $this->browseSearch();
29 $pager = id(new PHUIPagerView())
30 ->readFromRequest($request);
32 $results = DiffusionBrowseResultSet
::newFromConduit(
33 $this->callConduitWithDiffusionRequest(
34 'diffusion.browsequery',
36 'path' => $drequest->getPath(),
37 'commit' => $drequest->getStableCommit(),
38 'offset' => $pager->getOffset(),
39 'limit' => $pager->getPageSize() +
1,
42 $reason = $results->getReasonForEmptyResultSet();
43 $is_file = ($reason == DiffusionBrowseResultSet
::REASON_IS_FILE
);
46 return $this->browseFile();
49 $paths = $results->getPaths();
50 $paths = $pager->sliceResults($paths);
51 $results->setPaths($paths);
53 return $this->browseDirectory($results, $pager);
56 private function browseSearch() {
57 $drequest = $this->getDiffusionRequest();
58 $header = $this->buildHeaderView($drequest);
59 $path = nonempty(basename($drequest->getPath()), '/');
61 $search_results = $this->renderSearchResults();
62 $search_form = $this->renderSearchForm($path);
64 $search_form = phutil_tag(
67 'class' => 'diffusion-mobile-search-form',
71 $crumbs = $this->buildCrumbs(
77 $crumbs->setBorder(true);
79 $tabs = $this->buildTabsView('code');
81 $view = id(new PHUITwoColumnView())
90 return $this->newPage()
93 nonempty(basename($drequest->getPath()), '/'),
94 $drequest->getRepository()->getDisplayName(),
100 private function browseFile() {
101 $viewer = $this->getViewer();
102 $request = $this->getRequest();
103 $drequest = $this->getDiffusionRequest();
104 $repository = $drequest->getRepository();
106 $before = $request->getStr('before');
108 return $this->buildBeforeResponse($before);
111 $path = $drequest->getPath();
113 'commit' => $drequest->getCommit(),
114 'path' => $drequest->getPath(),
117 $view = $request->getStr('view');
120 if ($view !== 'raw') {
121 $byte_limit = PhabricatorFileStorageEngine
::getChunkThreshold();
125 'timeout' => $time_limit,
126 'byteLimit' => $byte_limit,
130 $response = $this->callConduitWithDiffusionRequest(
131 'diffusion.filecontentquery',
134 $hit_byte_limit = $response['tooHuge'];
135 $hit_time_limit = $response['tooSlow'];
137 $file_phid = $response['filePHID'];
138 $show_editor = false;
139 if ($hit_byte_limit) {
140 $corpus = $this->buildErrorCorpus(
142 'This file is larger than %s byte(s), and too large to display '.
144 phutil_format_bytes($byte_limit)));
145 } else if ($hit_time_limit) {
146 $corpus = $this->buildErrorCorpus(
148 'This file took too long to load from the repository (more than '.
150 new PhutilNumber($time_limit)));
152 $file = id(new PhabricatorFileQuery())
154 ->withPHIDs(array($file_phid))
157 throw new Exception(pht('Failed to load content file!'));
160 if ($view === 'raw') {
161 return $file->getRedirectResponse();
164 $data = $file->loadFileData();
166 $lfs_ref = $this->getGitLFSRef($repository, $data);
168 if ($view == 'git-lfs') {
169 $file = $this->loadGitLFSFile($lfs_ref);
171 // Rename the file locally so we generate a better vanity URI for
172 // it. In storage, it just has a name like "lfs-13f9a94c0923...",
173 // since we don't get any hints about possible human-readable names
175 $basename = basename($drequest->getPath());
176 $file->makeEphemeral();
177 $file->setName($basename);
179 return $file->getRedirectResponse();
182 $corpus = $this->buildGitLFSCorpus($lfs_ref);
186 $ref = id(new PhabricatorDocumentRef())
189 $engine = id(new DiffusionDocumentRenderingEngine())
190 ->setRequest($request)
191 ->setDiffusionRequest($drequest);
193 $corpus = $engine->newDocumentView($ref);
195 $this->corpusButtons
[] = $this->renderFileButton();
199 $bar = $this->buildButtonBar($drequest, $show_editor);
200 $header = $this->buildHeaderView($drequest);
201 $header->setHeaderIcon('fa-file-code-o');
203 $follow = $request->getStr('follow');
204 $follow_notice = null;
206 $follow_notice = id(new PHUIInfoView())
207 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
208 ->setTitle(pht('Unable to Continue'));
211 $follow_notice->appendChild(
213 'Unable to continue tracing the history of this file because '.
214 'this commit is the first commit in the repository.'));
217 $follow_notice->appendChild(
219 'Unable to continue tracing the history of this file because '.
220 'this commit created the file.'));
225 $renamed = $request->getStr('renamed');
226 $renamed_notice = null;
228 $renamed_notice = id(new PHUIInfoView())
229 ->setSeverity(PHUIInfoView
::SEVERITY_NOTICE
)
230 ->setTitle(pht('File Renamed'))
233 'File history passes through a rename from "%s" to "%s".',
234 $drequest->getPath(),
238 $open_revisions = $this->buildOpenRevisions();
239 $owners_list = $this->buildOwnersList($drequest);
241 $crumbs = $this->buildCrumbs(
247 $crumbs->setBorder(true);
249 $basename = basename($this->getDiffusionRequest()->getPath());
250 $tabs = $this->buildTabsView('code');
251 $bar->setRight($this->corpusButtons
);
253 $view = id(new PHUITwoColumnView())
265 $title = array($basename, $repository->getDisplayName());
267 return $this->newPage()
277 public function browseDirectory(
278 DiffusionBrowseResultSet
$results,
279 PHUIPagerView
$pager) {
281 $request = $this->getRequest();
282 $drequest = $this->getDiffusionRequest();
283 $repository = $drequest->getRepository();
285 $reason = $results->getReasonForEmptyResultSet();
287 $this->buildActionButtons($drequest, true);
288 $details = $this->buildPropertyView($drequest);
290 $header = $this->buildHeaderView($drequest);
291 $header->setHeaderIcon('fa-folder-open');
293 $empty_result = null;
294 $browse_panel = null;
295 if (!$results->isValidResults()) {
296 $empty_result = new DiffusionEmptyResultView();
297 $empty_result->setDiffusionRequest($drequest);
298 $empty_result->setDiffusionBrowseResultSet($results);
299 $empty_result->setView($request->getStr('view'));
301 $browse_table = id(new DiffusionBrowseTableView())
302 ->setDiffusionRequest($drequest)
303 ->setPaths($results->getPaths())
304 ->setUser($request->getUser());
306 $title = nonempty(basename($drequest->getPath()), '/');
307 $icon = 'fa-folder-open';
308 $browse_header = $this->buildPanelHeaderView($title, $icon);
310 $browse_panel = id(new PHUIObjectBoxView())
311 ->setHeader($browse_header)
312 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
313 ->setTable($browse_table)
314 ->addClass('diffusion-mobile-view')
318 $open_revisions = $this->buildOpenRevisions();
319 $readme = $this->renderDirectoryReadme($results);
321 $crumbs = $this->buildCrumbs(
328 $crumbs->setBorder(true);
329 $tabs = $this->buildTabsView('code');
330 $owners_list = $this->buildOwnersList($drequest);
331 $bar = id(new PHUILeftRightView())
332 ->setRight($this->corpusButtons
)
333 ->addClass('diffusion-action-bar');
335 $view = id(new PHUITwoColumnView())
349 $view->addPropertySection(pht('Details'), $details);
352 return $this->newPage()
354 nonempty(basename($drequest->getPath()), '/'),
355 $repository->getDisplayName(),
364 private function renderSearchResults() {
365 $request = $this->getRequest();
367 $drequest = $this->getDiffusionRequest();
368 $repository = $drequest->getRepository();
371 $pager = id(new PHUIPagerView())
372 ->readFromRequest($request);
375 switch ($repository->getVersionControlSystem()) {
376 case PhabricatorRepositoryType
::REPOSITORY_TYPE_SVN
:
380 if (strlen($this->getRequest()->getStr('grep'))) {
381 $search_mode = 'grep';
382 $query_string = $request->getStr('grep');
383 $results = $this->callConduitWithDiffusionRequest(
384 'diffusion.searchquery',
386 'grep' => $query_string,
387 'commit' => $drequest->getStableCommit(),
388 'path' => $drequest->getPath(),
389 'limit' => $pager->getPageSize() +
1,
390 'offset' => $pager->getOffset(),
395 $results = $pager->sliceResults($results);
399 if ($search_mode == 'grep') {
400 $table = $this->renderGrepResults($results, $query_string);
402 'File content matching "%s" under "%s"',
404 nonempty($drequest->getPath(), '/'));
405 $header = id(new PHUIHeaderView())
407 ->addClass('diffusion-search-result-header');
410 return array($header, $table, $pager);
414 private function renderGrepResults(array $results, $pattern) {
415 $drequest = $this->getDiffusionRequest();
416 require_celerity_resource('phabricator-search-results-css');
419 return id(new PHUIInfoView())
420 ->setSeverity(PHUIInfoView
::SEVERITY_NODATA
)
423 'The pattern you searched for was not found in the content of any '.
428 foreach ($results as $file) {
429 list($path, $line, $string) = $file;
430 $grouped[$path][] = array($line, $string);
434 foreach ($grouped as $path => $matches) {
435 $view[] = id(new DiffusionPatternSearchView())
437 ->setMatches($matches)
438 ->setPattern($pattern)
439 ->setDiffusionRequest($drequest)
446 private function buildButtonBar(
447 DiffusionRequest
$drequest,
450 $viewer = $this->getViewer();
451 $base_uri = $this->getRequest()->getRequestURI();
453 $repository = $drequest->getRepository();
454 $path = $drequest->getPath();
455 $line = nonempty((int)$drequest->getLine(), 1);
459 $editor_template = null;
461 $link_engine = PhabricatorEditorURIEngine
::newForViewer($viewer);
463 $link_engine->setRepository($repository);
465 $editor_uri = $link_engine->getURIForPath($path, $line);
466 $editor_template = $link_engine->getURITokensForPath($path);
470 id(new PHUIButtonView())
472 ->setText(pht('Last Change'))
473 ->setColor(PHUIButtonView
::GREY
)
475 $drequest->generateURI(
477 'action' => 'change',
479 ->setIcon('fa-backward');
483 id(new PHUIButtonView())
485 ->setText(pht('Open File'))
486 ->setHref($editor_uri)
487 ->setIcon('fa-pencil')
488 ->setID('editor_link')
489 ->setMetadata(array('template' => $editor_template))
490 ->setDisabled(!$editor_uri)
491 ->setColor(PHUIButtonView
::GREY
);
494 $bar = id(new PHUILeftRightView())
496 ->addClass('diffusion-action-bar full-mobile-buttons');
500 private function buildOwnersList(DiffusionRequest
$drequest) {
501 $viewer = $this->getViewer();
503 $have_owners = PhabricatorApplication
::isClassInstalledForViewer(
504 'PhabricatorOwnersApplication',
510 $repository = $drequest->getRepository();
512 $package_query = id(new PhabricatorOwnersPackageQuery())
514 ->withStatuses(array(PhabricatorOwnersPackage
::STATUS_ACTIVE
))
516 $repository->getPHID(),
518 $drequest->getPath(),
521 $package_query->execute();
523 $packages = $package_query->getControllingPackagesForPath(
524 $repository->getPHID(),
525 $drequest->getPath());
527 $ownership = id(new PHUIObjectItemListView())
529 ->setNoDataString(pht('No Owners'));
532 foreach ($packages as $package) {
533 $item = id(new PHUIObjectItemView())
534 ->setObject($package)
535 ->setObjectName($package->getMonogram())
536 ->setHeader($package->getName())
537 ->setHref($package->getURI());
539 $owners = $package->getOwners();
541 $owner_list = $viewer->renderHandleList(
542 mpull($owners, 'getUserPHID'));
544 $owner_list = phutil_tag('em', array(), pht('None'));
546 $item->addAttribute(pht('Owners: %s', $owner_list));
548 $auto = $package->getAutoReview();
549 $autoreview_map = PhabricatorOwnersPackage
::getAutoreviewOptionsMap();
550 $spec = idx($autoreview_map, $auto, array());
551 $name = idx($spec, 'name', $auto);
552 $item->addIcon('fa-code', $name);
554 $rule = $package->newAuditingRule();
555 $item->addIcon($rule->getIconIcon(), $rule->getDisplayName());
557 if ($package->isArchived()) {
558 $item->setDisabled(true);
561 $ownership->addItem($item);
565 $view = id(new PHUIObjectBoxView())
566 ->setHeaderText(pht('Owner Packages'))
567 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
568 ->addClass('diffusion-mobile-view')
569 ->setObjectList($ownership);
574 private function renderFileButton($file_uri = null, $label = null) {
576 $base_uri = $this->getRequest()->getRequestURI();
579 $text = pht('Download File');
581 $icon = 'fa-download';
583 $text = pht('Raw File');
584 $href = $base_uri->alter('view', 'raw');
585 $icon = 'fa-file-text';
588 if ($label !== null) {
592 $button = id(new PHUIButtonView())
597 ->setColor(PHUIButtonView
::GREY
);
602 private function renderGitLFSButton() {
603 $viewer = $this->getViewer();
605 $uri = $this->getRequest()->getRequestURI();
606 $href = $uri->alter('view', 'git-lfs');
608 $text = pht('Download from Git LFS');
609 $icon = 'fa-download';
611 return id(new PHUIButtonView())
616 ->setColor(PHUIButtonView
::GREY
);
619 private function buildErrorCorpus($message) {
620 $text = id(new PHUIBoxView())
621 ->addPadding(PHUI
::PADDING_LARGE
)
622 ->appendChild($message);
624 $header = id(new PHUIHeaderView())
625 ->setHeader(pht('Details'));
627 $box = id(new PHUIObjectBoxView())
629 ->appendChild($text);
634 private function buildBeforeResponse($before) {
635 $request = $this->getRequest();
636 $drequest = $this->getDiffusionRequest();
638 // NOTE: We need to get the grandparent so we can capture filename changes
641 $parent = $this->loadParentCommitOf($before);
642 $old_filename = null;
643 $was_created = false;
645 $grandparent = $this->loadParentCommitOf($parent);
648 $rename_query = new DiffusionRenameHistoryQuery();
649 $rename_query->setRequest($drequest);
650 $rename_query->setOldCommit($grandparent);
651 $rename_query->setViewer($request->getUser());
652 $old_filename = $rename_query->loadOldFilename();
653 $was_created = $rename_query->getWasCreated();
659 // If the file was created in history, that means older commits won't
660 // have it. Since we know it existed at 'before', it must have been
661 // created then; jump there.
662 $target_commit = $before;
664 } else if ($parent) {
665 // If we found a parent, jump to it. This is the normal case.
666 $target_commit = $parent;
668 // If there's no parent, this was probably created in the initial commit?
669 // And the "was_created" check will fail because we can't identify the
670 // grandparent. Keep the user at 'before'.
671 $target_commit = $before;
675 $path = $drequest->getPath();
677 if ($old_filename !== null &&
678 $old_filename !== '/'.$path) {
680 $path = $old_filename;
684 // If there's a follow error, drop the line so the user sees the message.
686 $line = $this->getBeforeLineNumber($target_commit);
689 $before_uri = $drequest->generateURI(
691 'action' => 'browse',
692 'commit' => $target_commit,
697 if ($renamed === null) {
698 $before_uri->removeQueryParam('renamed');
700 $before_uri->replaceQueryParam('renamed', $renamed);
703 if ($follow === null) {
704 $before_uri->removeQueryParam('follow');
706 $before_uri->replaceQueryParam('follow', $follow);
709 return id(new AphrontRedirectResponse())->setURI($before_uri);
712 private function getBeforeLineNumber($target_commit) {
713 $drequest = $this->getDiffusionRequest();
714 $viewer = $this->getViewer();
716 $line = $drequest->getLine();
721 $diff_info = $this->callConduitWithDiffusionRequest(
722 'diffusion.rawdiffquery',
724 'commit' => $drequest->getCommit(),
725 'path' => $drequest->getPath(),
726 'againstCommit' => $target_commit,
729 $file_phid = $diff_info['filePHID'];
730 $file = id(new PhabricatorFileQuery())
732 ->withPHIDs(array($file_phid))
737 'Failed to load file ("%s") returned by "%s".',
739 'diffusion.rawdiffquery.'));
742 $raw_diff = $file->loadFileData();
747 foreach (explode("\n", $raw_diff) as $text) {
748 if ($text[0] == '-' ||
$text[0] == ' ') {
751 if ($text[0] == '+' ||
$text[0] == ' ') {
754 if ($new_line == $line) {
759 // We didn't find the target line.
763 private function loadParentCommitOf($commit) {
764 $drequest = $this->getDiffusionRequest();
765 $user = $this->getRequest()->getUser();
767 $before_req = DiffusionRequest
::newFromDictionary(
770 'repository' => $drequest->getRepository(),
774 $parents = DiffusionQuery
::callConduitWithDiffusionRequest(
777 'diffusion.commitparentsquery',
782 return head($parents);
785 protected function markupText($text) {
786 $engine = PhabricatorMarkupEngine
::newDiffusionMarkupEngine();
787 $engine->setConfig('viewer', $this->getRequest()->getUser());
788 $text = $engine->markupText($text);
793 'class' => 'phabricator-remarkup',
800 protected function buildHeaderView(DiffusionRequest
$drequest) {
801 $viewer = $this->getViewer();
802 $repository = $drequest->getRepository();
804 $commit_tag = $this->renderCommitHashTag($drequest);
806 $path = nonempty($drequest->getPath(), '/');
808 $search = $this->renderSearchForm($path);
810 $header = id(new PHUIHeaderView())
812 ->setHeader($this->renderPathLinks($drequest, $mode = 'browse'))
813 ->addActionItem($search)
814 ->addTag($commit_tag)
815 ->addClass('diffusion-browse-header');
817 if (!$repository->isSVN()) {
818 $branch_tag = $this->renderBranchTag($drequest);
819 $header->addTag($branch_tag);
825 protected function buildPanelHeaderView($title, $icon) {
827 $header = id(new PHUIHeaderView())
829 ->setHeaderIcon($icon)
830 ->addClass('diffusion-panel-header-view');
836 protected function buildActionButtons(
837 DiffusionRequest
$drequest,
838 $is_directory = false) {
840 $viewer = $this->getViewer();
841 $repository = $drequest->getRepository();
842 $history_uri = $drequest->generateURI(array('action' => 'history'));
843 $behind_head = $drequest->getSymbolicCommit();
845 $head_uri = $drequest->generateURI(
848 'action' => 'browse',
851 if ($repository->supportsBranchComparison() && $is_directory) {
852 $compare_uri = $drequest->generateURI(array('action' => 'compare'));
853 $compare = id(new PHUIButtonView())
854 ->setText(pht('Compare'))
855 ->setIcon('fa-code-fork')
858 ->setHref($compare_uri)
859 ->setColor(PHUIButtonView
::GREY
);
860 $this->corpusButtons
[] = $compare;
865 $head = id(new PHUIButtonView())
867 ->setText(pht('Back to HEAD'))
870 ->setColor(PHUIButtonView
::GREY
);
871 $this->corpusButtons
[] = $head;
874 $history = id(new PHUIButtonView())
875 ->setText(pht('History'))
876 ->setHref($history_uri)
878 ->setIcon('fa-history')
879 ->setColor(PHUIButtonView
::GREY
);
880 $this->corpusButtons
[] = $history;
884 protected function buildPropertyView(
885 DiffusionRequest
$drequest) {
887 $viewer = $this->getViewer();
888 $view = id(new PHUIPropertyListView())
891 if ($drequest->getSymbolicType() == 'tag') {
892 $symbolic = $drequest->getSymbolicCommit();
893 $view->addProperty(pht('Tag'), $symbolic);
895 $tags = $this->callConduitWithDiffusionRequest(
896 'diffusion.tagsquery',
898 'names' => array($symbolic),
899 'needMessages' => true,
901 $tags = DiffusionRepositoryTag
::newFromConduit($tags);
903 $tags = mpull($tags, null, 'getName');
904 $tag = idx($tags, $symbolic);
906 if ($tag && strlen($tag->getMessage())) {
907 $view->addSectionHeader(
908 pht('Tag Content'), 'fa-tag');
909 $view->addTextContent($this->markupText($tag->getMessage()));
913 if ($view->hasAnyProperties()) {
920 private function buildOpenRevisions() {
921 $viewer = $this->getViewer();
923 $drequest = $this->getDiffusionRequest();
924 $repository = $drequest->getRepository();
925 $path = $drequest->getPath();
927 $recent = (PhabricatorTime
::getNow() - phutil_units('30 days in seconds'));
929 $revisions = id(new DifferentialRevisionQuery())
931 ->withPaths(array($path))
932 ->withRepositoryPHIDs(array($repository->getPHID()))
934 ->withUpdatedEpochBetween($recent, null)
935 ->setOrder(DifferentialRevisionQuery
::ORDER_MODIFIED
)
937 ->needReviewers(true)
946 $header = id(new PHUIHeaderView())
947 ->setHeader(pht('Recent Open Revisions'));
949 $list = id(new DifferentialRevisionListView())
951 ->setRevisions($revisions)
954 $view = id(new PHUIObjectBoxView())
956 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
957 ->addClass('diffusion-mobile-view')
958 ->appendChild($list);
963 private function getGitLFSRef(PhabricatorRepository
$repository, $data) {
964 if (!$repository->canUseGitLFS()) {
968 $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])';
969 if (!preg_match($lfs_pattern, $data)) {
974 if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) {
981 return id(new PhabricatorRepositoryGitLFSRefQuery())
982 ->setViewer($this->getViewer())
983 ->withRepositoryPHIDs(array($repository->getPHID()))
984 ->withObjectHashes(array($hash))
988 private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef
$ref) {
989 // TODO: We should probably test if we can load the file PHID here and
990 // show the user an error if we can't, rather than making them click
991 // through to hit an error.
993 $title = basename($this->getDiffusionRequest()->getPath());
994 $icon = 'fa-archive';
995 $drequest = $this->getDiffusionRequest();
996 $this->buildActionButtons($drequest);
997 $header = $this->buildPanelHeaderView($title, $icon);
999 $severity = PHUIInfoView
::SEVERITY_NOTICE
;
1001 $messages = array();
1003 'This %s file is stored in Git Large File Storage.',
1004 phutil_format_bytes($ref->getByteSize()));
1007 $file = $this->loadGitLFSFile($ref);
1008 $this->corpusButtons
[] = $this->renderGitLFSButton();
1009 } catch (Exception
$ex) {
1010 $severity = PHUIInfoView
::SEVERITY_ERROR
;
1011 $messages[] = pht('The data for this file could not be loaded.');
1014 $this->corpusButtons
[] = $this->renderFileButton(
1015 null, pht('View Raw LFS Pointer'));
1017 $corpus = id(new PHUIObjectBoxView())
1018 ->setHeader($header)
1019 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
1020 ->addClass('diffusion-mobile-view')
1021 ->setCollapsed(true);
1024 $corpus->setInfoView(
1025 id(new PHUIInfoView())
1026 ->setSeverity($severity)
1027 ->setErrors($messages));
1033 private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef
$ref) {
1034 $viewer = $this->getViewer();
1036 $file = id(new PhabricatorFileQuery())
1037 ->setViewer($viewer)
1038 ->withPHIDs(array($ref->getFilePHID()))
1041 throw new Exception(
1043 'Failed to load file object for Git LFS ref "%s"!',
1044 $ref->getObjectHash()));