Path: blob/master/src/applications/differential/parser/DifferentialChangesetParser.php
12256 views
<?php12final class DifferentialChangesetParser extends Phobject {34const HIGHLIGHT_BYTE_LIMIT = 262144;56protected $visible = array();7protected $new = array();8protected $old = array();9protected $intra = array();10protected $depthOnlyLines = array();11protected $newRender = null;12protected $oldRender = null;1314protected $filename = null;15protected $hunkStartLines = array();1617protected $comments = array();18protected $specialAttributes = array();1920protected $changeset;2122protected $renderCacheKey = null;2324private $handles = array();25private $user;2627private $leftSideChangesetID;28private $leftSideAttachesToNewFile;2930private $rightSideChangesetID;31private $rightSideAttachesToNewFile;3233private $originalLeft;34private $originalRight;3536private $renderingReference;37private $isSubparser;3839private $isTopLevel;4041private $coverage;42private $markupEngine;43private $highlightErrors;44private $disableCache;45private $renderer;46private $highlightingDisabled;47private $showEditAndReplyLinks = true;48private $canMarkDone;49private $objectOwnerPHID;50private $offsetMode;5152private $rangeStart;53private $rangeEnd;54private $mask;55private $linesOfContext = 8;5657private $highlightEngine;58private $viewer;5960private $viewState;61private $availableDocumentEngines;6263public function setRange($start, $end) {64$this->rangeStart = $start;65$this->rangeEnd = $end;66return $this;67}6869public function setMask(array $mask) {70$this->mask = $mask;71return $this;72}7374public function renderChangeset() {75return $this->render($this->rangeStart, $this->rangeEnd, $this->mask);76}7778public function setShowEditAndReplyLinks($bool) {79$this->showEditAndReplyLinks = $bool;80return $this;81}8283public function getShowEditAndReplyLinks() {84return $this->showEditAndReplyLinks;85}8687public function setViewState(PhabricatorChangesetViewState $view_state) {88$this->viewState = $view_state;89return $this;90}9192public function getViewState() {93return $this->viewState;94}9596public function setRenderer(DifferentialChangesetRenderer $renderer) {97$this->renderer = $renderer;98return $this;99}100101public function getRenderer() {102return $this->renderer;103}104105public function setDisableCache($disable_cache) {106$this->disableCache = $disable_cache;107return $this;108}109110public function getDisableCache() {111return $this->disableCache;112}113114public function setCanMarkDone($can_mark_done) {115$this->canMarkDone = $can_mark_done;116return $this;117}118119public function getCanMarkDone() {120return $this->canMarkDone;121}122123public function setObjectOwnerPHID($phid) {124$this->objectOwnerPHID = $phid;125return $this;126}127128public function getObjectOwnerPHID() {129return $this->objectOwnerPHID;130}131132public function setOffsetMode($offset_mode) {133$this->offsetMode = $offset_mode;134return $this;135}136137public function getOffsetMode() {138return $this->offsetMode;139}140141public function setViewer(PhabricatorUser $viewer) {142$this->viewer = $viewer;143return $this;144}145146public function getViewer() {147return $this->viewer;148}149150private function newRenderer() {151$viewer = $this->getViewer();152$viewstate = $this->getViewstate();153154$renderer_key = $viewstate->getRendererKey();155156if ($renderer_key === null) {157$is_unified = $viewer->compareUserSetting(158PhabricatorUnifiedDiffsSetting::SETTINGKEY,159PhabricatorUnifiedDiffsSetting::VALUE_ALWAYS_UNIFIED);160161if ($is_unified) {162$renderer_key = '1up';163} else {164$renderer_key = $viewstate->getDefaultDeviceRendererKey();165}166}167168switch ($renderer_key) {169case '1up':170$renderer = new DifferentialChangesetOneUpRenderer();171break;172default:173$renderer = new DifferentialChangesetTwoUpRenderer();174break;175}176177return $renderer;178}179180const CACHE_VERSION = 14;181const CACHE_MAX_SIZE = 8e6;182183const ATTR_GENERATED = 'attr:generated';184const ATTR_DELETED = 'attr:deleted';185const ATTR_UNCHANGED = 'attr:unchanged';186const ATTR_MOVEAWAY = 'attr:moveaway';187188public function setOldLines(array $lines) {189$this->old = $lines;190return $this;191}192193public function setNewLines(array $lines) {194$this->new = $lines;195return $this;196}197198public function setSpecialAttributes(array $attributes) {199$this->specialAttributes = $attributes;200return $this;201}202203public function setIntraLineDiffs(array $diffs) {204$this->intra = $diffs;205return $this;206}207208public function setDepthOnlyLines(array $lines) {209$this->depthOnlyLines = $lines;210return $this;211}212213public function getDepthOnlyLines() {214return $this->depthOnlyLines;215}216217public function setVisibleLinesMask(array $mask) {218$this->visible = $mask;219return $this;220}221222public function setLinesOfContext($lines_of_context) {223$this->linesOfContext = $lines_of_context;224return $this;225}226227public function getLinesOfContext() {228return $this->linesOfContext;229}230231232/**233* Configure which Changeset comments added to the right side of the visible234* diff will be attached to. The ID must be the ID of a real Differential235* Changeset.236*237* The complexity here is that we may show an arbitrary side of an arbitrary238* changeset as either the left or right part of a diff. This method allows239* the left and right halves of the displayed diff to be correctly mapped to240* storage changesets.241*242* @param id The Differential Changeset ID that comments added to the right243* side of the visible diff should be attached to.244* @param bool If true, attach new comments to the right side of the storage245* changeset. Note that this may be false, if the left side of246* some storage changeset is being shown as the right side of247* a display diff.248* @return this249*/250public function setRightSideCommentMapping($id, $is_new) {251$this->rightSideChangesetID = $id;252$this->rightSideAttachesToNewFile = $is_new;253return $this;254}255256/**257* See setRightSideCommentMapping(), but this sets information for the left258* side of the display diff.259*/260public function setLeftSideCommentMapping($id, $is_new) {261$this->leftSideChangesetID = $id;262$this->leftSideAttachesToNewFile = $is_new;263return $this;264}265266public function setOriginals(267DifferentialChangeset $left,268DifferentialChangeset $right) {269270$this->originalLeft = $left;271$this->originalRight = $right;272return $this;273}274275public function diffOriginals() {276$engine = new PhabricatorDifferenceEngine();277$changeset = $engine->generateChangesetFromFileContent(278implode('', mpull($this->originalLeft->getHunks(), 'getChanges')),279implode('', mpull($this->originalRight->getHunks(), 'getChanges')));280281$parser = new DifferentialHunkParser();282283return $parser->parseHunksForHighlightMasks(284$changeset->getHunks(),285$this->originalLeft->getHunks(),286$this->originalRight->getHunks());287}288289/**290* Set a key for identifying this changeset in the render cache. If set, the291* parser will attempt to use the changeset render cache, which can improve292* performance for frequently-viewed changesets.293*294* By default, there is no render cache key and parsers do not use the cache.295* This is appropriate for rarely-viewed changesets.296*297* @param string Key for identifying this changeset in the render cache.298* @return this299*/300public function setRenderCacheKey($key) {301$this->renderCacheKey = $key;302return $this;303}304305private function getRenderCacheKey() {306return $this->renderCacheKey;307}308309public function setChangeset(DifferentialChangeset $changeset) {310$this->changeset = $changeset;311312$this->setFilename($changeset->getFilename());313314return $this;315}316317public function setRenderingReference($ref) {318$this->renderingReference = $ref;319return $this;320}321322private function getRenderingReference() {323return $this->renderingReference;324}325326public function getChangeset() {327return $this->changeset;328}329330public function setFilename($filename) {331$this->filename = $filename;332return $this;333}334335public function setHandles(array $handles) {336assert_instances_of($handles, 'PhabricatorObjectHandle');337$this->handles = $handles;338return $this;339}340341public function setMarkupEngine(PhabricatorMarkupEngine $engine) {342$this->markupEngine = $engine;343return $this;344}345346public function setCoverage($coverage) {347$this->coverage = $coverage;348return $this;349}350private function getCoverage() {351return $this->coverage;352}353354public function parseInlineComment(355PhabricatorInlineComment $comment) {356357// Parse only comments which are actually visible.358if ($this->isCommentVisibleOnRenderedDiff($comment)) {359$this->comments[] = $comment;360}361return $this;362}363364private function loadCache() {365$render_cache_key = $this->getRenderCacheKey();366if (!$render_cache_key) {367return false;368}369370$data = null;371372$changeset = new DifferentialChangeset();373$conn_r = $changeset->establishConnection('r');374$data = queryfx_one(375$conn_r,376'SELECT * FROM %T WHERE cacheIndex = %s',377DifferentialChangeset::TABLE_CACHE,378PhabricatorHash::digestForIndex($render_cache_key));379380if (!$data) {381return false;382}383384if ($data['cache'][0] == '{') {385// This is likely an old-style JSON cache which we will not be able to386// deserialize.387return false;388}389390$data = unserialize($data['cache']);391if (!is_array($data) || !$data) {392return false;393}394395foreach (self::getCacheableProperties() as $cache_key) {396if (!array_key_exists($cache_key, $data)) {397// If we're missing a cache key, assume we're looking at an old cache398// and ignore it.399return false;400}401}402403if ($data['cacheVersion'] !== self::CACHE_VERSION) {404return false;405}406407// Someone displays contents of a partially cached shielded file.408if (!isset($data['newRender']) && (!$this->isTopLevel || $this->comments)) {409return false;410}411412unset($data['cacheVersion'], $data['cacheHost']);413$cache_prop = array_select_keys($data, self::getCacheableProperties());414foreach ($cache_prop as $cache_key => $v) {415$this->$cache_key = $v;416}417418return true;419}420421protected static function getCacheableProperties() {422return array(423'visible',424'new',425'old',426'intra',427'depthOnlyLines',428'newRender',429'oldRender',430'specialAttributes',431'hunkStartLines',432'cacheVersion',433'cacheHost',434'highlightingDisabled',435);436}437438public function saveCache() {439if (PhabricatorEnv::isReadOnly()) {440return false;441}442443if ($this->highlightErrors) {444return false;445}446447$render_cache_key = $this->getRenderCacheKey();448if (!$render_cache_key) {449return false;450}451452$cache = array();453foreach (self::getCacheableProperties() as $cache_key) {454switch ($cache_key) {455case 'cacheVersion':456$cache[$cache_key] = self::CACHE_VERSION;457break;458case 'cacheHost':459$cache[$cache_key] = php_uname('n');460break;461default:462$cache[$cache_key] = $this->$cache_key;463break;464}465}466$cache = serialize($cache);467468// We don't want to waste too much space by a single changeset.469if (strlen($cache) > self::CACHE_MAX_SIZE) {470return;471}472473$changeset = new DifferentialChangeset();474$conn_w = $changeset->establishConnection('w');475476$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();477try {478queryfx(479$conn_w,480'INSERT INTO %T (cacheIndex, cache, dateCreated) VALUES (%s, %B, %d)481ON DUPLICATE KEY UPDATE cache = VALUES(cache)',482DifferentialChangeset::TABLE_CACHE,483PhabricatorHash::digestForIndex($render_cache_key),484$cache,485PhabricatorTime::getNow());486} catch (AphrontQueryException $ex) {487// Ignore these exceptions. A common cause is that the cache is488// larger than 'max_allowed_packet', in which case we're better off489// not writing it.490491// TODO: It would be nice to tailor this more narrowly.492}493unset($unguarded);494}495496private function markGenerated($new_corpus_block = '') {497$generated_guess = (strpos($new_corpus_block, '@'.'generated') !== false);498499if (!$generated_guess) {500$generated_path_regexps = PhabricatorEnv::getEnvConfig(501'differential.generated-paths');502foreach ($generated_path_regexps as $regexp) {503if (preg_match($regexp, $this->changeset->getFilename())) {504$generated_guess = true;505break;506}507}508}509510$event = new PhabricatorEvent(511PhabricatorEventType::TYPE_DIFFERENTIAL_WILLMARKGENERATED,512array(513'corpus' => $new_corpus_block,514'is_generated' => $generated_guess,515)516);517PhutilEventEngine::dispatchEvent($event);518519$generated = $event->getValue('is_generated');520521$attribute = $this->changeset->isGeneratedChangeset();522if ($attribute) {523$generated = true;524}525526$this->specialAttributes[self::ATTR_GENERATED] = $generated;527}528529public function isGenerated() {530return idx($this->specialAttributes, self::ATTR_GENERATED, false);531}532533public function isDeleted() {534return idx($this->specialAttributes, self::ATTR_DELETED, false);535}536537public function isUnchanged() {538return idx($this->specialAttributes, self::ATTR_UNCHANGED, false);539}540541public function isMoveAway() {542return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false);543}544545private function applyIntraline(&$render, $intra, $corpus) {546547foreach ($render as $key => $text) {548$result = $text;549550if (isset($intra[$key])) {551$result = PhabricatorDifferenceEngine::applyIntralineDiff(552$result,553$intra[$key]);554}555556$result = $this->adjustRenderedLineForDisplay($result);557558$render[$key] = $result;559}560}561562private function getHighlightFuture($corpus) {563$language = $this->getViewState()->getHighlightLanguage();564565if (!$language) {566$language = $this->highlightEngine->getLanguageFromFilename(567$this->filename);568569if (($language != 'txt') &&570(strlen($corpus) > self::HIGHLIGHT_BYTE_LIMIT)) {571$this->highlightingDisabled = true;572$language = 'txt';573}574}575576return $this->highlightEngine->getHighlightFuture(577$language,578$corpus);579}580581protected function processHighlightedSource($data, $result) {582583$result_lines = phutil_split_lines($result);584foreach ($data as $key => $info) {585if (!$info) {586unset($result_lines[$key]);587}588}589return $result_lines;590}591592private function tryCacheStuff() {593$changeset = $this->getChangeset();594if (!$changeset->hasSourceTextBody()) {595596// TODO: This isn't really correct (the change is not "generated"), the597// intent is just to not render a text body for Subversion directory598// changes, etc.599$this->markGenerated();600601return;602}603604$viewstate = $this->getViewState();605606$skip_cache = false;607608if ($this->disableCache) {609$skip_cache = true;610}611612$character_encoding = $viewstate->getCharacterEncoding();613if ($character_encoding !== null) {614$skip_cache = true;615}616617$highlight_language = $viewstate->getHighlightLanguage();618if ($highlight_language !== null) {619$skip_cache = true;620}621622if ($skip_cache || !$this->loadCache()) {623$this->process();624if (!$skip_cache) {625$this->saveCache();626}627}628}629630private function process() {631$changeset = $this->changeset;632633$hunk_parser = new DifferentialHunkParser();634$hunk_parser->parseHunksForLineData($changeset->getHunks());635636$this->realignDiff($changeset, $hunk_parser);637638$hunk_parser->reparseHunksForSpecialAttributes();639640$unchanged = false;641if (!$hunk_parser->getHasAnyChanges()) {642$filetype = $this->changeset->getFileType();643if ($filetype == DifferentialChangeType::FILE_TEXT ||644$filetype == DifferentialChangeType::FILE_SYMLINK) {645$unchanged = true;646}647}648649$moveaway = false;650$changetype = $this->changeset->getChangeType();651if ($changetype == DifferentialChangeType::TYPE_MOVE_AWAY) {652$moveaway = true;653}654655$this->setSpecialAttributes(array(656self::ATTR_UNCHANGED => $unchanged,657self::ATTR_DELETED => $hunk_parser->getIsDeleted(),658self::ATTR_MOVEAWAY => $moveaway,659));660661$lines_context = $this->getLinesOfContext();662663$hunk_parser->generateIntraLineDiffs();664$hunk_parser->generateVisibleLinesMask($lines_context);665666$this->setOldLines($hunk_parser->getOldLines());667$this->setNewLines($hunk_parser->getNewLines());668$this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs());669$this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines());670$this->setVisibleLinesMask($hunk_parser->getVisibleLinesMask());671$this->hunkStartLines = $hunk_parser->getHunkStartLines(672$changeset->getHunks());673674$new_corpus = $hunk_parser->getNewCorpus();675$new_corpus_block = implode('', $new_corpus);676$this->markGenerated($new_corpus_block);677678if ($this->isTopLevel &&679!$this->comments &&680($this->isGenerated() ||681$this->isUnchanged() ||682$this->isDeleted())) {683return;684}685686$old_corpus = $hunk_parser->getOldCorpus();687$old_corpus_block = implode('', $old_corpus);688$old_future = $this->getHighlightFuture($old_corpus_block);689$new_future = $this->getHighlightFuture($new_corpus_block);690$futures = array(691'old' => $old_future,692'new' => $new_future,693);694$corpus_blocks = array(695'old' => $old_corpus_block,696'new' => $new_corpus_block,697);698699$this->highlightErrors = false;700foreach (new FutureIterator($futures) as $key => $future) {701try {702try {703$highlighted = $future->resolve();704} catch (PhutilSyntaxHighlighterException $ex) {705$this->highlightErrors = true;706$highlighted = id(new PhutilDefaultSyntaxHighlighter())707->getHighlightFuture($corpus_blocks[$key])708->resolve();709}710switch ($key) {711case 'old':712$this->oldRender = $this->processHighlightedSource(713$this->old,714$highlighted);715break;716case 'new':717$this->newRender = $this->processHighlightedSource(718$this->new,719$highlighted);720break;721}722} catch (Exception $ex) {723phlog($ex);724throw $ex;725}726}727728$this->applyIntraline(729$this->oldRender,730ipull($this->intra, 0),731$old_corpus);732$this->applyIntraline(733$this->newRender,734ipull($this->intra, 1),735$new_corpus);736}737738private function shouldRenderPropertyChangeHeader($changeset) {739if (!$this->isTopLevel) {740// We render properties only at top level; otherwise we get multiple741// copies of them when a user clicks "Show More".742return false;743}744745return true;746}747748public function render(749$range_start = null,750$range_len = null,751$mask_force = array()) {752753$viewer = $this->getViewer();754755$renderer = $this->getRenderer();756if (!$renderer) {757$renderer = $this->newRenderer();758$this->setRenderer($renderer);759}760761// "Top level" renders are initial requests for the whole file, versus762// requests for a specific range generated by clicking "show more". We763// generate property changes and "shield" UI elements only for toplevel764// requests.765$this->isTopLevel = (($range_start === null) && ($range_len === null));766$this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine();767768$viewstate = $this->getViewState();769770$encoding = null;771772$character_encoding = $viewstate->getCharacterEncoding();773if ($character_encoding) {774// We are forcing this changeset to be interpreted with a specific775// character encoding, so force all the hunks into that encoding and776// propagate it to the renderer.777$encoding = $character_encoding;778foreach ($this->changeset->getHunks() as $hunk) {779$hunk->forceEncoding($character_encoding);780}781} else {782// We're just using the default, so tell the renderer what that is783// (by reading the encoding from the first hunk).784foreach ($this->changeset->getHunks() as $hunk) {785$encoding = $hunk->getDataEncoding();786break;787}788}789790$this->tryCacheStuff();791792// If we're rendering in an offset mode, treat the range numbers as line793// numbers instead of rendering offsets.794$offset_mode = $this->getOffsetMode();795if ($offset_mode) {796if ($offset_mode == 'new') {797$offset_map = $this->new;798} else {799$offset_map = $this->old;800}801802// NOTE: Inline comments use zero-based lengths. For example, a comment803// that starts and ends on line 123 has length 0. Rendering considers804// this range to have length 1. Probably both should agree, but that805// ship likely sailed long ago. Tweak things here to get the two systems806// to agree. See PHI985, where this affected mail rendering of inline807// comments left on the final line of a file.808809$range_end = $this->getOffset($offset_map, $range_start + $range_len);810$range_start = $this->getOffset($offset_map, $range_start);811$range_len = ($range_end - $range_start) + 1;812}813814$render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset);815816$rows = max(817count($this->old),818count($this->new));819820$renderer = $this->getRenderer()821->setUser($this->getViewer())822->setChangeset($this->changeset)823->setRenderPropertyChangeHeader($render_pch)824->setIsTopLevel($this->isTopLevel)825->setOldRender($this->oldRender)826->setNewRender($this->newRender)827->setHunkStartLines($this->hunkStartLines)828->setOldChangesetID($this->leftSideChangesetID)829->setNewChangesetID($this->rightSideChangesetID)830->setOldAttachesToNewFile($this->leftSideAttachesToNewFile)831->setNewAttachesToNewFile($this->rightSideAttachesToNewFile)832->setCodeCoverage($this->getCoverage())833->setRenderingReference($this->getRenderingReference())834->setHandles($this->handles)835->setOldLines($this->old)836->setNewLines($this->new)837->setOriginalCharacterEncoding($encoding)838->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks())839->setCanMarkDone($this->getCanMarkDone())840->setObjectOwnerPHID($this->getObjectOwnerPHID())841->setHighlightingDisabled($this->highlightingDisabled)842->setDepthOnlyLines($this->getDepthOnlyLines());843844if ($this->markupEngine) {845$renderer->setMarkupEngine($this->markupEngine);846}847848list($engine, $old_ref, $new_ref) = $this->newDocumentEngine();849if ($engine) {850$engine_blocks = $engine->newEngineBlocks(851$old_ref,852$new_ref);853} else {854$engine_blocks = null;855}856857$has_document_engine = ($engine_blocks !== null);858859// Remove empty comments that don't have any unsaved draft data.860PhabricatorInlineComment::loadAndAttachVersionedDrafts(861$viewer,862$this->comments);863foreach ($this->comments as $key => $comment) {864if ($comment->isVoidComment($viewer)) {865unset($this->comments[$key]);866}867}868869// See T13515. Sometimes, we collapse file content by default: for870// example, if the file is marked as containing generated code.871872// If a file has inline comments, that normally means we never collapse873// it. However, if the viewer has already collapsed all of the inlines,874// it's fine to collapse the file.875876$expanded_comments = array();877foreach ($this->comments as $comment) {878if ($comment->isHidden()) {879continue;880}881$expanded_comments[] = $comment;882}883884$collapsed_count = (count($this->comments) - count($expanded_comments));885886$shield_raw = null;887$shield_text = null;888$shield_type = null;889if ($this->isTopLevel && !$expanded_comments && !$has_document_engine) {890if ($this->isGenerated()) {891$shield_text = pht(892'This file contains generated code, which does not normally '.893'need to be reviewed.');894} else if ($this->isMoveAway()) {895// We put an empty shield on these files. Normally, they do not have896// any diff content anyway. However, if they come through `arc`, they897// may have content. We don't want to show it (it's not useful) and898// we bailed out of fully processing it earlier anyway.899900// We could show a message like "this file was moved", but we show901// that as a change header anyway, so it would be redundant. Instead,902// just render an empty shield to skip rendering the diff body.903$shield_raw = '';904} else if ($this->isUnchanged()) {905$type = 'text';906if (!$rows) {907// NOTE: Normally, diffs which don't change files do not include908// file content (for example, if you "chmod +x" a file and then909// run "git show", the file content is not available). Similarly,910// if you move a file from A to B without changing it, diffs normally911// do not show the file content. In some cases `arc` is able to912// synthetically generate content for these diffs, but for raw diffs913// we'll never have it so we need to be prepared to not render a link.914$type = 'none';915}916917$shield_type = $type;918919$type_add = DifferentialChangeType::TYPE_ADD;920if ($this->changeset->getChangeType() == $type_add) {921// Although the generic message is sort of accurate in a technical922// sense, this more-tailored message is less confusing.923$shield_text = pht('This is an empty file.');924} else {925$shield_text = pht('The contents of this file were not changed.');926}927} else if ($this->isDeleted()) {928$shield_text = pht('This file was completely deleted.');929} else if ($this->changeset->getAffectedLineCount() > 2500) {930$shield_text = pht(931'This file has a very large number of changes (%s lines).',932new PhutilNumber($this->changeset->getAffectedLineCount()));933}934}935936$shield = null;937if ($shield_raw !== null) {938$shield = $shield_raw;939} else if ($shield_text !== null) {940if ($shield_type === null) {941$shield_type = 'default';942}943944// If we have inlines and the shield would normally show the whole file,945// downgrade it to show only text around the inlines.946if ($collapsed_count) {947if ($shield_type === 'text') {948$shield_type = 'default';949}950951$shield_text = array(952$shield_text,953' ',954pht(955'This file has %d collapsed inline comment(s).',956new PhutilNumber($collapsed_count)),957);958}959960$shield = $renderer->renderShield($shield_text, $shield_type);961}962963if ($shield !== null) {964return $renderer->renderChangesetTable($shield);965}966967// This request should render the "undershield" headers if it's a top-level968// request which made it this far (indicating the changeset has no shield)969// or it's a request with no mask information (indicating it's the request970// that removes the rendering shield). Possibly, this second class of971// request might need to be made more explicit.972$is_undershield = (empty($mask_force) || $this->isTopLevel);973$renderer->setIsUndershield($is_undershield);974975$old_comments = array();976$new_comments = array();977$old_mask = array();978$new_mask = array();979$feedback_mask = array();980$lines_context = $this->getLinesOfContext();981982if ($this->comments) {983// If there are any comments which appear in sections of the file which984// we don't have, we're going to move them backwards to the closest985// earlier line. Two cases where this may happen are:986//987// - Porting ghost comments forward into a file which was mostly988// deleted.989// - Porting ghost comments forward from a full-context diff to a990// partial-context diff.991992list($old_backmap, $new_backmap) = $this->buildLineBackmaps();993994foreach ($this->comments as $comment) {995$new_side = $this->isCommentOnRightSideWhenDisplayed($comment);996997$line = $comment->getLineNumber();998999// See T13524. Lint inlines from Harbormaster may not have a line1000// number.1001if ($line === null) {1002$back_line = null;1003} else if ($new_side) {1004$back_line = idx($new_backmap, $line);1005} else {1006$back_line = idx($old_backmap, $line);1007}10081009if ($back_line != $line) {1010// TODO: This should probably be cleaner, but just be simple and1011// obvious for now.1012$ghost = $comment->getIsGhost();1013if ($ghost) {1014$moved = pht(1015'This comment originally appeared on line %s, but that line '.1016'does not exist in this version of the diff. It has been '.1017'moved backward to the nearest line.',1018new PhutilNumber($line));1019$ghost['reason'] = $ghost['reason']."\n\n".$moved;1020$comment->setIsGhost($ghost);1021}10221023$comment->setLineNumber($back_line);1024$comment->setLineLength(0);1025}10261027$start = max($comment->getLineNumber() - $lines_context, 0);1028$end = $comment->getLineNumber() +1029$comment->getLineLength() +1030$lines_context;1031for ($ii = $start; $ii <= $end; $ii++) {1032if ($new_side) {1033$new_mask[$ii] = true;1034} else {1035$old_mask[$ii] = true;1036}1037}1038}10391040foreach ($this->old as $ii => $old) {1041if (isset($old['line']) && isset($old_mask[$old['line']])) {1042$feedback_mask[$ii] = true;1043}1044}10451046foreach ($this->new as $ii => $new) {1047if (isset($new['line']) && isset($new_mask[$new['line']])) {1048$feedback_mask[$ii] = true;1049}1050}10511052$this->comments = id(new PHUIDiffInlineThreader())1053->reorderAndThreadCommments($this->comments);10541055$old_max_display = 1;1056foreach ($this->old as $old) {1057if (isset($old['line'])) {1058$old_max_display = $old['line'];1059}1060}10611062$new_max_display = 1;1063foreach ($this->new as $new) {1064if (isset($new['line'])) {1065$new_max_display = $new['line'];1066}1067}10681069foreach ($this->comments as $comment) {1070$display_line = $comment->getLineNumber() + $comment->getLineLength();1071$display_line = max(1, $display_line);10721073if ($this->isCommentOnRightSideWhenDisplayed($comment)) {1074$display_line = min($new_max_display, $display_line);1075$new_comments[$display_line][] = $comment;1076} else {1077$display_line = min($old_max_display, $display_line);1078$old_comments[$display_line][] = $comment;1079}1080}1081}10821083$renderer1084->setOldComments($old_comments)1085->setNewComments($new_comments);10861087if ($engine_blocks !== null) {1088$reference = $this->getRenderingReference();1089$parts = explode('/', $reference);1090if (count($parts) == 2) {1091list($id, $vs) = $parts;1092} else {1093$id = $parts[0];1094$vs = 0;1095}10961097// If we don't have an explicit "vs" changeset, it's the left side of1098// the "id" changeset.1099if (!$vs) {1100$vs = $id;1101}11021103if ($mask_force) {1104$engine_blocks->setRevealedIndexes(array_keys($mask_force));1105}11061107if ($range_start !== null || $range_len !== null) {1108$range_min = $range_start;11091110if ($range_len === null) {1111$range_max = null;1112} else {1113$range_max = (int)$range_start + (int)$range_len;1114}11151116$engine_blocks->setRange($range_min, $range_max);1117}11181119$renderer1120->setDocumentEngine($engine)1121->setDocumentEngineBlocks($engine_blocks);11221123return $renderer->renderDocumentEngineBlocks(1124$engine_blocks,1125(string)$id,1126(string)$vs);1127}11281129// If we've made it here with a type of file we don't know how to render,1130// bail out with a default empty rendering. Normally, we'd expect a1131// document engine to catch these changes before we make it this far.1132switch ($this->changeset->getFileType()) {1133case DifferentialChangeType::FILE_DIRECTORY:1134case DifferentialChangeType::FILE_BINARY:1135case DifferentialChangeType::FILE_IMAGE:1136$output = $renderer->renderChangesetTable(null);1137return $output;1138}11391140if ($this->originalLeft && $this->originalRight) {1141list($highlight_old, $highlight_new) = $this->diffOriginals();1142$highlight_old = array_flip($highlight_old);1143$highlight_new = array_flip($highlight_new);1144$renderer1145->setHighlightOld($highlight_old)1146->setHighlightNew($highlight_new);1147}1148$renderer1149->setOriginalOld($this->originalLeft)1150->setOriginalNew($this->originalRight);11511152if ($range_start === null) {1153$range_start = 0;1154}1155if ($range_len === null) {1156$range_len = $rows;1157}1158$range_len = min($range_len, $rows - $range_start);11591160list($gaps, $mask) = $this->calculateGapsAndMask(1161$mask_force,1162$feedback_mask,1163$range_start,1164$range_len);11651166$renderer1167->setGaps($gaps)1168->setMask($mask);11691170$html = $renderer->renderTextChange(1171$range_start,1172$range_len,1173$rows);11741175return $renderer->renderChangesetTable($html);1176}11771178/**1179* This function calculates a lot of stuff we need to know to display1180* the diff:1181*1182* Gaps - compute gaps in the visible display diff, where we will render1183* "Show more context" spacers. If a gap is smaller than the context size,1184* we just display it. Otherwise, we record it into $gaps and will render a1185* "show more context" element instead of diff text below. A given $gap1186* is a tuple of $gap_line_number_start and $gap_length.1187*1188* Mask - compute the actual lines that need to be shown (because they1189* are near changes lines, near inline comments, or the request has1190* explicitly asked for them, i.e. resulting from the user clicking1191* "show more"). The $mask returned is a sparsely populated dictionary1192* of $visible_line_number => true.1193*1194* @return array($gaps, $mask)1195*/1196private function calculateGapsAndMask(1197$mask_force,1198$feedback_mask,1199$range_start,1200$range_len) {12011202$lines_context = $this->getLinesOfContext();12031204$gaps = array();1205$gap_start = 0;1206$in_gap = false;1207$base_mask = $this->visible + $mask_force + $feedback_mask;1208$base_mask[$range_start + $range_len] = true;1209for ($ii = $range_start; $ii <= $range_start + $range_len; $ii++) {1210if (isset($base_mask[$ii])) {1211if ($in_gap) {1212$gap_length = $ii - $gap_start;1213if ($gap_length <= $lines_context) {1214for ($jj = $gap_start; $jj <= $gap_start + $gap_length; $jj++) {1215$base_mask[$jj] = true;1216}1217} else {1218$gaps[] = array($gap_start, $gap_length);1219}1220$in_gap = false;1221}1222} else {1223if (!$in_gap) {1224$gap_start = $ii;1225$in_gap = true;1226}1227}1228}1229$gaps = array_reverse($gaps);1230$mask = $base_mask;12311232return array($gaps, $mask);1233}12341235/**1236* Determine if an inline comment will appear on the rendered diff,1237* taking into consideration which halves of which changesets will actually1238* be shown.1239*1240* @param PhabricatorInlineComment Comment to test for visibility.1241* @return bool True if the comment is visible on the rendered diff.1242*/1243private function isCommentVisibleOnRenderedDiff(1244PhabricatorInlineComment $comment) {12451246$changeset_id = $comment->getChangesetID();1247$is_new = $comment->getIsNewFile();12481249if ($changeset_id == $this->rightSideChangesetID &&1250$is_new == $this->rightSideAttachesToNewFile) {1251return true;1252}12531254if ($changeset_id == $this->leftSideChangesetID &&1255$is_new == $this->leftSideAttachesToNewFile) {1256return true;1257}12581259return false;1260}126112621263/**1264* Determine if a comment will appear on the right side of the display diff.1265* Note that the comment must appear somewhere on the rendered changeset, as1266* per isCommentVisibleOnRenderedDiff().1267*1268* @param PhabricatorInlineComment Comment to test for display1269* location.1270* @return bool True for right, false for left.1271*/1272private function isCommentOnRightSideWhenDisplayed(1273PhabricatorInlineComment $comment) {12741275if (!$this->isCommentVisibleOnRenderedDiff($comment)) {1276throw new Exception(pht('Comment is not visible on changeset!'));1277}12781279$changeset_id = $comment->getChangesetID();1280$is_new = $comment->getIsNewFile();12811282if ($changeset_id == $this->rightSideChangesetID &&1283$is_new == $this->rightSideAttachesToNewFile) {1284return true;1285}12861287return false;1288}12891290/**1291* Parse the 'range' specification that this class and the client-side JS1292* emit to indicate that a user clicked "Show more..." on a diff. Generally,1293* use is something like this:1294*1295* $spec = $request->getStr('range');1296* $parsed = DifferentialChangesetParser::parseRangeSpecification($spec);1297* list($start, $end, $mask) = $parsed;1298* $parser->render($start, $end, $mask);1299*1300* @param string Range specification, indicating the range of the diff that1301* should be rendered.1302* @return tuple List of <start, end, mask> suitable for passing to1303* @{method:render}.1304*/1305public static function parseRangeSpecification($spec) {1306$range_s = null;1307$range_e = null;1308$mask = array();13091310if ($spec) {1311$match = null;1312if (preg_match('@^(\d+)-(\d+)(?:/(\d+)-(\d+))?$@', $spec, $match)) {1313$range_s = (int)$match[1];1314$range_e = (int)$match[2];1315if (count($match) > 3) {1316$start = (int)$match[3];1317$len = (int)$match[4];1318for ($ii = $start; $ii < $start + $len; $ii++) {1319$mask[$ii] = true;1320}1321}1322}1323}13241325return array($range_s, $range_e, $mask);1326}13271328/**1329* Render "modified coverage" information; test coverage on modified lines.1330* This synthesizes diff information with unit test information into a useful1331* indicator of how well tested a change is.1332*/1333public function renderModifiedCoverage() {1334$na = phutil_tag('em', array(), '-');13351336$coverage = $this->getCoverage();1337if (!$coverage) {1338return $na;1339}13401341$covered = 0;1342$not_covered = 0;13431344foreach ($this->new as $k => $new) {1345if ($new === null) {1346continue;1347}13481349if (!$new['line']) {1350continue;1351}13521353if (!$new['type']) {1354continue;1355}13561357if (empty($coverage[$new['line'] - 1])) {1358continue;1359}13601361switch ($coverage[$new['line'] - 1]) {1362case 'C':1363$covered++;1364break;1365case 'U':1366$not_covered++;1367break;1368}1369}13701371if (!$covered && !$not_covered) {1372return $na;1373}13741375return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered)));1376}13771378/**1379* Build maps from lines comments appear on to actual lines.1380*/1381private function buildLineBackmaps() {1382$old_back = array();1383$new_back = array();1384foreach ($this->old as $ii => $old) {1385if ($old === null) {1386continue;1387}1388$old_back[$old['line']] = $old['line'];1389}1390foreach ($this->new as $ii => $new) {1391if ($new === null) {1392continue;1393}1394$new_back[$new['line']] = $new['line'];1395}13961397$max_old_line = 0;1398$max_new_line = 0;1399foreach ($this->comments as $comment) {1400if ($this->isCommentOnRightSideWhenDisplayed($comment)) {1401$max_new_line = max($max_new_line, $comment->getLineNumber());1402} else {1403$max_old_line = max($max_old_line, $comment->getLineNumber());1404}1405}14061407$cursor = 1;1408for ($ii = 1; $ii <= $max_old_line; $ii++) {1409if (empty($old_back[$ii])) {1410$old_back[$ii] = $cursor;1411} else {1412$cursor = $old_back[$ii];1413}1414}14151416$cursor = 1;1417for ($ii = 1; $ii <= $max_new_line; $ii++) {1418if (empty($new_back[$ii])) {1419$new_back[$ii] = $cursor;1420} else {1421$cursor = $new_back[$ii];1422}1423}14241425return array($old_back, $new_back);1426}14271428private function getOffset(array $map, $line) {1429if (!$map) {1430return null;1431}14321433$line = (int)$line;1434foreach ($map as $key => $spec) {1435if ($spec && isset($spec['line'])) {1436if ((int)$spec['line'] >= $line) {1437return $key;1438}1439}1440}14411442return $key;1443}14441445private function realignDiff(1446DifferentialChangeset $changeset,1447DifferentialHunkParser $hunk_parser) {1448// Normalizing and realigning the diff depends on rediffing the files, and1449// we currently need complete representations of both files to do anything1450// reasonable. If we only have parts of the files, skip realignment.14511452// We have more than one hunk, so we're definitely missing part of the file.1453$hunks = $changeset->getHunks();1454if (count($hunks) !== 1) {1455return null;1456}14571458// The first hunk doesn't start at the beginning of the file, so we're1459// missing some context.1460$first_hunk = head($hunks);1461if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) {1462return null;1463}14641465$old_file = $changeset->makeOldFile();1466$new_file = $changeset->makeNewFile();1467if ($old_file === $new_file) {1468// If the old and new files are exactly identical, the synthetic1469// diff below will give us nonsense and whitespace modes are1470// irrelevant anyway. This occurs when you, e.g., copy a file onto1471// itself in Subversion (see T271).1472return null;1473}147414751476$engine = id(new PhabricatorDifferenceEngine())1477->setNormalize(true);14781479$normalized_changeset = $engine->generateChangesetFromFileContent(1480$old_file,1481$new_file);14821483$type_parser = new DifferentialHunkParser();1484$type_parser->parseHunksForLineData($normalized_changeset->getHunks());14851486$hunk_parser->setNormalized(true);1487$hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap());1488$hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap());1489}14901491private function adjustRenderedLineForDisplay($line) {1492// IMPORTANT: We're using "str_replace()" against raw HTML here, which can1493// easily become unsafe. The input HTML has already had syntax highlighting1494// and intraline diff highlighting applied, so it's full of "<span />" tags.14951496static $search;1497static $replace;1498if ($search === null) {1499$rules = $this->newSuspiciousCharacterRules();15001501$map = array();1502foreach ($rules as $key => $spec) {1503$tag = phutil_tag(1504'span',1505array(1506'data-copy-text' => $key,1507'class' => $spec['class'],1508'title' => $spec['title'],1509),1510$spec['replacement']);1511$map[$key] = phutil_string_cast($tag);1512}15131514$search = array_keys($map);1515$replace = array_values($map);1516}15171518$is_html = false;1519if ($line instanceof PhutilSafeHTML) {1520$is_html = true;1521$line = hsprintf('%s', $line);1522}15231524$line = phutil_string_cast($line);15251526// TODO: This should be flexible, eventually.1527$tab_width = 8;15281529$line = self::replaceTabsWithSpaces($line, $tab_width);1530$line = str_replace($search, $replace, $line);15311532if ($is_html) {1533$line = phutil_safe_html($line);1534}15351536return $line;1537}15381539private function newSuspiciousCharacterRules() {1540// The "title" attributes are cached in the database, so they're1541// intentionally not wrapped in "pht(...)".15421543$rules = array(1544"\xE2\x80\x8B" => array(1545'title' => 'ZWS',1546'class' => 'suspicious-character',1547'replacement' => '!',1548),1549"\xC2\xA0" => array(1550'title' => 'NBSP',1551'class' => 'suspicious-character',1552'replacement' => '!',1553),1554"\x7F" => array(1555'title' => 'DEL (0x7F)',1556'class' => 'suspicious-character',1557'replacement' => "\xE2\x90\xA1",1558),1559);15601561// Unicode defines special pictures for the control characters in the1562// range between "0x00" and "0x1F".15631564$control = array(1565'NULL',1566'SOH',1567'STX',1568'ETX',1569'EOT',1570'ENQ',1571'ACK',1572'BEL',1573'BS',1574null, // "\t" Tab1575null, // "\n" New Line1576'VT',1577'FF',1578null, // "\r" Carriage Return,1579'SO',1580'SI',1581'DLE',1582'DC1',1583'DC2',1584'DC3',1585'DC4',1586'NAK',1587'SYN',1588'ETB',1589'CAN',1590'EM',1591'SUB',1592'ESC',1593'FS',1594'GS',1595'RS',1596'US',1597);15981599foreach ($control as $idx => $label) {1600if ($label === null) {1601continue;1602}16031604$rules[chr($idx)] = array(1605'title' => sprintf('%s (0x%02X)', $label, $idx),1606'class' => 'suspicious-character',1607'replacement' => "\xE2\x90".chr(0x80 + $idx),1608);1609}16101611return $rules;1612}16131614public static function replaceTabsWithSpaces($line, $tab_width) {1615static $tags = array();1616if (empty($tags[$tab_width])) {1617for ($ii = 1; $ii <= $tab_width; $ii++) {1618$tag = phutil_tag(1619'span',1620array(1621'data-copy-text' => "\t",1622),1623str_repeat(' ', $ii));1624$tag = phutil_string_cast($tag);1625$tags[$ii] = $tag;1626}1627}16281629// Expand all prefix tabs until we encounter any non-tab character. This1630// is cheap and often immediately produces the correct result with no1631// further work (and, particularly, no need to handle any unicode cases).16321633$len = strlen($line);16341635$head = 0;1636for ($head = 0; $head < $len; $head++) {1637$char = $line[$head];1638if ($char !== "\t") {1639break;1640}1641}16421643if ($head) {1644if (empty($tags[$tab_width * $head])) {1645$tags[$tab_width * $head] = str_repeat($tags[$tab_width], $head);1646}1647$prefix = $tags[$tab_width * $head];1648$line = substr($line, $head);1649} else {1650$prefix = '';1651}16521653// If we have no remaining tabs elsewhere in the string after taking care1654// of all the prefix tabs, we're done.1655if (strpos($line, "\t") === false) {1656return $prefix.$line;1657}16581659$len = strlen($line);16601661// If the line is particularly long, don't try to do anything special with1662// it. Use a faster approximation of the correct tabstop expansion instead.1663// This usually still arrives at the right result.1664if ($len > 256) {1665return $prefix.str_replace("\t", $tags[$tab_width], $line);1666}16671668$in_tag = false;1669$pos = 0;16701671// See PHI1210. If the line only has single-byte characters, we don't need1672// to vectorize it and can avoid an expensive UTF8 call.16731674$fast_path = preg_match('/^[\x01-\x7F]*\z/', $line);1675if ($fast_path) {1676$replace = array();1677for ($ii = 0; $ii < $len; $ii++) {1678$char = $line[$ii];1679if ($char === '>') {1680$in_tag = false;1681continue;1682}16831684if ($in_tag) {1685continue;1686}16871688if ($char === '<') {1689$in_tag = true;1690continue;1691}16921693if ($char === "\t") {1694$count = $tab_width - ($pos % $tab_width);1695$pos += $count;1696$replace[$ii] = $tags[$count];1697continue;1698}16991700$pos++;1701}17021703if ($replace) {1704// Apply replacements starting at the end of the string so they1705// don't mess up the offsets for following replacements.1706$replace = array_reverse($replace, true);17071708foreach ($replace as $replace_pos => $replacement) {1709$line = substr_replace($line, $replacement, $replace_pos, 1);1710}1711}1712} else {1713$line = phutil_utf8v_combined($line);1714foreach ($line as $key => $char) {1715if ($char === '>') {1716$in_tag = false;1717continue;1718}17191720if ($in_tag) {1721continue;1722}17231724if ($char === '<') {1725$in_tag = true;1726continue;1727}17281729if ($char === "\t") {1730$count = $tab_width - ($pos % $tab_width);1731$pos += $count;1732$line[$key] = $tags[$count];1733continue;1734}17351736$pos++;1737}17381739$line = implode('', $line);1740}17411742return $prefix.$line;1743}17441745private function newDocumentEngine() {1746$changeset = $this->changeset;1747$viewer = $this->getViewer();17481749list($old_file, $new_file) = $this->loadFileObjectsForChangeset();17501751$no_old = !$changeset->hasOldState();1752$no_new = !$changeset->hasNewState();17531754if ($no_old) {1755$old_ref = null;1756} else {1757$old_ref = id(new PhabricatorDocumentRef())1758->setName($changeset->getOldFile());1759if ($old_file) {1760$old_ref->setFile($old_file);1761} else {1762$old_data = $this->getRawDocumentEngineData($this->old);1763$old_ref->setData($old_data);1764}1765}17661767if ($no_new) {1768$new_ref = null;1769} else {1770$new_ref = id(new PhabricatorDocumentRef())1771->setName($changeset->getFilename());1772if ($new_file) {1773$new_ref->setFile($new_file);1774} else {1775$new_data = $this->getRawDocumentEngineData($this->new);1776$new_ref->setData($new_data);1777}1778}17791780$old_engines = null;1781if ($old_ref) {1782$old_engines = PhabricatorDocumentEngine::getEnginesForRef(1783$viewer,1784$old_ref);1785}17861787$new_engines = null;1788if ($new_ref) {1789$new_engines = PhabricatorDocumentEngine::getEnginesForRef(1790$viewer,1791$new_ref);1792}17931794if ($new_engines !== null && $old_engines !== null) {1795$shared_engines = array_intersect_key($new_engines, $old_engines);1796$default_engine = head_key($new_engines);1797} else if ($new_engines !== null) {1798$shared_engines = $new_engines;1799$default_engine = head_key($shared_engines);1800} else if ($old_engines !== null) {1801$shared_engines = $old_engines;1802$default_engine = head_key($shared_engines);1803} else {1804return null;1805}18061807foreach ($shared_engines as $key => $shared_engine) {1808if (!$shared_engine->canDiffDocuments($old_ref, $new_ref)) {1809unset($shared_engines[$key]);1810}1811}18121813$this->availableDocumentEngines = $shared_engines;18141815$viewstate = $this->getViewState();18161817$engine_key = $viewstate->getDocumentEngineKey();1818if (phutil_nonempty_string($engine_key)) {1819if (isset($shared_engines[$engine_key])) {1820$document_engine = $shared_engines[$engine_key];1821} else {1822$document_engine = null;1823}1824} else {1825// If we aren't rendering with a specific engine, only use a default1826// engine if the best engine for the new file is a shared engine which1827// can diff files. If we're less picky (for example, by accepting any1828// shared engine) we can end up with silly behavior (like ".json" files1829// rendering as Jupyter documents).18301831if (isset($shared_engines[$default_engine])) {1832$document_engine = $shared_engines[$default_engine];1833} else {1834$document_engine = null;1835}1836}18371838if ($document_engine) {1839return array(1840$document_engine,1841$old_ref,1842$new_ref);1843}18441845return null;1846}18471848private function loadFileObjectsForChangeset() {1849$changeset = $this->changeset;1850$viewer = $this->getViewer();18511852$old_phid = $changeset->getOldFileObjectPHID();1853$new_phid = $changeset->getNewFileObjectPHID();18541855$old_file = null;1856$new_file = null;18571858if ($old_phid || $new_phid) {1859$file_phids = array();1860if ($old_phid) {1861$file_phids[] = $old_phid;1862}1863if ($new_phid) {1864$file_phids[] = $new_phid;1865}18661867$files = id(new PhabricatorFileQuery())1868->setViewer($viewer)1869->withPHIDs($file_phids)1870->execute();1871$files = mpull($files, null, 'getPHID');18721873if ($old_phid) {1874$old_file = idx($files, $old_phid);1875if (!$old_file) {1876throw new Exception(1877pht(1878'Failed to load file data for changeset ("%s").',1879$old_phid));1880}1881$changeset->attachOldFileObject($old_file);1882}18831884if ($new_phid) {1885$new_file = idx($files, $new_phid);1886if (!$new_file) {1887throw new Exception(1888pht(1889'Failed to load file data for changeset ("%s").',1890$new_phid));1891}1892$changeset->attachNewFileObject($new_file);1893}1894}18951896return array($old_file, $new_file);1897}18981899public function newChangesetResponse() {1900// NOTE: This has to happen first because it has side effects. Yuck.1901$rendered_changeset = $this->renderChangeset();19021903$renderer = $this->getRenderer();1904$renderer_key = $renderer->getRendererKey();19051906$viewstate = $this->getViewState();19071908$undo_templates = $renderer->renderUndoTemplates();1909foreach ($undo_templates as $key => $undo_template) {1910$undo_templates[$key] = hsprintf('%s', $undo_template);1911}19121913$document_engine = $renderer->getDocumentEngine();1914if ($document_engine) {1915$document_engine_key = $document_engine->getDocumentEngineKey();1916} else {1917$document_engine_key = null;1918}19191920$available_keys = array();1921$engines = $this->availableDocumentEngines;1922if (!$engines) {1923$engines = array();1924}19251926$available_keys = mpull($engines, 'getDocumentEngineKey');19271928// TODO: Always include "source" as a usable engine to default to1929// the buitin rendering. This is kind of a hack and does not actually1930// use the source engine. The source engine isn't a diff engine, so1931// selecting it causes us to fall through and render with builtin1932// behavior. For now, overall behavir is reasonable.19331934$available_keys[] = PhabricatorSourceDocumentEngine::ENGINEKEY;1935$available_keys = array_fuse($available_keys);1936$available_keys = array_values($available_keys);19371938$state = array(1939'undoTemplates' => $undo_templates,1940'rendererKey' => $renderer_key,1941'highlight' => $viewstate->getHighlightLanguage(),1942'characterEncoding' => $viewstate->getCharacterEncoding(),1943'requestDocumentEngineKey' => $viewstate->getDocumentEngineKey(),1944'responseDocumentEngineKey' => $document_engine_key,1945'availableDocumentEngineKeys' => $available_keys,1946'isHidden' => $viewstate->getHidden(),1947);19481949return id(new PhabricatorChangesetResponse())1950->setRenderedChangeset($rendered_changeset)1951->setChangesetState($state);1952}19531954private function getRawDocumentEngineData(array $lines) {1955$text = array();19561957foreach ($lines as $line) {1958if ($line === null) {1959continue;1960}19611962// If this is a "No newline at end of file." annotation, don't hand it1963// off to the DocumentEngine.1964if ($line['type'] === '\\') {1965continue;1966}19671968$text[] = $line['text'];1969}19701971return implode('', $text);1972}19731974}197519761977