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; + } + +}