Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index be6a6546a2..fb2d32aa4c 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -1,1354 +1,1470 @@
<?php
final class DifferentialChangesetParser {
protected $visible = array();
protected $new = array();
protected $old = array();
protected $intra = array();
protected $newRender = null;
protected $oldRender = null;
protected $filename = null;
protected $missingOld = array();
protected $missingNew = array();
protected $comments = array();
protected $specialAttributes = array();
protected $changeset;
protected $whitespaceMode = null;
protected $subparser;
protected $renderCacheKey = null;
private $handles = array();
private $user;
private $leftSideChangesetID;
private $leftSideAttachesToNewFile;
private $rightSideChangesetID;
private $rightSideAttachesToNewFile;
private $originalLeft;
private $originalRight;
private $renderingReference;
private $isSubparser;
private $isTopLevel;
private $coverage;
private $markupEngine;
private $highlightErrors;
const CACHE_VERSION = 8;
const CACHE_MAX_SIZE = 8e6;
const ATTR_GENERATED = 'attr:generated';
const ATTR_DELETED = 'attr:deleted';
const ATTR_UNCHANGED = 'attr:unchanged';
const ATTR_WHITELINES = 'attr:white';
const LINES_CONTEXT = 8;
const WHITESPACE_SHOW_ALL = 'show-all';
const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing';
// TODO: This is now "Ignore Most" in the UI.
const WHITESPACE_IGNORE_ALL = 'ignore-all';
const WHITESPACE_IGNORE_FORCE = 'ignore-force';
/**
* Configure which Changeset comments added to the right side of the visible
* diff will be attached to. The ID must be the ID of a real Differential
* Changeset.
*
* The complexity here is that we may show an arbitrary side of an arbitrary
* changeset as either the left or right part of a diff. This method allows
* the left and right halves of the displayed diff to be correctly mapped to
* storage changesets.
*
* @param id The Differential Changeset ID that comments added to the right
* side of the visible diff should be attached to.
* @param bool If true, attach new comments to the right side of the storage
* changeset. Note that this may be false, if the left side of
* some storage changeset is being shown as the right side of
* a display diff.
* @return this
*/
public function setRightSideCommentMapping($id, $is_new) {
$this->rightSideChangesetID = $id;
$this->rightSideAttachesToNewFile = $is_new;
return $this;
}
/**
* See setRightSideCommentMapping(), but this sets information for the left
* side of the display diff.
*/
public function setLeftSideCommentMapping($id, $is_new) {
$this->leftSideChangesetID = $id;
$this->leftSideAttachesToNewFile = $is_new;
return $this;
}
public function setOriginals(
DifferentialChangeset $left,
DifferentialChangeset $right) {
$this->originalLeft = $left;
$this->originalRight = $right;
}
public function diffOriginals() {
$engine = new PhabricatorDifferenceEngine();
$changeset = $engine->generateChangesetFromFileContent(
implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),
implode('', mpull($this->originalRight->getHunks(), 'getChanges')));
// Put changes side by side.
$olds = array();
$news = array();
foreach ($changeset->getHunks() as $hunk) {
$n_old = $hunk->getOldOffset();
$n_new = $hunk->getNewOffset();
$changes = phutil_split_lines($hunk->getChanges());
foreach ($changes as $line) {
$diff_type = $line[0]; // Change type in diff of diffs.
$orig_type = $line[1]; // Change type in the original diff.
if ($diff_type == ' ') {
// Use the same key for lines that are next to each other.
$key = max(last_key($olds), last_key($news)) + 1;
$olds[$key] = null;
$news[$key] = null;
} else if ($diff_type == '-') {
$olds[] = array($n_old, $orig_type);
} else if ($diff_type == '+') {
$news[] = array($n_new, $orig_type);
}
if (($diff_type == '-' || $diff_type == ' ') && $orig_type != '-') {
$n_old++;
}
if (($diff_type == '+' || $diff_type == ' ') && $orig_type != '-') {
$n_new++;
}
}
}
$offsets_old = $this->originalLeft->computeOffsets();
$offsets_new = $this->originalRight->computeOffsets();
// Highlight lines that were added on each side or removed on the other
// side.
$highlight_old = array();
$highlight_new = array();
$last = max(last_key($olds), last_key($news));
for ($i = 0; $i <= $last; $i++) {
if (isset($olds[$i])) {
list($n, $type) = $olds[$i];
if ($type == '+' ||
($type == ' ' && isset($news[$i]) && $news[$i][1] != ' ')) {
$highlight_old[] = $offsets_old[$n];
}
}
if (isset($news[$i])) {
list($n, $type) = $news[$i];
if ($type == '+' ||
($type == ' ' && isset($olds[$i]) && $olds[$i][1] != ' ')) {
$highlight_new[] = $offsets_new[$n];
}
}
}
return array($highlight_old, $highlight_new);
}
/**
* Set a key for identifying this changeset in the render cache. If set, the
* parser will attempt to use the changeset render cache, which can improve
* performance for frequently-viewed changesets.
*
* By default, there is no render cache key and parsers do not use the cache.
* This is appropriate for rarely-viewed changesets.
*
* NOTE: Currently, this key must be a valid Differential Changeset ID.
*
* @param string Key for identifying this changeset in the render cache.
* @return this
*/
public function setRenderCacheKey($key) {
$this->renderCacheKey = $key;
return $this;
}
private function getRenderCacheKey() {
return $this->renderCacheKey;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
return $this;
}
public function setWhitespaceMode($whitespace_mode) {
$this->whitespaceMode = $whitespace_mode;
return $this;
}
public function setRenderingReference($ref) {
$this->renderingReference = $ref;
return $this;
}
private function getRenderingReference() {
return $this->renderingReference;
}
public function getChangeset() {
return $this->changeset;
}
public function setFilename($filename) {
$this->filename = $filename;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function setCoverage($coverage) {
$this->coverage = $coverage;
return $this;
}
+ private function getCoverage() {
+ return $this->coverage;
+ }
public function parseHunk(DifferentialHunk $hunk) {
$lines = $hunk->getChanges();
$lines = phutil_split_lines($lines);
$types = array();
foreach ($lines as $line_index => $line) {
if (isset($line[0])) {
$char = $line[0];
if ($char == ' ') {
$types[$line_index] = null;
} else {
$types[$line_index] = $char;
}
} else {
$types[$line_index] = null;
}
}
$old_line = $hunk->getOldOffset();
$new_line = $hunk->getNewOffset();
$num_lines = count($lines);
if ($old_line > 1) {
$this->missingOld[$old_line] = true;
} else if ($new_line > 1) {
$this->missingNew[$new_line] = true;
}
for ($cursor = 0; $cursor < $num_lines; $cursor++) {
$type = $types[$cursor];
$data = array(
'type' => $type,
'text' => (string)substr($lines[$cursor], 1),
'line' => $new_line,
);
if ($type == '\\') {
$type = $types[$cursor - 1];
$data['text'] = ltrim($data['text']);
}
switch ($type) {
case '+':
$this->new[] = $data;
++$new_line;
break;
case '-':
$data['line'] = $old_line;
$this->old[] = $data;
++$old_line;
break;
default:
$this->new[] = $data;
$data['line'] = $old_line;
$this->old[] = $data;
++$new_line;
++$old_line;
break;
}
}
}
public function parseInlineComment(
PhabricatorInlineCommentInterface $comment) {
// Parse only comments which are actually visible.
if ($this->isCommentVisibleOnRenderedDiff($comment)) {
$this->comments[] = $comment;
}
return $this;
}
public function process() {
$old = array();
$new = array();
$this->old = array_reverse($this->old);
$this->new = array_reverse($this->new);
$whitelines = false;
$changed = false;
$skip_intra = array();
while (count($this->old) || count($this->new)) {
$o_desc = array_pop($this->old);
$n_desc = array_pop($this->new);
if ($o_desc) {
$o_type = $o_desc['type'];
} else {
$o_type = null;
}
if ($n_desc) {
$n_type = $n_desc['type'];
} else {
$n_type = null;
}
if (($o_type != null) && ($n_type == null)) {
$old[] = $o_desc;
$new[] = null;
if ($n_desc) {
array_push($this->new, $n_desc);
}
$changed = true;
continue;
}
if (($n_type != null) && ($o_type == null)) {
$old[] = null;
$new[] = $n_desc;
if ($o_desc) {
array_push($this->old, $o_desc);
}
$changed = true;
continue;
}
if ($this->whitespaceMode != self::WHITESPACE_SHOW_ALL) {
$similar = false;
switch ($this->whitespaceMode) {
case self::WHITESPACE_IGNORE_TRAILING:
if (rtrim($o_desc['text']) == rtrim($n_desc['text'])) {
if ($o_desc['type']) {
// If we're converting this into an unchanged line because of
// a trailing whitespace difference, mark it as a whitespace
// change so we can show "This file was modified only by
// adding or removing trailing whitespace." instead of
// "This file was not modified.".
$whitelines = true;
}
$similar = true;
}
break;
default:
// In this case, the lines are similar if there is no change type
// (that is, just trust the diff algorithm).
if (!$o_desc['type']) {
$similar = true;
}
break;
}
if ($similar) {
if ($o_desc['type'] == '\\') {
// These are similar because they're "No newline at end of file"
// comments.
} else {
$o_desc['type'] = null;
$n_desc['type'] = null;
$skip_intra[count($old)] = true;
}
} else {
$changed = true;
}
} else {
$changed = true;
}
$old[] = $o_desc;
$new[] = $n_desc;
}
$this->old = $old;
$this->new = $new;
$unchanged = false;
if ($this->subparser) {
$unchanged = $this->subparser->isUnchanged();
$whitelines = $this->subparser->isWhitespaceOnly();
} else if (!$changed) {
$filetype = $this->changeset->getFileType();
if ($filetype == DifferentialChangeType::FILE_TEXT ||
$filetype == DifferentialChangeType::FILE_SYMLINK) {
$unchanged = true;
}
}
$changetype = $this->changeset->getChangeType();
if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {
// sometimes we show moved files as unchanged, sometimes deleted,
// and sometimes inconsistent with what actually happened at the
// destination of the move. Rather than make a false claim,
// omit the 'not changed' notice if this is the source of a move
$unchanged = false;
}
$this->specialAttributes = array(
self::ATTR_UNCHANGED => $unchanged,
self::ATTR_DELETED => array_filter($this->old) &&
!array_filter($this->new),
self::ATTR_WHITELINES => $whitelines
);
if ($this->isSubparser) {
// The rest of this function deals with formatting the diff for display;
// we can exit early if we're a subparser and avoid doing extra work.
return;
}
if ($this->subparser) {
// Use this parser's side-by-side line information -- notably, the
// change types -- but replace all the line text with the subparser's.
// This lets us render whitespace-only changes without marking them as
// different.
$old = $this->old;
$new = $this->new;
$old_text = ipull($this->subparser->old, 'text', 'line');
$new_text = ipull($this->subparser->new, 'text', 'line');
foreach ($old as $k => $desc) {
if (empty($desc)) {
continue;
}
$old[$k]['text'] = idx($old_text, $desc['line']);
}
foreach ($new as $k => $desc) {
if (empty($desc)) {
continue;
}
$new[$k]['text'] = idx($new_text, $desc['line']);
if ($this->whitespaceMode == self::WHITESPACE_IGNORE_FORCE) {
// Under forced ignore mode, ignore even internal whitespace
// changes.
continue;
}
// If there's a corresponding "old" text and the line is marked as
// unchanged, test if there are internal whitespace changes between
// non-whitespace characters, e.g. spaces added to a string or spaces
// added around operators. If we find internal spaces, mark the line
// as changed.
//
// We only need to do this for "new" lines because any line that is
// missing either "old" or "new" text certainly can not have internal
// whitespace changes without also having non-whitespace changes,
// because characters had to be either added or removed to create the
// possibility of internal whitespace.
if (isset($old[$k]['text']) && empty($new[$k]['type'])) {
if (trim($old[$k]['text']) != trim($new[$k]['text'])) {
// The strings aren't the same when trimmed, so there are internal
// whitespace changes. Mark this line changed.
$old[$k]['type'] = '-';
$new[$k]['type'] = '+';
// Re-mark this line for intraline diffing.
unset($skip_intra[$k]);
}
}
}
$this->old = $old;
$this->new = $new;
}
$min_length = min(count($this->old), count($this->new));
for ($ii = 0; $ii < $min_length; $ii++) {
if ($this->old[$ii] || $this->new[$ii]) {
if (isset($this->old[$ii]['text'])) {
$otext = $this->old[$ii]['text'];
} else {
$otext = '';
}
if (isset($this->new[$ii]['text'])) {
$ntext = $this->new[$ii]['text'];
} else {
$ntext = '';
}
if ($otext != $ntext && empty($skip_intra[$ii])) {
$this->intra[$ii] = ArcanistDiffUtils::generateIntralineDiff(
$otext,
$ntext);
}
}
}
$lines_context = self::LINES_CONTEXT;
$max_length = max(count($this->old), count($this->new));
$old = $this->old;
$new = $this->new;
$visible = false;
$last = 0;
for ($cursor = -$lines_context; $cursor < $max_length; $cursor++) {
$offset = $cursor + $lines_context;
if ((isset($old[$offset]) && $old[$offset]['type']) ||
(isset($new[$offset]) && $new[$offset]['type'])) {
$visible = true;
$last = $offset;
} else if ($cursor > $last + $lines_context) {
$visible = false;
}
if ($visible && $cursor > 0) {
$this->visible[$cursor] = 1;
}
}
$old_corpus = array();
foreach ($this->old as $o) {
if ($o['type'] != '\\') {
if ($o['text'] === null) {
// There's no text on this side of the diff, but insert a placeholder
// newline so the highlighted line numbers match up.
$old_corpus[] = "\n";
} else {
$old_corpus[] = $o['text'];
}
}
}
$old_corpus_block = implode('', $old_corpus);
$new_corpus = array();
foreach ($this->new as $n) {
if ($n['type'] != '\\') {
if ($n['text'] === null) {
$new_corpus[] = "\n";
} else {
$new_corpus[] = $n['text'];
}
}
}
$new_corpus_block = implode('', $new_corpus);
$this->markGenerated($new_corpus_block);
if ($this->isTopLevel && !$this->comments &&
($this->isGenerated() || $this->isUnchanged() || $this->isDeleted())) {
return;
}
$old_future = $this->getHighlightFuture($old_corpus_block);
$new_future = $this->getHighlightFuture($new_corpus_block);
$futures = array(
'old' => $old_future,
'new' => $new_future,
);
$corpus_blocks = array(
'old' => $old_corpus_block,
'new' => $new_corpus_block,
);
$this->highlightErrors = false;
foreach (Futures($futures) as $key => $future) {
try {
try {
$highlighted = $future->resolve();
} catch (PhutilSyntaxHighlighterException $ex) {
$this->highlightErrors = true;
$highlighted = id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($corpus_blocks[$key])
->resolve();
}
switch ($key) {
case 'old':
$this->oldRender = $this->processHighlightedSource(
$this->old,
$highlighted);
break;
case 'new':
$this->newRender = $this->processHighlightedSource(
$this->new,
$highlighted);
break;
}
} catch (Exception $ex) {
phlog($ex);
throw $ex;
}
}
$this->applyIntraline(
$this->oldRender,
ipull($this->intra, 0),
$old_corpus);
$this->applyIntraline(
$this->newRender,
ipull($this->intra, 1),
$new_corpus);
}
public function loadCache() {
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$data = null;
$changeset = new DifferentialChangeset();
$conn_r = $changeset->establishConnection('r');
$data = queryfx_one(
$conn_r,
'SELECT * FROM %T WHERE id = %d',
$changeset->getTableName().'_parse_cache',
$render_cache_key);
if (!$data) {
return false;
}
$data = json_decode($data['cache'], true);
if (!is_array($data) || !$data) {
return false;
}
foreach (self::getCacheableProperties() as $cache_key) {
if (!array_key_exists($cache_key, $data)) {
// If we're missing a cache key, assume we're looking at an old cache
// and ignore it.
return false;
}
}
if ($data['cacheVersion'] !== self::CACHE_VERSION) {
return false;
}
// Someone displays contents of a partially cached shielded file.
if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {
return false;
}
unset($data['cacheVersion'], $data['cacheHost']);
$cache_prop = array_select_keys($data, self::getCacheableProperties());
foreach ($cache_prop as $cache_key => $v) {
$this->$cache_key = $v;
}
return true;
}
protected static function getCacheableProperties() {
return array(
'visible',
'new',
'old',
'intra',
'newRender',
'oldRender',
'specialAttributes',
'missingOld',
'missingNew',
'cacheVersion',
'cacheHost',
);
}
public function saveCache() {
if ($this->highlightErrors) {
return false;
}
$render_cache_key = $this->getRenderCacheKey();
if (!$render_cache_key) {
return false;
}
$cache = array();
foreach (self::getCacheableProperties() as $cache_key) {
switch ($cache_key) {
case 'cacheVersion':
$cache[$cache_key] = self::CACHE_VERSION;
break;
case 'cacheHost':
$cache[$cache_key] = php_uname('n');
break;
default:
$cache[$cache_key] = $this->$cache_key;
break;
}
}
$cache = json_encode($cache);
// We don't want to waste too much space by a single changeset.
if (strlen($cache) > self::CACHE_MAX_SIZE) {
return;
}
try {
$changeset = new DifferentialChangeset();
$conn_w = $changeset->establishConnection('w');
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
queryfx(
$conn_w,
'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %s, %d)
ON DUPLICATE KEY UPDATE cache = VALUES(cache)',
DifferentialChangeset::TABLE_CACHE,
$render_cache_key,
$cache,
time());
} catch (AphrontQueryException $ex) {
// TODO: uhoh
}
}
private function markGenerated($new_corpus_block = '') {
$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);
if (!$generated_guess) {
$config_key = 'differential.generated-paths';
$generated_path_regexps = PhabricatorEnv::getEnvConfig($config_key);
foreach ($generated_path_regexps as $regexp) {
if (preg_match($regexp, $this->changeset->getFilename())) {
$generated_guess = true;
break;
}
}
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,
array(
'corpus' => $new_corpus_block,
'is_generated' => $generated_guess,
)
);
PhutilEventEngine::dispatchEvent($event);
$generated = $event->getValue('is_generated');
$this->specialAttributes[self::ATTR_GENERATED] = $generated;
}
public function isGenerated() {
return idx($this->specialAttributes, self::ATTR_GENERATED, false);
}
public function isDeleted() {
return idx($this->specialAttributes, self::ATTR_DELETED, false);
}
public function isUnchanged() {
return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);
}
public function isWhitespaceOnly() {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
protected function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
if (isset($intra[$key])) {
$render[$key] = ArcanistDiffUtils::applyIntralineDiff(
$text,
$intra[$key]);
}
}
}
protected function getHighlightFuture($corpus) {
if (preg_match('/\r(?!\n)/', $corpus)) {
// TODO: Pygments converts "\r" newlines into "\n" newlines, so we can't
// use it on files with "\r" newlines. If we have "\r" not followed by
// "\n" in the file, skip highlighting.
$result = phutil_escape_html($corpus);
return new ImmediateFuture($result);
}
return $this->highlightEngine->getHighlightFuture(
$this->highlightEngine->getLanguageFromFilename($this->filename),
$corpus);
}
protected function processHighlightedSource($data, $result) {
$result_lines = phutil_split_lines($result);
foreach ($data as $key => $info) {
if (!$info) {
unset($result_lines[$key]);
}
}
return $result_lines;
}
private function tryCacheStuff() {
$whitespace_mode = $this->whitespaceMode;
switch ($whitespace_mode) {
case self::WHITESPACE_SHOW_ALL:
case self::WHITESPACE_IGNORE_TRAILING:
case self::WHITESPACE_IGNORE_FORCE:
break;
default:
$whitespace_mode = self::WHITESPACE_IGNORE_ALL;
break;
}
$skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL);
$this->whitespaceMode = $whitespace_mode;
$changeset = $this->changeset;
if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT &&
$changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) {
$this->markGenerated();
} else {
if ($skip_cache || !$this->loadCache()) {
$ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_ALL) ||
($whitespace_mode == self::WHITESPACE_IGNORE_FORCE));
$force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE);
if (!$force_ignore) {
if ($ignore_all && $changeset->getWhitespaceMatters()) {
$ignore_all = false;
}
}
// The "ignore all whitespace" algorithm depends on rediffing the
// files, and we currently need complete representations of both
// files to do anything reasonable. If we only have parts of the files,
// don't use the "ignore all" algorithm.
if ($ignore_all) {
$hunks = $changeset->getHunks();
if (count($hunks) !== 1) {
$ignore_all = false;
} else {
$first_hunk = reset($hunks);
if ($first_hunk->getOldOffset() != 1 ||
$first_hunk->getNewOffset() != 1) {
$ignore_all = false;
}
}
}
if ($ignore_all) {
$old_file = $changeset->makeOldFile();
$new_file = $changeset->makeNewFile();
if ($old_file == $new_file) {
// If the old and new files are exactly identical, the synthetic
// diff below will give us nonsense and whitespace modes are
// irrelevant anyway. This occurs when you, e.g., copy a file onto
// itself in Subversion (see T271).
$ignore_all = false;
}
}
if ($ignore_all) {
// Huge mess. Generate a "-bw" (ignore all whitespace changes) diff,
// parse it out, and then play a shell game with the parsed format
// in process() so we highlight only changed lines but render
// whitespace differences. If we don't do this, we either fail to
// render whitespace changes (which is incredibly confusing,
// especially for python) or often produce a much larger set of
// differences than necessary.
$engine = new PhabricatorDifferenceEngine();
$engine->setIgnoreWhitespace(true);
$no_whitespace_changeset = $engine->generateChangesetFromFileContent(
$old_file,
$new_file);
// subparser takes over the current non-whitespace-ignoring changeset
$subparser = new DifferentialChangesetParser();
$subparser->isSubparser = true;
$subparser->setChangeset($changeset);
foreach ($changeset->getHunks() as $hunk) {
$subparser->parseHunk($hunk);
}
// We need to call process() so that the subparser's values for
// metadata (like 'unchanged') is correct.
$subparser->process();
$this->subparser = $subparser;
// While we aren't updating $this->changeset (since it has a bunch
// of metadata we need to preserve, so that headers like "this file
// was moved" render correctly), we're overwriting the local
// $changeset so that the block below will choose the synthetic
// hunks we've built instead of the original hunks.
$changeset = $no_whitespace_changeset;
}
// This either uses the real hunks, or synthetic hunks we built above.
foreach ($changeset->getHunks() as $hunk) {
$this->parseHunk($hunk);
}
$this->process();
if (!$skip_cache) {
$this->saveCache();
}
}
}
}
private function shouldRenderPropertyChangeHeader($changeset) {
if (!$this->isTopLevel) {
// We render properties only at top level; otherwise we get multiple
// copies of them when a user clicks "Show More".
return false;
}
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
if ($old === $new) {
return false;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
$new == array('unix:filemode' => '100644')) {
return false;
}
if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
$old == array('unix:filemode' => '100644')) {
return false;
}
return true;
}
public function render(
$range_start = null,
$range_len = null,
$mask_force = array()) {
// "Top level" renders are initial requests for the whole file, versus
// requests for a specific range generated by clicking "show more". We
// generate property changes and "shield" UI elements only for toplevel
// requests.
$this->isTopLevel = (($range_start === null) && ($range_len === null));
$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();
$this->tryCacheStuff();
$render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);
$renderer = id(new DifferentialChangesetTwoUpRenderer())
->setChangeset($this->changeset)
->setRenderPropertyChangeHeader($render_pch)
- ->setOldLines($this->old)
- ->setNewLines($this->new)
->setOldRender($this->oldRender)
->setNewRender($this->newRender)
->setMissingOldLines($this->missingOld)
->setMissingNewLines($this->missingNew)
- ->setVisibleLines($this->visible)
->setOldChangesetID($this->leftSideChangesetID)
->setNewChangesetID($this->rightSideChangesetID)
->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)
->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)
- ->setLinesOfContext(self::LINES_CONTEXT)
- ->setCodeCoverage($this->coverage)
+ ->setCodeCoverage($this->getCoverage())
->setRenderingReference($this->getRenderingReference())
->setMarkupEngine($this->markupEngine)
->setHandles($this->handles);
if ($this->user) {
$renderer->setUser($this->user);
}
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
$shield = $renderer->renderShield(
pht(
'This file contains generated code, which does not normally '.
'need to be reviewed.'),
true);
} else if ($this->isUnchanged()) {
if ($this->isWhitespaceOnly()) {
$shield = $renderer->renderShield(
pht(
'This file was changed only by adding or removing trailing '.
'whitespace.'),
false);
} else {
$shield = $renderer->renderShield(
pht("The contents of this file were not changed."),
false);
}
} else if ($this->isDeleted()) {
$shield = $renderer->renderShield(
pht("This file was completely deleted."),
true);
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
$shield = $renderer->renderShield(
pht(
'This file has a very large number of changes ({%s} lines).',
$lines),
true);
}
}
if ($shield) {
return $renderer->renderChangesetTable($shield);
}
$old_comments = array();
$new_comments = array();
$old_mask = array();
$new_mask = array();
$feedback_mask = array();
if ($this->comments) {
foreach ($this->comments as $comment) {
$start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0);
$end = $comment->getLineNumber() +
$comment->getLineLength() +
self::LINES_CONTEXT;
$new_side = $this->isCommentOnRightSideWhenDisplayed($comment);
for ($ii = $start; $ii <= $end; $ii++) {
if ($new_side) {
$new_mask[$ii] = true;
} else {
$old_mask[$ii] = true;
}
}
}
foreach ($this->old as $ii => $old) {
if (isset($old['line']) && isset($old_mask[$old['line']])) {
$feedback_mask[$ii] = true;
}
}
foreach ($this->new as $ii => $new) {
if (isset($new['line']) && isset($new_mask[$new['line']])) {
$feedback_mask[$ii] = true;
}
}
$this->comments = msort($this->comments, 'getID');
foreach ($this->comments as $comment) {
$final = $comment->getLineNumber() +
$comment->getLineLength();
$final = max(1, $final);
if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
$new_comments[$final][] = $comment;
} else {
$old_comments[$final][] = $comment;
}
}
}
$renderer
->setOldComments($old_comments)
->setNewComments($new_comments);
switch ($this->changeset->getFileType()) {
case DifferentialChangeType::FILE_IMAGE:
$old = null;
$cur = null;
// TODO: Improve the architectural issue as discussed in D955
// https://secure.phabricator.com/D955
$reference = $this->getRenderingReference();
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
} else {
$id = $parts[0];
$vs = 0;
}
$id = (int)$id;
$vs = (int)$vs;
if (!$vs) {
$metadata = $this->changeset->getMetadata();
$data = idx($metadata, 'attachment-data');
$old_phid = idx($metadata, 'old:binary-phid');
$new_phid = idx($metadata, 'new:binary-phid');
} else {
$vs_changeset = id(new DifferentialChangeset())->load($vs);
$vs_metadata = $vs_changeset->getMetadata();
$old_phid = idx($vs_metadata, 'new:binary-phid');
$changeset = id(new DifferentialChangeset())->load($id);
$metadata = $changeset->getMetadata();
$new_phid = idx($metadata, 'new:binary-phid');
}
if ($old_phid || $new_phid) {
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
$file_phids[] = $old_phid;
}
if ($new_phid) {
$file_phids[] = $new_phid;
}
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
$old = $file;
} else if ($file->getPHID() == $new_phid) {
$new = $file;
}
}
}
return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
$output = $renderer->renderChangesetTable(null);
return $output;
}
if ($this->originalLeft && $this->originalRight) {
list($highlight_old, $highlight_new) = $this->diffOriginals();
$highlight_old = array_flip($highlight_old);
$highlight_new = array_flip($highlight_new);
$renderer
->setHighlightOld($highlight_old)
->setHighlightNew($highlight_new);
}
$renderer
->setOriginalOld($this->originalLeft)
->setOriginalNew($this->originalRight);
+ $rows = max(
+ count($this->old),
+ count($this->new));
+ if ($range_start === null) {
+ $range_start = 0;
+ }
+ if ($range_len === null) {
+ $range_len = $rows;
+ }
+ $range_len = min($range_len, $rows - $range_start);
+
+ list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths(
+ $mask_force,
+ $feedback_mask,
+ $range_start,
+ $range_len
+ );
+
+ $renderer
+ ->setOldLines($this->old)
+ ->setNewLines($this->new)
+ ->setGaps($gaps)
+ ->setMask($mask)
+ ->setDepths($depths);
+
$html = $renderer->renderTextChange(
$range_start,
$range_len,
- $mask_force,
- $feedback_mask
+ $rows
);
return $renderer->renderChangesetTable($html);
}
+ /**
+ * This function calculates a lot of stuff we need to know to display
+ * the diff:
+ *
+ * Gaps - compute gaps in the visible display diff, where we will render
+ * "Show more context" spacers. If a gap is smaller than the context size,
+ * we just display it. Otherwise, we record it into $gaps and will render a
+ * "show more context" element instead of diff text below. A given $gap
+ * is a tuple of $gap_line_number_start and $gap_length.
+ *
+ * Mask - compute the actual lines that need to be shown (because they
+ * are near changes lines, near inline comments, or the request has
+ * explicitly asked for them, i.e. resulting from the user clicking
+ * "show more"). The $mask returned is a sparesely populated dictionary
+ * of $visible_line_number => true.
+ *
+ * Depths - compute how indented any given line is. The $depths returned
+ * is a sparesely populated dictionary of $visible_line_number => $depth.
+ *
+ * This function also has the side effect of modifying member variable
+ * new such that tabs are normalized to spaces for each line of the diff.
+ *
+ * @return array($gaps, $mask, $depths)
+ */
+ private function calculateGapsMaskAndDepths($mask_force,
+ $feedback_mask,
+ $range_start,
+ $range_len) {
+
+ // Calculate gaps and mask first
+ $gaps = array();
+ $gap_start = 0;
+ $in_gap = false;
+ $base_mask = $this->visible + $mask_force + $feedback_mask;
+ $base_mask[$range_start + $range_len] = true;
+ for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
+ if (isset($base_mask[$ii])) {
+ if ($in_gap) {
+ $gap_length = $ii - $gap_start;
+ if ($gap_length <= self::LINES_CONTEXT) {
+ for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
+ $base_mask[$jj] = true;
+ }
+ } else {
+ $gaps[] = array($gap_start, $gap_length);
+ }
+ $in_gap = false;
+ }
+ } else {
+ if (!$in_gap) {
+ $gap_start = $ii;
+ $in_gap = true;
+ }
+ }
+ }
+ $gaps = array_reverse($gaps);
+ $mask = $base_mask;
+
+ // Time to calculate depth.
+ // We need to go backwards to properly indent whitespace in this code:
+ //
+ // 0: class C {
+ // 1:
+ // 1: function f() {
+ // 2:
+ // 2: return;
+ // 3:
+ // 3: }
+ // 4:
+ // 4: }
+ //
+ $depths = array();
+ $last_depth = 0;
+ $range_end = $range_start + $range_len;
+ if (!isset($this->new[$range_end])) {
+ $range_end--;
+ }
+ for ($ii = $range_end; $ii >= $range_start; $ii--) {
+ // We need to expand tabs to process mixed indenting and to round
+ // correctly later.
+ $line = str_replace("\t", " ", $this->new[$ii]['text']);
+ $trimmed = ltrim($line);
+ if ($trimmed != '') {
+ // We round down to flatten "/**" and " *".
+ $last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
+ }
+ $depths[$ii] = $last_depth;
+ }
+
+ return array($gaps, $mask, $depths);
+ }
+
/**
* Determine if an inline comment will appear on the rendered diff,
* taking into consideration which halves of which changesets will actually
* be shown.
*
* @param PhabricatorInlineCommentInterface Comment to test for visibility.
* @return bool True if the comment is visible on the rendered diff.
*/
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
return true;
}
return false;
}
/**
* Determine if a comment will appear on the right side of the display diff.
* Note that the comment must appear somewhere on the rendered changeset, as
* per isCommentVisibleOnRenderedDiff().
*
* @param PhabricatorInlineCommentInterface Comment to test for display
* location.
* @return bool True for right, false for left.
*/
private function isCommentOnRightSideWhenDisplayed(
PhabricatorInlineCommentInterface $comment) {
if (!$this->isCommentVisibleOnRenderedDiff($comment)) {
throw new Exception("Comment is not visible on changeset!");
}
$changeset_id = $comment->getChangesetID();
$is_new = $comment->getIsNewFile();
if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
return true;
}
return false;
}
public function renderForEmail() {
$ret = '';
$min = min(count($this->old), count($this->new));
for ($i = 0; $i < $min; $i++) {
$o = $this->old[$i];
$n = $this->new[$i];
if (!isset($this->visible[$i])) {
continue;
}
if ($o['line'] && $n['line']) {
// It is quite possible there are better ways to achieve this. For
// example, "white-space: pre;" can do a better job, WERE IT NOT for
// broken email clients like OWA which use newlines to do weird
// wrapping. So dont give them newlines.
if (isset($this->intra[$i])) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
} else {
$ret .= sprintf("&nbsp;&nbsp;%s<br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
} else if ($o['line'] && !$n['line']) {
$ret .= sprintf(
"<font color=\"red\">-&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($o['text']))
);
} else {
$ret .= sprintf(
"<font color=\"green\">+&nbsp;%s</font><br/>",
str_replace(" ", "&nbsp;", phutil_escape_html($n['text']))
);
}
}
return $ret;
}
/**
* Parse the 'range' specification that this class and the client-side JS
* emit to indicate that a user clicked "Show more..." on a diff. Generally,
* use is something like this:
*
* $spec = $request->getStr('range');
* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);
* list($start, $end, $mask) = $parsed;
* $parser->render($start, $end, $mask);
*
* @param string Range specification, indicating the range of the diff that
* should be rendered.
* @return tuple List of <start, end, mask> suitable for passing to
* @{method:render}.
*/
public static function parseRangeSpecification($spec) {
$range_s = null;
$range_e = null;
$mask = array();
if ($spec) {
$match = null;
if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {
$range_s = (int)$match[1];
$range_e = (int)$match[2];
if (count($match) > 3) {
$start = (int)$match[3];
$len = (int)$match[4];
for ($ii = $start; $ii < $start + $len; $ii++) {
$mask[$ii] = true;
}
}
}
}
return array($range_s, $range_e, $mask);
}
/**
* Render "modified coverage" information; test coverage on modified lines.
* This synthesizes diff information with unit test information into a useful
* indicator of how well tested a change is.
*/
public function renderModifiedCoverage() {
$na = '<em>-</em>';
- if (!$this->coverage) {
+ $coverage = $this->getCoverage();
+ if (!$coverage) {
return $na;
}
$covered = 0;
$not_covered = 0;
foreach ($this->new as $k => $new) {
if (!$new['line']) {
continue;
}
if (!$new['type']) {
continue;
}
- if (empty($this->coverage[$new['line'] - 1])) {
+ if (empty($coverage[$new['line'] - 1])) {
continue;
}
- switch ($this->coverage[$new['line'] - 1]) {
+ switch ($coverage[$new['line'] - 1]) {
case 'C':
$covered++;
break;
case 'U':
$not_covered++;
break;
}
}
if (!$covered && !$not_covered) {
return $na;
}
return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
index f18f980ded..acac198ae6 100644
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -1,601 +1,609 @@
<?php
abstract class DifferentialChangesetRenderer {
private $user;
private $changeset;
private $renderingReference;
private $renderPropertyChangeHeader;
private $missingOldLines;
private $missingNewLines;
private $oldLines;
private $newLines;
- private $visibleLines;
private $oldComments;
private $newComments;
private $oldChangesetID;
private $newChangesetID;
private $oldAttachesToNewFile;
private $newAttachesToNewFile;
private $highlightOld = array();
private $highlightNew = array();
- private $linesOfContext;
private $codeCoverage;
private $handles;
private $markupEngine;
private $oldRender;
private $newRender;
private $originalOld;
private $originalNew;
+ private $gaps;
+ private $mask;
+ private $depths;
+
+ public function setDepths($depths) {
+ $this->depths = $depths;
+ return $this;
+ }
+ protected function getDepths() {
+ return $this->depths;
+ }
+
+ public function setMask($mask) {
+ $this->mask = $mask;
+ return $this;
+ }
+ protected function getMask() {
+ return $this->mask;
+ }
+
+ public function setGaps($gaps) {
+ $this->gaps = $gaps;
+ return $this;
+ }
+ protected function getGaps() {
+ return $this->gaps;
+ }
public function setOriginalNew($original_new) {
$this->originalNew = $original_new;
return $this;
}
protected function getOriginalNew() {
return $this->originalNew;
}
public function setOriginalOld($original_old) {
$this->originalOld = $original_old;
return $this;
}
protected function getOriginalOld() {
return $this->originalOld;
}
public function setNewRender($new_render) {
$this->newRender = $new_render;
return $this;
}
protected function getNewRender() {
return $this->newRender;
}
public function setOldRender($old_render) {
$this->oldRender = $old_render;
return $this;
}
protected function getOldRender() {
return $this->oldRender;
}
public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
$this->markupEngine = $markup_engine;
return $this;
}
public function getMarkupEngine() {
return $this->markupEngine;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
protected function getHandles() {
return $this->handles;
}
public function setCodeCoverage($code_coverage) {
$this->codeCoverage = $code_coverage;
return $this;
}
protected function getCodeCoverage() {
return $this->codeCoverage;
}
- public function setLinesOfContext($lines_of_context) {
- $this->linesOfContext = $lines_of_context;
- return $this;
- }
- protected function getLinesOfContext() {
- return $this->linesOfContext;
- }
-
public function setHighlightNew($highlight_new) {
$this->highlightNew = $highlight_new;
return $this;
}
protected function getHighlightNew() {
return $this->highlightNew;
}
public function setHighlightOld($highlight_old) {
$this->highlightOld = $highlight_old;
return $this;
}
protected function getHighlightOld() {
return $this->highlightOld;
}
public function setNewAttachesToNewFile($attaches) {
$this->newAttachesToNewFile = $attaches;
return $this;
}
protected function getNewAttachesToNewFile() {
return $this->newAttachesToNewFile;
}
public function setOldAttachesToNewFile($attaches) {
$this->oldAttachesToNewFile = $attaches;
return $this;
}
protected function getOldAttachesToNewFile() {
return $this->oldAttachesToNewFile;
}
public function setNewChangesetID($new_changeset_id) {
$this->newChangesetID = $new_changeset_id;
return $this;
}
protected function getNewChangesetID() {
return $this->newChangesetID;
}
public function setOldChangesetID($old_changeset_id) {
$this->oldChangesetID = $old_changeset_id;
return $this;
}
protected function getOldChangesetID() {
return $this->oldChangesetID;
}
public function setNewComments(array $new_comments) {
foreach ($new_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->newComments = $new_comments;
return $this;
}
protected function getNewComments() {
return $this->newComments;
}
public function setOldComments(array $old_comments) {
foreach ($old_comments as $line_number => $comments) {
assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
}
$this->oldComments = $old_comments;
return $this;
}
protected function getOldComments() {
return $this->oldComments;
}
- public function setVisibleLines(array $visible_lines) {
- $this->visibleLines = $visible_lines;
- return $this;
- }
- protected function getVisibleLines() {
- return $this->visibleLines;
- }
-
public function setNewLines(array $new_lines) {
$this->newLines = $new_lines;
return $this;
}
protected function getNewLines() {
return $this->newLines;
}
public function setOldLines(array $old_lines) {
$this->oldLines = $old_lines;
return $this;
}
protected function getOldLines() {
return $this->oldLines;
}
public function setMissingNewLines(array $missing_new_lines) {
$this->missingNewLines = $missing_new_lines;
return $this;
}
protected function getMissingNewLines() {
return $this->missingNewLines;
}
public function setMissingOldLines(array $missing_old_lines) {
$this->missingOldLines = $missing_old_lines;
return $this;
}
protected function getMissingOldLines() {
return $this->missingOldLines;
}
public function setUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
protected function getUser() {
return $this->user;
}
public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
return $this;
}
protected function getChangeset() {
return $this->changeset;
}
public function setRenderingReference($rendering_reference) {
$this->renderingReference = $rendering_reference;
return $this;
}
protected function getRenderingReference() {
return $this->renderingReference;
}
public function setRenderPropertyChangeHeader($should_render) {
$this->renderPropertyChangeHeader = $should_render;
return $this;
}
private function shouldRenderPropertyChangeHeader() {
return $this->renderPropertyChangeHeader;
}
abstract public function renderChangesetTable($contents);
abstract public function renderTextChange(
$range_start,
$range_len,
- $mask_force,
- $feedback_mask
+ $rows
);
abstract public function renderFileChange(
$old = null,
$new = null,
$id = 0,
$vs = 0
);
public function renderShield($message, $more) {
if ($more) {
$end = max(
count($this->getOldLines()),
count($this->getNewLines())
);
$reference = $this->getRenderingReference();
$more =
' '.
javelin_render_tag(
'a',
array(
'mustcapture' => true,
'sigil' => 'show-more',
'class' => 'complete',
'href' => '#',
'meta' => array(
'ref' => $reference,
'range' => "0-{$end}",
),
),
'Show File Contents');
} else {
$more = null;
}
return javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td class="differential-shield" colspan="6">'.
phutil_escape_html($message).
$more.
'</td>');
}
protected function renderPropertyChangeHeader($changeset) {
if (!$this->shouldRenderPropertyChangeHeader()) {
return null;
}
$old = $changeset->getOldProperties();
$new = $changeset->getNewProperties();
$keys = array_keys($old + $new);
sort($keys);
$rows = array();
foreach ($keys as $key) {
$oval = idx($old, $key);
$nval = idx($new, $key);
if ($oval !== $nval) {
if ($oval === null) {
$oval = '<em>null</em>';
} else {
$oval = nl2br(phutil_escape_html($oval));
}
if ($nval === null) {
$nval = '<em>null</em>';
} else {
$nval = nl2br(phutil_escape_html($nval));
}
$rows[] =
'<tr>'.
'<th>'.phutil_escape_html($key).'</th>'.
'<td class="oval">'.$oval.'</td>'.
'<td class="nval">'.$nval.'</td>'.
'</tr>';
}
}
return
'<table class="differential-property-table">'.
'<tr class="property-table-header">'.
'<th>Property Changes</th>'.
'<td class="oval">Old Value</td>'.
'<td class="nval">New Value</td>'.
'</tr>'.
implode('', $rows).
'</table>';
}
protected function renderChangeTypeHeader($changeset, $force) {
$change = $changeset->getChangeType();
$file = $changeset->getFileType();
$message = null;
if ($change == DifferentialChangeType::TYPE_CHANGE &&
$file == DifferentialChangeType::FILE_TEXT) {
if ($force) {
// We have to force something to render because there were no changes
// of other kinds.
$message = pht('This file was not modified.');
} else {
// Default case of changes to a text file, no metadata.
return null;
}
} else {
switch ($change) {
case DifferentialChangeType::TYPE_ADD:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was <strong>added</strong>.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was <strong>added</strong>.');
break;
}
break;
case DifferentialChangeType::TYPE_DELETE:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was <strong>deleted</strong>.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was <strong>deleted</strong>.');
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_HERE:
$from =
"<strong>".
phutil_escape_html($changeset->getOldFile()).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was moved from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was moved from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_HERE:
$from =
"<strong>".
phutil_escape_html($changeset->getOldFile()).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was copied from %s.', $from);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was copied from %s.', $from);
break;
}
break;
case DifferentialChangeType::TYPE_MOVE_AWAY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was moved to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was moved to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_COPY_AWAY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This image was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This directory was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This binary file was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This symlink was copied to %s.', $paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This submodule was copied to %s.', $paths);
break;
}
break;
case DifferentialChangeType::TYPE_MULTICOPY:
$paths =
"<strong>".
phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
"</strong>";
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht(
'This file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht(
'This image was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht(
'This directory was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht(
'This binary file was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht(
'This symlink was deleted after being copied to %s.',
$paths);
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht(
'This submodule was deleted after being copied to %s.',
$paths);
break;
}
break;
default:
switch ($file) {
case DifferentialChangeType::FILE_TEXT:
$message = pht('This is a file.');
break;
case DifferentialChangeType::FILE_IMAGE:
$message = pht('This is an image.');
break;
case DifferentialChangeType::FILE_DIRECTORY:
$message = pht('This is a directory.');
break;
case DifferentialChangeType::FILE_BINARY:
$message = pht('This is a binary file.');
break;
case DifferentialChangeType::FILE_SYMLINK:
$message = pht('This is a symlink.');
break;
case DifferentialChangeType::FILE_SUBMODULE:
$message = pht('This is a submodule.');
break;
}
break;
}
}
return
'<div class="differential-meta-notice">'.
$message.
'</div>';
}
protected function renderInlineComment(
PhabricatorInlineCommentInterface $comment,
$on_right = false) {
$user = $this->getUser();
$edit = $user &&
($comment->getAuthorPHID() == $user->getPHID()) &&
($comment->isDraft());
$allow_reply = (bool)$user;
return id(new DifferentialInlineCommentView())
->setInlineComment($comment)
->setOnRight($on_right)
->setHandles($this->getHandles())
->setMarkupEngine($this->getMarkupEngine())
->setEditable($edit)
->setAllowReply($allow_reply)
->render();
}
}
diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
index dc458f15ff..52dde106eb 100644
--- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -1,534 +1,454 @@
<?php
final class DifferentialChangesetTwoUpRenderer
extends DifferentialChangesetRenderer {
public function renderChangesetTable($contents) {
$changeset = $this->getChangeset();
$props = $this->renderPropertyChangeHeader($changeset);
$table = null;
if ($contents) {
$table = javelin_render_tag(
'table',
array(
'class' => 'differential-diff remarkup-code PhabricatorMonospaced',
'sigil' => 'differential-diff',
),
$contents);
}
if (!$table && !$props) {
$notice = $this->renderChangeTypeHeader($changeset, true);
} else {
$notice = $this->renderChangeTypeHeader($changeset, false);
}
$result = implode(
"\n",
array(
$notice,
$props,
$table,
));
// TODO: Let the user customize their tab width / display style.
$result = str_replace("\t", ' ', $result);
// TODO: We should possibly post-process "\r" as well.
return $result;
}
public function renderTextChange(
$range_start,
$range_len,
- $mask_force,
- $feedback_mask) {
+ $rows) {
$missing_old = $this->getMissingOldLines();
$missing_new = $this->getMissingNewLines();
$context_not_available = null;
if ($missing_old || $missing_new) {
$context_not_available = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
phutil_render_tag(
'td',
array(
'colspan' => 6,
'class' => 'show-more'
),
pht('Context not available.')
)
);
}
$html = array();
$old_lines = $this->getOldLines();
$new_lines = $this->getNewLines();
-
- $rows = max(
- count($old_lines),
- count($new_lines));
-
- if ($range_start === null) {
- $range_start = 0;
- }
-
- if ($range_len === null) {
- $range_len = $rows;
- }
-
- $range_len = min($range_len, $rows - $range_start);
-
- // Gaps - compute gaps in the visible display diff, where we will render
- // "Show more context" spacers. This builds an aggregate $mask of all the
- // lines we must show (because they are near changed lines, near inline
- // comments, or the request has explicitly asked for them, i.e. resulting
- // from the user clicking "show more") and then finds all the gaps between
- // visible lines. If a gap is smaller than the context size, we just
- // display it. Otherwise, we record it into $gaps and will render a
- // "show more context" element instead of diff text below.
-
- $gaps = array();
- $gap_start = 0;
- $in_gap = false;
- $lines_of_context = $this->getLinesOfContext();
- $mask = $this->getVisibleLines() + $mask_force + $feedback_mask;
- $mask[$range_start + $range_len] = true;
- for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {
- if (isset($mask[$ii])) {
- if ($in_gap) {
- $gap_length = $ii - $gap_start;
- if ($gap_length <= $lines_of_context) {
- for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {
- $mask[$jj] = true;
- }
- } else {
- $gaps[] = array($gap_start, $gap_length);
- }
- $in_gap = false;
- }
- } else {
- if (!$in_gap) {
- $gap_start = $ii;
- $in_gap = true;
- }
- }
- }
-
- $gaps = array_reverse($gaps);
-
+ $gaps = $this->getGaps();
$reference = $this->getRenderingReference();
-
$left_id = $this->getOldChangesetID();
$right_id = $this->getNewChangesetID();
// "N" stands for 'new' and means the comment should attach to the new file
// when stored, i.e. DifferentialInlineComment->setIsNewFile().
// "O" stands for 'old' and means the comment should attach to the old file.
$left_char = $this->getOldAttachesToNewFile()
? 'N'
: 'O';
$right_char = $this->getNewAttachesToNewFile()
? 'N'
: 'O';
$changeset = $this->getChangeset();
$copy_lines = idx($changeset->getMetadata(), 'copy:lines', array());
$highlight_old = $this->getHighlightOld();
$highlight_new = $this->getHighlightNew();
$old_render = $this->getOldRender();
$new_render = $this->getNewRender();
$original_left = $this->getOriginalOld();
$original_right = $this->getOriginalNew();
-
- // We need to go backwards to properly indent whitespace in this code:
- //
- // 0: class C {
- // 1:
- // 1: function f() {
- // 2:
- // 2: return;
- // 3:
- // 3: }
- // 4:
- // 4: }
- //
- $depths = array();
- $last_depth = 0;
- $range_end = $range_start + $range_len;
- if (!isset($new_lines[$range_end])) {
- $range_end--;
- }
- for ($ii = $range_end; $ii >= $range_start; $ii--) {
- // We need to expand tabs to process mixed indenting and to round
- // correctly later.
- $line = str_replace("\t", " ", $new_lines[$ii]['text']);
- $trimmed = ltrim($line);
- if ($trimmed != '') {
- // We round down to flatten "/**" and " *".
- $last_depth = floor((strlen($line) - strlen($trimmed)) / 2);
- }
- $depths[$ii] = $last_depth;
- }
+ $depths = $this->getDepths();
+ $mask = $this->getMask();
for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
if (empty($mask[$ii])) {
// If we aren't going to show this line, we've just entered a gap.
// Pop information about the next gap off the $gaps stack and render
// an appropriate "Show more context" element. This branch eventually
// increments $ii by the entire size of the gap and then continues
// the loop.
$gap = array_pop($gaps);
$top = $gap[0];
$len = $gap[1];
$end = $top + $len - 20;
$contents = array();
if ($len > 40) {
$is_first_block = false;
if ($ii == 0) {
$is_first_block = true;
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-20",
),
),
$is_first_block
? "Show First 20 Lines"
: "\xE2\x96\xB2 Show 20 Lines");
}
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'type' => 'all',
'ref' => $reference,
'range' => "{$top}-{$len}/{$top}-{$len}",
),
),
'Show All '.$len.' Lines');
$is_last_block = false;
if ($ii + $len >= $rows) {
$is_last_block = true;
}
if ($len > 40) {
$contents[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'show-more',
'meta' => array(
'ref' => $reference,
'range' => "{$top}-{$len}/{$end}-20",
),
),
$is_last_block
? "Show Last 20 Lines"
: "\xE2\x96\xBC Show 20 Lines");
}
$context = null;
$context_line = null;
if (!$is_last_block && $depths[$ii + $len]) {
for ($l = $ii + $len - 1; $l >= $ii; $l--) {
$line = $new_lines[$l]['text'];
if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
$context = $new_render[$l];
$context_line = $new_lines[$l]['line'];
break;
}
}
}
$container = javelin_render_tag(
'tr',
array(
'sigil' => 'context-target',
),
'<td colspan="2" class="show-more">'.
implode(' &bull; ', $contents).
'</td>'.
'<th class="show-context-line">'.$context_line.'</td>'.
'<td colspan="3" class="show-context">'.$context.'</td>');
$html[] = $container;
$ii += ($len - 1);
continue;
}
$o_num = null;
$o_classes = 'left';
$o_text = null;
if (isset($old_lines[$ii])) {
$o_num = $old_lines[$ii]['line'];
$o_text = isset($old_render[$ii]) ? $old_render[$ii] : null;
if ($old_lines[$ii]['type']) {
if ($old_lines[$ii]['type'] == '\\') {
$o_text = $old_lines[$ii]['text'];
$o_classes .= ' comment';
} else if ($original_left && !isset($highlight_old[$o_num])) {
$o_classes .= ' old-rebase';
} else if (empty($new_lines[$ii])) {
$o_classes .= ' old old-full';
} else {
$o_classes .= ' old';
}
}
}
$n_copy = '<td class="copy" />';
$n_cov = null;
$n_colspan = 2;
$n_classes = '';
$n_num = null;
$n_text = null;
if (isset($new_lines[$ii])) {
$n_num = $new_lines[$ii]['line'];
$n_text = isset($new_render[$ii]) ? $new_render[$ii] : null;
$coverage = $this->getCodeCoverage();
if ($coverage !== null) {
if (empty($coverage[$n_num - 1])) {
$cov_class = 'N';
} else {
$cov_class = $coverage[$n_num - 1];
}
$cov_class = 'cov-'.$cov_class;
$n_cov = '<td class="cov '.$cov_class.'"></td>';
$n_colspan--;
}
if ($new_lines[$ii]['type']) {
if ($new_lines[$ii]['type'] == '\\') {
$n_text = $new_lines[$ii]['text'];
$n_class = 'comment';
} else if ($original_right && !isset($highlight_new[$n_num])) {
$n_class = 'new-rebase';
} else if (empty($old_lines[$ii])) {
$n_class = 'new new-full';
} else {
$n_class = 'new';
}
$n_classes = $n_class;
if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
$n_copy = '<td class="copy '.$n_class.'"></td>';
} else {
list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num];
$title = ($orig_type == '-' ? 'Moved' : 'Copied').' from ';
if ($orig_file == '') {
$title .= "line {$orig_line}";
} else {
$title .=
basename($orig_file).
":{$orig_line} in dir ".
dirname('/'.$orig_file);
}
$class = ($orig_type == '-' ? 'new-move' : 'new-copy');
$n_copy = javelin_render_tag(
'td',
array(
'meta' => array(
'msg' => $title,
),
'class' => 'copy '.$class,
),
'');
}
}
}
$n_classes .= ' right'.$n_colspan;
if (($o_num && !empty($missing_old[$o_num])) ||
($n_num && !empty($missing_new[$n_num]))) {
$html[] = $context_not_available;
}
if ($o_num && $left_id) {
$o_id = ' id="C'.$left_id.$left_char.'L'.$o_num.'"';
} else {
$o_id = null;
}
if ($n_num && $right_id) {
$n_id = ' id="C'.$right_id.$right_char.'L'.$n_num.'"';
} else {
$n_id = null;
}
// NOTE: The Javascript is sensitive to whitespace changes in this
// block!
$html[] =
'<tr>'.
'<th'.$o_id.'>'.$o_num.'</th>'.
'<td class="'.$o_classes.'">'.$o_text.'</td>'.
'<th'.$n_id.'>'.$n_num.'</th>'.
$n_copy.
// NOTE: This is a unicode zero-width space, which we use as a hint
// when intercepting 'copy' events to make sure sensible text ends
// up on the clipboard. See the 'phabricator-oncopy' behavior.
'<td class="'.$n_classes.'" colspan="'.$n_colspan.'">'.
"\xE2\x80\x8B".$n_text.
'</td>'.
$n_cov.
'</tr>';
if ($context_not_available && ($ii == $rows - 1)) {
$html[] = $context_not_available;
}
$old_comments = $this->getOldComments();
$new_comments = $this->getNewComments();
if ($o_num && isset($old_comments[$o_num])) {
foreach ($old_comments[$o_num] as $comment) {
- $xhp = $this->renderInlineComment($comment, $on_right = false);
+ $comment_html = $this->renderInlineComment($comment,
+ $on_right = false);
$new = '';
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $key => $new_comment) {
if ($comment->isCompatible($new_comment)) {
$new = $this->renderInlineComment($new_comment,
$on_right = true);
unset($new_comments[$n_num][$key]);
}
}
}
$html[] =
'<tr class="inline">'.
'<th />'.
- '<td class="left">'.$xhp.'</td>'.
+ '<td class="left">'.$comment_html.'</td>'.
'<th />'.
'<td colspan="3" class="right3">'.$new.'</td>'.
'</tr>';
}
}
if ($n_num && isset($new_comments[$n_num])) {
foreach ($new_comments[$n_num] as $comment) {
- $xhp = $this->renderInlineComment($comment, $on_right = true);
+ $comment_html = $this->renderInlineComment($comment,
+ $on_right = true);
$html[] =
'<tr class="inline">'.
'<th />'.
'<td class="left" />'.
'<th />'.
- '<td colspan="3" class="right3">'.$xhp.'</td>'.
+ '<td colspan="3" class="right3">'.$comment_html.'</td>'.
'</tr>';
}
}
}
return implode('', $html);
}
public function renderFileChange($old_file = null,
$new_file = null,
$id = 0,
$vs = 0) {
$old = null;
if ($old_file) {
$old = phutil_render_tag(
'div',
array(
'class' => 'differential-image-stage'
),
phutil_render_tag(
'img',
array(
'src' => $old_file->getBestURI(),
)
)
);
}
$new = null;
if ($new_file) {
$new = phutil_render_tag(
'div',
array(
'class' => 'differential-image-stage'
),
phutil_render_tag(
'img',
array(
'src' => $new_file->getBestURI(),
)
)
);
}
$html_old = array();
$html_new = array();
foreach ($this->getOldComments() as $comment) {
- $xhp = $this->renderInlineComment($comment, $on_right = false);
+ $comment_html = $this->renderInlineComment($comment, $on_right = false);
$html_old[] =
'<tr class="inline">'.
'<th />'.
- '<td class="left">'.$xhp.'</td>'.
+ '<td class="left">'.$comment_html.'</td>'.
'<th />'.
'<td class="right3" colspan="3" />'.
'</tr>';
}
foreach ($this->getNewComments() as $comment) {
- $xhp = $this->renderInlineComment($comment, $on_right = true);
+ $comment_html = $this->renderInlineComment($comment, $on_right = true);
$html_new[] =
'<tr class="inline">'.
'<th />'.
'<td class="left" />'.
'<th />'.
- '<td class="right3" colspan="3">'.$xhp.'</td>'.
+ '<td class="right3" colspan="3">'.$comment_html.'</td>'.
'</tr>';
}
if (!$old) {
$th_old = '<th></th>';
} else {
$th_old = '<th id="C'.$vs.'OL1">1</th>';
}
if (!$new) {
$th_new = '<th></th>';
} else {
$th_new = '<th id="C'.$id.'NL1">1</th>';
}
$output = $this->renderChangesetTable(
'<tr class="differential-image-diff">'.
$th_old.
'<td class="left differential-old-image">'.$old.'</td>'.
$th_new.
'<td class="right3 differential-new-image" colspan="3">'.
$new.
'</td>'.
'</tr>'.
implode('', $html_old).
implode('', $html_new));
return $output;
}
}
diff --git a/src/applications/differential/view/DifferentialInlineCommentView.php b/src/applications/differential/view/DifferentialInlineCommentView.php
index 26b123a40e..19cab3f2d8 100644
--- a/src/applications/differential/view/DifferentialInlineCommentView.php
+++ b/src/applications/differential/view/DifferentialInlineCommentView.php
@@ -1,261 +1,261 @@
<?php
final class DifferentialInlineCommentView extends AphrontView {
private $inlineComment;
private $onRight;
private $buildScaffolding;
private $handles;
private $markupEngine;
private $editable;
private $preview;
private $allowReply;
public function setInlineComment(PhabricatorInlineCommentInterface $comment) {
$this->inlineComment = $comment;
return $this;
}
public function setOnRight($on_right) {
$this->onRight = $on_right;
return $this;
}
public function setBuildScaffolding($scaffold) {
$this->buildScaffolding = $scaffold;
return $this;
}
public function setHandles(array $handles) {
assert_instances_of($handles, 'PhabricatorObjectHandle');
$this->handles = $handles;
return $this;
}
public function setMarkupEngine(PhabricatorMarkupEngine $engine) {
$this->markupEngine = $engine;
return $this;
}
public function setEditable($editable) {
$this->editable = $editable;
return $this;
}
public function setPreview($preview) {
$this->preview = $preview;
return $this;
}
public function setAllowReply($allow_reply) {
$this->allowReply = $allow_reply;
return $this;
}
public function render() {
$inline = $this->inlineComment;
$start = $inline->getLineNumber();
$length = $inline->getLineLength();
if ($length) {
$end = $start + $length;
$line = 'Lines '.number_format($start).'-'.number_format($end);
} else {
$line = 'Line '.number_format($start);
}
$metadata = array(
'id' => $inline->getID(),
'number' => $inline->getLineNumber(),
'length' => $inline->getLineLength(),
'on_right' => $this->onRight,
'original' => $inline->getContent(),
);
$sigil = 'differential-inline-comment';
if ($this->preview) {
$sigil = $sigil . ' differential-inline-comment-preview';
}
$content = $inline->getContent();
$handles = $this->handles;
$links = array();
$is_synthetic = false;
if ($inline->getSyntheticAuthor()) {
$is_synthetic = true;
}
$is_draft = false;
if ($inline->isDraft() && !$is_synthetic) {
$links[] = 'Not Submitted Yet';
$is_draft = true;
}
if (!$this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-prev',
),
'Previous');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-next',
),
'Next');
if ($this->allowReply) {
if (!$is_synthetic) {
// NOTE: No product reason why you can't reply to these, but the reply
// mechanism currently sends the inline comment ID to the server, not
// file/line information, and synthetic comments don't have an inline
// comment ID.
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-reply',
),
'Reply');
}
}
}
$anchor_name = 'inline-'.$inline->getID();
if ($this->editable && !$this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-edit',
),
'Edit');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-delete',
),
'Delete');
} else if ($this->preview) {
$links[] = javelin_render_tag(
'a',
array(
'meta' => array(
'anchor' => $anchor_name,
),
'sigil' => 'differential-inline-preview-jump',
),
'Not Visible');
$links[] = javelin_render_tag(
'a',
array(
'href' => '#',
'mustcapture' => true,
'sigil' => 'differential-inline-delete',
),
'Delete');
}
if ($links) {
$links =
'<span class="differential-inline-comment-links">'.
implode(' &middot; ', $links).
'</span>';
} else {
$links = null;
}
$content = $this->markupEngine->getOutput(
$inline,
PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY);
if ($this->preview) {
$anchor = null;
} else {
$anchor = phutil_render_tag(
'a',
array(
'name' => $anchor_name,
'id' => $anchor_name,
'class' => 'differential-inline-comment-anchor',
),
'');
}
$classes = array(
'differential-inline-comment',
);
if ($is_draft) {
$classes[] = 'differential-inline-comment-unsaved-draft';
}
if ($is_synthetic) {
$classes[] = 'differential-inline-comment-synthetic';
}
$classes = implode(' ', $classes);
if ($is_synthetic) {
$author = $inline->getSyntheticAuthor();
} else {
$author = $handles[$inline->getAuthorPHID()]->getName();
}
$markup = javelin_render_tag(
'div',
array(
'class' => $classes,
'sigil' => $sigil,
'meta' => $metadata,
),
'<div class="differential-inline-comment-head">'.
$anchor.
$links.
' <span class="differential-inline-comment-line">'.$line.'</span> '.
phutil_escape_html($author).
'</div>'.
'<div class="differential-inline-comment-content">'.
'<div class="phabricator-remarkup">'.
$content.
'</div>'.
'</div>');
return $this->scaffoldMarkup($markup);
}
private function scaffoldMarkup($markup) {
if (!$this->buildScaffolding) {
return $markup;
}
$left_markup = !$this->onRight ? $markup : '';
$right_markup = $this->onRight ? $markup : '';
return
'<table>'.
'<tr class="inline">'.
'<th></th>'.
- '<td>'.$left_markup.'</td>'.
+ '<td class="left">'.$left_markup.'</td>'.
'<th></th>'.
- '<td colspan="2">'.$right_markup.'</td>'.
+ '<td class="right3" colspan="3">'.$right_markup.'</td>'.
'</tr>'.
'</table>';
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 20:10 (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128332
Default Alt Text
(89 KB)

Event Timeline