Path: blob/master/src/applications/differential/render/DifferentialChangesetRenderer.php
12256 views
<?php12abstract class DifferentialChangesetRenderer extends Phobject {34private $user;5private $changeset;6private $renderingReference;7private $renderPropertyChangeHeader;8private $isTopLevel;9private $isUndershield;10private $hunkStartLines;11private $oldLines;12private $newLines;13private $oldComments;14private $newComments;15private $oldChangesetID;16private $newChangesetID;17private $oldAttachesToNewFile;18private $newAttachesToNewFile;19private $highlightOld = array();20private $highlightNew = array();21private $codeCoverage;22private $handles;23private $markupEngine;24private $oldRender;25private $newRender;26private $originalOld;27private $originalNew;28private $gaps;29private $mask;30private $originalCharacterEncoding;31private $showEditAndReplyLinks;32private $canMarkDone;33private $objectOwnerPHID;34private $highlightingDisabled;35private $scopeEngine = false;36private $depthOnlyLines;3738private $documentEngine;39private $documentEngineBlocks;4041private $oldFile = false;42private $newFile = false;4344abstract public function getRendererKey();4546public function setShowEditAndReplyLinks($bool) {47$this->showEditAndReplyLinks = $bool;48return $this;49}5051public function getShowEditAndReplyLinks() {52return $this->showEditAndReplyLinks;53}5455public function setHighlightingDisabled($highlighting_disabled) {56$this->highlightingDisabled = $highlighting_disabled;57return $this;58}5960public function getHighlightingDisabled() {61return $this->highlightingDisabled;62}6364public function setOriginalCharacterEncoding($original_character_encoding) {65$this->originalCharacterEncoding = $original_character_encoding;66return $this;67}6869public function getOriginalCharacterEncoding() {70return $this->originalCharacterEncoding;71}7273public function setIsUndershield($is_undershield) {74$this->isUndershield = $is_undershield;75return $this;76}7778public function getIsUndershield() {79return $this->isUndershield;80}8182public function setMask($mask) {83$this->mask = $mask;84return $this;85}86protected function getMask() {87return $this->mask;88}8990public function setGaps($gaps) {91$this->gaps = $gaps;92return $this;93}94protected function getGaps() {95return $this->gaps;96}9798public function setDepthOnlyLines(array $lines) {99$this->depthOnlyLines = $lines;100return $this;101}102103public function getDepthOnlyLines() {104return $this->depthOnlyLines;105}106107public function attachOldFile(PhabricatorFile $old = null) {108$this->oldFile = $old;109return $this;110}111112public function getOldFile() {113if ($this->oldFile === false) {114throw new PhabricatorDataNotAttachedException($this);115}116return $this->oldFile;117}118119public function hasOldFile() {120return (bool)$this->oldFile;121}122123public function attachNewFile(PhabricatorFile $new = null) {124$this->newFile = $new;125return $this;126}127128public function getNewFile() {129if ($this->newFile === false) {130throw new PhabricatorDataNotAttachedException($this);131}132return $this->newFile;133}134135public function hasNewFile() {136return (bool)$this->newFile;137}138139public function setOriginalNew($original_new) {140$this->originalNew = $original_new;141return $this;142}143protected function getOriginalNew() {144return $this->originalNew;145}146147public function setOriginalOld($original_old) {148$this->originalOld = $original_old;149return $this;150}151protected function getOriginalOld() {152return $this->originalOld;153}154155public function setNewRender($new_render) {156$this->newRender = $new_render;157return $this;158}159protected function getNewRender() {160return $this->newRender;161}162163public function setOldRender($old_render) {164$this->oldRender = $old_render;165return $this;166}167protected function getOldRender() {168return $this->oldRender;169}170171public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {172$this->markupEngine = $markup_engine;173return $this;174}175public function getMarkupEngine() {176return $this->markupEngine;177}178179public function setHandles(array $handles) {180assert_instances_of($handles, 'PhabricatorObjectHandle');181$this->handles = $handles;182return $this;183}184protected function getHandles() {185return $this->handles;186}187188public function setCodeCoverage($code_coverage) {189$this->codeCoverage = $code_coverage;190return $this;191}192protected function getCodeCoverage() {193return $this->codeCoverage;194}195196public function setHighlightNew($highlight_new) {197$this->highlightNew = $highlight_new;198return $this;199}200protected function getHighlightNew() {201return $this->highlightNew;202}203204public function setHighlightOld($highlight_old) {205$this->highlightOld = $highlight_old;206return $this;207}208protected function getHighlightOld() {209return $this->highlightOld;210}211212public function setNewAttachesToNewFile($attaches) {213$this->newAttachesToNewFile = $attaches;214return $this;215}216protected function getNewAttachesToNewFile() {217return $this->newAttachesToNewFile;218}219220public function setOldAttachesToNewFile($attaches) {221$this->oldAttachesToNewFile = $attaches;222return $this;223}224protected function getOldAttachesToNewFile() {225return $this->oldAttachesToNewFile;226}227228public function setNewChangesetID($new_changeset_id) {229$this->newChangesetID = $new_changeset_id;230return $this;231}232protected function getNewChangesetID() {233return $this->newChangesetID;234}235236public function setOldChangesetID($old_changeset_id) {237$this->oldChangesetID = $old_changeset_id;238return $this;239}240protected function getOldChangesetID() {241return $this->oldChangesetID;242}243244public function setDocumentEngine(PhabricatorDocumentEngine $engine) {245$this->documentEngine = $engine;246return $this;247}248249public function getDocumentEngine() {250return $this->documentEngine;251}252253public function setDocumentEngineBlocks(254PhabricatorDocumentEngineBlocks $blocks) {255$this->documentEngineBlocks = $blocks;256return $this;257}258259public function getDocumentEngineBlocks() {260return $this->documentEngineBlocks;261}262263public function setNewComments(array $new_comments) {264foreach ($new_comments as $line_number => $comments) {265assert_instances_of($comments, 'PhabricatorInlineComment');266}267$this->newComments = $new_comments;268return $this;269}270protected function getNewComments() {271return $this->newComments;272}273274public function setOldComments(array $old_comments) {275foreach ($old_comments as $line_number => $comments) {276assert_instances_of($comments, 'PhabricatorInlineComment');277}278$this->oldComments = $old_comments;279return $this;280}281protected function getOldComments() {282return $this->oldComments;283}284285public function setNewLines(array $new_lines) {286$this->newLines = $new_lines;287return $this;288}289protected function getNewLines() {290return $this->newLines;291}292293public function setOldLines(array $old_lines) {294$this->oldLines = $old_lines;295return $this;296}297protected function getOldLines() {298return $this->oldLines;299}300301public function setHunkStartLines(array $hunk_start_lines) {302$this->hunkStartLines = $hunk_start_lines;303return $this;304}305306protected function getHunkStartLines() {307return $this->hunkStartLines;308}309310public function setUser(PhabricatorUser $user) {311$this->user = $user;312return $this;313}314protected function getUser() {315return $this->user;316}317318public function setChangeset(DifferentialChangeset $changeset) {319$this->changeset = $changeset;320return $this;321}322protected function getChangeset() {323return $this->changeset;324}325326public function setRenderingReference($rendering_reference) {327$this->renderingReference = $rendering_reference;328return $this;329}330protected function getRenderingReference() {331return $this->renderingReference;332}333334public function setRenderPropertyChangeHeader($should_render) {335$this->renderPropertyChangeHeader = $should_render;336return $this;337}338339private function shouldRenderPropertyChangeHeader() {340return $this->renderPropertyChangeHeader;341}342343public function setIsTopLevel($is) {344$this->isTopLevel = $is;345return $this;346}347348private function getIsTopLevel() {349return $this->isTopLevel;350}351352public function setCanMarkDone($can_mark_done) {353$this->canMarkDone = $can_mark_done;354return $this;355}356357public function getCanMarkDone() {358return $this->canMarkDone;359}360361public function setObjectOwnerPHID($phid) {362$this->objectOwnerPHID = $phid;363return $this;364}365366public function getObjectOwnerPHID() {367return $this->objectOwnerPHID;368}369370final public function renderChangesetTable($content) {371$props = null;372if ($this->shouldRenderPropertyChangeHeader()) {373$props = $this->renderPropertyChangeHeader();374}375376$notice = null;377if ($this->getIsTopLevel()) {378$force = (!$content && !$props);379380// If we have DocumentEngine messages about the blocks, assume they381// explain why there's no content.382$blocks = $this->getDocumentEngineBlocks();383if ($blocks) {384if ($blocks->getMessages()) {385$force = false;386}387}388389$notice = $this->renderChangeTypeHeader($force);390}391392$undershield = null;393if ($this->getIsUndershield()) {394$undershield = $this->renderUndershieldHeader();395}396397$result = array(398$notice,399$props,400$undershield,401$content,402);403404return hsprintf('%s', $result);405}406407abstract public function isOneUpRenderer();408abstract public function renderTextChange(409$range_start,410$range_len,411$rows);412413public function renderDocumentEngineBlocks(414PhabricatorDocumentEngineBlocks $blocks,415$old_changeset_key,416$new_changeset_key) {417return null;418}419420abstract protected function renderChangeTypeHeader($force);421abstract protected function renderUndershieldHeader();422423protected function didRenderChangesetTableContents($contents) {424return $contents;425}426427/**428* Render a "shield" over the diff, with a message like "This file is429* generated and does not need to be reviewed." or "This file was completely430* deleted." This UI element hides unimportant text so the reviewer doesn't431* need to scroll past it.432*433* The shield includes a link to view the underlying content. This link434* may force certain rendering modes when the link is clicked:435*436* - `"default"`: Render the diff normally, as though it was not437* shielded. This is the default and appropriate if the underlying438* diff is a normal change, but was hidden for reasons of not being439* important (e.g., generated code).440* - `"text"`: Force the text to be shown. This is probably only relevant441* when a file is not changed.442* - `"none"`: Don't show the link (e.g., text not available).443*444* @param string Message explaining why the diff is hidden.445* @param string|null Force mode, see above.446* @return string Shield markup.447*/448abstract public function renderShield($message, $force = 'default');449450abstract protected function renderPropertyChangeHeader();451452protected function buildPrimitives($range_start, $range_len) {453$primitives = array();454455$hunk_starts = $this->getHunkStartLines();456457$mask = $this->getMask();458$gaps = $this->getGaps();459460$old = $this->getOldLines();461$new = $this->getNewLines();462$old_render = $this->getOldRender();463$new_render = $this->getNewRender();464$old_comments = $this->getOldComments();465$new_comments = $this->getNewComments();466467$size = count($old);468for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {469if (empty($mask[$ii])) {470list($top, $len) = array_pop($gaps);471$primitives[] = array(472'type' => 'context',473'top' => $top,474'len' => $len,475);476477$ii += ($len - 1);478continue;479}480481$ospec = array(482'type' => 'old',483'htype' => null,484'cursor' => $ii,485'line' => null,486'oline' => null,487'render' => null,488);489490$nspec = array(491'type' => 'new',492'htype' => null,493'cursor' => $ii,494'line' => null,495'oline' => null,496'render' => null,497'copy' => null,498'coverage' => null,499);500501if (isset($old[$ii])) {502$ospec['line'] = (int)$old[$ii]['line'];503$nspec['oline'] = (int)$old[$ii]['line'];504$ospec['htype'] = $old[$ii]['type'];505if (isset($old_render[$ii])) {506$ospec['render'] = $old_render[$ii];507} else if ($ospec['htype'] === '\\') {508$ospec['render'] = $old[$ii]['text'];509}510}511512if (isset($new[$ii])) {513$nspec['line'] = (int)$new[$ii]['line'];514$ospec['oline'] = (int)$new[$ii]['line'];515$nspec['htype'] = $new[$ii]['type'];516if (isset($new_render[$ii])) {517$nspec['render'] = $new_render[$ii];518} else if ($nspec['htype'] === '\\') {519$nspec['render'] = $new[$ii]['text'];520}521}522523if (isset($hunk_starts[$ospec['line']])) {524$primitives[] = array(525'type' => 'no-context',526);527}528529$primitives[] = $ospec;530$primitives[] = $nspec;531532if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {533foreach ($old_comments[$ospec['line']] as $comment) {534$primitives[] = array(535'type' => 'inline',536'comment' => $comment,537'right' => false,538);539}540}541542if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {543foreach ($new_comments[$nspec['line']] as $comment) {544$primitives[] = array(545'type' => 'inline',546'comment' => $comment,547'right' => true,548);549}550}551552if ($hunk_starts && ($ii == $size - 1)) {553$primitives[] = array(554'type' => 'no-context',555);556}557}558559if ($this->isOneUpRenderer()) {560$primitives = $this->processPrimitivesForOneUp($primitives);561}562563return $primitives;564}565566private function processPrimitivesForOneUp(array $primitives) {567// Primitives come out of buildPrimitives() in two-up format, because it568// is the most general, flexible format. To put them into one-up format,569// we need to filter and reorder them. In particular:570//571// - We discard unchanged lines in the old file; in one-up format, we572// render them only once.573// - We group contiguous blocks of old-modified and new-modified lines, so574// they render in "block of old, block of new" order instead of575// alternating old and new lines.576577$out = array();578579$old_buf = array();580$new_buf = array();581foreach ($primitives as $primitive) {582$type = $primitive['type'];583584if ($type == 'old') {585if (!$primitive['htype']) {586// This is a line which appears in both the old file and the new587// file, or the spacer corresponding to a line added in the new file.588// Ignore it when rendering a one-up diff.589continue;590}591$old_buf[] = $primitive;592} else if ($type == 'new') {593if ($primitive['line'] === null) {594// This is an empty spacer corresponding to a line removed from the595// old file. Ignore it when rendering a one-up diff.596continue;597}598if (!$primitive['htype']) {599// If this line is the same in both versions of the file, put it in600// the old line buffer. This makes sure inlines on old, unchanged601// lines end up in the right place.602603// First, we need to flush the line buffers if they're not empty.604if ($old_buf) {605$out[] = $old_buf;606$old_buf = array();607}608if ($new_buf) {609$out[] = $new_buf;610$new_buf = array();611}612$old_buf[] = $primitive;613} else {614$new_buf[] = $primitive;615}616} else if ($type == 'context' || $type == 'no-context') {617$out[] = $old_buf;618$out[] = $new_buf;619$old_buf = array();620$new_buf = array();621$out[] = array($primitive);622} else if ($type == 'inline') {623624// If this inline is on the left side, put it after the old lines.625if (!$primitive['right']) {626$out[] = $old_buf;627$out[] = array($primitive);628$old_buf = array();629} else {630$out[] = $old_buf;631$out[] = $new_buf;632$out[] = array($primitive);633$old_buf = array();634$new_buf = array();635}636637} else {638throw new Exception(pht("Unknown primitive type '%s'!", $primitive));639}640}641642$out[] = $old_buf;643$out[] = $new_buf;644$out = array_mergev($out);645646return $out;647}648649protected function getChangesetProperties($changeset) {650$old = $changeset->getOldProperties();651$new = $changeset->getNewProperties();652653// If a property has been changed, but is not present on one side of the654// change and has an uninteresting default value on the other, remove it.655// This most commonly happens when a change adds or removes a file: the656// side of the change with the file has a "100644" filemode in Git.657658$defaults = array(659'unix:filemode' => '100644',660);661662foreach ($defaults as $default_key => $default_value) {663$old_value = idx($old, $default_key, $default_value);664$new_value = idx($new, $default_key, $default_value);665666$old_default = ($old_value === $default_value);667$new_default = ($new_value === $default_value);668669if ($old_default && $new_default) {670unset($old[$default_key]);671unset($new[$default_key]);672}673}674675$metadata = $changeset->getMetadata();676677if ($this->hasOldFile()) {678$file = $this->getOldFile();679if ($file->getImageWidth()) {680$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();681$old['file:dimensions'] = $dimensions;682}683$old['file:mimetype'] = $file->getMimeType();684$old['file:size'] = phutil_format_bytes($file->getByteSize());685} else {686$old['file:mimetype'] = idx($metadata, 'old:file:mime-type');687$size = idx($metadata, 'old:file:size');688if ($size !== null) {689$old['file:size'] = phutil_format_bytes($size);690}691}692693if ($this->hasNewFile()) {694$file = $this->getNewFile();695if ($file->getImageWidth()) {696$dimensions = $file->getImageWidth().'x'.$file->getImageHeight();697$new['file:dimensions'] = $dimensions;698}699$new['file:mimetype'] = $file->getMimeType();700$new['file:size'] = phutil_format_bytes($file->getByteSize());701} else {702$new['file:mimetype'] = idx($metadata, 'new:file:mime-type');703$size = idx($metadata, 'new:file:size');704if ($size !== null) {705$new['file:size'] = phutil_format_bytes($size);706}707}708709return array($old, $new);710}711712public function renderUndoTemplates() {713$views = array(714'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),715'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),716);717718foreach ($views as $key => $view) {719$scaffold = $this->getRowScaffoldForInline($view);720721$scaffold->setIsUndoTemplate(true);722723$views[$key] = id(new PHUIDiffInlineCommentTableScaffold())724->addRowScaffold($scaffold);725}726727return $views;728}729730final protected function getScopeEngine() {731if ($this->scopeEngine === false) {732$hunk_starts = $this->getHunkStartLines();733734// If this change is missing context, don't try to identify scopes, since735// we won't really be able to get anywhere.736$has_multiple_hunks = (count($hunk_starts) > 1);737738$has_offset_hunks = false;739if ($hunk_starts) {740$has_offset_hunks = (head_key($hunk_starts) != 1);741}742743$missing_context = ($has_multiple_hunks || $has_offset_hunks);744745if ($missing_context) {746$scope_engine = null;747} else {748$line_map = $this->getNewLineTextMap();749$scope_engine = id(new PhabricatorDiffScopeEngine())750->setLineTextMap($line_map);751}752753$this->scopeEngine = $scope_engine;754}755756return $this->scopeEngine;757}758759private function getNewLineTextMap() {760$new = $this->getNewLines();761762$text_map = array();763foreach ($new as $new_line) {764if (!isset($new_line['line'])) {765continue;766}767$text_map[$new_line['line']] = $new_line['text'];768}769770return $text_map;771}772773}774775776