diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php index 9d9f47315..7e0624834 100644 --- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php +++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php @@ -1,66 +1,67 @@ buildChangesetParser($type, $data, $file); $actual = $parser->render(null, null, array()); $expect = Filesystem::readFile($dir.$file.'.'.$type.'.expect'); $this->assertEqual($expect, (string)$actual, $file.'.'.$type); } } } private function buildChangesetParser($type, $data, $file) { $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($data); $diff = DifferentialDiff::newFromRawChanges( PhabricatorUser::getOmnipotentUser(), $changes); if (count($diff->getChangesets()) !== 1) { throw new Exception("Expected one changeset: {$file}"); } $changeset = head($diff->getChangesets()); $engine = new PhabricatorMarkupEngine(); $engine->setViewer(new PhabricatorUser()); $cparser = new DifferentialChangesetParser(); + $cparser->setUser(new PhabricatorUser()); $cparser->setDisableCache(true); $cparser->setChangeset($changeset); $cparser->setMarkupEngine($engine); if ($type == 'one') { $cparser->setRenderer(new DifferentialChangesetOneUpTestRenderer()); } else if ($type == 'two') { $cparser->setRenderer(new DifferentialChangesetTwoUpTestRenderer()); } else { throw new Exception("Unknown renderer type '{$type}'!"); } return $cparser; } } diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php index 3d25524e7..48f009b27 100644 --- a/src/applications/differential/controller/DifferentialChangesetViewController.php +++ b/src/applications/differential/controller/DifferentialChangesetViewController.php @@ -1,383 +1,384 @@ getRequest(); $author_phid = $request->getUser()->getPHID(); $rendering_reference = $request->getStr('ref'); $parts = explode('/', $rendering_reference); if (count($parts) == 2) { list($id, $vs) = $parts; } else { $id = $parts[0]; $vs = 0; } $id = (int)$id; $vs = (int)$vs; $load_ids = array($id); if ($vs && ($vs != -1)) { $load_ids[] = $vs; } $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withIDs($load_ids) ->needHunks(true) ->execute(); $changesets = mpull($changesets, null, 'getID'); $changeset = idx($changesets, $id); if (!$changeset) { return new Aphront404Response(); } $vs_changeset = null; if ($vs && ($vs != -1)) { $vs_changeset = idx($changesets, $vs); if (!$vs_changeset) { return new Aphront404Response(); } } $view = $request->getStr('view'); if ($view) { $phid = idx($changeset->getMetadata(), "$view:binary-phid"); if ($phid) { return id(new AphrontRedirectResponse())->setURI("/file/info/$phid/"); } switch ($view) { case 'new': return $this->buildRawFileResponse($changeset, $is_new = true); case 'old': if ($vs_changeset) { return $this->buildRawFileResponse($vs_changeset, $is_new = true); } return $this->buildRawFileResponse($changeset, $is_new = false); default: return new Aphront400Response(); } } if (!$vs) { $right = $changeset; $left = null; $right_source = $right->getID(); $right_new = true; $left_source = $right->getID(); $left_new = false; $render_cache_key = $right->getID(); } else if ($vs == -1) { $right = null; $left = $changeset; $right_source = $left->getID(); $right_new = false; $left_source = $left->getID(); $left_new = true; $render_cache_key = null; } else { $right = $changeset; $left = $vs_changeset; $right_source = $right->getID(); $right_new = true; $left_source = $left->getID(); $left_new = true; $render_cache_key = null; } if ($left) { $left_data = $left->makeNewFile(); if ($right) { $right_data = $right->makeNewFile(); } else { $right_data = $left->makeOldFile(); } $engine = new PhabricatorDifferenceEngine(); $synthetic = $engine->generateChangesetFromFileContent( $left_data, $right_data); $choice = clone nonempty($left, $right); $choice->attachHunks($synthetic->getHunks()); $changeset = $choice; } $coverage = null; if ($right && $right->getDiffID()) { $unit = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $right->getDiffID(), 'arc:unit'); if ($unit) { $coverage = array(); foreach ($unit->getData() as $result) { $result_coverage = idx($result, 'coverage'); if (!$result_coverage) { continue; } $file_coverage = idx($result_coverage, $right->getFileName()); if (!$file_coverage) { continue; } $coverage[] = $file_coverage; } $coverage = ArcanistUnitTestResult::mergeCoverage($coverage); } } $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $parser = new DifferentialChangesetParser(); $parser->setCoverage($coverage); $parser->setChangeset($changeset); $parser->setRenderingReference($rendering_reference); $parser->setRenderCacheKey($render_cache_key); $parser->setRightSideCommentMapping($right_source, $right_new); $parser->setLeftSideCommentMapping($left_source, $left_new); $parser->setWhitespaceMode($request->getStr('whitespace')); $parser->setCharacterEncoding($request->getStr('encoding')); $parser->setHighlightAs($request->getStr('highlight')); if ($request->getStr('renderer') == '1up') { $parser->setRenderer(new DifferentialChangesetOneUpRenderer()); } if ($left && $right) { $parser->setOriginals($left, $right); } // Load both left-side and right-side inline comments. $inlines = $this->loadInlineComments( array($left_source, $right_source), $author_phid); if ($left_new) { $inlines = array_merge( $inlines, $this->buildLintInlineComments($left)); } if ($right_new) { $inlines = array_merge( $inlines, $this->buildLintInlineComments($right)); } $phids = array(); foreach ($inlines as $inline) { $parser->parseInlineComment($inline); if ($inline->getAuthorPHID()) { $phids[$inline->getAuthorPHID()] = true; } } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $parser->setHandles($handles); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($request->getUser()); foreach ($inlines as $inline) { $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $parser->setMarkupEngine($engine); + $parser->setUser($request->getUser()); if ($request->isAjax()) { - // TODO: This is sort of lazy, the effect is just to not render "Edit" - // and "Reply" links on the "standalone view". - $parser->setUser($request->getUser()); + $parser->setShowEditAndReplyLinks(true); + } else { + $parser->setShowEditAndReplyLinks(false); } $output = $parser->render($range_s, $range_e, $mask); $mcov = $parser->renderModifiedCoverage(); if ($request->isAjax()) { $coverage = array( 'differential-mcoverage-'.md5($changeset->getFilename()) => $mcov, ); return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($output) ->setCoverage($coverage); } Javelin::initBehavior('differential-show-more', array( 'uri' => '/differential/changeset/', 'whitespace' => $request->getStr('whitespace'), )); Javelin::initBehavior('differential-comment-jump', array()); // TODO: [HTML] Clean up DifferentialChangesetParser output, but it's // undergoing like six kinds of refactoring anyway. $output = phutil_safe_html($output); $detail = new DifferentialChangesetDetailView(); $detail->setChangeset($changeset); $detail->appendChild($output); $detail->setVsChangesetID($left_source); $panel = new DifferentialPrimaryPaneView(); $panel->appendChild( phutil_tag( 'div', array( 'class' => 'differential-review-stage', 'id' => 'differential-review-stage', ), $detail->render())); $crumbs = $this->buildApplicationCrumbs(); $revision_id = $changeset->getDiff()->getRevisionID(); if ($revision_id) { $crumbs->addTextCrumb('D'.$revision_id, '/D'.$revision_id); } $diff_id = $changeset->getDiff()->getID(); if ($diff_id) { $crumbs->addTextCrumb( pht('Diff %d', $diff_id), $this->getApplicationURI('diff/'.$diff_id)); } $crumbs->addTextCrumb($changeset->getDisplayFilename()); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Standalone View')) ->appendChild($panel); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => pht('Changeset View'), 'device' => false, )); } private function loadInlineComments(array $changeset_ids, $author_phid) { $changeset_ids = array_unique(array_filter($changeset_ids)); if (!$changeset_ids) { return; } return id(new DifferentialInlineCommentQuery()) ->withViewerAndChangesetIDs($author_phid, $changeset_ids) ->execute(); } private function buildRawFileResponse( DifferentialChangeset $changeset, $is_new) { $viewer = $this->getRequest()->getUser(); if ($is_new) { $key = 'raw:new:phid'; } else { $key = 'raw:old:phid'; } $metadata = $changeset->getMetadata(); $file = null; $phid = idx($metadata, $key); if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->execute(); if ($file) { $file = head($file); } } if (!$file) { // This is just building a cache of the changeset content in the file // tool, and is safe to run on a read pathway. $unguard = AphrontWriteGuard::beginScopedUnguardedWrites(); if ($is_new) { $data = $changeset->makeNewFile(); } else { $data = $changeset->makeOldFile(); } $file = PhabricatorFile::newFromFileData( $data, array( 'name' => $changeset->getFilename(), 'mime-type' => 'text/plain', )); $metadata[$key] = $file->getPHID(); $changeset->setMetadata($metadata); $changeset->save(); unset($unguard); } return $file->getRedirectResponse(); } private function buildLintInlineComments($changeset) { $lint = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $changeset->getDiffID(), 'arc:lint'); if (!$lint) { return array(); } $lint = $lint->getData(); $inlines = array(); foreach ($lint as $msg) { if ($msg['path'] != $changeset->getFilename()) { continue; } $inline = new DifferentialInlineComment(); $inline->setChangesetID($changeset->getID()); $inline->setIsNewFile(1); $inline->setSyntheticAuthor('Lint: '.$msg['name']); $inline->setLineNumber($msg['line']); $inline->setLineLength(0); $inline->setContent('%%%'.$msg['description'].'%%%'); $inlines[] = $inline; } return $inlines; } } diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index a27f8c823..8ff90a256 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1,1297 +1,1308 @@ showEditAndReplyLinks = $bool; + return $this; + } + public function getShowEditAndReplyLinks() { + return $this->showEditAndReplyLinks; + } public function setHighlightAs($highlight_as) { $this->highlightAs = $highlight_as; return $this; } public function getHighlightAs() { return $this->highlightAs; } public function setCharacterEncoding($character_encoding) { $this->characterEncoding = $character_encoding; return $this; } public function getCharacterEncoding() { return $this->characterEncoding; } - public function setRenderer($renderer) { + public function setRenderer(DifferentialChangesetRenderer $renderer) { $this->renderer = $renderer; return $this; } public function getRenderer() { if (!$this->renderer) { return new DifferentialChangesetTwoUpRenderer(); } return $this->renderer; } public function setDisableCache($disable_cache) { $this->disableCache = $disable_cache; return $this; } public function getDisableCache() { return $this->disableCache; } const CACHE_VERSION = 11; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; const ATTR_DELETED = 'attr:deleted'; const ATTR_UNCHANGED = 'attr:unchanged'; const ATTR_WHITELINES = 'attr:white'; const ATTR_MOVEAWAY = 'attr:moveaway'; const LINES_CONTEXT = 8; const WHITESPACE_SHOW_ALL = 'show-all'; const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing'; // TODO: This is now "Ignore Most" in the UI. const WHITESPACE_IGNORE_ALL = 'ignore-all'; const WHITESPACE_IGNORE_FORCE = 'ignore-force'; public function setOldLines(array $lines) { $this->old = $lines; return $this; } public function setNewLines(array $lines) { $this->new = $lines; return $this; } public function setSpecialAttributes(array $attributes) { $this->specialAttributes = $attributes; return $this; } public function setIntraLineDiffs(array $diffs) { $this->intra = $diffs; return $this; } public function setVisibileLinesMask(array $mask) { $this->visible = $mask; return $this; } /** * Configure which Changeset comments added to the right side of the visible * diff will be attached to. The ID must be the ID of a real Differential * Changeset. * * The complexity here is that we may show an arbitrary side of an arbitrary * changeset as either the left or right part of a diff. This method allows * the left and right halves of the displayed diff to be correctly mapped to * storage changesets. * * @param id The Differential Changeset ID that comments added to the right * side of the visible diff should be attached to. * @param bool If true, attach new comments to the right side of the storage * changeset. Note that this may be false, if the left side of * some storage changeset is being shown as the right side of * a display diff. * @return this */ public function setRightSideCommentMapping($id, $is_new) { $this->rightSideChangesetID = $id; $this->rightSideAttachesToNewFile = $is_new; return $this; } /** * See setRightSideCommentMapping(), but this sets information for the left * side of the display diff. */ public function setLeftSideCommentMapping($id, $is_new) { $this->leftSideChangesetID = $id; $this->leftSideAttachesToNewFile = $is_new; return $this; } public function setOriginals( DifferentialChangeset $left, DifferentialChangeset $right) { $this->originalLeft = $left; $this->originalRight = $right; } public function diffOriginals() { $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent( implode('', mpull($this->originalLeft->getHunks(), 'getChanges')), implode('', mpull($this->originalRight->getHunks(), 'getChanges'))); $parser = new DifferentialHunkParser(); return $parser->parseHunksForHighlightMasks( $changeset->getHunks(), $this->originalLeft->getHunks(), $this->originalRight->getHunks()); } /** * Set a key for identifying this changeset in the render cache. If set, the * parser will attempt to use the changeset render cache, which can improve * performance for frequently-viewed changesets. * * By default, there is no render cache key and parsers do not use the cache. * This is appropriate for rarely-viewed changesets. * * NOTE: Currently, this key must be a valid Differential Changeset ID. * * @param string Key for identifying this changeset in the render cache. * @return this */ public function setRenderCacheKey($key) { $this->renderCacheKey = $key; return $this; } private function getRenderCacheKey() { return $this->renderCacheKey; } public function setChangeset(DifferentialChangeset $changeset) { $this->changeset = $changeset; $this->setFilename($changeset->getFilename()); return $this; } public function setWhitespaceMode($whitespace_mode) { $this->whitespaceMode = $whitespace_mode; return $this; } public function setRenderingReference($ref) { $this->renderingReference = $ref; return $this; } private function getRenderingReference() { return $this->renderingReference; } public function getChangeset() { return $this->changeset; } public function setFilename($filename) { $this->filename = $filename; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } + public function getUser() { + return $this->user; + } + public function setCoverage($coverage) { $this->coverage = $coverage; return $this; } private function getCoverage() { return $this->coverage; } public function parseInlineComment( PhabricatorInlineCommentInterface $comment) { // Parse only comments which are actually visible. if ($this->isCommentVisibleOnRenderedDiff($comment)) { $this->comments[] = $comment; } return $this; } private function loadCache() { $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $data = null; $changeset = new DifferentialChangeset(); $conn_r = $changeset->establishConnection('r'); $data = queryfx_one( $conn_r, 'SELECT * FROM %T WHERE id = %d', $changeset->getTableName().'_parse_cache', $render_cache_key); if (!$data) { return false; } if ($data['cache'][0] == '{') { // This is likely an old-style JSON cache which we will not be able to // deserialize. return false; } $data = unserialize($data['cache']); if (!is_array($data) || !$data) { return false; } foreach (self::getCacheableProperties() as $cache_key) { if (!array_key_exists($cache_key, $data)) { // If we're missing a cache key, assume we're looking at an old cache // and ignore it. return false; } } if ($data['cacheVersion'] !== self::CACHE_VERSION) { return false; } // Someone displays contents of a partially cached shielded file. if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) { return false; } unset($data['cacheVersion'], $data['cacheHost']); $cache_prop = array_select_keys($data, self::getCacheableProperties()); foreach ($cache_prop as $cache_key => $v) { $this->$cache_key = $v; } return true; } protected static function getCacheableProperties() { return array( 'visible', 'new', 'old', 'intra', 'newRender', 'oldRender', 'specialAttributes', 'hunkStartLines', 'cacheVersion', 'cacheHost', ); } public function saveCache() { if ($this->highlightErrors) { return false; } $render_cache_key = $this->getRenderCacheKey(); if (!$render_cache_key) { return false; } $cache = array(); foreach (self::getCacheableProperties() as $cache_key) { switch ($cache_key) { case 'cacheVersion': $cache[$cache_key] = self::CACHE_VERSION; break; case 'cacheHost': $cache[$cache_key] = php_uname('n'); break; default: $cache[$cache_key] = $this->$cache_key; break; } } $cache = serialize($cache); // We don't want to waste too much space by a single changeset. if (strlen($cache) > self::CACHE_MAX_SIZE) { return; } $changeset = new DifferentialChangeset(); $conn_w = $changeset->establishConnection('w'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { queryfx( $conn_w, 'INSERT INTO %T (id, cache, dateCreated) VALUES (%d, %B, %d) ON DUPLICATE KEY UPDATE cache = VALUES(cache)', DifferentialChangeset::TABLE_CACHE, $render_cache_key, $cache, time()); } catch (AphrontQueryException $ex) { // Ignore these exceptions. A common cause is that the cache is // larger than 'max_allowed_packet', in which case we're better off // not writing it. // TODO: It would be nice to tailor this more narrowly. } unset($unguarded); } private function markGenerated($new_corpus_block = '') { $generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false); if (!$generated_guess) { $generated_path_regexps = PhabricatorEnv::getEnvConfig( 'differential.generated-paths'); foreach ($generated_path_regexps as $regexp) { if (preg_match($regexp, $this->changeset->getFilename())) { $generated_guess = true; break; } } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED, array( 'corpus' => $new_corpus_block, 'is_generated' => $generated_guess, ) ); PhutilEventEngine::dispatchEvent($event); $generated = $event->getValue('is_generated'); $this->specialAttributes[self::ATTR_GENERATED] = $generated; } public function isGenerated() { return idx($this->specialAttributes, self::ATTR_GENERATED, false); } public function isDeleted() { return idx($this->specialAttributes, self::ATTR_DELETED, false); } public function isUnchanged() { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } public function isWhitespaceOnly() { return idx($this->specialAttributes, self::ATTR_WHITELINES, false); } public function isMoveAway() { return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); } private function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { if (isset($intra[$key])) { $render[$key] = ArcanistDiffUtils::applyIntralineDiff( $text, $intra[$key]); } } } private function getHighlightFuture($corpus) { $language = $this->highlightAs; if (!$language) { $language = $this->highlightEngine->getLanguageFromFilename( $this->filename); } return $this->highlightEngine->getHighlightFuture( $language, $corpus); } protected function processHighlightedSource($data, $result) { $result_lines = phutil_split_lines($result); foreach ($data as $key => $info) { if (!$info) { unset($result_lines[$key]); } } return $result_lines; } private function tryCacheStuff() { $whitespace_mode = $this->whitespaceMode; switch ($whitespace_mode) { case self::WHITESPACE_SHOW_ALL: case self::WHITESPACE_IGNORE_TRAILING: case self::WHITESPACE_IGNORE_FORCE: break; default: $whitespace_mode = self::WHITESPACE_IGNORE_ALL; break; } $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_ALL); if ($this->disableCache) { $skip_cache = true; } if ($this->characterEncoding) { $skip_cache = true; } if ($this->highlightAs) { $skip_cache = true; } $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT && $changeset->getFileType() != DifferentialChangeType::FILE_SYMLINK) { $this->markGenerated(); } else { if ($skip_cache || !$this->loadCache()) { $this->process(); if (!$skip_cache) { $this->saveCache(); } } } } private function process() { $whitespace_mode = $this->whitespaceMode; $changeset = $this->changeset; $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_ALL) || ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE)); $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_FORCE); if (!$force_ignore) { if ($ignore_all && $changeset->getWhitespaceMatters()) { $ignore_all = false; } } // The "ignore all whitespace" algorithm depends on rediffing the // files, and we currently need complete representations of both // files to do anything reasonable. If we only have parts of the files, // don't use the "ignore all" algorithm. if ($ignore_all) { $hunks = $changeset->getHunks(); if (count($hunks) !== 1) { $ignore_all = false; } else { $first_hunk = reset($hunks); if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) { $ignore_all = false; } } } if ($ignore_all) { $old_file = $changeset->makeOldFile(); $new_file = $changeset->makeNewFile(); if ($old_file == $new_file) { // If the old and new files are exactly identical, the synthetic // diff below will give us nonsense and whitespace modes are // irrelevant anyway. This occurs when you, e.g., copy a file onto // itself in Subversion (see T271). $ignore_all = false; } } $hunk_parser = new DifferentialHunkParser(); $hunk_parser->setWhitespaceMode($whitespace_mode); $hunk_parser->parseHunksForLineData($changeset->getHunks()); // Depending on the whitespace mode, we may need to compute a different // set of changes than the set of changes in the hunk data (specificaly, // we might want to consider changed lines which have only whitespace // changes as unchanged). if ($ignore_all) { $engine = new PhabricatorDifferenceEngine(); $engine->setIgnoreWhitespace(true); $no_whitespace_changeset = $engine->generateChangesetFromFileContent( $old_file, $new_file); $type_parser = new DifferentialHunkParser(); $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks()); $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); } $hunk_parser->reparseHunksForSpecialAttributes(); $unchanged = false; if (!$hunk_parser->getHasAnyChanges()) { $filetype = $this->changeset->getFileType(); if ($filetype == DifferentialChangeType::FILE_TEXT || $filetype == DifferentialChangeType::FILE_SYMLINK) { $unchanged = true; } } $moveaway = false; $changetype = $this->changeset->getChangeType(); if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) { // sometimes we show moved files as unchanged, sometimes deleted, // and sometimes inconsistent with what actually happened at the // destination of the move. Rather than make a false claim, // omit the 'not changed' notice if this is the source of a move $unchanged = false; $moveaway = true; } $this->setSpecialAttributes(array( self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => $hunk_parser->getIsDeleted(), self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), self::ATTR_MOVEAWAY => $moveaway, )); $hunk_parser->generateIntraLineDiffs(); $hunk_parser->generateVisibileLinesMask(); $this->setOldLines($hunk_parser->getOldLines()); $this->setNewLines($hunk_parser->getNewLines()); $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); $this->hunkStartLines = $hunk_parser->getHunkStartLines( $changeset->getHunks()); $new_corpus = $hunk_parser->getNewCorpus(); $new_corpus_block = implode('', $new_corpus); $this->markGenerated($new_corpus_block); if ($this->isTopLevel && !$this->comments && ($this->isGenerated() || $this->isUnchanged() || $this->isDeleted())) { return; } $old_corpus = $hunk_parser->getOldCorpus(); $old_corpus_block = implode('', $old_corpus); $old_future = $this->getHighlightFuture($old_corpus_block); $new_future = $this->getHighlightFuture($new_corpus_block); $futures = array( 'old' => $old_future, 'new' => $new_future, ); $corpus_blocks = array( 'old' => $old_corpus_block, 'new' => $new_corpus_block, ); $this->highlightErrors = false; foreach (new FutureIterator($futures) as $key => $future) { try { try { $highlighted = $future->resolve(); } catch (PhutilSyntaxHighlighterException $ex) { $this->highlightErrors = true; $highlighted = id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($corpus_blocks[$key]) ->resolve(); } switch ($key) { case 'old': $this->oldRender = $this->processHighlightedSource( $this->old, $highlighted); break; case 'new': $this->newRender = $this->processHighlightedSource( $this->new, $highlighted); break; } } catch (Exception $ex) { phlog($ex); throw $ex; } } $this->applyIntraline( $this->oldRender, ipull($this->intra, 0), $old_corpus); $this->applyIntraline( $this->newRender, ipull($this->intra, 1), $new_corpus); } 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; } return true; } public function render( $range_start = null, $range_len = null, $mask_force = array()) { // "Top level" renders are initial requests for the whole file, versus // requests for a specific range generated by clicking "show more". We // generate property changes and "shield" UI elements only for toplevel // requests. $this->isTopLevel = (($range_start === null) && ($range_len === null)); $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine(); $encoding = null; if ($this->characterEncoding) { // We are forcing this changeset to be interpreted with a specific // character encoding, so force all the hunks into that encoding and // propagate it to the renderer. $encoding = $this->characterEncoding; foreach ($this->changeset->getHunks() as $hunk) { $hunk->forceEncoding($this->characterEncoding); } } else { // We're just using the default, so tell the renderer what that is // (by reading the encoding from the first hunk). foreach ($this->changeset->getHunks() as $hunk) { $encoding = $hunk->getDataEncoding(); break; } } $this->tryCacheStuff(); $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset); $rows = max( count($this->old), count($this->new)); $renderer = $this->getRenderer() + ->setUser($this->getUser()) ->setChangeset($this->changeset) ->setRenderPropertyChangeHeader($render_pch) ->setIsTopLevel($this->isTopLevel) ->setOldRender($this->oldRender) ->setNewRender($this->newRender) ->setHunkStartLines($this->hunkStartLines) ->setOldChangesetID($this->leftSideChangesetID) ->setNewChangesetID($this->rightSideChangesetID) ->setOldAttachesToNewFile($this->leftSideAttachesToNewFile) ->setNewAttachesToNewFile($this->rightSideAttachesToNewFile) ->setCodeCoverage($this->getCoverage()) ->setRenderingReference($this->getRenderingReference()) ->setMarkupEngine($this->markupEngine) ->setHandles($this->handles) ->setOldLines($this->old) ->setNewLines($this->new) - ->setOriginalCharacterEncoding($encoding); - - if ($this->user) { - $renderer->setUser($this->user); - } + ->setOriginalCharacterEncoding($encoding) + ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks()); $shield = null; if ($this->isTopLevel && !$this->comments) { if ($this->isGenerated()) { $shield = $renderer->renderShield( pht( 'This file contains generated code, which does not normally '. 'need to be reviewed.')); } else if ($this->isUnchanged()) { $type = 'text'; if (!$rows) { // NOTE: Normally, diffs which don't change files do not include // file content (for example, if you "chmod +x" a file and then // run "git show", the file content is not available). Similarly, // if you move a file from A to B without changing it, diffs normally // do not show the file content. In some cases `arc` is able to // synthetically generate content for these diffs, but for raw diffs // we'll never have it so we need to be prepared to not render a link. $type = 'none'; } $shield = $renderer->renderShield( pht('The contents of this file were not changed.'), $type); } else if ($this->isMoveAway()) { $shield = null; } else if ($this->isWhitespaceOnly()) { $shield = $renderer->renderShield( pht('This file was changed only by adding or removing whitespace.'), 'whitespace'); } else if ($this->isDeleted()) { $shield = $renderer->renderShield( pht('This file was completely deleted.')); } else if ($this->changeset->getAffectedLineCount() > 2500) { $lines = number_format($this->changeset->getAffectedLineCount()); $shield = $renderer->renderShield( pht( 'This file has a very large number of changes (%s lines).', $lines)); } } if ($shield) { return $renderer->renderChangesetTable($shield); } $old_comments = array(); $new_comments = array(); $old_mask = array(); $new_mask = array(); $feedback_mask = array(); if ($this->comments) { foreach ($this->comments as $comment) { $start = max($comment->getLineNumber() - self::LINES_CONTEXT, 0); $end = $comment->getLineNumber() + $comment->getLineLength() + self::LINES_CONTEXT; $new_side = $this->isCommentOnRightSideWhenDisplayed($comment); for ($ii = $start; $ii <= $end; $ii++) { if ($new_side) { $new_mask[$ii] = true; } else { $old_mask[$ii] = true; } } } foreach ($this->old as $ii => $old) { if (isset($old['line']) && isset($old_mask[$old['line']])) { $feedback_mask[$ii] = true; } } foreach ($this->new as $ii => $new) { if (isset($new['line']) && isset($new_mask[$new['line']])) { $feedback_mask[$ii] = true; } } $this->comments = msort($this->comments, 'getID'); foreach ($this->comments as $comment) { $final = $comment->getLineNumber() + $comment->getLineLength(); $final = max(1, $final); if ($this->isCommentOnRightSideWhenDisplayed($comment)) { $new_comments[$final][] = $comment; } else { $old_comments[$final][] = $comment; } } } $renderer ->setOldComments($old_comments) ->setNewComments($new_comments); switch ($this->changeset->getFileType()) { case DifferentialChangeType::FILE_IMAGE: $old = null; $new = null; // TODO: Improve the architectural issue as discussed in D955 // https://secure.phabricator.com/D955 $reference = $this->getRenderingReference(); $parts = explode('/', $reference); if (count($parts) == 2) { list($id, $vs) = $parts; } else { $id = $parts[0]; $vs = 0; } $id = (int)$id; $vs = (int)$vs; if (!$vs) { $metadata = $this->changeset->getMetadata(); $data = idx($metadata, 'attachment-data'); $old_phid = idx($metadata, 'old:binary-phid'); $new_phid = idx($metadata, 'new:binary-phid'); } else { $vs_changeset = id(new DifferentialChangeset())->load($vs); $old_phid = null; $new_phid = null; // TODO: This is spooky, see D6851 if ($vs_changeset) { $vs_metadata = $vs_changeset->getMetadata(); $old_phid = idx($vs_metadata, 'new:binary-phid'); } $changeset = id(new DifferentialChangeset())->load($id); if ($changeset) { $metadata = $changeset->getMetadata(); $new_phid = idx($metadata, 'new:binary-phid'); } } if ($old_phid || $new_phid) { // grab the files, (micro) optimization for 1 query not 2 $file_phids = array(); if ($old_phid) { $file_phids[] = $old_phid; } if ($new_phid) { $file_phids[] = $new_phid; } - // TODO: (T603) Probably fine to use omnipotent viewer here? - $files = id(new PhabricatorFile())->loadAllWhere( - 'phid IN (%Ls)', - $file_phids); + $files = id(new PhabricatorFileQuery()) + ->setViewer($this->getUser()) + ->withPHIDs($file_phids) + ->execute(); foreach ($files as $file) { if (empty($file)) { continue; } if ($file->getPHID() == $old_phid) { $old = $file; } else if ($file->getPHID() == $new_phid) { $new = $file; } } } $renderer->attachOldFile($old); $renderer->attachNewFile($new); return $renderer->renderFileChange($old, $new, $id, $vs); case DifferentialChangeType::FILE_DIRECTORY: case DifferentialChangeType::FILE_BINARY: $output = $renderer->renderChangesetTable(null); return $output; } if ($this->originalLeft && $this->originalRight) { list($highlight_old, $highlight_new) = $this->diffOriginals(); $highlight_old = array_flip($highlight_old); $highlight_new = array_flip($highlight_new); $renderer ->setHighlightOld($highlight_old) ->setHighlightNew($highlight_new); } $renderer ->setOriginalOld($this->originalLeft) ->setOriginalNew($this->originalRight); if ($range_start === null) { $range_start = 0; } if ($range_len === null) { $range_len = $rows; } $range_len = min($range_len, $rows - $range_start); list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths( $mask_force, $feedback_mask, $range_start, $range_len); $renderer ->setGaps($gaps) ->setMask($mask) ->setDepths($depths); $html = $renderer->renderTextChange( $range_start, $range_len, $rows); return $renderer->renderChangesetTable($html); } /** * This function calculates a lot of stuff we need to know to display * the diff: * * Gaps - compute gaps in the visible display diff, where we will render * "Show more context" spacers. If a gap is smaller than the context size, * we just display it. Otherwise, we record it into $gaps and will render a * "show more context" element instead of diff text below. A given $gap * is a tuple of $gap_line_number_start and $gap_length. * * Mask - compute the actual lines that need to be shown (because they * are near changes lines, near inline comments, or the request has * explicitly asked for them, i.e. resulting from the user clicking * "show more"). The $mask returned is a sparesely populated dictionary * of $visible_line_number => true. * * Depths - compute how indented any given line is. The $depths returned * is a sparesely populated dictionary of $visible_line_number => $depth. * * This function also has the side effect of modifying member variable * new such that tabs are normalized to spaces for each line of the diff. * * @return array($gaps, $mask, $depths) */ private function calculateGapsMaskAndDepths($mask_force, $feedback_mask, $range_start, $range_len) { // Calculate gaps and mask first $gaps = array(); $gap_start = 0; $in_gap = false; $base_mask = $this->visible + $mask_force + $feedback_mask; $base_mask[$range_start + $range_len] = true; for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) { if (isset($base_mask[$ii])) { if ($in_gap) { $gap_length = $ii - $gap_start; if ($gap_length <= self::LINES_CONTEXT) { for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) { $base_mask[$jj] = true; } } else { $gaps[] = array($gap_start, $gap_length); } $in_gap = false; } } else { if (!$in_gap) { $gap_start = $ii; $in_gap = true; } } } $gaps = array_reverse($gaps); $mask = $base_mask; // Time to calculate depth. // We need to go backwards to properly indent whitespace in this code: // // 0: class C { // 1: // 1: function f() { // 2: // 2: return; // 1: // 1: } // 0: // 0: } // $depths = array(); $last_depth = 0; $range_end = $range_start + $range_len; if (!isset($this->new[$range_end])) { $range_end--; } for ($ii = $range_end; $ii >= $range_start; $ii--) { // We need to expand tabs to process mixed indenting and to round // correctly later. $line = str_replace("\t", ' ', $this->new[$ii]['text']); $trimmed = ltrim($line); if ($trimmed != '') { // We round down to flatten "/**" and " *". $last_depth = floor((strlen($line) - strlen($trimmed)) / 2); } $depths[$ii] = $last_depth; } return array($gaps, $mask, $depths); } /** * Determine if an inline comment will appear on the rendered diff, * taking into consideration which halves of which changesets will actually * be shown. * * @param PhabricatorInlineCommentInterface Comment to test for visibility. * @return bool True if the comment is visible on the rendered diff. */ private function isCommentVisibleOnRenderedDiff( PhabricatorInlineCommentInterface $comment) { $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } if ($changeset_id == $this->leftSideChangesetID && $is_new == $this->leftSideAttachesToNewFile) { return true; } return false; } /** * Determine if a comment will appear on the right side of the display diff. * Note that the comment must appear somewhere on the rendered changeset, as * per isCommentVisibleOnRenderedDiff(). * * @param PhabricatorInlineCommentInterface Comment to test for display * location. * @return bool True for right, false for left. */ private function isCommentOnRightSideWhenDisplayed( PhabricatorInlineCommentInterface $comment) { if (!$this->isCommentVisibleOnRenderedDiff($comment)) { throw new Exception('Comment is not visible on changeset!'); } $changeset_id = $comment->getChangesetID(); $is_new = $comment->getIsNewFile(); if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { return true; } return false; } /** * Parse the 'range' specification that this class and the client-side JS * emit to indicate that a user clicked "Show more..." on a diff. Generally, * use is something like this: * * $spec = $request->getStr('range'); * $parsed = DifferentialChangesetParser::parseRangeSpecification($spec); * list($start, $end, $mask) = $parsed; * $parser->render($start, $end, $mask); * * @param string Range specification, indicating the range of the diff that * should be rendered. * @return tuple List of suitable for passing to * @{method:render}. */ public static function parseRangeSpecification($spec) { $range_s = null; $range_e = null; $mask = array(); if ($spec) { $match = null; if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) { $range_s = (int)$match[1]; $range_e = (int)$match[2]; if (count($match) > 3) { $start = (int)$match[3]; $len = (int)$match[4]; for ($ii = $start; $ii < $start + $len; $ii++) { $mask[$ii] = true; } } } } return array($range_s, $range_e, $mask); } /** * Render "modified coverage" information; test coverage on modified lines. * This synthesizes diff information with unit test information into a useful * indicator of how well tested a change is. */ public function renderModifiedCoverage() { $na = phutil_tag('em', array(), '-'); $coverage = $this->getCoverage(); if (!$coverage) { return $na; } $covered = 0; $not_covered = 0; foreach ($this->new as $k => $new) { if (!$new['line']) { continue; } if (!$new['type']) { continue; } if (empty($coverage[$new['line'] - 1])) { continue; } switch ($coverage[$new['line'] - 1]) { case 'C': $covered++; break; case 'U': $not_covered++; break; } } if (!$covered && !$not_covered) { return $na; } return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); } public function detectCopiedCode( array $changesets, $min_width = 30, $min_lines = 3) { assert_instances_of($changesets, 'DifferentialChangeset'); $map = array(); $files = array(); $types = array(); foreach ($changesets as $changeset) { $file = $changeset->getFilename(); foreach ($changeset->getHunks() as $hunk) { $line = $hunk->getOldOffset(); foreach (explode("\n", $hunk->getChanges()) as $code) { $type = (isset($code[0]) ? $code[0] : ''); if ($type == '-' || $type == ' ') { $code = trim(substr($code, 1)); $files[$file][$line] = $code; $types[$file][$line] = $type; if (strlen($code) >= $min_width) { $map[$code][] = array($file, $line); } $line++; } } } } foreach ($changesets as $changeset) { $copies = array(); foreach ($changeset->getHunks() as $hunk) { $added = array_map('trim', $hunk->getAddedLines()); for (reset($added); list($line, $code) = each($added); ) { if (isset($map[$code])) { // We found a long matching line. if (count($map[$code]) > 16) { // If there are a large number of identical lines in this diff, // don't try to figure out where this block came from: the // analysis is O(N^2), since we need to compare every line // against every other line. Even if we arrive at a result, it // is unlikely to be meaningful. See T5041. continue 2; } $best_length = 0; foreach ($map[$code] as $val) { // Explore all candidates. list($file, $orig_line) = $val; $length = 1; // Search also backwards for short lines. foreach (array(-1, 1) as $direction) { $offset = $direction; while (!isset($copies[$line + $offset]) && isset($added[$line + $offset]) && idx($files[$file], $orig_line + $offset) === $added[$line + $offset]) { $length++; $offset += $direction; } } if ($length > $best_length || ($length == $best_length && // Prefer moves. idx($types[$file], $orig_line) == '-')) { $best_length = $length; // ($offset - 1) contains number of forward matching lines. $best_offset = $offset - 1; $best_file = $file; $best_line = $orig_line; } } $file = ($best_file == $changeset->getFilename() ? '' : $best_file); for ($i = $best_length; $i--; ) { $type = idx($types[$best_file], $best_line + $best_offset - $i); $copies[$line + $best_offset - $i] = ($best_length < $min_lines ? array() // Ignore short blocks. : array($file, $best_line + $best_offset - $i, $type)); } for ($i = 0; $i < $best_offset; $i++) { next($added); } } } } $copies = array_filter($copies); if ($copies) { $metadata = $changeset->getMetadata(); $metadata['copy:lines'] = $copies; $changeset->setMetadata($metadata); } } return $changesets; } } diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index bddbe46d7..f3790e4b7 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -1,458 +1,459 @@ getChangeset(); $change = $changeset->getChangeType(); $file = $changeset->getFileType(); $messages = array(); $none = hsprintf(''); switch ($change) { case DifferentialChangeType::TYPE_ADD: switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was added.', $none); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was added.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was added.', $none); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was added.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was added.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was added.', $none); break; } break; case DifferentialChangeType::TYPE_DELETE: switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was deleted.', $none); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was deleted.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was deleted.', $none); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was deleted.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was deleted.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was deleted.', $none); break; } break; case DifferentialChangeType::TYPE_MOVE_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was moved from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was moved from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was moved from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was moved from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was moved from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was moved from %s.', $from); break; } break; case DifferentialChangeType::TYPE_COPY_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was copied from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was copied from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was copied from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was copied from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was copied from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was copied from %s.', $from); break; } break; case DifferentialChangeType::TYPE_MOVE_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was moved to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was moved to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was moved to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_COPY_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was copied to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_MULTICOPY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was deleted after being copied to %s.', $paths); break; } break; default: switch ($file) { case DifferentialChangeType::FILE_TEXT: // This is the default case, so we only render this header if // forced to since it's not very useful. if ($force) { $messages[] = pht('This file was not modified.'); } break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This is an image.'); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This is a directory.'); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This is a binary file.'); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This is a symlink.'); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This is a submodule.'); break; } break; } // If this is a text file with at least one hunk, we may have converted // the text encoding. In this case, show a note. $show_encoding = ($file == DifferentialChangeType::FILE_TEXT) && ($changeset->getHunks()); if ($show_encoding) { $encoding = $this->getOriginalCharacterEncoding(); if ($encoding != 'utf8') { if ($encoding) { $messages[] = pht( 'This file was converted from %s for display.', phutil_tag('strong', array(), $encoding)); } else { $messages[] = pht( 'This file uses an unknown character encoding.'); } } } if (!$messages) { return null; } foreach ($messages as $key => $message) { $messages[$key] = phutil_tag('li', array(), $message); } return phutil_tag( 'ul', array( 'class' => 'differential-meta-notice', ), $messages); } protected function renderPropertyChangeHeader() { $changeset = $this->getChangeset(); list($old, $new) = $this->getChangesetProperties($changeset); // If we don't have any property changes, don't render this table. if ($old === $new) { return null; } $keys = array_keys($old + $new); sort($keys); $key_map = array( 'unix:filemode' => pht('File Mode'), 'file:dimensions' => pht('Image Dimensions'), 'file:mimetype' => pht('MIME Type'), 'file:size' => pht('File Size'), ); $rows = array(); foreach ($keys as $key) { $oval = idx($old, $key); $nval = idx($new, $key); if ($oval !== $nval) { if ($oval === null) { $oval = phutil_tag('em', array(), 'null'); } else { $oval = phutil_escape_html_newlines($oval); } if ($nval === null) { $nval = phutil_tag('em', array(), 'null'); } else { $nval = phutil_escape_html_newlines($nval); } $readable_key = idx($key_map, $key, $key); $row = array( $readable_key, $oval, $nval, ); $rows[] = $row; } } $classes = array('', 'oval', 'nval'); $headers = array( pht('Property'), pht('Old Value'), pht('New Value'), ); $table = id(new AphrontTableView($rows)) ->setHeaders($headers) ->setColumnClasses($classes); return phutil_tag( 'div', array( 'class' => 'differential-property-table', ), $table); } public function renderShield($message, $force = 'default') { $end = count($this->getOldLines()); $reference = $this->getRenderingReference(); if ($force !== 'text' && $force !== 'whitespace' && $force !== 'none' && $force !== 'default') { throw new Exception("Invalid 'force' parameter '{$force}'!"); } $range = "0-{$end}"; if ($force == 'text') { // If we're forcing text, force the whole file to be rendered. $range = "{$range}/0-{$end}"; } $meta = array( 'ref' => $reference, 'range' => $range, ); if ($force == 'whitespace') { $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; } $content = array(); $content[] = $message; if ($force !== 'none') { $content[] = ' '; $content[] = javelin_tag( 'a', array( 'mustcapture' => true, 'sigil' => 'show-more', 'class' => 'complete', 'href' => '#', 'meta' => $meta, ), pht('Show File Contents')); } return $this->wrapChangeInTable( javelin_tag( 'tr', array( 'sigil' => 'context-target', ), phutil_tag( 'td', array( 'class' => 'differential-shield', 'colspan' => 6, ), $content))); } abstract protected function renderColgroup(); protected function wrapChangeInTable($content) { if (!$content) { return null; } return javelin_tag( 'table', array( 'class' => 'differential-diff remarkup-code PhabricatorMonospaced', 'sigil' => 'differential-diff', ), array( $this->renderColgroup(), $content, )); } protected function renderInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { return $this->buildInlineComment($comment, $on_right)->render(); } protected function buildInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { $user = $this->getUser(); $edit = $user && ($comment->getAuthorPHID() == $user->getPHID()) && - ($comment->isDraft()); - $allow_reply = (bool)$user; + ($comment->isDraft()) + && $this->getShowEditAndReplyLinks(); + $allow_reply = (bool)$user && $this->getShowEditAndReplyLinks(); return id(new DifferentialInlineCommentView()) ->setInlineComment($comment) ->setOnRight($on_right) ->setHandles($this->getHandles()) ->setMarkupEngine($this->getMarkupEngine()) ->setEditable($edit) ->setAllowReply($allow_reply); } } diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index 8327a4103..be440b47e 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -1,575 +1,584 @@ showEditAndReplyLinks = $bool; + return $this; + } + public function getShowEditAndReplyLinks() { + return $this->showEditAndReplyLinks; + } + public function setOriginalCharacterEncoding($original_character_encoding) { $this->originalCharacterEncoding = $original_character_encoding; return $this; } public function getOriginalCharacterEncoding() { return $this->originalCharacterEncoding; } public function setDepths($depths) { $this->depths = $depths; return $this; } protected function getDepths() { return $this->depths; } public function setMask($mask) { $this->mask = $mask; return $this; } protected function getMask() { return $this->mask; } public function setGaps($gaps) { $this->gaps = $gaps; return $this; } protected function getGaps() { return $this->gaps; } public function attachOldFile(PhabricatorFile $old = null) { $this->oldFile = $old; return $this; } public function getOldFile() { if ($this->oldFile === false) { throw new PhabricatorDataNotAttachedException($this); } return $this->oldFile; } public function hasOldFile() { return (bool)$this->oldFile; } public function attachNewFile(PhabricatorFile $new = null) { $this->newFile = $new; return $this; } public function getNewFile() { if ($this->newFile === false) { throw new PhabricatorDataNotAttachedException($this); } return $this->newFile; } public function hasNewFile() { return (bool)$this->newFile; } public function setOriginalNew($original_new) { $this->originalNew = $original_new; return $this; } protected function getOriginalNew() { return $this->originalNew; } public function setOriginalOld($original_old) { $this->originalOld = $original_old; return $this; } protected function getOriginalOld() { return $this->originalOld; } public function setNewRender($new_render) { $this->newRender = $new_render; return $this; } protected function getNewRender() { return $this->newRender; } public function setOldRender($old_render) { $this->oldRender = $old_render; return $this; } protected function getOldRender() { return $this->oldRender; } public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { $this->markupEngine = $markup_engine; return $this; } public function getMarkupEngine() { return $this->markupEngine; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } protected function getHandles() { return $this->handles; } public function setCodeCoverage($code_coverage) { $this->codeCoverage = $code_coverage; return $this; } protected function getCodeCoverage() { return $this->codeCoverage; } public function 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 setNewLines(array $new_lines) { $this->newLines = $new_lines; return $this; } protected function getNewLines() { return $this->newLines; } public function setOldLines(array $old_lines) { $this->oldLines = $old_lines; return $this; } protected function getOldLines() { return $this->oldLines; } public function setHunkStartLines(array $hunk_start_lines) { $this->hunkStartLines = $hunk_start_lines; return $this; } protected function getHunkStartLines() { return $this->hunkStartLines; } 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; } public function setIsTopLevel($is) { $this->isTopLevel = $is; return $this; } private function getIsTopLevel() { return $this->isTopLevel; } final public function renderChangesetTable($content) { $props = null; if ($this->shouldRenderPropertyChangeHeader()) { $props = $this->renderPropertyChangeHeader(); } $notice = null; if ($this->getIsTopLevel()) { $force = (!$content && !$props); $notice = $this->renderChangeTypeHeader($force); } $result = $notice.$props.$content; // TODO: Let the user customize their tab width / display style. // TODO: We should possibly post-process "\r" as well. // TODO: Both these steps should happen earlier. $result = str_replace("\t", ' ', $result); return phutil_safe_html($result); } abstract public function isOneUpRenderer(); abstract public function renderTextChange( $range_start, $range_len, $rows); abstract public function renderFileChange( $old = null, $new = null, $id = 0, $vs = 0); abstract protected function renderChangeTypeHeader($force); protected function didRenderChangesetTableContents($contents) { return $contents; } /** * Render a "shield" over the diff, with a message like "This file is * generated and does not need to be reviewed." or "This file was completely * deleted." This UI element hides unimportant text so the reviewer doesn't * need to scroll past it. * * The shield includes a link to view the underlying content. This link * may force certain rendering modes when the link is clicked: * * - `"default"`: Render the diff normally, as though it was not * shielded. This is the default and appropriate if the underlying * diff is a normal change, but was hidden for reasons of not being * important (e.g., generated code). * - `"text"`: Force the text to be shown. This is probably only relevant * when a file is not changed. * - `"whitespace"`: Force the text to be shown, and the diff to be * rendered with all whitespace shown. This is probably only relevant * when a file is changed only by altering whitespace. * - `"none"`: Don't show the link (e.g., text not available). * * @param string Message explaining why the diff is hidden. * @param string|null Force mode, see above. * @return string Shield markup. */ abstract public function renderShield($message, $force = 'default'); abstract protected function renderPropertyChangeHeader(); protected function buildPrimitives($range_start, $range_len) { $primitives = array(); $hunk_starts = $this->getHunkStartLines(); $mask = $this->getMask(); $gaps = $this->getGaps(); $old = $this->getOldLines(); $new = $this->getNewLines(); $old_render = $this->getOldRender(); $new_render = $this->getNewRender(); $old_comments = $this->getOldComments(); $new_comments = $this->getNewComments(); $size = count($old); for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { list($top, $len) = array_pop($gaps); $primitives[] = array( 'type' => 'context', 'top' => $top, 'len' => $len, ); $ii += ($len - 1); continue; } $ospec = array( 'type' => 'old', 'htype' => null, 'cursor' => $ii, 'line' => null, 'oline' => null, 'render' => null, ); $nspec = array( 'type' => 'new', 'htype' => null, 'cursor' => $ii, 'line' => null, 'oline' => null, 'render' => null, 'copy' => null, 'coverage' => null, ); if (isset($old[$ii])) { $ospec['line'] = (int)$old[$ii]['line']; $nspec['oline'] = (int)$old[$ii]['line']; $ospec['htype'] = $old[$ii]['type']; if (isset($old_render[$ii])) { $ospec['render'] = $old_render[$ii]; } } if (isset($new[$ii])) { $nspec['line'] = (int)$new[$ii]['line']; $ospec['oline'] = (int)$new[$ii]['line']; $nspec['htype'] = $new[$ii]['type']; if (isset($new_render[$ii])) { $nspec['render'] = $new_render[$ii]; } } if (isset($hunk_starts[$ospec['line']])) { $primitives[] = array( 'type' => 'no-context', ); } $primitives[] = $ospec; $primitives[] = $nspec; if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) { foreach ($old_comments[$ospec['line']] as $comment) { $primitives[] = array( 'type' => 'inline', 'comment' => $comment, 'right' => false, ); } } if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) { foreach ($new_comments[$nspec['line']] as $comment) { $primitives[] = array( 'type' => 'inline', 'comment' => $comment, 'right' => true, ); } } if ($hunk_starts && ($ii == $size - 1)) { $primitives[] = array( 'type' => 'no-context', ); } } if ($this->isOneUpRenderer()) { $primitives = $this->processPrimitivesForOneUp($primitives); } return $primitives; } private function processPrimitivesForOneUp(array $primitives) { // Primitives come out of buildPrimitives() in two-up format, because it // is the most general, flexible format. To put them into one-up format, // we need to filter and reorder them. In particular: // // - We discard unchanged lines in the old file; in one-up format, we // render them only once. // - We group contiguous blocks of old-modified and new-modified lines, so // they render in "block of old, block of new" order instead of // alternating old and new lines. $out = array(); $old_buf = array(); $new_buf = array(); foreach ($primitives as $primitive) { $type = $primitive['type']; if ($type == 'old') { if (!$primitive['htype']) { // This is a line which appears in both the old file and the new // file, or the spacer corresponding to a line added in the new file. // Ignore it when rendering a one-up diff. continue; } if ($new_buf) { $out[] = $new_buf; $new_buf = array(); } $old_buf[] = $primitive; } else if ($type == 'new') { if ($primitive['line'] === null) { // This is an empty spacer corresponding to a line removed from the // old file. Ignore it when rendering a one-up diff. continue; } if ($old_buf) { $out[] = $old_buf; $old_buf = array(); } $new_buf[] = $primitive; } else if ($type == 'context' || $type == 'no-context') { $out[] = $old_buf; $out[] = $new_buf; $old_buf = array(); $new_buf = array(); $out[] = array($primitive); } else if ($type == 'inline') { $out[] = $old_buf; $out[] = $new_buf; $old_buf = array(); $new_buf = array(); $out[] = array($primitive); } else { throw new Exception("Unknown primitive type '{$primitive}'!"); } } $out[] = $old_buf; $out[] = $new_buf; $out = array_mergev($out); return $out; } protected function getChangesetProperties($changeset) { $old = $changeset->getOldProperties(); $new = $changeset->getNewProperties(); // When adding files, don't show the uninteresting 644 filemode change. if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD && $new == array('unix:filemode' => '100644')) { unset($new['unix:filemode']); } // Likewise when removing files. if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE && $old == array('unix:filemode' => '100644')) { unset($old['unix:filemode']); } if ($this->hasOldFile()) { $file = $this->getOldFile(); if ($file->getImageWidth()) { $dimensions = $file->getImageWidth().'x'.$file->getImageHeight(); $old['file:dimensions'] = $dimensions; } $old['file:mimetype'] = $file->getMimeType(); $old['file:size'] = phutil_format_bytes($file->getByteSize()); } if ($this->hasNewFile()) { $file = $this->getNewFile(); if ($file->getImageWidth()) { $dimensions = $file->getImageWidth().'x'.$file->getImageHeight(); $new['file:dimensions'] = $dimensions; } $new['file:mimetype'] = $file->getMimeType(); $new['file:size'] = phutil_format_bytes($file->getByteSize()); } return array($old, $new); } } diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php index 41f515dfb..b979d7f9b 100644 --- a/src/applications/phriction/controller/PhrictionDiffController.php +++ b/src/applications/phriction/controller/PhrictionDiffController.php @@ -1,293 +1,294 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needContent(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $current = $document->getContent(); $l = $request->getInt('l'); $r = $request->getInt('r'); $ref = $request->getStr('ref'); if ($ref) { list($l, $r) = explode(',', $ref); } $content = id(new PhrictionContent())->loadAllWhere( 'documentID = %d AND version IN (%Ld)', $document->getID(), array($l, $r)); $content = mpull($content, null, 'getVersion'); $content_l = idx($content, $l, null); $content_r = idx($content, $r, null); if (!$content_l || !$content_r) { return new Aphront404Response(); } $text_l = $content_l->getContent(); $text_r = $content_r->getContent(); $text_l = phutil_utf8_hard_wrap($text_l, 80); $text_l = implode("\n", $text_l); $text_r = phutil_utf8_hard_wrap($text_r, 80); $text_r = implode("\n", $text_r); $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($text_l, $text_r); $changeset->setOldProperties( array( 'Title' => $content_l->getTitle(), )); $changeset->setNewProperties( array( 'Title' => $content_r->getTitle(), )); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $parser = new DifferentialChangesetParser(); + $parser->setUser($user); $parser->setChangeset($changeset); $parser->setRenderingReference("{$l},{$r}"); $parser->setWhitespaceMode($whitespace_mode); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->process(); $parser->setMarkupEngine($engine); $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); if ($request->isAjax()) { return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($output); } require_celerity_resource('differential-changeset-view-css'); require_celerity_resource('syntax-highlighting-css'); require_celerity_resource('phriction-document-css'); Javelin::initBehavior('differential-show-more', array( 'uri' => '/phriction/diff/'.$document->getID().'/', 'whitespace' => $whitespace_mode, )); $slug = $document->getSlug(); $revert_l = $this->renderRevertButton($content_l, $current); $revert_r = $this->renderRevertButton($content_r, $current); $crumbs = $this->buildApplicationCrumbs(); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $crumbs->addTextCrumb( pht('History'), PhrictionDocument::getSlugURI($slug, 'history')); $title = pht('Version %s vs %s', $l, $r); $header = id(new PHUIHeaderView()) ->setHeader($title); $crumbs->addTextCrumb($title, $request->getRequestURI()); $comparison_table = $this->renderComparisonTable( array( $content_r, $content_l, )); $navigation_table = null; if ($l + 1 == $r) { $nav_l = ($l > 1); $nav_r = ($r != $current->getVersion()); $uri = $request->getRequestURI(); if ($nav_l) { $link_l = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1), 'class' => 'button', ), pht("\xC2\xAB Previous Change")); } else { $link_l = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button grey disabled', ), pht('Original Change')); } $link_r = null; if ($nav_r) { $link_r = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1), 'class' => 'button', ), pht("Next Change \xC2\xBB")); } else { $link_r = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button grey disabled', ), pht('Most Recent Change')); } $navigation_table = phutil_tag( 'table', array('class' => 'phriction-history-nav-table'), phutil_tag('tr', array(), array( phutil_tag('td', array('class' => 'nav-prev'), $link_l), phutil_tag('td', array('class' => 'nav-next'), $link_r), ))); } $output = hsprintf( '
'. '%s%s'. ''. ''. '
%s%s
'. '%s'. '
', $comparison_table->render(), $navigation_table, $revert_l, $revert_r, $output); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($output); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => pht('Document History'), )); } private function renderRevertButton( PhrictionContent $content, PhrictionContent $current) { $document_id = $content->getDocumentID(); $version = $content->getVersion(); $hidden_statuses = array( PhrictionChangeType::CHANGE_DELETE => true, // Silly PhrictionChangeType::CHANGE_MOVE_AWAY => true, // Plain silly PhrictionChangeType::CHANGE_STUB => true, // Utterly silly ); if (isset($hidden_statuses[$content->getChangeType()])) { // Don't show an edit/revert button for changes which deleted, moved or // stubbed the content since it's silly. return null; } if ($content->getID() == $current->getID()) { return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/', 'class' => 'button grey', ), pht('Edit Current Version')); } return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/?revert='.$version, 'class' => 'button grey', ), pht('Revert to Version %s...', $version)); } private function renderComparisonTable(array $content) { assert_instances_of($content, 'PhrictionContent'); $user = $this->getRequest()->getUser(); $phids = mpull($content, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $list = new PHUIObjectItemListView(); $list->setFlush(true); $first = true; foreach ($content as $c) { $author = $handles[$c->getAuthorPHID()]->renderLink(); $item = id(new PHUIObjectItemView()) ->setHeader(pht('%s by %s, %s', PhrictionChangeType::getChangeTypeLabel($c->getChangeType()), $author, pht('Version %s', $c->getVersion()))) ->addAttribute(pht('%s %s', phabricator_date($c->getDateCreated(), $user), phabricator_time($c->getDateCreated(), $user))); if ($c->getDescription()) { $item->addAttribute($c->getDescription()); } if ($first == true) { $item->setBarColor('green'); $first = false; } else { $item->setBarColor('red'); } $list->addItem($item); } return $list; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php index 55fe586bc..0e8465223 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionTextDiffDetailView.php @@ -1,56 +1,57 @@ newText = $new_text; return $this; } public function setOldText($old_text) { $this->oldText = $old_text; return $this; } public function render() { $old = $this->oldText; $new = $this->newText; // TODO: On mobile, or perhaps by default, we should switch to 1-up once // that is built. if (strlen($old)) { $old = phutil_utf8_hard_wrap($old, 80); $old = implode("\n", $old)."\n"; } if (strlen($new)) { $new = phutil_utf8_hard_wrap($new, 80); $new = implode("\n", $new)."\n"; } try { $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($old, $new); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $markup_engine = new PhabricatorMarkupEngine(); $markup_engine->setViewer($this->getUser()); $parser = new DifferentialChangesetParser(); + $parser->setUser($this->getUser()); $parser->setChangeset($changeset); $parser->setMarkupEngine($markup_engine); $parser->setWhitespaceMode($whitespace_mode); return $parser->render(0, PHP_INT_MAX, array()); } catch (Exception $ex) { return $ex->getMessage(); } } }