diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php index a6f5ca632..f2ac3271a 100644 --- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php @@ -1,231 +1,258 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class DiffusionBrowseFileController extends DiffusionController { public function processRequest() { // Build the view selection form. $select_map = array( 'highlighted' => 'View as Highlighted Text', 'blame' => 'View as Highlighted Text with Blame', 'plain' => 'View as Plain Text', 'plainblame' => 'View as Plain Text with Blame', ); $request = $this->getRequest(); $selected = $request->getStr('view'); $select = '<select name="view">'; foreach ($select_map as $k => $v) { $option = phutil_render_tag( 'option', array( 'value' => $k, 'selected' => ($k == $selected) ? 'selected' : null, ), phutil_escape_html($v)); $select .= $option; } $select .= '</select>'; $view_select_panel = new AphrontPanelView(); $view_select_form = phutil_render_tag( 'form', array( 'action' => $request->getRequestURI(), 'method' => 'get', 'style' => 'display: inline', ), $select. '<button>view</button>'); $view_select_panel->appendChild($view_select_form); // Build the content of the file. $corpus = $this->buildCorpus($selected); // Render the page. $content = array(); $content[] = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $content[] = $view_select_panel; $content[] = $corpus; $nav = $this->buildSideNav('browse', true); $nav->appendChild($content); return $this->buildStandardPageResponse( $nav, array( 'title' => 'Browse', )); } - public static function renderRevision( - DiffusionRequest $drequest, - $revision) { - - $callsign = $drequest->getCallsign(); - - $name = 'r'.$callsign.$revision; - return phutil_render_tag( - 'a', - array( - 'href' => '/'.$name, - ), - $name - ); - } - - private function buildCorpus($selected) { - $blame = ($selected == 'blame' || $selected == 'plainblame'); + $needs_blame = ($selected == 'blame' || $selected == 'plainblame'); $file_query = DiffusionFileContentQuery::newFromDiffusionRequest( $this->diffusionRequest); - $file_query->setNeedsBlame($blame); + $file_query->setNeedsBlame($needs_blame); + $file_query->loadFileContent(); // TODO: image // TODO: blame of blame. switch ($selected) { case 'plain': - case 'plainblame': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; $corpus = phutil_render_tag( 'textarea', array( 'style' => $style, ), phutil_escape_html($file_query->getRawData())); + break; + case 'plainblame': + $style = + "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; + list($text_list, $rev_list, $blame_dict) = + $file_query->getBlameData(); + + $rows = array(); + foreach ($text_list as $k => $line) { + $rev = $rev_list[$k]; + $author = $blame_dict[$rev]['author']; + $rows[] = + sprintf("%-10s %-15s %s", substr($rev, 0, 7), $author, $line); + } + + $corpus = phutil_render_tag( + 'textarea', + array( + 'style' => $style, + ), + phutil_escape_html(implode("\n", $rows))); + + break; + case 'highlighted': case 'blame': default: require_celerity_resource('syntax-highlighting-css'); require_celerity_resource('diffusion-source-css'); - list($data, $blamedata, $revs) = $file_query->getTokenizedData(); + list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $highlightEngine = new PhutilDefaultSyntaxHighlighterEngine(); - $data = $highlightEngine->highlightSource($path, $data); - $data = explode("\n", rtrim($data)); + $text_list = explode("\n", $highlightEngine->highlightSource($path, + implode("\n", $text_list))); - $rows = $this->buildDisplayRows($data, $blame, $blamedata, $drequest, - $revs); + $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, + $needs_blame, $drequest); $corpus_table = phutil_render_tag( 'table', array( 'class' => "diffusion-source remarkup-code PhabricatorMonospaced", ), implode("\n", $rows)); $corpus = phutil_render_tag( 'div', array( 'style' => 'padding: 0pt 2em;', ), $corpus_table); break; } return $corpus; } - private static function buildDisplayRows($data, $blame, $blamedata, $drequest, - $revs) { - $last = null; + private static function buildDisplayRows($text_list, $rev_list, $blame_dict, + $needs_blame, DiffusionRequest $drequest) { + $last_rev = null; $color = null; $rows = array(); $n = 1; - foreach ($data as $k => $line) { - if ($blame) { - if ($last == $blamedata[$k][0]) { - $blameinfo = + + $epoch_list = ipull($blame_dict, 'epoch'); + $max = max($epoch_list); + $min = min($epoch_list); + $range = $max - $min + 1; + + foreach ($text_list as $k => $line) { + if ($needs_blame) { + // If the line's rev is same as the line above, show empty content + // with same color; otherwise generate blame info. The newer a change + // is, the darker the color. + $rev = $rev_list[$k]; + if ($last_rev == $rev) { + $blame_info = '<th style="background: '.$color.'; width: 9em;"></th>'. '<th style="background: '.$color.'"></th>'; } else { - switch ($drequest->getRepository()->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - // TODO: better color for git. - $color = '#dddddd'; - break; - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: - $color = sprintf( - '#%02xee%02x', - $revs[$blamedata[$k][0]], - $revs[$blamedata[$k][0]]); - break; - default: - throw new Exception('repository type not supported'); - } + + $color_number = (int)(0xEE - + 0xEE * ($blame_dict[$rev]['epoch'] - $min) / $range); + $color = sprintf('#%02xee%02x', $color_number, $color_number); + $revision_link = self::renderRevision( - $drequest, - $blamedata[$k][0]); + $drequest, + substr($rev, 0, 7)); - $author_link = $blamedata[$k][1]; - $blameinfo = + $author_link = $blame_dict[$rev]['author']; + $blame_info = '<th style="background: '.$color. '; width: 9em;">'.$revision_link.'</th>'. '<th style="background: '.$color. '; font-weight: normal; color: #333;">'.$author_link.'</th>'; - $last = $blamedata[$k][0]; + $last_rev = $rev; } } else { - $blameinfo = null; + $blame_info = null; } + // Highlight the line of interest if needed. if ($n == $drequest->getLine()) { $tr = '<tr style="background: #ffff00;">'; $targ = '<a id="scroll_target"></a>'; Javelin::initBehavior('diffusion-jump-to', array('target' => 'scroll_target')); } else { $tr = '<tr>'; $targ = null; } + // Create the row display. $uri_path = $drequest->getUriPath(); $uri_rev = $drequest->getCommit(); $l = phutil_render_tag( 'a', array( 'href' => $uri_path.';'.$uri_rev.'$'.$n, ), $n); - $rows[] = $tr.$blameinfo.'<th>'.$l.'</th><td>'.$targ.$line.'</td></tr>'; + $rows[] = $tr.$blame_info.'<th>'.$l.'</th><td>'.$targ.$line.'</td></tr>'; ++$n; } return $rows; } + + + private static function renderRevision(DiffusionRequest $drequest, + $revision) { + + $callsign = $drequest->getCallsign(); + + $name = 'r'.$callsign.$revision; + return phutil_render_tag( + 'a', + array( + 'href' => '/'.$name, + ), + $name + ); + } + } diff --git a/src/applications/diffusion/controller/file/__init__.php b/src/applications/diffusion/controller/file/__init__.php index 7b85e05b1..529bf8288 100644 --- a/src/applications/diffusion/controller/file/__init__.php +++ b/src/applications/diffusion/controller/file/__init__.php @@ -1,20 +1,20 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/diffusion/controller/base'); phutil_require_module('phabricator', 'applications/diffusion/query/filecontent/base'); -phutil_require_module('phabricator', 'applications/repository/constants/repositorytype'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/api'); phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'markup/syntax/engine/default'); +phutil_require_module('phutil', 'utils'); phutil_require_source('DiffusionBrowseFileController.php'); diff --git a/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php b/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php index a832e6cc7..42be58a64 100644 --- a/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php @@ -1,81 +1,115 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ abstract class DiffusionFileContentQuery { private $request; private $needsBlame; + private $fileContent; final private function __construct() { // <private> } final public static function newFromDiffusionRequest( DiffusionRequest $request) { $repository = $request->getRepository(); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'DiffusionGitFileContentQuery'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'DiffusionSvnFileContentQuery'; break; default: throw new Exception("Unsupported VCS!"); } PhutilSymbolLoader::loadClass($class); $query = new $class(); $query->request = $request; return $query; } final protected function getRequest() { return $this->request; } final public function loadFileContent() { - return $this->executeQuery(); + $this->fileContent = $this->executeQuery(); } abstract protected function executeQuery(); final public function getRawData() { - return $this->loadFileContent()->getCorpus(); + return $this->fileContent->getCorpus(); } - final public function getTokenizedData() { - return $this->tokenizeData($this->getRawData()); + final public function getBlameData() { + $raw_data = $this->getRawData(); + + $text_list = array(); + $rev_list = array(); + $blame_dict = array(); + + if (!$this->getNeedsBlame()) { + $text_list = explode("\n", rtrim($raw_data)); + } else { + foreach (explode("\n", rtrim($raw_data)) as $k => $line) { + list($rev_id, $author, $text) = $this->tokenizeLine($line); + + $text_list[$k] = $text; + $rev_list[$k] = $rev_id; + + if (!isset($blame_dict[$rev_id]) && + !isset($blame_dict[$rev_id]['author'] )) { + $blame_dict[$rev_id]['author'] = $author; + } + } + + $repository = $this->getRequest()->getRepository(); + $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( + 'repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), + array_unique($rev_list)); + + foreach ($commits as $commit) { + $commitIdentifier = $commit->getCommitIdentifier(); + $epoch = $commit->getEpoch(); + $blame_dict[$commitIdentifier]['epoch'] = $epoch; + } + } + + return array($text_list, $rev_list, $blame_dict); } - abstract protected function tokenizeData($data); + abstract protected function tokenizeLine($line); public function setNeedsBlame($needs_blame) { $this->needsBlame = $needs_blame; } public function getNeedsBlame() { return $this->needsBlame; } } diff --git a/src/applications/diffusion/query/filecontent/base/__init__.php b/src/applications/diffusion/query/filecontent/base/__init__.php index f964d4982..7ad4326f0 100644 --- a/src/applications/diffusion/query/filecontent/base/__init__.php +++ b/src/applications/diffusion/query/filecontent/base/__init__.php @@ -1,14 +1,16 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/repository/constants/repositorytype'); +phutil_require_module('phabricator', 'applications/repository/storage/commit'); phutil_require_module('phutil', 'symbols'); +phutil_require_module('phutil', 'utils'); phutil_require_source('DiffusionFileContentQuery.php'); diff --git a/src/applications/diffusion/query/filecontent/git/DiffusionGitFileContentQuery.php b/src/applications/diffusion/query/filecontent/git/DiffusionGitFileContentQuery.php index 0465f6ccf..9176fbfeb 100644 --- a/src/applications/diffusion/query/filecontent/git/DiffusionGitFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/git/DiffusionGitFileContentQuery.php @@ -1,80 +1,62 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class DiffusionGitFileContentQuery extends DiffusionFileContentQuery { protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit = $drequest->getCommit(); $local_path = $repository->getDetail('local-path'); if ($this->getNeedsBlame()) { list($corpus) = execx( - '(cd %s && git --no-pager blame -c --date short %s -- %s)', + '(cd %s && git --no-pager blame -c -l --date short %s -- %s)', $local_path, $commit, $path); } else { list($corpus) = execx( '(cd %s && git cat-file blob %s:%s)', $local_path, $commit, $path); } $file_content = new DiffusionFileContent(); $file_content->setCorpus($corpus); return $file_content; } - protected function tokenizeData($data) { - $m = array(); - $blamedata = array(); - $revs = array(); - - if ($this->getNeedsBlame()) { - $data = explode("\n", rtrim($data)); - foreach ($data as $k => $line) { - // sample line: - // d1b4fcdd ( hzhao 2009-05-01 202)function print(); - preg_match('/^\s*?(\S+?)\s*\(\s*(\S+)\s+\S+\s+\d+\)(.*)?$/', - $line, $m); - $data[$k] = idx($m, 3); - $blamedata[$k] = array($m[1], $m[2]); - - $revs[$m[1]] = true; - } - $data = implode("\n", $data); - krsort($revs); - $ii = 0; - $len = count($revs); - foreach ($revs as $rev => $ignored) { - $revs[$rev] = (int)(0xEE * ($ii / $len)); - ++$ii; - } - } - - return array($data, $blamedata, $revs); + protected function tokenizeLine($line) { + $m = array(); + // sample line: + // d1b4fcdd2a7c8c0f8cbdd01ca839d992135424dc + // ( hzhao 2009-05-01 202)function print(); + preg_match('/^\s*?(\S+?)\s*\(\s*(\S+)\s+\S+\s+\d+\)(.*)?$/', $line, $m); + $rev_id = $m[1]; + $author = $m[2]; + $text = idx($m, 3); + return array($rev_id, $author, $text); } } diff --git a/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php b/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php index 27f1b0532..64e7f9f08 100644 --- a/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php @@ -1,90 +1,71 @@ <?php /* * Copyright 2011 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class DiffusionSvnFileContentQuery extends DiffusionFileContentQuery { protected function executeQuery() { $drequest = $this->getRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $commit = $drequest->getCommit(); $remote_uri = $repository->getDetail('remote-uri'); try { list($corpus) = execx( 'svn --non-interactive %s %s%s@%s', $this->getNeedsBlame() ? 'blame' : 'cat', $remote_uri, $path, $commit); } catch (CommandException $ex) { $stderr = $ex->getStdErr(); if (preg_match('/path not found$/', trim($stderr))) { // TODO: Improve user experience for this. One way to end up here // is to have the parser behind and look at a file which was recently // nuked; Diffusion will think it still exists and try to grab content // at HEAD. throw new Exception( "Failed to retrieve file content from Subversion. The file may ". "have been recently deleted, or the Diffusion cache may be out of ". "date."); } else { throw $ex; } } $file_content = new DiffusionFileContent(); $file_content->setCorpus($corpus); return $file_content; } - protected function tokenizeData($data) - { + protected function tokenizeLine($line) { + // sample line: + // 347498 yliu function print(); $m = array(); - $blamedata = array(); - $revs = array(); + preg_match('/^\s*(\d+)\s+(\S+)(?: (.*))?$/', $line, $m); + $rev_id = $m[1]; + $author = $m[2]; + $text = idx($m, 3); - if ($this->getNeedsBlame()) { - $data = explode("\n", rtrim($data)); - foreach ($data as $k => $line) { - // sample line: - // 347498 yliu function print(); - preg_match('/^\s*(\d+)\s+(\S+)(?: (.*))?$/', $line, $m); - $data[$k] = idx($m, 3); - $blamedata[$k] = array($m[1], $m[2]); - - $revs[$m[1]] = true; - } - $data = implode("\n", $data); - - krsort($revs); - $ii = 0; - $len = count($revs); - foreach ($revs as $rev => $ignored) { - $revs[$rev] = (int)(0xEE * ($ii / $len)); - ++$ii; - } - } - - return array($data, $blamedata, $revs); + return array($rev_id, $author, $text); } }