diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 377dae0d33..e53237625c 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -227,6 +227,8 @@ phutil_register_library_map(array(
'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php',
'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php',
'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php',
+ 'DifferentialChangesetRenderer' => 'applications/differential/render/DifferentialChangesetRenderer.php',
+ 'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php',
'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php',
'DifferentialComment' => 'applications/differential/storage/DifferentialComment.php',
'DifferentialCommentEditor' => 'applications/differential/editor/DifferentialCommentEditor.php',
@@ -1510,6 +1512,7 @@ phutil_register_library_map(array(
'DifferentialChangesetDetailView' => 'AphrontView',
'DifferentialChangesetListView' => 'AphrontView',
'DifferentialChangesetParserTestCase' => 'ArcanistPhutilTestCase',
+ 'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetRenderer',
'DifferentialChangesetViewController' => 'DifferentialController',
'DifferentialComment' =>
array(
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
index fd0cbffa8d..3f7420c80b 100644
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -188,7 +188,7 @@ final class DifferentialChangesetParser {
return $this->renderCacheKey;
}
- public function setChangeset($changeset) {
+ public function setChangeset(DifferentialChangeset $changeset) {
$this->changeset = $changeset;
$this->setFilename($changeset->getFilename());
@@ -206,6 +206,10 @@ final class DifferentialChangesetParser {
return $this;
}
+ private function getRenderingReference() {
+ return $this->renderingReference;
+ }
+
public function getChangeset() {
return $this->changeset;
}
@@ -775,10 +779,6 @@ final class DifferentialChangesetParser {
return idx($this->specialAttributes, self::ATTR_WHITELINES, false);
}
- public function getLength() {
- return max(count($this->old), count($this->new));
- }
-
protected function applyIntraline(&$render, $intra, $corpus) {
foreach ($render as $key => $text) {
@@ -928,6 +928,33 @@ final class DifferentialChangesetParser {
}
}
+ 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,
@@ -938,54 +965,124 @@ final class DifferentialChangesetParser {
// 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)
+ ->setRenderingReference($this->getRenderingReference())
+ ->setMarkupEngine($this->markupEngine)
+ ->setHandles($this->handles);
$shield = null;
if ($this->isTopLevel && !$this->comments) {
if ($this->isGenerated()) {
- $shield = $this->renderShield(
- "This file contains generated code, which does not normally need ".
- "to be reviewed.",
+ $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 = $this->renderShield(
- "This file was changed only by adding or removing trailing ".
- "whitespace.",
+ $shield = $renderer->renderShield(
+ pht(
+ 'This file was changed only by adding or removing trailing '.
+ 'whitespace.'),
false);
} else {
- $shield = $this->renderShield(
- "The contents of this file were not changed.",
+ $shield = $renderer->renderShield(
+ pht("The contents of this file were not changed."),
false);
}
} else if ($this->isDeleted()) {
- $shield = $this->renderShield(
- "This file was completely deleted.",
+ $shield = $renderer->renderShield(
+ pht("This file was completely deleted."),
true);
} else if ($this->changeset->getAffectedLineCount() > 2500) {
$lines = number_format($this->changeset->getAffectedLineCount());
- $shield = $this->renderShield(
- "This file has a very large number of changes ({$lines} lines).",
+ $shield = $renderer->renderShield(
+ pht(
+ 'This file has a very large number of changes ({%s} lines).',
+ $lines),
true);
}
}
if ($shield) {
- return $this->renderChangesetTable($this->changeset, $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->renderingReference;
+ $reference = $this->getRenderingReference();
$parts = explode('/', $reference);
if (count($parts) == 2) {
list($id, $vs) = $parts;
@@ -1013,7 +1110,6 @@ final class DifferentialChangesetParser {
}
if ($old_phid || $new_phid) {
-
// grab the files, (micro) optimization for 1 query not 2
$file_phids = array();
if ($old_phid) {
@@ -1026,163 +1122,44 @@ final class DifferentialChangesetParser {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
-
foreach ($files as $file) {
if (empty($file)) {
continue;
}
if ($file->getPHID() == $old_phid) {
- $old = phutil_render_tag(
- 'div',
- array(
- 'class' => 'differential-image-stage'
- ),
- phutil_render_tag(
- 'img',
- array(
- 'src' => $file->getBestURI(),
- )
- )
- );
- } else {
- $cur = phutil_render_tag(
- 'div',
- array(
- 'class' => 'differential-image-stage'
- ),
- phutil_render_tag(
- 'img',
- array(
- 'src' => $file->getBestURI(),
- )
- )
- );
+ $old = $file;
+ } else if ($file->getPHID() == $new_phid) {
+ $new = $file;
}
}
}
-
- $this->comments = msort($this->comments, 'getID');
- $old_comments = array();
- $new_comments = array();
- foreach ($this->comments as $comment) {
- if ($this->isCommentOnRightSideWhenDisplayed($comment)) {
- $new_comments[] = $comment;
- } else {
- $old_comments[] = $comment;
- }
- }
-
- $html_old = array();
- $html_new = array();
- foreach ($old_comments as $comment) {
- $xhp = $this->renderInlineComment($comment);
- $html_old[] =
- '
'.
- ' | '.
- ''.$xhp.' | '.
- ' | '.
- ' | '.
- '
';
- }
- foreach ($new_comments as $comment) {
- $xhp = $this->renderInlineComment($comment);
- $html_new[] =
- ''.
- ' | '.
- ' | '.
- ' | '.
- ''.$xhp.' | '.
- '
';
- }
-
- if (!$old) {
- $th_old = ' | ';
- } else {
- $th_old = '1 | ';
- }
-
- if (!$cur) {
- $th_new = ' | ';
- } else {
- $th_new = '1 | ';
- }
-
- $output = $this->renderChangesetTable(
- $this->changeset,
- ''.
- $th_old.
- '| '.$old.' | '.
- $th_new.
- ''.
- $cur.
- ' | '.
- '
'.
- implode('', $html_old).
- implode('', $html_new));
-
- return $output;
+ return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
- $output = $this->renderChangesetTable($this->changeset, null);
+ $output = $renderer->renderChangesetTable(null);
return $output;
}
- $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 = $this->isCommentOnRightSideWhenDisplayed($comment);
- for ($ii = $start; $ii <= $end; $ii++) {
- if ($new) {
- $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;
- }
- }
+ 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);
- $html = $this->renderTextChange(
+ $html = $renderer->renderTextChange(
$range_start,
$range_len,
$mask_force,
- $feedback_mask,
- $old_comments,
- $new_comments);
+ $feedback_mask
+ );
- return $this->renderChangesetTable($this->changeset, $html);
+ return $renderer->renderChangesetTable($html);
}
/**
@@ -1196,18 +1173,18 @@ final class DifferentialChangesetParser {
private function isCommentVisibleOnRenderedDiff(
PhabricatorInlineCommentInterface $comment) {
- $changeset_id = $comment->getChangesetID();
- $is_new = $comment->getIsNewFile();
+ $changeset_id = $comment->getChangesetID();
+ $is_new = $comment->getIsNewFile();
- if ($changeset_id == $this->rightSideChangesetID &&
+ if ($changeset_id == $this->rightSideChangesetID &&
$is_new == $this->rightSideAttachesToNewFile) {
- return true;
- }
+ return true;
+ }
- if ($changeset_id == $this->leftSideChangesetID &&
+ if ($changeset_id == $this->leftSideChangesetID &&
$is_new == $this->leftSideAttachesToNewFile) {
- return true;
- }
+ return true;
+ }
return false;
}
@@ -1240,799 +1217,6 @@ final class DifferentialChangesetParser {
return false;
}
- protected function renderShield($message, $more) {
-
- if ($more) {
- $end = $this->getLength();
- $reference = $this->renderingReference;
- $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',
- ),
- ''.
- phutil_escape_html($message).
- $more.
- ' | ');
- }
-
- protected function renderTextChange(
- $range_start,
- $range_len,
- $mask_force,
- $feedback_mask,
- array $old_comments,
- array $new_comments) {
- foreach (array_merge($old_comments, $new_comments) as $comments) {
- assert_instances_of($comments, 'PhabricatorInlineCommentInterface');
- }
-
- $context_not_available = null;
- if ($this->missingOld || $this->missingNew) {
- $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();
-
- $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);
-
- // 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;
- $mask = $this->visible + $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 <= self::LINES_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);
-
- $reference = $this->renderingReference;
-
- $left_id = $this->leftSideChangesetID;
- $right_id = $this->rightSideChangesetID;
-
- // "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->leftSideAttachesToNewFile
- ? 'N'
- : 'O';
- $right_char = $this->rightSideAttachesToNewFile
- ? 'N'
- : 'O';
-
- $copy_lines = idx($this->changeset->getMetadata(), 'copy:lines', array());
-
- if ($this->originalLeft && $this->originalRight) {
- list($highlight_old, $highlight_new) = $this->diffOriginals();
- $highlight_old = array_flip($highlight_old);
- $highlight_new = array_flip($highlight_new);
- }
-
- // We need to go backwards to properly indent whitespace in this code:
- //
- // 0: class C {
- // 1:
- // 1: function f() {
- // 2:
- // 2: return;
- //
- $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;
- }
-
- 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 = $this->new[$l]['text'];
- if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') {
- $context = $this->newRender[$l];
- $context_line = $this->new[$l]['line'];
- break;
- }
- }
- }
-
- $container = javelin_render_tag(
- 'tr',
- array(
- 'sigil' => 'context-target',
- ),
- ''.
- implode(' • ', $contents).
- ' | '.
- ''.$context_line.''.
- ' | '.$context.' | ');
-
- $html[] = $container;
-
- $ii += ($len - 1);
- continue;
- }
-
- $o_num = null;
- $o_classes = 'left';
- $o_text = null;
- if (isset($this->old[$ii])) {
- $o_num = $this->old[$ii]['line'];
- $o_text = isset($this->oldRender[$ii]) ? $this->oldRender[$ii] : null;
- if ($this->old[$ii]['type']) {
- if ($this->old[$ii]['type'] == '\\') {
- $o_text = $this->old[$ii]['text'];
- $o_classes .= ' comment';
- } else if ($this->originalLeft && !isset($highlight_old[$o_num])) {
- $o_classes .= ' old-rebase';
- } else if (empty($this->new[$ii])) {
- $o_classes .= ' old old-full';
- } else {
- $o_classes .= ' old';
- }
- }
- }
-
- $n_copy = ' | ';
- $n_cov = null;
- $n_colspan = 2;
- $n_classes = '';
- $n_num = null;
- $n_text = null;
-
- if (isset($this->new[$ii])) {
- $n_num = $this->new[$ii]['line'];
- $n_text = isset($this->newRender[$ii]) ? $this->newRender[$ii] : null;
-
- if ($this->coverage !== null) {
- if (empty($this->coverage[$n_num - 1])) {
- $cov_class = 'N';
- } else {
- $cov_class = $this->coverage[$n_num - 1];
- }
- $cov_class = 'cov-'.$cov_class;
- $n_cov = ' | ';
- $n_colspan--;
- }
-
- if ($this->new[$ii]['type']) {
- if ($this->new[$ii]['type'] == '\\') {
- $n_text = $this->new[$ii]['text'];
- $n_class = 'comment';
- } else if ($this->originalRight && !isset($highlight_new[$n_num])) {
- $n_class = 'new-rebase';
- } else if (empty($this->old[$ii])) {
- $n_class = 'new new-full';
- } else {
- $n_class = 'new';
- }
- $n_classes = $n_class;
-
- if ($this->new[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) {
- $n_copy = ' | ';
- } 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($this->missingOld[$o_num])) ||
- ($n_num && !empty($this->missingNew[$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[] =
- ''.
- '| '.$o_num.' | '.
- ''.$o_text.' | '.
- ''.$n_num.' | '.
- $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.
- ''.
- "\xE2\x80\x8B".$n_text.
- ' | '.
- $n_cov.
- '
';
-
- if ($context_not_available && ($ii == $rows - 1)) {
- $html[] = $context_not_available;
- }
-
- if ($o_num && isset($old_comments[$o_num])) {
- foreach ($old_comments[$o_num] as $comment) {
- $xhp = $this->renderInlineComment($comment);
- $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);
- unset($new_comments[$n_num][$key]);
- }
- }
- }
- $html[] =
- ''.
- ' | '.
- ''.$xhp.' | '.
- ' | '.
- ''.$new.' | '.
- '
';
- }
- }
- if ($n_num && isset($new_comments[$n_num])) {
- foreach ($new_comments[$n_num] as $comment) {
- $xhp = $this->renderInlineComment($comment);
- $html[] =
- ''.
- ' | '.
- ' | '.
- ' | '.
- ''.$xhp.' | '.
- '
';
- }
- }
- }
-
- return implode('', $html);
- }
-
- private function renderInlineComment(
- PhabricatorInlineCommentInterface $comment) {
-
- $user = $this->user;
- $edit = $user &&
- ($comment->getAuthorPHID() == $user->getPHID()) &&
- ($comment->isDraft());
- $allow_reply = (bool)$this->user;
-
- $on_right = $this->isCommentOnRightSideWhenDisplayed($comment);
-
- return id(new DifferentialInlineCommentView())
- ->setInlineComment($comment)
- ->setOnRight($on_right)
- ->setHandles($this->handles)
- ->setMarkupEngine($this->markupEngine)
- ->setEditable($edit)
- ->setAllowReply($allow_reply)
- ->render();
- }
-
- protected function renderPropertyChangeHeader($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 null;
- }
-
- $old = $changeset->getOldProperties();
- $new = $changeset->getNewProperties();
-
- if ($old === $new) {
- return null;
- }
-
- if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
- $new == array('unix:filemode' => '100644')) {
- return null;
- }
-
- if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
- $old == array('unix:filemode' => '100644')) {
- return null;
- }
-
- $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 = 'null';
- } else {
- $oval = nl2br(phutil_escape_html($oval));
- }
-
- if ($nval === null) {
- $nval = 'null';
- } else {
- $nval = nl2br(phutil_escape_html($nval));
- }
-
- $rows[] =
- ''.
- '| '.phutil_escape_html($key).' | '.
- ''.$oval.' | '.
- ''.$nval.' | '.
- '
';
- }
- }
-
- return
- ''.
- ''.
- implode('', $rows).
- '
';
- }
-
- protected function renderChangesetTable($changeset, $contents) {
- $props = $this->renderPropertyChangeHeader($this->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($this->changeset, true);
- } else {
- $notice = $this->renderChangeTypeHeader($this->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;
- }
-
- 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 added.');
- break;
- case DifferentialChangeType::FILE_IMAGE:
- $message = pht('This image was added.');
- break;
- case DifferentialChangeType::FILE_DIRECTORY:
- $message = pht('This directory was added.');
- break;
- case DifferentialChangeType::FILE_BINARY:
- $message = pht('This binary file was added.');
- break;
- case DifferentialChangeType::FILE_SYMLINK:
- $message = pht('This symlink was added.');
- break;
- case DifferentialChangeType::FILE_SUBMODULE:
- $message = pht('This submodule was added.');
- break;
- }
- break;
-
- case DifferentialChangeType::TYPE_DELETE:
- switch ($file) {
- case DifferentialChangeType::FILE_TEXT:
- $message = pht('This file was deleted.');
- break;
- case DifferentialChangeType::FILE_IMAGE:
- $message = pht('This image was deleted.');
- break;
- case DifferentialChangeType::FILE_DIRECTORY:
- $message = pht('This directory was deleted.');
- break;
- case DifferentialChangeType::FILE_BINARY:
- $message = pht('This binary file was deleted.');
- break;
- case DifferentialChangeType::FILE_SYMLINK:
- $message = pht('This symlink was deleted.');
- break;
- case DifferentialChangeType::FILE_SUBMODULE:
- $message = pht('This submodule was deleted.');
- break;
- }
- break;
-
- case DifferentialChangeType::TYPE_MOVE_HERE:
- $from =
- "".
- phutil_escape_html($changeset->getOldFile()).
- "";
- 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 =
- "".
- phutil_escape_html($changeset->getOldFile()).
- "";
- 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 =
- "".
- phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
- "";
- 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 =
- "".
- phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
- "";
- 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 =
- "".
- phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
- "";
- 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
- ''.
- $message.
- '
';
- }
-
public function renderForEmail() {
$ret = '';
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
new file mode 100644
index 0000000000..900a04e551
--- /dev/null
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -0,0 +1,603 @@
+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) {
+ phlog(print_r($new_lines, true));
+ $this->newLines = $new_lines;
+ return $this;
+ }
+ protected function getNewLines() {
+ return $this->newLines;
+ }
+
+ public function setOldLines(array $old_lines) {
+ phlog(print_r($old_lines, true));
+ $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
+ );
+ 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',
+ ),
+ ''.
+ phutil_escape_html($message).
+ $more.
+ ' | ');
+ }
+
+
+ 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 = 'null';
+ } else {
+ $oval = nl2br(phutil_escape_html($oval));
+ }
+
+ if ($nval === null) {
+ $nval = 'null';
+ } else {
+ $nval = nl2br(phutil_escape_html($nval));
+ }
+
+ $rows[] =
+ ''.
+ '| '.phutil_escape_html($key).' | '.
+ ''.$oval.' | '.
+ ''.$nval.' | '.
+ '
';
+ }
+ }
+
+ return
+ ''.
+ ''.
+ implode('', $rows).
+ '
';
+ }
+
+ 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 added.');
+ break;
+ case DifferentialChangeType::FILE_IMAGE:
+ $message = pht('This image was added.');
+ break;
+ case DifferentialChangeType::FILE_DIRECTORY:
+ $message = pht('This directory was added.');
+ break;
+ case DifferentialChangeType::FILE_BINARY:
+ $message = pht('This binary file was added.');
+ break;
+ case DifferentialChangeType::FILE_SYMLINK:
+ $message = pht('This symlink was added.');
+ break;
+ case DifferentialChangeType::FILE_SUBMODULE:
+ $message = pht('This submodule was added.');
+ break;
+ }
+ break;
+
+ case DifferentialChangeType::TYPE_DELETE:
+ switch ($file) {
+ case DifferentialChangeType::FILE_TEXT:
+ $message = pht('This file was deleted.');
+ break;
+ case DifferentialChangeType::FILE_IMAGE:
+ $message = pht('This image was deleted.');
+ break;
+ case DifferentialChangeType::FILE_DIRECTORY:
+ $message = pht('This directory was deleted.');
+ break;
+ case DifferentialChangeType::FILE_BINARY:
+ $message = pht('This binary file was deleted.');
+ break;
+ case DifferentialChangeType::FILE_SYMLINK:
+ $message = pht('This symlink was deleted.');
+ break;
+ case DifferentialChangeType::FILE_SUBMODULE:
+ $message = pht('This submodule was deleted.');
+ break;
+ }
+ break;
+
+ case DifferentialChangeType::TYPE_MOVE_HERE:
+ $from =
+ "".
+ phutil_escape_html($changeset->getOldFile()).
+ "";
+ 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 =
+ "".
+ phutil_escape_html($changeset->getOldFile()).
+ "";
+ 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 =
+ "".
+ phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
+ "";
+ 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 =
+ "".
+ phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
+ "";
+ 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 =
+ "".
+ phutil_escape_html(implode(', ', $changeset->getAwayPaths())).
+ "";
+ 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
+ ''.
+ $message.
+ '
';
+ }
+
+ 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
new file mode 100644
index 0000000000..b95408e180
--- /dev/null
+++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php
@@ -0,0 +1,536 @@
+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) {
+
+ $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));
+
+ phlog($rows);
+
+ 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);
+
+ $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;
+ }
+
+ 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',
+ ),
+ ''.
+ implode(' • ', $contents).
+ ' | '.
+ ''.$context_line.''.
+ ' | '.$context.' | ');
+
+ $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 = ' | ';
+ $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 = ' | ';
+ $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 = ' | ';
+ } 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[] =
+ ''.
+ '| '.$o_num.' | '.
+ ''.$o_text.' | '.
+ ''.$n_num.' | '.
+ $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.
+ ''.
+ "\xE2\x80\x8B".$n_text.
+ ' | '.
+ $n_cov.
+ '
';
+
+ 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);
+ $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[] =
+ ''.
+ ' | '.
+ ''.$xhp.' | '.
+ ' | '.
+ ''.$new.' | '.
+ '
';
+ }
+ }
+ if ($n_num && isset($new_comments[$n_num])) {
+ foreach ($new_comments[$n_num] as $comment) {
+ $xhp = $this->renderInlineComment($comment, $on_right = true);
+ $html[] =
+ ''.
+ ' | '.
+ ' | '.
+ ' | '.
+ ''.$xhp.' | '.
+ '
';
+ }
+ }
+ }
+
+ 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);
+ $html_old[] =
+ ''.
+ ' | '.
+ ''.$xhp.' | '.
+ ' | '.
+ ' | '.
+ '
';
+ }
+ foreach ($this->getNewComments() as $comment) {
+ $xhp = $this->renderInlineComment($comment, $on_right = true);
+ $html_new[] =
+ ''.
+ ' | '.
+ ' | '.
+ ' | '.
+ ''.$xhp.' | '.
+ '
';
+ }
+
+ if (!$old) {
+ $th_old = ' | ';
+ } else {
+ $th_old = '1 | ';
+ }
+
+ if (!$new) {
+ $th_new = ' | ';
+ } else {
+ $th_new = '1 | ';
+ }
+
+ $output = $this->renderChangesetTable(
+ ''.
+ $th_old.
+ '| '.$old.' | '.
+ $th_new.
+ ''.
+ $new.
+ ' | '.
+ '
'.
+ implode('', $html_old).
+ implode('', $html_new));
+
+ return $output;
+ }
+
+}