From ee278a302e8c8f16dd4e1d29544d1b7ec9d45a54 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 7 Apr 2012 17:24:35 -0700 Subject: [PATCH] Improve Diffusion blame views Summary: - Make some effort to simplify the code. - Make "Skip Past This Commit" work in Git and Mercurial. - Make blame work in Mercurial. - Add tooltip hover state to show more information about commits. Test Plan: Viewed blame views in SVN, Git, Hg. Clicked line numbers, hovered/clicked commits, hovered/clicked "blame past..." Reviewers: btrahan, vrana, jungejason Reviewed By: vrana CC: aran Maniphest Tasks: T378 Differential Revision: https://secure.phabricator.com/D2142 --- src/__celerity_resource_map__.php | 2 +- .../commit/PhabricatorAuditCommitQuery.php | 23 ++ .../file/DiffusionBrowseFileController.php | 312 +++++++++++++----- .../diffusion/controller/file/__init__.php | 4 + .../base/DiffusionFileContentQuery.php | 33 +- .../query/filecontent/base/__init__.php | 2 +- .../DiffusionMercurialFileContentQuery.php | 61 +++- .../svn/DiffusionSvnFileContentQuery.php | 10 +- .../request/base/DiffusionRequest.php | 15 + .../diffusion/diffusion-source.css | 34 +- 10 files changed, 362 insertions(+), 134 deletions(-) diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 788dfc1dc7..17e3a8fd73 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -295,7 +295,7 @@ celerity_register_resource_map(array( ), 'diffusion-source-css' => array( - 'uri' => '/res/5f5ac1d6/rsrc/css/application/diffusion/diffusion-source.css', + 'uri' => '/res/9f79ed12/rsrc/css/application/diffusion/diffusion-source.css', 'type' => 'css', 'requires' => array( diff --git a/src/applications/audit/query/commit/PhabricatorAuditCommitQuery.php b/src/applications/audit/query/commit/PhabricatorAuditCommitQuery.php index 35fce40425..f49b6803e6 100644 --- a/src/applications/audit/query/commit/PhabricatorAuditCommitQuery.php +++ b/src/applications/audit/query/commit/PhabricatorAuditCommitQuery.php @@ -24,6 +24,7 @@ final class PhabricatorAuditCommitQuery { private $commitPHIDs; private $authorPHIDs; private $packagePHIDs; + private $identifiers = array(); private $needCommitData; @@ -51,6 +52,11 @@ final class PhabricatorAuditCommitQuery { return $this; } + public function withIdentifiers($repository_id, array $identifiers) { + $this->identifiers[] = array($repository_id, $identifiers); + return $this; + } + public function needCommitData($need) { $this->needCommitData = $need; return $this; @@ -152,6 +158,23 @@ final class PhabricatorAuditCommitQuery { $this->packagePHIDs); } + if ($this->identifiers) { + $clauses = array(); + foreach ($this->identifiers as $spec) { + list($repository_id, $identifiers) = $spec; + if ($identifiers) { + $clauses[] = qsprintf( + $conn_r, + 'c.repositoryID = %d AND c.commitIdentifier IN (%Ls)', + $repository_id, + $identifiers); + } + } + if ($clauses) { + $where[] = '('.implode(') OR (', $clauses).')'; + } + } + $status = $this->status; switch ($status) { case self::STATUS_OPEN: diff --git a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php index f52a252cbe..b254ca7e9f 100644 --- a/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/file/DiffusionBrowseFileController.php @@ -23,11 +23,17 @@ final class DiffusionBrowseFileController extends DiffusionController { public function processRequest() { $request = $this->getRequest(); - $drequest = $this->getDiffusionRequest(); + + $before = $request->getStr('before'); + if ($before) { + return $this->buildBeforeResponse($before); + } + $path = $drequest->getPath(); $selected = $request->getStr('view'); $needs_blame = ($selected == 'blame' || $selected == 'plainblame'); + $file_query = DiffusionFileContentQuery::newFromDiffusionRequest( $this->diffusionRequest); $file_query->setNeedsBlame($needs_blame); @@ -99,8 +105,6 @@ final class DiffusionBrowseFileController extends DiffusionController { } } - - // TODO: blame of blame. switch ($selected) { case 'plain': $style = @@ -166,7 +170,7 @@ final class DiffusionBrowseFileController extends DiffusionController { $corpus = phutil_render_tag( 'div', array( - 'style' => 'padding: 0pt 2em;', + 'style' => 'padding: 0 2em;', ), $corpus_table); @@ -245,20 +249,13 @@ final class DiffusionBrowseFileController extends DiffusionController { DiffusionFileContentQuery $file_query, $selected) { - $last_rev = null; - $color = '#eeeeee'; - $rows = array(); - $n = 1; - $view = $this->getRequest()->getStr('view'); - if ($blame_dict) { - $epoch_list = ipull($blame_dict, 'epoch'); - $epoch_max = max($epoch_list); - $epoch_min = min($epoch_list); - $epoch_range = $epoch_max - $epoch_min + 1; + $epoch_list = ipull($blame_dict, 'epoch'); + $epoch_min = min($epoch_list); + $epoch_max = max($epoch_list); + $epoch_range = ($epoch_max - $epoch_min) + 1; } - $targ = ''; $min_line = 0; $line = $drequest->getLine(); if (strpos($line, '-') !== false) { @@ -270,109 +267,219 @@ final class DiffusionBrowseFileController extends DiffusionController { $max_line = $line; } + $display = array(); + + $line_number = 1; + $last_rev = null; + $color = null; foreach ($text_list as $k => $line) { + $display_line = array( + 'color' => null, + 'epoch' => null, + 'commit' => null, + 'author' => null, + 'target' => null, + 'highlighted' => null, + 'line' => $line_number, + 'data' => $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]; + // is, the more saturated the color. + + // TODO: SVN doesn't always give us blame for the last line, if empty? + // Bug with our stuff or with SVN? + $rev = idx($rev_list, $k, $last_rev); + if ($last_rev == $rev) { - $blame_info = - ($file_query->getSupportsBlameOnBlame() ? - '' : ''). - ''. - ''; + $display_line['color'] = $color; } else { + $blame = $blame_dict[$rev]; - $revision_time = null; - if ($blame_dict) { - $color_number = (int)(0xEE - - 0xEE * ($blame_dict[$rev]['epoch'] - $epoch_min) / $epoch_range); - $color = sprintf('#%02xee%02x', $color_number, $color_number); - $revision_time = phabricator_datetime( - $blame_dict[$rev]['epoch'], - $this->getRequest()->getUser()); - } + $color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range; - $revision_link = self::renderRevision( - $drequest, - substr($rev, 0, 7)); + $color_value = 0xF6 * (1.0 - $color_ratio); + $color = sprintf( + '#%02x%02x%02x', + $color_value, + 0xF6, + $color_value); - if (!$file_query->getSupportsBlameOnBlame()) { - $prev_link = ''; - } else { - $prev_rev = $file_query->getPrevRev($rev); - $path = $drequest->getPath(); - $prev_link = self::renderBrowse( - $drequest, - $path, - "\xC2\xAB", - $prev_rev, - $n, - $selected, - 'Blame previous revision'); - $prev_link = phutil_render_tag( - 'th', - array( - 'class' => 'diffusion-wide-link', - 'style' => 'background: '.$color.'; width: 2em;', - ), - $prev_link); - } + $display_line['epoch'] = $blame['epoch']; + $display_line['color'] = $color; + $display_line['commit'] = $rev; if (isset($blame_dict[$rev]['handle'])) { $author_link = $blame_dict[$rev]['handle']->renderLink(); } else { - $author_link = phutil_escape_html($blame_dict[$rev]['author']); + $author_link = phutil_render_tag( + 'span', + array( + ), + phutil_escape_html($blame_dict[$rev]['author'])); } - $blame_info = - $prev_link . - ''.$revision_link.''. - ''.$author_link.''; + $display_line['author'] = $author_link; + $last_rev = $rev; } - } else { - $blame_info = null; } - // Highlight the line of interest if needed. - if ($min_line > 0 && ($n >= $min_line && $n <= $max_line)) { - $tr = ''; - if ($targ == '') { - $targ = ''; - Javelin::initBehavior('diffusion-jump-to', - array('target' => 'scroll_target')); + if ($min_line) { + if ($line_number == $min_line) { + $display_line['target'] = true; + } + if ($line_number >= $min_line && $line_number <= $max_line) { + $display_line['highlighted'] = true; } - } else { - $tr = ''; - $targ = null; } - $href = $drequest->generateURI( + $display[] = $display_line; + ++$line_number; + } + + $commits = id(new PhabricatorAuditCommitQuery()) + ->withIdentifiers( + $drequest->getRepository()->getID(), + array_filter(ipull($display, 'commit'))) + ->needCommitData(true) + ->execute(); + $commits = mpull($commits, null, 'getCommitIdentifier'); + + $request = $this->getRequest(); + $user = $request->getUser(); + + $rows = array(); + foreach ($display as $line) { + + $line_href = $drequest->generateURI( array( - 'action' => 'browse', - 'stable' => true, + 'action' => 'browse', + 'line' => $line['line'], + 'stable' => true, )); - $href = (string)$href; - $query_params = null; - if ($view) { - $query_params = '?view='.$view; + $line_href->setQueryParams($request->getRequestURI()->getQueryParams()); + + $blame = array(); + if ($line['color']) { + $color = $line['color']; + + $before_link = null; + $commit_link = null; + if (idx($line, 'commit')) { + $commit = $line['commit']; + + $summary = 'Unknown'; + if (idx($commits, $commit)) { + $summary = $commits[$commit]->getCommitData()->getSummary(); + } + + $tooltip = phabricator_date( + $line['epoch'], + $user)." \xC2\xB7 ".$summary; + + Javelin::initBehavior('phabricator-tooltips', array()); + require_celerity_resource('aphront-tooltip-css'); + + $commit_link = javelin_render_tag( + 'a', + array( + 'href' => $drequest->generateURI( + array( + 'action' => 'commit', + 'commit' => $line['commit'], + )), + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $tooltip, + 'align' => 'E', + 'size' => 600, + ), + ), + phutil_escape_html(phutil_utf8_shorten($line['commit'], 9, ''))); + + $before_link = javelin_render_tag( + 'a', + array( + 'href' => $line_href->alter('before', $commit), + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => 'Skip Past This Commit', + 'align' => 'E', + 'size' => 300, + ), + ), + "\xC2\xAB"); + } + + $blame[] = phutil_render_tag( + 'th', + array( + 'class' => 'diffusion-blame-link', + 'style' => 'background: '.$color, + ), + $before_link); + + $blame[] = phutil_render_tag( + 'th', + array( + 'class' => 'diffusion-rev-link', + 'style' => 'background: '.$color, + ), + $commit_link); + + $blame[] = phutil_render_tag( + 'th', + array( + 'class' => 'diffusion-author-link', + 'style' => 'background: '.$color, + ), + idx($line, 'author')); } - $link = phutil_render_tag( + $line_link = phutil_render_tag( 'a', array( - 'href' => $href.'$'.$n.$query_params, + 'href' => $line_href, ), - $n); + phutil_escape_html($line['line'])); - $rows[] = $tr.$blame_info. - ''.$link.''. - ''.$targ.$line.''; - ++$n; + $blame[] = phutil_render_tag( + 'th', + array( + 'class' => 'diffusion-line-link', + 'style' => isset($color) ? 'background: '.$color : null, + ), + $line_link); + + $blame = implode('', $blame); + + if ($line['target']) { + Javelin::initBehavior( + 'diffusion-jump-to', + array( + 'target' => 'scroll_target', + )); + $anchor_text = ''; + } else { + $anchor_text = null; + } + + $line_text = phutil_render_tag( + 'td', + array( + ), + $anchor_text.$line['data']); + + $rows[] = phutil_render_tag( + 'tr', + array( + 'style' => ($line['highlighted'] ? 'background: #ffff00;' : null), + ), + $blame. + $line_text); } return $rows; @@ -493,5 +600,34 @@ final class DiffusionBrowseFileController extends DiffusionController { return $panel; } + private function buildBeforeResponse($before) { + $request = $this->getRequest(); + $drequest = $this->getDiffusionRequest(); + + $before_req = DiffusionRequest::newFromDictionary( + array( + 'repository' => $drequest->getRepository(), + 'commit' => $before, + )); + + $query = DiffusionCommitParentsQuery::newFromDiffusionRequest($before_req); + $parents = $query->loadParents(); + $parent = head($parents); + + // NOTE: If they get back to the very first commit, we just keep them there. + // We could maybe show a message or something. + + $before_uri = $drequest->generateURI( + array( + 'action' => 'browse', + 'commit' => $parent ? $parent->getCommitIdentifier() : $before, + 'line' => $drequest->getLine(), + )); + + $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); + $before_uri = $before_uri->alter('before', null); + + return id(new AphrontRedirectResponse())->setURI($before_uri); + } } diff --git a/src/applications/diffusion/controller/file/__init__.php b/src/applications/diffusion/controller/file/__init__.php index 6ed4027961..39a096ffec 100644 --- a/src/applications/diffusion/controller/file/__init__.php +++ b/src/applications/diffusion/controller/file/__init__.php @@ -10,12 +10,16 @@ phutil_require_module('arcanist', 'difference'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'aphront/writeguard'); +phutil_require_module('phabricator', 'applications/audit/query/commit'); phutil_require_module('phabricator', 'applications/diffusion/controller/base'); phutil_require_module('phabricator', 'applications/diffusion/query/filecontent/base'); +phutil_require_module('phabricator', 'applications/diffusion/query/parents/base'); +phutil_require_module('phabricator', 'applications/diffusion/request/base'); phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/markup/syntax'); phutil_require_module('phabricator', 'infrastructure/celerity/api'); phutil_require_module('phabricator', 'infrastructure/javelin/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/markup'); phutil_require_module('phabricator', 'infrastructure/util/hash'); phutil_require_module('phabricator', 'view/form/control/select'); phutil_require_module('phabricator', 'view/layout/panel'); diff --git a/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php b/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php index 6b291c348f..cf38d0bb06 100644 --- a/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/base/DiffusionFileContentQuery.php @@ -26,16 +26,6 @@ abstract class DiffusionFileContentQuery extends DiffusionQuery { return parent::newQueryObject(__CLASS__, $request); } - public function getSupportsBlameOnBlame() { - return false; - } - - public function getPrevRev($rev) { - // TODO: support git once the 'parent' info of a commit is saved - // to the database. - throw new Exception("Unsupported VCS!"); - } - final public function loadFileContent() { $this->fileContent = $this->executeQuery(); } @@ -54,11 +44,20 @@ abstract class DiffusionFileContentQuery extends DiffusionQuery { if (!$this->getNeedsBlame()) { $text_list = explode("\n", rtrim($raw_data)); } else { + $lines = array(); foreach (explode("\n", rtrim($raw_data)) as $k => $line) { - list($rev_id, $author, $text) = $this->tokenizeLine($line); + $lines[$k] = $this->tokenizeLine($line); + list($rev_id, $author, $text) = $lines[$k]; $text_list[$k] = $text; $rev_list[$k] = $rev_id; + } + + $rev_list = $this->processRevList($rev_list); + + foreach ($lines as $k => $line) { + list($rev_id, $author, $text) = $line; + $rev_id = $rev_list[$k]; if (!isset($blame_dict[$rev_id]) && !isset($blame_dict[$rev_id]['author'] )) { @@ -68,9 +67,11 @@ abstract class DiffusionFileContentQuery extends DiffusionQuery { $repository = $this->getRequest()->getRepository(); - $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( - 'repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), - array_unique($rev_list)); + $commits = id(new PhabricatorAuditCommitQuery()) + ->withIdentifiers( + $repository->getID(), + array_unique($rev_list)) + ->execute(); foreach ($commits as $commit) { $blame_dict[$commit->getCommitIdentifier()]['epoch'] = @@ -115,4 +116,8 @@ abstract class DiffusionFileContentQuery extends DiffusionQuery { public function getNeedsBlame() { return $this->needsBlame; } + + protected function processRevList(array $rev_list) { + return $rev_list; + } } diff --git a/src/applications/diffusion/query/filecontent/base/__init__.php b/src/applications/diffusion/query/filecontent/base/__init__.php index cba7efae94..db6a0b7560 100644 --- a/src/applications/diffusion/query/filecontent/base/__init__.php +++ b/src/applications/diffusion/query/filecontent/base/__init__.php @@ -6,9 +6,9 @@ +phutil_require_module('phabricator', 'applications/audit/query/commit'); phutil_require_module('phabricator', 'applications/diffusion/query/base'); phutil_require_module('phabricator', 'applications/phid/handle/data'); -phutil_require_module('phabricator', 'applications/repository/storage/commit'); phutil_require_module('phabricator', 'applications/repository/storage/commitdata'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/diffusion/query/filecontent/mercurial/DiffusionMercurialFileContentQuery.php b/src/applications/diffusion/query/filecontent/mercurial/DiffusionMercurialFileContentQuery.php index 383120c36c..070176ec03 100644 --- a/src/applications/diffusion/query/filecontent/mercurial/DiffusionMercurialFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/mercurial/DiffusionMercurialFileContentQuery.php @@ -1,7 +1,7 @@ getPath(); $commit = $drequest->getCommit(); - list($corpus) = $repository->execxLocalCommand( - 'cat --rev %s -- %s', - $commit, - $path); + if ($this->getNeedsBlame()) { + // NOTE: We're using "--number" instead of "--changeset" because there is + // no way to get "--changeset" to show us the full commit hashes. + list($corpus) = $repository->execxLocalCommand( + 'annotate --user --number --rev %s -- %s', + $commit, + $path); + } else { + list($corpus) = $repository->execxLocalCommand( + 'cat --rev %s -- %s', + $commit, + $path); + } $file_content = new DiffusionFileContent(); $file_content->setCorpus($corpus); @@ -37,11 +46,45 @@ final class DiffusionMercurialFileContentQuery return $file_content; } - protected function tokenizeLine($line) { - // TODO: Support blame. - throw new Exception( - "Diffusion does not currently support blame for Mercurial."); + $matches = null; + + preg_match( + '/^(.*?)\s+([0-9]+): (.*)$/', + $line, + $matches); + + return array($matches[2], $matches[1], $matches[3]); + } + + /** + * Convert local revision IDs into full commit identifier hashes. + */ + protected function processRevList(array $rev_list) { + $drequest = $this->getRequest(); + $repository = $drequest->getRepository(); + + $revs = array_unique($rev_list); + foreach ($revs as $key => $rev) { + $revs[$key] = '--rev '.(int)$rev; + } + + list($stdout) = $repository->execxLocalCommand( + 'log --template=%s %C', + '{rev} {node}\\n', + implode(' ', $revs)); + + $map = array(); + foreach (explode("\n", trim($stdout)) as $line) { + list($rev, $node) = explode(' ', $line); + $map[$rev] = $node; + } + + foreach ($rev_list as $k => $rev) { + $rev_list[$k] = $map[$rev]; + } + + return $rev_list; } } diff --git a/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php b/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php index 2104823b64..29b9baccb7 100644 --- a/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php +++ b/src/applications/diffusion/query/filecontent/svn/DiffusionSvnFileContentQuery.php @@ -1,7 +1,7 @@ getRequest(); diff --git a/src/applications/diffusion/request/base/DiffusionRequest.php b/src/applications/diffusion/request/base/DiffusionRequest.php index 7a820777a5..fe1c74a0a2 100644 --- a/src/applications/diffusion/request/base/DiffusionRequest.php +++ b/src/applications/diffusion/request/base/DiffusionRequest.php @@ -348,6 +348,7 @@ abstract class DiffusionRequest { $req_callsign = false; $req_branch = false; + $req_commit = false; switch ($action) { case 'history': @@ -360,6 +361,10 @@ abstract class DiffusionRequest { $req_callsign = true; $req_branch = true; break; + case 'commit': + $req_callsign = true; + $req_commit = true; + break; } if ($req_callsign && !strlen($callsign)) { @@ -372,6 +377,11 @@ abstract class DiffusionRequest { "Diffusion URI action '{$action}' requires branch!"); } + if ($req_commit && !strlen($commit)) { + throw new Exception( + "Diffusion URI action '{$action}' requires commit!"); + } + switch ($action) { case 'change': case 'history': @@ -392,6 +402,11 @@ abstract class DiffusionRequest { // it came from a URI. $uri = "{$path}{$commit}"; break; + case 'commit': + $commit = ltrim($commit, ';'); + $callsign = rtrim($callsign, '/'); + $uri = "/r{$callsign}{$commit}"; + break; default: throw new Exception("Unknown Diffusion URI action '{$action}'!"); } diff --git a/webroot/rsrc/css/application/diffusion/diffusion-source.css b/webroot/rsrc/css/application/diffusion/diffusion-source.css index c2a32fd97d..572b896620 100644 --- a/webroot/rsrc/css/application/diffusion/diffusion-source.css +++ b/webroot/rsrc/css/application/diffusion/diffusion-source.css @@ -4,7 +4,6 @@ .diffusion-source { margin: 1em 0 2em; - table-layout: fixed; width: 100%; font-family: "Monaco", Consolas, monospace; font-size: 10px; @@ -12,35 +11,46 @@ .diffusion-source th { text-align: right; - padding: 2px 6px; - width: 44px; vertical-align: top; background: #eeeeee; color: #888888; border-style: solid; border-width: 0px 1px; border-color: #eeeeee #999999 #eeeeee #dddddd; - font-weight: bold; font-family: "Verdana"; font-size: 11px; - overflow: hidden; } .diffusion-source td { letter-spacing: 0.0083334px; vertical-align: top; - white-space: pre; + white-space: pre-wrap; padding-bottom: 1px; padding-left: 8px; line-height: 16px; - overflow: hidden; -} - -.diffusion-wide-link a { - /* Give the user a larger click target. */ - display: block; + width: 100%; } .diffusion-browse-type-form { float: right; } + +.diffusion-blame-link, +.diffusion-rev-link, +.diffusion-author-link { + white-space: nowrap; +} + +.diffusion-blame-link a, +.diffusion-rev-link a, +.diffusion-author-link a, +.diffusion-line-link a { + /* Give the user a larger click target. */ + display: block; + padding: 2px 8px; + font-weight: bold; +} + +.diffusion-author-link span { + padding: 2px 8px; +}