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');
25 if (phutil_nonempty_string($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');
294 if ($drequest->getPath() !== null) {
295 $title = nonempty(basename($drequest->getPath()), '/');
298 $empty_result = null;
299 $browse_panel = null;
300 if (!$results->isValidResults()) {
301 $empty_result = new DiffusionEmptyResultView();
302 $empty_result->setDiffusionRequest($drequest);
303 $empty_result->setDiffusionBrowseResultSet($results);
304 $empty_result->setView($request->getStr('view'));
306 $browse_table = id(new DiffusionBrowseTableView())
307 ->setDiffusionRequest($drequest)
308 ->setPaths($results->getPaths())
309 ->setUser($request->getUser());
311 $icon = 'fa-folder-open';
312 $browse_header = $this->buildPanelHeaderView($title, $icon);
314 $browse_panel = id(new PHUIObjectBoxView())
315 ->setHeader($browse_header)
316 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
317 ->setTable($browse_table)
318 ->addClass('diffusion-mobile-view')
322 $open_revisions = $this->buildOpenRevisions();
323 $readme = $this->renderDirectoryReadme($results);
325 $crumbs = $this->buildCrumbs(
332 $crumbs->setBorder(true);
333 $tabs = $this->buildTabsView('code');
334 $owners_list = $this->buildOwnersList($drequest);
335 $bar = id(new PHUILeftRightView())
336 ->setRight($this->corpusButtons
)
337 ->addClass('diffusion-action-bar');
339 $view = id(new PHUITwoColumnView())
353 $view->addPropertySection(pht('Details'), $details);
356 return $this->newPage()
359 $repository->getDisplayName(),
368 private function renderSearchResults() {
369 $request = $this->getRequest();
371 $drequest = $this->getDiffusionRequest();
372 $repository = $drequest->getRepository();
375 $pager = id(new PHUIPagerView())
376 ->readFromRequest($request);
379 switch ($repository->getVersionControlSystem()) {
380 case PhabricatorRepositoryType
::REPOSITORY_TYPE_SVN
:
384 if (strlen($this->getRequest()->getStr('grep'))) {
385 $search_mode = 'grep';
386 $query_string = $request->getStr('grep');
387 $results = $this->callConduitWithDiffusionRequest(
388 'diffusion.searchquery',
390 'grep' => $query_string,
391 'commit' => $drequest->getStableCommit(),
392 'path' => $drequest->getPath(),
393 'limit' => $pager->getPageSize() +
1,
394 'offset' => $pager->getOffset(),
399 $results = $pager->sliceResults($results);
403 if ($search_mode == 'grep') {
404 $table = $this->renderGrepResults($results, $query_string);
406 'File content matching "%s" under "%s"',
408 nonempty($drequest->getPath(), '/'));
409 $header = id(new PHUIHeaderView())
411 ->addClass('diffusion-search-result-header');
414 return array($header, $table, $pager);
418 private function renderGrepResults(array $results, $pattern) {
419 $drequest = $this->getDiffusionRequest();
420 require_celerity_resource('phabricator-search-results-css');
423 return id(new PHUIInfoView())
424 ->setSeverity(PHUIInfoView
::SEVERITY_NODATA
)
427 'The pattern you searched for was not found in the content of any '.
432 foreach ($results as $file) {
433 list($path, $line, $string) = $file;
434 $grouped[$path][] = array($line, $string);
438 foreach ($grouped as $path => $matches) {
439 $view[] = id(new DiffusionPatternSearchView())
441 ->setMatches($matches)
442 ->setPattern($pattern)
443 ->setDiffusionRequest($drequest)
450 private function buildButtonBar(
451 DiffusionRequest
$drequest,
454 $viewer = $this->getViewer();
455 $base_uri = $this->getRequest()->getRequestURI();
457 $repository = $drequest->getRepository();
458 $path = $drequest->getPath();
459 $line = nonempty((int)$drequest->getLine(), 1);
463 $editor_template = null;
465 $link_engine = PhabricatorEditorURIEngine
::newForViewer($viewer);
467 $link_engine->setRepository($repository);
469 $editor_uri = $link_engine->getURIForPath($path, $line);
470 $editor_template = $link_engine->getURITokensForPath($path);
474 id(new PHUIButtonView())
476 ->setText(pht('Last Change'))
477 ->setColor(PHUIButtonView
::GREY
)
479 $drequest->generateURI(
481 'action' => 'change',
483 ->setIcon('fa-backward');
487 id(new PHUIButtonView())
489 ->setText(pht('Open File'))
490 ->setHref($editor_uri)
491 ->setIcon('fa-pencil')
492 ->setID('editor_link')
493 ->setMetadata(array('template' => $editor_template))
494 ->setDisabled(!$editor_uri)
495 ->setColor(PHUIButtonView
::GREY
);
498 $bar = id(new PHUILeftRightView())
500 ->addClass('diffusion-action-bar full-mobile-buttons');
504 private function buildOwnersList(DiffusionRequest
$drequest) {
505 $viewer = $this->getViewer();
507 $have_owners = PhabricatorApplication
::isClassInstalledForViewer(
508 'PhabricatorOwnersApplication',
514 $repository = $drequest->getRepository();
516 $package_query = id(new PhabricatorOwnersPackageQuery())
518 ->withStatuses(array(PhabricatorOwnersPackage
::STATUS_ACTIVE
))
520 $repository->getPHID(),
522 $drequest->getPath(),
525 $package_query->execute();
527 $packages = $package_query->getControllingPackagesForPath(
528 $repository->getPHID(),
529 $drequest->getPath());
531 $ownership = id(new PHUIObjectItemListView())
533 ->setNoDataString(pht('No Owners'));
536 foreach ($packages as $package) {
537 $item = id(new PHUIObjectItemView())
538 ->setObject($package)
539 ->setObjectName($package->getMonogram())
540 ->setHeader($package->getName())
541 ->setHref($package->getURI());
543 $owners = $package->getOwners();
545 $owner_list = $viewer->renderHandleList(
546 mpull($owners, 'getUserPHID'));
548 $owner_list = phutil_tag('em', array(), pht('None'));
550 $item->addAttribute(pht('Owners: %s', $owner_list));
552 $auto = $package->getAutoReview();
553 $autoreview_map = PhabricatorOwnersPackage
::getAutoreviewOptionsMap();
554 $spec = idx($autoreview_map, $auto, array());
555 $name = idx($spec, 'name', $auto);
556 $item->addIcon('fa-code', $name);
558 $rule = $package->newAuditingRule();
559 $item->addIcon($rule->getIconIcon(), $rule->getDisplayName());
561 if ($package->isArchived()) {
562 $item->setDisabled(true);
565 $ownership->addItem($item);
569 $view = id(new PHUIObjectBoxView())
570 ->setHeaderText(pht('Owner Packages'))
571 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
572 ->addClass('diffusion-mobile-view')
573 ->setObjectList($ownership);
578 private function renderFileButton($file_uri = null, $label = null) {
580 $base_uri = $this->getRequest()->getRequestURI();
583 $text = pht('Download File');
585 $icon = 'fa-download';
587 $text = pht('Raw File');
588 $href = $base_uri->alter('view', 'raw');
589 $icon = 'fa-file-text';
592 if ($label !== null) {
596 $button = id(new PHUIButtonView())
601 ->setColor(PHUIButtonView
::GREY
);
606 private function renderGitLFSButton() {
607 $viewer = $this->getViewer();
609 $uri = $this->getRequest()->getRequestURI();
610 $href = $uri->alter('view', 'git-lfs');
612 $text = pht('Download from Git LFS');
613 $icon = 'fa-download';
615 return id(new PHUIButtonView())
620 ->setColor(PHUIButtonView
::GREY
);
623 private function buildErrorCorpus($message) {
624 $text = id(new PHUIBoxView())
625 ->addPadding(PHUI
::PADDING_LARGE
)
626 ->appendChild($message);
628 $header = id(new PHUIHeaderView())
629 ->setHeader(pht('Details'));
631 $box = id(new PHUIObjectBoxView())
633 ->appendChild($text);
638 private function buildBeforeResponse($before) {
639 $request = $this->getRequest();
640 $drequest = $this->getDiffusionRequest();
642 // NOTE: We need to get the grandparent so we can capture filename changes
645 $parent = $this->loadParentCommitOf($before);
646 $old_filename = null;
647 $was_created = false;
649 $grandparent = $this->loadParentCommitOf($parent);
652 $rename_query = new DiffusionRenameHistoryQuery();
653 $rename_query->setRequest($drequest);
654 $rename_query->setOldCommit($grandparent);
655 $rename_query->setViewer($request->getUser());
656 $old_filename = $rename_query->loadOldFilename();
657 $was_created = $rename_query->getWasCreated();
663 // If the file was created in history, that means older commits won't
664 // have it. Since we know it existed at 'before', it must have been
665 // created then; jump there.
666 $target_commit = $before;
668 } else if ($parent) {
669 // If we found a parent, jump to it. This is the normal case.
670 $target_commit = $parent;
672 // If there's no parent, this was probably created in the initial commit?
673 // And the "was_created" check will fail because we can't identify the
674 // grandparent. Keep the user at 'before'.
675 $target_commit = $before;
679 $path = $drequest->getPath();
681 if ($old_filename !== null &&
682 $old_filename !== '/'.$path) {
684 $path = $old_filename;
688 // If there's a follow error, drop the line so the user sees the message.
690 $line = $this->getBeforeLineNumber($target_commit);
693 $before_uri = $drequest->generateURI(
695 'action' => 'browse',
696 'commit' => $target_commit,
701 if ($renamed === null) {
702 $before_uri->removeQueryParam('renamed');
704 $before_uri->replaceQueryParam('renamed', $renamed);
707 if ($follow === null) {
708 $before_uri->removeQueryParam('follow');
710 $before_uri->replaceQueryParam('follow', $follow);
713 return id(new AphrontRedirectResponse())->setURI($before_uri);
716 private function getBeforeLineNumber($target_commit) {
717 $drequest = $this->getDiffusionRequest();
718 $viewer = $this->getViewer();
720 $line = $drequest->getLine();
725 $diff_info = $this->callConduitWithDiffusionRequest(
726 'diffusion.rawdiffquery',
728 'commit' => $drequest->getCommit(),
729 'path' => $drequest->getPath(),
730 'againstCommit' => $target_commit,
733 $file_phid = $diff_info['filePHID'];
734 $file = id(new PhabricatorFileQuery())
736 ->withPHIDs(array($file_phid))
741 'Failed to load file ("%s") returned by "%s".',
743 'diffusion.rawdiffquery.'));
746 $raw_diff = $file->loadFileData();
751 foreach (explode("\n", $raw_diff) as $text) {
752 if ($text[0] == '-' ||
$text[0] == ' ') {
755 if ($text[0] == '+' ||
$text[0] == ' ') {
758 if ($new_line == $line) {
763 // We didn't find the target line.
767 private function loadParentCommitOf($commit) {
768 $drequest = $this->getDiffusionRequest();
769 $user = $this->getRequest()->getUser();
771 $before_req = DiffusionRequest
::newFromDictionary(
774 'repository' => $drequest->getRepository(),
778 $parents = DiffusionQuery
::callConduitWithDiffusionRequest(
781 'diffusion.commitparentsquery',
786 return head($parents);
789 protected function markupText($text) {
790 $engine = PhabricatorMarkupEngine
::newDiffusionMarkupEngine();
791 $engine->setConfig('viewer', $this->getRequest()->getUser());
792 $text = $engine->markupText($text);
797 'class' => 'phabricator-remarkup',
804 protected function buildHeaderView(DiffusionRequest
$drequest) {
805 $viewer = $this->getViewer();
806 $repository = $drequest->getRepository();
808 $commit_tag = $this->renderCommitHashTag($drequest);
810 $path = nonempty($drequest->getPath(), '/');
812 $search = $this->renderSearchForm($path);
814 $header = id(new PHUIHeaderView())
816 ->setHeader($this->renderPathLinks($drequest, $mode = 'browse'))
817 ->addActionItem($search)
818 ->addTag($commit_tag)
819 ->addClass('diffusion-browse-header');
821 if (!$repository->isSVN()) {
822 $branch_tag = $this->renderBranchTag($drequest);
823 $header->addTag($branch_tag);
829 protected function buildPanelHeaderView($title, $icon) {
831 $header = id(new PHUIHeaderView())
833 ->setHeaderIcon($icon)
834 ->addClass('diffusion-panel-header-view');
840 protected function buildActionButtons(
841 DiffusionRequest
$drequest,
842 $is_directory = false) {
844 $viewer = $this->getViewer();
845 $repository = $drequest->getRepository();
846 $history_uri = $drequest->generateURI(array('action' => 'history'));
847 $behind_head = $drequest->getSymbolicCommit();
849 $head_uri = $drequest->generateURI(
852 'action' => 'browse',
855 if ($repository->supportsBranchComparison() && $is_directory) {
856 $compare_uri = $drequest->generateURI(array('action' => 'compare'));
857 $compare = id(new PHUIButtonView())
858 ->setText(pht('Compare'))
859 ->setIcon('fa-code-fork')
862 ->setHref($compare_uri)
863 ->setColor(PHUIButtonView
::GREY
);
864 $this->corpusButtons
[] = $compare;
869 $head = id(new PHUIButtonView())
871 ->setText(pht('Back to HEAD'))
874 ->setColor(PHUIButtonView
::GREY
);
875 $this->corpusButtons
[] = $head;
878 $history = id(new PHUIButtonView())
879 ->setText(pht('History'))
880 ->setHref($history_uri)
882 ->setIcon('fa-history')
883 ->setColor(PHUIButtonView
::GREY
);
884 $this->corpusButtons
[] = $history;
888 protected function buildPropertyView(
889 DiffusionRequest
$drequest) {
891 $viewer = $this->getViewer();
892 $view = id(new PHUIPropertyListView())
895 if ($drequest->getSymbolicType() == 'tag') {
896 $symbolic = $drequest->getSymbolicCommit();
897 $view->addProperty(pht('Tag'), $symbolic);
899 $tags = $this->callConduitWithDiffusionRequest(
900 'diffusion.tagsquery',
902 'names' => array($symbolic),
903 'needMessages' => true,
905 $tags = DiffusionRepositoryTag
::newFromConduit($tags);
907 $tags = mpull($tags, null, 'getName');
908 $tag = idx($tags, $symbolic);
910 if ($tag && strlen($tag->getMessage())) {
911 $view->addSectionHeader(
912 pht('Tag Content'), 'fa-tag');
913 $view->addTextContent($this->markupText($tag->getMessage()));
917 if ($view->hasAnyProperties()) {
924 private function buildOpenRevisions() {
925 $viewer = $this->getViewer();
927 $drequest = $this->getDiffusionRequest();
928 $repository = $drequest->getRepository();
929 $path = $drequest->getPath();
931 $recent = (PhabricatorTime
::getNow() - phutil_units('30 days in seconds'));
933 $revisions = id(new DifferentialRevisionQuery())
935 ->withPaths(array($path))
936 ->withRepositoryPHIDs(array($repository->getPHID()))
938 ->withUpdatedEpochBetween($recent, null)
939 ->setOrder(DifferentialRevisionQuery
::ORDER_MODIFIED
)
941 ->needReviewers(true)
950 $header = id(new PHUIHeaderView())
951 ->setHeader(pht('Recent Open Revisions'));
953 $list = id(new DifferentialRevisionListView())
955 ->setRevisions($revisions)
958 $view = id(new PHUIObjectBoxView())
960 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
961 ->addClass('diffusion-mobile-view')
962 ->appendChild($list);
967 private function getGitLFSRef(PhabricatorRepository
$repository, $data) {
968 if (!$repository->canUseGitLFS()) {
972 $lfs_pattern = '(^version https://git-lfs\\.github\\.com/spec/v1[\r\n])';
973 if (!preg_match($lfs_pattern, $data)) {
978 if (!preg_match('(^oid sha256:(.*)$)m', $data, $matches)) {
985 return id(new PhabricatorRepositoryGitLFSRefQuery())
986 ->setViewer($this->getViewer())
987 ->withRepositoryPHIDs(array($repository->getPHID()))
988 ->withObjectHashes(array($hash))
992 private function buildGitLFSCorpus(PhabricatorRepositoryGitLFSRef
$ref) {
993 // TODO: We should probably test if we can load the file PHID here and
994 // show the user an error if we can't, rather than making them click
995 // through to hit an error.
997 $title = basename($this->getDiffusionRequest()->getPath());
998 $icon = 'fa-archive';
999 $drequest = $this->getDiffusionRequest();
1000 $this->buildActionButtons($drequest);
1001 $header = $this->buildPanelHeaderView($title, $icon);
1003 $severity = PHUIInfoView
::SEVERITY_NOTICE
;
1005 $messages = array();
1007 'This %s file is stored in Git Large File Storage.',
1008 phutil_format_bytes($ref->getByteSize()));
1011 $file = $this->loadGitLFSFile($ref);
1012 $this->corpusButtons
[] = $this->renderGitLFSButton();
1013 } catch (Exception
$ex) {
1014 $severity = PHUIInfoView
::SEVERITY_ERROR
;
1015 $messages[] = pht('The data for this file could not be loaded.');
1018 $this->corpusButtons
[] = $this->renderFileButton(
1019 null, pht('View Raw LFS Pointer'));
1021 $corpus = id(new PHUIObjectBoxView())
1022 ->setHeader($header)
1023 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
1024 ->addClass('diffusion-mobile-view')
1025 ->setCollapsed(true);
1028 $corpus->setInfoView(
1029 id(new PHUIInfoView())
1030 ->setSeverity($severity)
1031 ->setErrors($messages));
1037 private function loadGitLFSFile(PhabricatorRepositoryGitLFSRef
$ref) {
1038 $viewer = $this->getViewer();
1040 $file = id(new PhabricatorFileQuery())
1041 ->setViewer($viewer)
1042 ->withPHIDs(array($ref->getFilePHID()))
1045 throw new Exception(
1047 'Failed to load file object for Git LFS ref "%s"!',
1048 $ref->getObjectHash()));