diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index d720ad8872..be2df62dad 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1,123 +1,296 @@ diffusionRequest; + $is_file = false; if ($this->getRequest()->getStr('before')) { - $results = array(); $is_file = true; - } else { + } else if ($this->getRequest()->getStr('grep') == '') { $browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest); $browse_query->setViewer($this->getRequest()->getUser()); $results = $browse_query->loadPaths(); $reason = $browse_query->getReasonForEmptyResultSet(); $is_file = ($reason == DiffusionBrowseQuery::REASON_IS_FILE); } + if ($is_file) { + $controller = new DiffusionBrowseFileController($this->getRequest()); + $controller->setDiffusionRequest($drequest); + $controller->setCurrentApplication($this->getCurrentApplication()); + return $this->delegateToController($controller); + } + $content = array(); if ($drequest->getTagContent()) { $title = 'Tag: '.$drequest->getSymbolicCommit(); $tag_view = new AphrontPanelView(); $tag_view->setHeader($title); $tag_view->appendChild( $this->markupText($drequest->getTagContent())); $content[] = $tag_view; } - if (!$results) { + $content[] = $this->renderSearchForm(); - if ($is_file) { - $controller = new DiffusionBrowseFileController($this->getRequest()); - $controller->setDiffusionRequest($drequest); - $controller->setCurrentApplication($this->getCurrentApplication()); - return $this->delegateToController($controller); - } - - $empty_result = new DiffusionEmptyResultView(); - $empty_result->setDiffusionRequest($drequest); - $empty_result->setBrowseQuery($browse_query); - $empty_result->setView($this->getRequest()->getStr('view')); - $content[] = $empty_result; + if ($this->getRequest()->getStr('grep') != '') { + $content[] = $this->renderSearchResults(); } else { + if (!$results) { + $empty_result = new DiffusionEmptyResultView(); + $empty_result->setDiffusionRequest($drequest); + $empty_result->setBrowseQuery($browse_query); + $empty_result->setView($this->getRequest()->getStr('view')); + $content[] = $empty_result; + + } else { - $phids = array(); - foreach ($results as $result) { - $data = $result->getLastCommitData(); - if ($data) { - if ($data->getCommitDetail('authorPHID')) { - $phids[$data->getCommitDetail('authorPHID')] = true; + $phids = array(); + foreach ($results as $result) { + $data = $result->getLastCommitData(); + if ($data) { + if ($data->getCommitDetail('authorPHID')) { + $phids[$data->getCommitDetail('authorPHID')] = true; + } } } - } - $phids = array_keys($phids); - $handles = $this->loadViewerHandles($phids); + $phids = array_keys($phids); + $handles = $this->loadViewerHandles($phids); - $browse_table = new DiffusionBrowseTableView(); - $browse_table->setDiffusionRequest($drequest); - $browse_table->setHandles($handles); - $browse_table->setPaths($results); - $browse_table->setUser($this->getRequest()->getUser()); + $browse_table = new DiffusionBrowseTableView(); + $browse_table->setDiffusionRequest($drequest); + $browse_table->setHandles($handles); + $browse_table->setPaths($results); + $browse_table->setUser($this->getRequest()->getUser()); - $browse_panel = new AphrontPanelView(); - $browse_panel->appendChild($browse_table); - $browse_panel->setNoBackground(); + $browse_panel = new AphrontPanelView(); + $browse_panel->appendChild($browse_table); + $browse_panel->setNoBackground(); - $content[] = $browse_panel; - } + $content[] = $browse_panel; + } - $content[] = $this->buildOpenRevisions(); + $content[] = $this->buildOpenRevisions(); - $readme_content = $browse_query->renderReadme($results); - if ($readme_content) { - $readme_panel = new AphrontPanelView(); - $readme_panel->setHeader('README'); - $readme_panel->appendChild($readme_content); + $readme_content = $browse_query->renderReadme($results); + if ($readme_content) { + $readme_panel = new AphrontPanelView(); + $readme_panel->setHeader('README'); + $readme_panel->appendChild($readme_content); - $content[] = $readme_panel; + $content[] = $readme_panel; + } } - $nav = $this->buildSideNav('browse', false); $nav->appendChild($content); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => array( nonempty(basename($drequest->getPath()), '/'), $drequest->getRepository()->getCallsign().' Repository', ), )); } + + private function renderSearchForm() { + $drequest = $this->getDiffusionRequest(); + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->setMethod('GET'); + + switch ($drequest->getRepository()->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: + $form->appendChild(pht('Search is not available in Subversion.')); + break; + + default: + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Search Here')) + ->setName('grep') + ->setValue($this->getRequest()->getStr('grep')) + ->setCaption(pht('Regular expression'))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Grep'))); + break; + } + + return $form; + } + + private function renderSearchResults() { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + $results = array(); + $no_data = pht('No results found.'); + + $limit = 100; + $page = $this->getRequest()->getInt('page', 0); + $pager = new AphrontPagerView(); + $pager->setPageSize($limit); + $pager->setOffset($page); + $pager->setURI($this->getRequest()->getRequestURI(), 'page'); + + try { + + switch ($repository->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $future = $repository->getLocalCommandFuture( + // NOTE: --perl-regexp is available only with libpcre compiled in. + 'grep --extended-regexp --null -n --no-color -e %s %s -- %s', + $this->getRequest()->getStr('grep'), + $drequest->getStableCommitName(), + $drequest->getPath()); + + $binary_pattern = '/Binary file [^:]*:(.+) matches/'; + $lines = new LinesOfALargeExecFuture($future); + foreach ($lines as $line) { + $result = null; + if (preg_match('/[^:]*:(.+)\0(.+)\0(.*)/', $line, $result)) { + $results[] = array_slice($result, 1); + } else if (preg_match($binary_pattern, $line, $result)) { + list(, $path) = $result; + $results[] = array($path, null, pht('Binary file')); + } else { + $results[] = array(null, null, $line); + } + if (count($results) > $page + $limit) { + break; + } + } + unset($lines); + + break; + + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $future = $repository->getLocalCommandFuture( + 'grep --rev %s --print0 --line-number %s %s', + hgsprintf('ancestors(%s)', $drequest->getStableCommitName()), + $this->getRequest()->getStr('grep'), + $drequest->getPath()); + + $lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0"); + $parts = array(); + foreach ($lines as $line) { + $parts[] = $line; + if (count($parts) == 4) { + list($path, $offset, $line, $string) = $parts; + $results[] = array($path, $line, $string); + if (count($results) > $page + $limit) { + break; + } + $parts = array(); + } + } + unset($lines); + + break; + } + + } catch (CommandException $ex) { + return id(new AphrontErrorView()) + ->setTitle(pht('Search Error')) + ->appendChild($ex->getStderr()); + } + + $results = array_slice($results, $page); + $results = $pager->sliceResults($results); + + require_celerity_resource('syntax-highlighting-css'); + + // NOTE: This can be wrong because we may find the string inside the + // comment. But it's correct in most cases and highlighting the whole file + // would be too expensive. + $futures = array(); + $engine = PhabricatorSyntaxHighlighter::newEngine(); + foreach ($results as $result) { + list($path, $line, $string) = $result; + $futures["{$path}:{$line}"] = $engine->getHighlightFuture( + $engine->getLanguageFromFilename($path), + ltrim($string)); + } + + try { + Futures($futures)->limit(8)->resolveAll(); + } catch (PhutilSyntaxHighlighterException $ex) { + } + + $rows = array(); + foreach ($results as $result) { + list($path, $line, $string) = $result; + + $href = $drequest->generateURI(array( + 'action' => 'browse', + 'path' => $path, + 'line' => $line, + )); + + try { + $string = $futures["{$path}:{$line}"]->resolve(); + } catch (PhutilSyntaxHighlighterException $ex) { + } + + $string = phutil_tag( + 'pre', + array('class' => 'PhabricatorMonospaced'), + $string); + + $path = Filesystem::readablePath($path, $drequest->getPath()); + + $rows[] = array( + phutil_tag('a', array('href' => $href), $path), + $line, + $string, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setClassName('remarkup-code') + ->setHeaders(array(pht('Path'), pht('Line'), pht('String'))) + ->setColumnClasses(array('', 'n', 'wide')) + ->setNoDataString($no_data); + + return id(new AphrontPanelView()) + ->setHeader(pht('Search Results')) + ->appendChild($table) + ->appendChild($pager); + } + + private function markupText($text) { $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getRequest()->getUser()); $text = $engine->markupText($text); $text = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $text); return $text; } } diff --git a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php index d2151dd183..a0711f9aa7 100644 --- a/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php +++ b/src/applications/diffusion/query/browse/DiffusionBrowseQuery.php @@ -1,157 +1,158 @@ viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } const REASON_IS_FILE = 'is-file'; const REASON_IS_DELETED = 'is-deleted'; const REASON_IS_NONEXISTENT = 'nonexistent'; const REASON_BAD_COMMIT = 'bad-commit'; const REASON_IS_EMPTY = 'empty'; const REASON_IS_UNTRACKED_PARENT = 'untracked-parent'; final private function __construct() { // } final public static function newFromDiffusionRequest( DiffusionRequest $request) { $repository = $request->getRepository(); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: // TODO: Verify local-path? $query = new DiffusionGitBrowseQuery(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $query = new DiffusionMercurialBrowseQuery(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $query = new DiffusionSvnBrowseQuery(); break; default: throw new Exception("Unsupported VCS!"); } $query->request = $request; return $query; } final protected function getRequest() { return $this->request; } final public function getReasonForEmptyResultSet() { return $this->reason; } final public function getExistedAtCommit() { return $this->existedAtCommit; } final public function getDeletedAtCommit() { return $this->deletedAtCommit; } final public function loadPaths() { + $this->reason = null; return $this->executeQuery(); } final public function shouldOnlyTestValidity() { return $this->validityOnly; } final public function needValidityOnly($need_validity_only) { $this->validityOnly = $need_validity_only; return $this; } final public function renderReadme(array $results) { $drequest = $this->getRequest(); $readme = null; foreach ($results as $result) { $file_type = $result->getFileType(); if (($file_type != ArcanistDiffChangeType::FILE_NORMAL) && ($file_type != ArcanistDiffChangeType::FILE_TEXT)) { // Skip directories, etc. continue; } $path = $result->getPath(); if (preg_match('/^readme(|\.txt|\.remarkup|\.rainbow)$/i', $path)) { $readme = $result; break; } } if (!$readme) { return null; } $readme_request = DiffusionRequest::newFromDictionary( array( 'repository' => $drequest->getRepository(), 'commit' => $drequest->getStableCommitName(), 'path' => $readme->getFullPath(), )); $content_query = DiffusionFileContentQuery::newFromDiffusionRequest( $readme_request); $content_query->setViewer($this->getViewer()); $content_query->loadFileContent(); $readme_content = $content_query->getRawData(); if (preg_match('/\\.txt$/', $readme->getPath())) { $readme_content = phutil_escape_html_newlines($readme_content); $class = null; } else if (preg_match('/\\.rainbow$/', $readme->getPath())) { $highlighter = new PhutilRainbowSyntaxHighlighter(); $readme_content = $highlighter ->getHighlightFuture($readme_content) ->resolve(); $readme_content = phutil_escape_html_newlines($readme_content); require_celerity_resource('syntax-highlighting-css'); $class = 'remarkup-code'; } else { // Markup extensionless files as remarkup so we get links and such. $engine = PhabricatorMarkupEngine::newDiffusionMarkupEngine(); $engine->setConfig('viewer', $this->getViewer()); $readme_content = $engine->markupText($readme_content); $class = 'phabricator-remarkup'; } $readme_content = phutil_tag( 'div', array( 'class' => $class, ), $readme_content); return $readme_content; } abstract protected function executeQuery(); }