From 75e8ff26f5eb65de6cabe8a35b0a45ee364cbf04 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Fri, 7 Dec 2012 16:19:57 -0800 Subject: [PATCH] Refactor DifferentialChangesetParser -- pass 1 of N Summary: basically did my darnedest to pull out a TwoUp rendering view. Made a base class for the rendering views with "old" and "new" terminology rather than "left" and "right. Future revisions will finish cleaning up the terminology within the DifferentialChangesetParser itself and more of the ideas within T2009. Test Plan: been playing with differential all day Reviewers: epriestley Reviewed By: epriestley CC: vrana, chad, aran, Korvin Maniphest Tasks: T2009 Differential Revision: https://secure.phabricator.com/D4117 --- src/__phutil_library_map__.php | 3 + .../parser/DifferentialChangesetParser.php | 1106 +++-------------- .../render/DifferentialChangesetRenderer.php | 603 +++++++++ .../DifferentialChangesetTwoUpRenderer.php | 536 ++++++++ 4 files changed, 1287 insertions(+), 961 deletions(-) create mode 100644 src/applications/differential/render/DifferentialChangesetRenderer.php create mode 100644 src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php 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). - '
Property ChangesOld ValueNew Value
'; - } - - 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). + '
Property ChangesOld ValueNew Value
'; + } + + 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; + } + +}