Summary:
Ref T4994. This stuff works:
- You can dump a blob of coverage information into `diffusion.updatecoverage`. This wipes existing coverage information and replaces it.
- It shows up when viewing files.
- It shows up when viewing commits.
This stuff does not work:
- When viewing files, the Javascript hover interaction isn't tied in yet.
- We always show this information, even if you're behind the commit where it was generated.
- You can't do incremental updates.
- There's no aggregation at the file (this file has 90% coverage), diff (the changes in this commit are 90% covered), or directory (the code in this directory has 90% coverage) levels yet.
- This is probably not the final form of the UI, storage, or API, so you should expect occasional changes over time. I've marked the method as "Unstable" for now.
Test Plan:
- Ran `save_lint.php` to check for collateral damage; it worked fine.
- Ran `save_lint.php` on a new branch to check creation.
- Published some fake coverage information.
- Viewed an affected commit.
- Viewed an affected file.
{F151915}
{F151916}
Reviewers: chad, btrahan
Reviewed By: btrahan
Subscribers: jhurwitz, epriestley, zeeg
Maniphest Tasks: T5044, T4994
Differential Revision: https://secure.phabricator.com/D9022
1088 lines
30 KiB
PHP
1088 lines
30 KiB
PHP
<?php
|
|
|
|
final class DiffusionBrowseFileController extends DiffusionBrowseController {
|
|
|
|
private $lintCommit;
|
|
private $lintMessages;
|
|
private $coverage;
|
|
|
|
public function processRequest() {
|
|
$request = $this->getRequest();
|
|
$drequest = $this->getDiffusionRequest();
|
|
|
|
$before = $request->getStr('before');
|
|
if ($before) {
|
|
return $this->buildBeforeResponse($before);
|
|
}
|
|
|
|
$path = $drequest->getPath();
|
|
|
|
$preferences = $request->getUser()->loadPreferences();
|
|
|
|
$show_blame = $request->getBool(
|
|
'blame',
|
|
$preferences->getPreference(
|
|
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME,
|
|
false));
|
|
$show_color = $request->getBool(
|
|
'color',
|
|
$preferences->getPreference(
|
|
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR,
|
|
true));
|
|
|
|
$view = $request->getStr('view');
|
|
if ($request->isFormPost() && $view != 'raw') {
|
|
$preferences->setPreference(
|
|
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME,
|
|
$show_blame);
|
|
$preferences->setPreference(
|
|
PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR,
|
|
$show_color);
|
|
$preferences->save();
|
|
|
|
$uri = $request->getRequestURI()
|
|
->alter('blame', null)
|
|
->alter('color', null);
|
|
|
|
return id(new AphrontRedirectResponse())->setURI($uri);
|
|
}
|
|
|
|
// We need the blame information if blame is on and we're building plain
|
|
// text, or blame is on and this is an Ajax request. If blame is on and
|
|
// this is a colorized request, we don't show blame at first (we ajax it
|
|
// in afterward) so we don't need to query for it.
|
|
$needs_blame = ($show_blame && !$show_color) ||
|
|
($show_blame && $request->isAjax());
|
|
|
|
$file_content = DiffusionFileContent::newFromConduit(
|
|
$this->callConduitWithDiffusionRequest(
|
|
'diffusion.filecontentquery',
|
|
array(
|
|
'commit' => $drequest->getCommit(),
|
|
'path' => $drequest->getPath(),
|
|
'needsBlame' => $needs_blame,
|
|
)));
|
|
$data = $file_content->getCorpus();
|
|
|
|
if ($view === 'raw') {
|
|
return $this->buildRawResponse($path, $data);
|
|
}
|
|
|
|
$this->loadLintMessages();
|
|
$this->coverage = $drequest->loadCoverage();
|
|
|
|
$binary_uri = null;
|
|
if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) {
|
|
$file = $this->loadFileForData($path, $data);
|
|
$file_uri = $file->getBestURI();
|
|
|
|
if ($file->isViewableImage()) {
|
|
$corpus = $this->buildImageCorpus($file_uri);
|
|
} else {
|
|
$corpus = $this->buildBinaryCorpus($file_uri, $data);
|
|
$binary_uri = $file_uri;
|
|
}
|
|
} else {
|
|
// Build the content of the file.
|
|
$corpus = $this->buildCorpus(
|
|
$show_blame,
|
|
$show_color,
|
|
$file_content,
|
|
$needs_blame,
|
|
$drequest,
|
|
$path,
|
|
$data);
|
|
}
|
|
|
|
if ($request->isAjax()) {
|
|
return id(new AphrontAjaxResponse())->setContent($corpus);
|
|
}
|
|
|
|
require_celerity_resource('diffusion-source-css');
|
|
|
|
// Render the page.
|
|
$view = $this->buildActionView($drequest);
|
|
$action_list = $this->enrichActionView(
|
|
$view,
|
|
$drequest,
|
|
$show_blame,
|
|
$show_color);
|
|
|
|
$properties = $this->buildPropertyView($drequest, $action_list);
|
|
$object_box = id(new PHUIObjectBoxView())
|
|
->setHeader($this->buildHeaderView($drequest))
|
|
->addPropertyList($properties);
|
|
|
|
$content = array();
|
|
$content[] = $object_box;
|
|
|
|
$follow = $request->getStr('follow');
|
|
if ($follow) {
|
|
$notice = new AphrontErrorView();
|
|
$notice->setSeverity(AphrontErrorView::SEVERITY_WARNING);
|
|
$notice->setTitle(pht('Unable to Continue'));
|
|
switch ($follow) {
|
|
case 'first':
|
|
$notice->appendChild(
|
|
pht("Unable to continue tracing the history of this file because ".
|
|
"this commit is the first commit in the repository."));
|
|
break;
|
|
case 'created':
|
|
$notice->appendChild(
|
|
pht("Unable to continue tracing the history of this file because ".
|
|
"this commit created the file."));
|
|
break;
|
|
}
|
|
$content[] = $notice;
|
|
}
|
|
|
|
$renamed = $request->getStr('renamed');
|
|
if ($renamed) {
|
|
$notice = new AphrontErrorView();
|
|
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
|
|
$notice->setTitle(pht('File Renamed'));
|
|
$notice->appendChild(
|
|
pht("File history passes through a rename from '%s' to '%s'.",
|
|
$drequest->getPath(), $renamed));
|
|
$content[] = $notice;
|
|
}
|
|
|
|
$content[] = $corpus;
|
|
$content[] = $this->buildOpenRevisions();
|
|
|
|
$crumbs = $this->buildCrumbs(
|
|
array(
|
|
'branch' => true,
|
|
'path' => true,
|
|
'view' => 'browse',
|
|
));
|
|
|
|
$basename = basename($this->getDiffusionRequest()->getPath());
|
|
|
|
return $this->buildApplicationPage(
|
|
array(
|
|
$crumbs,
|
|
$content,
|
|
),
|
|
array(
|
|
'title' => $basename,
|
|
));
|
|
}
|
|
|
|
private function loadLintMessages() {
|
|
$drequest = $this->getDiffusionRequest();
|
|
$branch = $drequest->loadBranch();
|
|
|
|
if (!$branch || !$branch->getLintCommit()) {
|
|
return;
|
|
}
|
|
|
|
$this->lintCommit = $branch->getLintCommit();
|
|
|
|
$conn = id(new PhabricatorRepository())->establishConnection('r');
|
|
|
|
$where = '';
|
|
if ($drequest->getLint()) {
|
|
$where = qsprintf(
|
|
$conn,
|
|
'AND code = %s',
|
|
$drequest->getLint());
|
|
}
|
|
|
|
$this->lintMessages = queryfx_all(
|
|
$conn,
|
|
'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s',
|
|
PhabricatorRepository::TABLE_LINTMESSAGE,
|
|
$branch->getID(),
|
|
$where,
|
|
'/'.$drequest->getPath());
|
|
}
|
|
|
|
private function buildCorpus(
|
|
$show_blame,
|
|
$show_color,
|
|
DiffusionFileContent $file_content,
|
|
$needs_blame,
|
|
DiffusionRequest $drequest,
|
|
$path,
|
|
$data) {
|
|
|
|
if (!$show_color) {
|
|
$style =
|
|
"border: none; width: 100%; height: 80em; font-family: monospace";
|
|
if (!$show_blame) {
|
|
$corpus = phutil_tag(
|
|
'textarea',
|
|
array(
|
|
'style' => $style,
|
|
),
|
|
$file_content->getCorpus());
|
|
} else {
|
|
$text_list = $file_content->getTextList();
|
|
$rev_list = $file_content->getRevList();
|
|
$blame_dict = $file_content->getBlameDict();
|
|
|
|
$rows = array();
|
|
foreach ($text_list as $k => $line) {
|
|
$rev = $rev_list[$k];
|
|
$author = $blame_dict[$rev]['author'];
|
|
$rows[] =
|
|
sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line);
|
|
}
|
|
|
|
$corpus = phutil_tag(
|
|
'textarea',
|
|
array(
|
|
'style' => $style,
|
|
),
|
|
implode("\n", $rows));
|
|
}
|
|
} else {
|
|
require_celerity_resource('syntax-highlighting-css');
|
|
$text_list = $file_content->getTextList();
|
|
$rev_list = $file_content->getRevList();
|
|
$blame_dict = $file_content->getBlameDict();
|
|
|
|
$text_list = implode("\n", $text_list);
|
|
$text_list = PhabricatorSyntaxHighlighter::highlightWithFilename(
|
|
$path,
|
|
$text_list);
|
|
$text_list = explode("\n", $text_list);
|
|
|
|
$rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict,
|
|
$needs_blame, $drequest, $show_blame, $show_color);
|
|
|
|
$corpus_table = javelin_tag(
|
|
'table',
|
|
array(
|
|
'class' => "diffusion-source remarkup-code PhabricatorMonospaced",
|
|
'sigil' => 'phabricator-source',
|
|
),
|
|
$rows);
|
|
|
|
if ($this->getRequest()->isAjax()) {
|
|
return $corpus_table;
|
|
}
|
|
|
|
$id = celerity_generate_unique_node_id();
|
|
|
|
$projects = $drequest->loadArcanistProjects();
|
|
$langs = array();
|
|
foreach ($projects as $project) {
|
|
$ls = $project->getSymbolIndexLanguages();
|
|
if (!$ls) {
|
|
continue;
|
|
}
|
|
$dep_projects = $project->getSymbolIndexProjects();
|
|
$dep_projects[] = $project->getPHID();
|
|
foreach ($ls as $lang) {
|
|
if (!isset($langs[$lang])) {
|
|
$langs[$lang] = array();
|
|
}
|
|
$langs[$lang] += $dep_projects + array($project);
|
|
}
|
|
}
|
|
|
|
$lang = last(explode('.', $drequest->getPath()));
|
|
|
|
if (isset($langs[$lang])) {
|
|
Javelin::initBehavior(
|
|
'repository-crossreference',
|
|
array(
|
|
'container' => $id,
|
|
'lang' => $lang,
|
|
'projects' => $langs[$lang],
|
|
));
|
|
}
|
|
|
|
$corpus = phutil_tag(
|
|
'div',
|
|
array(
|
|
'id' => $id,
|
|
),
|
|
$corpus_table);
|
|
|
|
Javelin::initBehavior('load-blame', array('id' => $id));
|
|
}
|
|
|
|
$edit = $this->renderEditButton();
|
|
$file = $this->renderFileButton();
|
|
$header = id(new PHUIHeaderView())
|
|
->setHeader(pht('File Contents'))
|
|
->addActionLink($edit)
|
|
->addActionLink($file);
|
|
|
|
$corpus = id(new PHUIObjectBoxView())
|
|
->setHeader($header)
|
|
->appendChild($corpus);
|
|
|
|
return $corpus;
|
|
}
|
|
|
|
private function enrichActionView(
|
|
PhabricatorActionListView $view,
|
|
DiffusionRequest $drequest,
|
|
$show_blame,
|
|
$show_color) {
|
|
|
|
$viewer = $this->getRequest()->getUser();
|
|
$base_uri = $this->getRequest()->getRequestURI();
|
|
|
|
$view->addAction(
|
|
id(new PhabricatorActionView())
|
|
->setName(pht('Show Last Change'))
|
|
->setHref(
|
|
$drequest->generateURI(
|
|
array(
|
|
'action' => 'change',
|
|
)))
|
|
->setIcon('fa-backward'));
|
|
|
|
if ($show_blame) {
|
|
$blame_text = pht('Disable Blame');
|
|
$blame_icon = 'fa-exclamation-circle lightgreytext';
|
|
$blame_value = 0;
|
|
} else {
|
|
$blame_text = pht('Enable Blame');
|
|
$blame_icon = 'fa-exclamation-circle';
|
|
$blame_value = 1;
|
|
}
|
|
|
|
$view->addAction(
|
|
id(new PhabricatorActionView())
|
|
->setName($blame_text)
|
|
->setHref($base_uri->alter('blame', $blame_value))
|
|
->setIcon($blame_icon)
|
|
->setUser($viewer)
|
|
->setRenderAsForm(true));
|
|
|
|
if ($show_color) {
|
|
$highlight_text = pht('Disable Highlighting');
|
|
$highlight_icon = 'fa-star-o grey';
|
|
$highlight_value = 0;
|
|
} else {
|
|
$highlight_text = pht('Enable Highlighting');
|
|
$highlight_icon = 'fa-star';
|
|
$highlight_value = 1;
|
|
}
|
|
|
|
$view->addAction(
|
|
id(new PhabricatorActionView())
|
|
->setName($highlight_text)
|
|
->setHref($base_uri->alter('color', $highlight_value))
|
|
->setIcon($highlight_icon)
|
|
->setUser($viewer)
|
|
->setRenderAsForm(true));
|
|
|
|
$href = null;
|
|
if ($this->getRequest()->getStr('lint') !== null) {
|
|
$lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages));
|
|
$href = $base_uri->alter('lint', null);
|
|
|
|
} else if ($this->lintCommit === null) {
|
|
$lint_text = pht('Lint not Available');
|
|
} else {
|
|
$lint_text = pht(
|
|
'Show %d Lint Message(s)',
|
|
count($this->lintMessages));
|
|
$href = $this->getDiffusionRequest()->generateURI(array(
|
|
'action' => 'browse',
|
|
'commit' => $this->lintCommit,
|
|
))->alter('lint', '');
|
|
}
|
|
|
|
$view->addAction(
|
|
id(new PhabricatorActionView())
|
|
->setName($lint_text)
|
|
->setHref($href)
|
|
->setIcon('fa-exclamation-triangle')
|
|
->setDisabled(!$href));
|
|
|
|
return $view;
|
|
}
|
|
|
|
private function renderEditButton() {
|
|
$request = $this->getRequest();
|
|
$user = $request->getUser();
|
|
|
|
$drequest = $this->getDiffusionRequest();
|
|
|
|
$repository = $drequest->getRepository();
|
|
$path = $drequest->getPath();
|
|
$line = nonempty((int)$drequest->getLine(), 1);
|
|
|
|
$callsign = $repository->getCallsign();
|
|
$editor_link = $user->loadEditorLink($path, $line, $callsign);
|
|
|
|
$icon_edit = id(new PHUIIconView())
|
|
->setIconFont('fa-pencil');
|
|
$button = id(new PHUIButtonView())
|
|
->setTag('a')
|
|
->setText(pht('Open in Editor'))
|
|
->setHref($editor_link)
|
|
->setIcon($icon_edit)
|
|
->setDisabled(!$editor_link);
|
|
|
|
return $button;
|
|
}
|
|
|
|
private function renderFileButton($file_uri = null) {
|
|
|
|
$base_uri = $this->getRequest()->getRequestURI();
|
|
|
|
if ($file_uri) {
|
|
$text = pht('Download Raw File');
|
|
$href = $file_uri;
|
|
$icon = 'fa-download';
|
|
} else {
|
|
$text = pht('View Raw File');
|
|
$href = $base_uri->alter('view', 'raw');
|
|
$icon = 'fa-file-text';
|
|
}
|
|
|
|
$iconview = id(new PHUIIconView())
|
|
->setIconFont($icon);
|
|
$button = id(new PHUIButtonView())
|
|
->setTag('a')
|
|
->setText($text)
|
|
->setHref($href)
|
|
->setIcon($iconview);
|
|
|
|
return $button;
|
|
}
|
|
|
|
|
|
private function buildDisplayRows(
|
|
array $text_list,
|
|
array $rev_list,
|
|
array $blame_dict,
|
|
$needs_blame,
|
|
DiffusionRequest $drequest,
|
|
$show_blame,
|
|
$show_color) {
|
|
|
|
$handles = array();
|
|
if ($blame_dict) {
|
|
$epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch');
|
|
$epoch_min = min($epoch_list);
|
|
$epoch_max = max($epoch_list);
|
|
$epoch_range = ($epoch_max - $epoch_min) + 1;
|
|
|
|
$author_phids = ipull(ifilter($blame_dict, 'authorPHID'), 'authorPHID');
|
|
$handles = $this->loadViewerHandles($author_phids);
|
|
}
|
|
|
|
$line_arr = array();
|
|
$line_str = $drequest->getLine();
|
|
$ranges = explode(',', $line_str);
|
|
foreach ($ranges as $range) {
|
|
if (strpos($range, '-') !== false) {
|
|
list($min, $max) = explode('-', $range, 2);
|
|
$line_arr[] = array(
|
|
'min' => min($min, $max),
|
|
'max' => max($min, $max),
|
|
);
|
|
} else if (strlen($range)) {
|
|
$line_arr[] = array(
|
|
'min' => $range,
|
|
'max' => $range,
|
|
);
|
|
}
|
|
}
|
|
|
|
$display = array();
|
|
|
|
$line_number = 1;
|
|
$last_rev = null;
|
|
$color = null;
|
|
foreach ($text_list as $k => $line) {
|
|
$display_line = array(
|
|
'epoch' => null,
|
|
'commit' => null,
|
|
'author' => null,
|
|
'target' => null,
|
|
'highlighted' => null,
|
|
'line' => $line_number,
|
|
'data' => $line,
|
|
);
|
|
|
|
if ($show_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 more saturated the color.
|
|
|
|
$rev = idx($rev_list, $k, $last_rev);
|
|
|
|
if ($last_rev == $rev) {
|
|
$display_line['color'] = $color;
|
|
} else {
|
|
$blame = $blame_dict[$rev];
|
|
|
|
if (!isset($blame['epoch'])) {
|
|
$color = '#ffd'; // Render as warning.
|
|
} else {
|
|
$color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range;
|
|
$color_value = 0xE6 * (1.0 - $color_ratio);
|
|
$color = sprintf(
|
|
'#%02x%02x%02x',
|
|
$color_value,
|
|
0xF6,
|
|
$color_value);
|
|
}
|
|
|
|
$display_line['epoch'] = idx($blame, 'epoch');
|
|
$display_line['color'] = $color;
|
|
$display_line['commit'] = $rev;
|
|
|
|
$author_phid = idx($blame, 'authorPHID');
|
|
if ($author_phid && $handles[$author_phid]) {
|
|
$author_link = $handles[$author_phid]->renderLink();
|
|
} else {
|
|
$author_link = $blame['author'];
|
|
}
|
|
$display_line['author'] = $author_link;
|
|
|
|
$last_rev = $rev;
|
|
}
|
|
}
|
|
|
|
if ($line_arr) {
|
|
if ($line_number == $line_arr[0]['min']) {
|
|
$display_line['target'] = true;
|
|
}
|
|
foreach ($line_arr as $range) {
|
|
if ($line_number >= $range['min'] &&
|
|
$line_number <= $range['max']) {
|
|
$display_line['highlighted'] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$display[] = $display_line;
|
|
++$line_number;
|
|
}
|
|
|
|
$request = $this->getRequest();
|
|
$viewer = $request->getUser();
|
|
|
|
$commits = array_filter(ipull($display, 'commit'));
|
|
if ($commits) {
|
|
$commits = id(new DiffusionCommitQuery())
|
|
->setViewer($viewer)
|
|
->withRepository($drequest->getRepository())
|
|
->withIdentifiers($commits)
|
|
->execute();
|
|
$commits = mpull($commits, null, 'getCommitIdentifier');
|
|
}
|
|
|
|
$revision_ids = id(new DifferentialRevision())
|
|
->loadIDsByCommitPHIDs(mpull($commits, 'getPHID'));
|
|
$revisions = array();
|
|
if ($revision_ids) {
|
|
$revisions = id(new DifferentialRevisionQuery())
|
|
->setViewer($viewer)
|
|
->withIDs($revision_ids)
|
|
->execute();
|
|
}
|
|
|
|
$phids = array();
|
|
foreach ($commits as $commit) {
|
|
if ($commit->getAuthorPHID()) {
|
|
$phids[] = $commit->getAuthorPHID();
|
|
}
|
|
}
|
|
foreach ($revisions as $revision) {
|
|
if ($revision->getAuthorPHID()) {
|
|
$phids[] = $revision->getAuthorPHID();
|
|
}
|
|
}
|
|
$handles = $this->loadViewerHandles($phids);
|
|
|
|
Javelin::initBehavior('phabricator-oncopy', array());
|
|
|
|
$engine = null;
|
|
$inlines = array();
|
|
if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) {
|
|
$engine = new PhabricatorMarkupEngine();
|
|
$engine->setViewer($viewer);
|
|
|
|
foreach ($this->lintMessages as $message) {
|
|
$inline = id(new PhabricatorAuditInlineComment())
|
|
->setID($message['id'])
|
|
->setSyntheticAuthor(
|
|
ArcanistLintSeverity::getStringForSeverity($message['severity']).
|
|
' '.$message['code'].' ('.$message['name'].')')
|
|
->setLineNumber($message['line'])
|
|
->setContent($message['description']);
|
|
$inlines[$message['line']][] = $inline;
|
|
|
|
$engine->addObject(
|
|
$inline,
|
|
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
|
|
}
|
|
|
|
$engine->process();
|
|
require_celerity_resource('differential-changeset-view-css');
|
|
}
|
|
|
|
$rows = $this->renderInlines(
|
|
idx($inlines, 0, array()),
|
|
$show_blame,
|
|
(bool)$this->coverage,
|
|
$engine);
|
|
|
|
foreach ($display as $line) {
|
|
|
|
$line_href = $drequest->generateURI(
|
|
array(
|
|
'action' => 'browse',
|
|
'line' => $line['line'],
|
|
'stable' => true,
|
|
));
|
|
|
|
$blame = array();
|
|
$style = null;
|
|
if (array_key_exists('color', $line)) {
|
|
if ($line['color']) {
|
|
$style = 'background: '.$line['color'].';';
|
|
}
|
|
|
|
$before_link = null;
|
|
$commit_link = null;
|
|
$revision_link = null;
|
|
if (idx($line, 'commit')) {
|
|
$commit = $line['commit'];
|
|
|
|
if (idx($commits, $commit)) {
|
|
$tooltip = $this->renderCommitTooltip(
|
|
$commits[$commit],
|
|
$handles,
|
|
$line['author']);
|
|
} else {
|
|
$tooltip = null;
|
|
}
|
|
|
|
Javelin::initBehavior('phabricator-tooltips', array());
|
|
require_celerity_resource('aphront-tooltip-css');
|
|
|
|
$commit_link = javelin_tag(
|
|
'a',
|
|
array(
|
|
'href' => $drequest->generateURI(
|
|
array(
|
|
'action' => 'commit',
|
|
'commit' => $line['commit'],
|
|
)),
|
|
'sigil' => 'has-tooltip',
|
|
'meta' => array(
|
|
'tip' => $tooltip,
|
|
'align' => 'E',
|
|
'size' => 600,
|
|
),
|
|
),
|
|
phutil_utf8_shorten($line['commit'], 9, ''));
|
|
|
|
$revision_id = null;
|
|
if (idx($commits, $commit)) {
|
|
$revision_id = idx($revision_ids, $commits[$commit]->getPHID());
|
|
}
|
|
|
|
if ($revision_id) {
|
|
$revision = idx($revisions, $revision_id);
|
|
if ($revision) {
|
|
$tooltip = $this->renderRevisionTooltip($revision, $handles);
|
|
$revision_link = javelin_tag(
|
|
'a',
|
|
array(
|
|
'href' => '/D'.$revision->getID(),
|
|
'sigil' => 'has-tooltip',
|
|
'meta' => array(
|
|
'tip' => $tooltip,
|
|
'align' => 'E',
|
|
'size' => 600,
|
|
),
|
|
),
|
|
'D'.$revision->getID());
|
|
}
|
|
}
|
|
|
|
$uri = $line_href->alter('before', $commit);
|
|
$before_link = javelin_tag(
|
|
'a',
|
|
array(
|
|
'href' => $uri->setQueryParam('view', 'blame'),
|
|
'sigil' => 'has-tooltip',
|
|
'meta' => array(
|
|
'tip' => pht('Skip Past This Commit'),
|
|
'align' => 'E',
|
|
'size' => 300,
|
|
),
|
|
),
|
|
"\xC2\xAB");
|
|
}
|
|
|
|
$blame[] = phutil_tag(
|
|
'th',
|
|
array(
|
|
'class' => 'diffusion-blame-link',
|
|
),
|
|
$before_link);
|
|
|
|
$object_links = array();
|
|
$object_links[] = $commit_link;
|
|
if ($revision_link) {
|
|
$object_links[] = phutil_tag('span', array(), '/');
|
|
$object_links[] = $revision_link;
|
|
}
|
|
|
|
$blame[] = phutil_tag(
|
|
'th',
|
|
array(
|
|
'class' => 'diffusion-rev-link',
|
|
),
|
|
$object_links);
|
|
}
|
|
|
|
$line_link = phutil_tag(
|
|
'a',
|
|
array(
|
|
'href' => $line_href,
|
|
'style' => $style,
|
|
),
|
|
$line['line']);
|
|
|
|
$blame[] = javelin_tag(
|
|
'th',
|
|
array(
|
|
'class' => 'diffusion-line-link',
|
|
'sigil' => 'phabricator-source-line',
|
|
'style' => $style,
|
|
),
|
|
$line_link);
|
|
|
|
Javelin::initBehavior('phabricator-line-linker');
|
|
|
|
if ($line['target']) {
|
|
Javelin::initBehavior(
|
|
'diffusion-jump-to',
|
|
array(
|
|
'target' => 'scroll_target',
|
|
));
|
|
$anchor_text = phutil_tag(
|
|
'a',
|
|
array(
|
|
'id' => 'scroll_target',
|
|
),
|
|
'');
|
|
} else {
|
|
$anchor_text = null;
|
|
}
|
|
|
|
$blame[] = phutil_tag(
|
|
'td',
|
|
array(
|
|
),
|
|
array(
|
|
$anchor_text,
|
|
|
|
// NOTE: See phabricator-oncopy behavior.
|
|
"\xE2\x80\x8B",
|
|
|
|
// TODO: [HTML] Not ideal.
|
|
phutil_safe_html(str_replace("\t", ' ', $line['data'])),
|
|
));
|
|
|
|
if ($this->coverage) {
|
|
require_celerity_resource('differential-changeset-view-css');
|
|
$cov_index = $line['line'] - 1;
|
|
|
|
if (isset($this->coverage[$cov_index])) {
|
|
$cov_class = $this->coverage[$cov_index];
|
|
} else {
|
|
$cov_class = 'N';
|
|
}
|
|
|
|
$blame[] = phutil_tag(
|
|
'td',
|
|
array(
|
|
'class' => 'cov cov-'.$cov_class,
|
|
),
|
|
'');
|
|
}
|
|
|
|
$rows[] = phutil_tag(
|
|
'tr',
|
|
array(
|
|
'class' => ($line['highlighted'] ?
|
|
'phabricator-source-highlight' :
|
|
null),
|
|
),
|
|
$blame);
|
|
|
|
$cur_inlines = $this->renderInlines(
|
|
idx($inlines, $line['line'], array()),
|
|
$show_blame,
|
|
$this->coverage,
|
|
$engine);
|
|
foreach ($cur_inlines as $cur_inline) {
|
|
$rows[] = $cur_inline;
|
|
}
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function renderInlines(
|
|
array $inlines,
|
|
$needs_blame,
|
|
$has_coverage,
|
|
$engine) {
|
|
|
|
$rows = array();
|
|
foreach ($inlines as $inline) {
|
|
$inline_view = id(new DifferentialInlineCommentView())
|
|
->setMarkupEngine($engine)
|
|
->setInlineComment($inline)
|
|
->render();
|
|
|
|
$row = array_fill(0, ($needs_blame ? 3 : 1), phutil_tag('th'));
|
|
|
|
$row[] = phutil_tag('td', array(), $inline_view);
|
|
|
|
if ($has_coverage) {
|
|
$row[] = phutil_tag(
|
|
'td',
|
|
array(
|
|
'class' => 'cov cov-I',
|
|
));
|
|
}
|
|
|
|
$rows[] = phutil_tag('tr', array('class' => 'inline'), $row);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function loadFileForData($path, $data) {
|
|
$file = PhabricatorFile::buildFromFileDataOrHash(
|
|
$data,
|
|
array(
|
|
'name' => basename($path),
|
|
'ttl' => time() + 60 * 60 * 24,
|
|
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
|
|
));
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
$file->attachToObject(
|
|
$this->getRequest()->getUser(),
|
|
$this->getDiffusionRequest()->getRepository()->getPHID());
|
|
unset($unguarded);
|
|
|
|
return $file;
|
|
}
|
|
|
|
private function buildRawResponse($path, $data) {
|
|
$file = $this->loadFileForData($path, $data);
|
|
return id(new AphrontRedirectResponse())->setURI($file->getBestURI());
|
|
}
|
|
|
|
private function buildImageCorpus($file_uri) {
|
|
$properties = new PHUIPropertyListView();
|
|
|
|
$properties->addImageContent(
|
|
phutil_tag(
|
|
'img',
|
|
array(
|
|
'src' => $file_uri,
|
|
)));
|
|
|
|
$file = $this->renderFileButton($file_uri);
|
|
$header = id(new PHUIHeaderView())
|
|
->setHeader(pht('Image'))
|
|
->addActionLink($file);
|
|
|
|
return id(new PHUIObjectBoxView())
|
|
->setHeader($header)
|
|
->addPropertyList($properties);
|
|
}
|
|
|
|
private function buildBinaryCorpus($file_uri, $data) {
|
|
|
|
$size = new PhutilNumber(strlen($data));
|
|
$text = pht('This is a binary file. It is %s byte(s) in length.', $size);
|
|
$text = id(new PHUIBoxView())
|
|
->addPadding(PHUI::PADDING_LARGE)
|
|
->appendChild($text);
|
|
|
|
$file = $this->renderFileButton($file_uri);
|
|
$header = id(new PHUIHeaderView())
|
|
->setHeader(pht('Details'))
|
|
->addActionLink($file);
|
|
|
|
$box = id(new PHUIObjectBoxView())
|
|
->setHeader($header)
|
|
->appendChild($text);
|
|
|
|
return $box;
|
|
}
|
|
|
|
private function buildBeforeResponse($before) {
|
|
$request = $this->getRequest();
|
|
$drequest = $this->getDiffusionRequest();
|
|
|
|
// NOTE: We need to get the grandparent so we can capture filename changes
|
|
// in the parent.
|
|
|
|
$parent = $this->loadParentCommitOf($before);
|
|
$old_filename = null;
|
|
$was_created = false;
|
|
if ($parent) {
|
|
$grandparent = $this->loadParentCommitOf($parent);
|
|
|
|
if ($grandparent) {
|
|
$rename_query = new DiffusionRenameHistoryQuery();
|
|
$rename_query->setRequest($drequest);
|
|
$rename_query->setOldCommit($grandparent);
|
|
$rename_query->setViewer($request->getUser());
|
|
$old_filename = $rename_query->loadOldFilename();
|
|
$was_created = $rename_query->getWasCreated();
|
|
}
|
|
}
|
|
|
|
$follow = null;
|
|
if ($was_created) {
|
|
// If the file was created in history, that means older commits won't
|
|
// have it. Since we know it existed at 'before', it must have been
|
|
// created then; jump there.
|
|
$target_commit = $before;
|
|
$follow = 'created';
|
|
} else if ($parent) {
|
|
// If we found a parent, jump to it. This is the normal case.
|
|
$target_commit = $parent;
|
|
} else {
|
|
// If there's no parent, this was probably created in the initial commit?
|
|
// And the "was_created" check will fail because we can't identify the
|
|
// grandparent. Keep the user at 'before'.
|
|
$target_commit = $before;
|
|
$follow = 'first';
|
|
}
|
|
|
|
$path = $drequest->getPath();
|
|
$renamed = null;
|
|
if ($old_filename !== null &&
|
|
$old_filename !== '/'.$path) {
|
|
$renamed = $path;
|
|
$path = $old_filename;
|
|
}
|
|
|
|
$line = null;
|
|
// If there's a follow error, drop the line so the user sees the message.
|
|
if (!$follow) {
|
|
$line = $this->getBeforeLineNumber($target_commit);
|
|
}
|
|
|
|
$before_uri = $drequest->generateURI(
|
|
array(
|
|
'action' => 'browse',
|
|
'commit' => $target_commit,
|
|
'line' => $line,
|
|
'path' => $path,
|
|
));
|
|
|
|
$before_uri->setQueryParams($request->getRequestURI()->getQueryParams());
|
|
$before_uri = $before_uri->alter('before', null);
|
|
$before_uri = $before_uri->alter('renamed', $renamed);
|
|
$before_uri = $before_uri->alter('follow', $follow);
|
|
|
|
return id(new AphrontRedirectResponse())->setURI($before_uri);
|
|
}
|
|
|
|
private function getBeforeLineNumber($target_commit) {
|
|
$drequest = $this->getDiffusionRequest();
|
|
|
|
$line = $drequest->getLine();
|
|
if (!$line) {
|
|
return null;
|
|
}
|
|
|
|
$raw_diff = $this->callConduitWithDiffusionRequest(
|
|
'diffusion.rawdiffquery',
|
|
array(
|
|
'commit' => $drequest->getCommit(),
|
|
'path' => $drequest->getPath(),
|
|
'againstCommit' => $target_commit));
|
|
$old_line = 0;
|
|
$new_line = 0;
|
|
|
|
foreach (explode("\n", $raw_diff) as $text) {
|
|
if ($text[0] == '-' || $text[0] == ' ') {
|
|
$old_line++;
|
|
}
|
|
if ($text[0] == '+' || $text[0] == ' ') {
|
|
$new_line++;
|
|
}
|
|
if ($new_line == $line) {
|
|
return $old_line;
|
|
}
|
|
}
|
|
|
|
// We didn't find the target line.
|
|
return $line;
|
|
}
|
|
|
|
private function loadParentCommitOf($commit) {
|
|
$drequest = $this->getDiffusionRequest();
|
|
$user = $this->getRequest()->getUser();
|
|
|
|
$before_req = DiffusionRequest::newFromDictionary(
|
|
array(
|
|
'user' => $user,
|
|
'repository' => $drequest->getRepository(),
|
|
'commit' => $commit,
|
|
));
|
|
|
|
$parents = DiffusionQuery::callConduitWithDiffusionRequest(
|
|
$user,
|
|
$before_req,
|
|
'diffusion.commitparentsquery',
|
|
array(
|
|
'commit' => $commit,
|
|
));
|
|
|
|
return head($parents);
|
|
}
|
|
|
|
private function renderRevisionTooltip(
|
|
DifferentialRevision $revision,
|
|
array $handles) {
|
|
$viewer = $this->getRequest()->getUser();
|
|
|
|
$date = phabricator_date($revision->getDateModified(), $viewer);
|
|
$id = $revision->getID();
|
|
$title = $revision->getTitle();
|
|
$header = "D{$id} {$title}";
|
|
|
|
$author = $handles[$revision->getAuthorPHID()]->getName();
|
|
|
|
return "{$header}\n{$date} \xC2\xB7 {$author}";
|
|
}
|
|
|
|
private function renderCommitTooltip(
|
|
PhabricatorRepositoryCommit $commit,
|
|
array $handles,
|
|
$author) {
|
|
|
|
$viewer = $this->getRequest()->getUser();
|
|
|
|
$date = phabricator_date($commit->getEpoch(), $viewer);
|
|
$summary = trim($commit->getSummary());
|
|
|
|
if ($commit->getAuthorPHID()) {
|
|
$author = $handles[$commit->getAuthorPHID()]->getName();
|
|
}
|
|
|
|
return "{$summary}\n{$date} \xC2\xB7 {$author}";
|
|
}
|
|
|
|
}
|