3 abstract class DifferentialChangesetRenderer
extends Phobject
{
7 private $renderingReference;
8 private $renderPropertyChangeHeader;
10 private $isUndershield;
11 private $hunkStartLines;
16 private $oldChangesetID;
17 private $newChangesetID;
18 private $oldAttachesToNewFile;
19 private $newAttachesToNewFile;
20 private $highlightOld = array();
21 private $highlightNew = array();
22 private $codeCoverage;
24 private $markupEngine;
31 private $originalCharacterEncoding;
32 private $showEditAndReplyLinks;
34 private $objectOwnerPHID;
35 private $highlightingDisabled;
36 private $scopeEngine = false;
37 private $depthOnlyLines;
39 private $documentEngine;
40 private $documentEngineBlocks;
42 private $oldFile = false;
43 private $newFile = false;
45 abstract public function getRendererKey();
47 public function setShowEditAndReplyLinks($bool) {
48 $this->showEditAndReplyLinks
= $bool;
52 public function getShowEditAndReplyLinks() {
53 return $this->showEditAndReplyLinks
;
56 public function setHighlightingDisabled($highlighting_disabled) {
57 $this->highlightingDisabled
= $highlighting_disabled;
61 public function getHighlightingDisabled() {
62 return $this->highlightingDisabled
;
65 public function setOriginalCharacterEncoding($original_character_encoding) {
66 $this->originalCharacterEncoding
= $original_character_encoding;
70 public function getOriginalCharacterEncoding() {
71 return $this->originalCharacterEncoding
;
74 public function setIsUndershield($is_undershield) {
75 $this->isUndershield
= $is_undershield;
79 public function getIsUndershield() {
80 return $this->isUndershield
;
83 public function setMask($mask) {
87 protected function getMask() {
91 public function setGaps($gaps) {
95 protected function getGaps() {
99 public function setDepthOnlyLines(array $lines) {
100 $this->depthOnlyLines
= $lines;
104 public function getDepthOnlyLines() {
105 return $this->depthOnlyLines
;
108 public function attachOldFile(PhabricatorFile
$old = null) {
109 $this->oldFile
= $old;
113 public function getOldFile() {
114 if ($this->oldFile
=== false) {
115 throw new PhabricatorDataNotAttachedException($this);
117 return $this->oldFile
;
120 public function hasOldFile() {
121 return (bool)$this->oldFile
;
124 public function attachNewFile(PhabricatorFile
$new = null) {
125 $this->newFile
= $new;
129 public function getNewFile() {
130 if ($this->newFile
=== false) {
131 throw new PhabricatorDataNotAttachedException($this);
133 return $this->newFile
;
136 public function hasNewFile() {
137 return (bool)$this->newFile
;
140 public function setOriginalNew($original_new) {
141 $this->originalNew
= $original_new;
144 protected function getOriginalNew() {
145 return $this->originalNew
;
148 public function setOriginalOld($original_old) {
149 $this->originalOld
= $original_old;
152 protected function getOriginalOld() {
153 return $this->originalOld
;
156 public function setNewRender($new_render) {
157 $this->newRender
= $new_render;
160 protected function getNewRender() {
161 return $this->newRender
;
164 public function setOldRender($old_render) {
165 $this->oldRender
= $old_render;
168 protected function getOldRender() {
169 return $this->oldRender
;
172 public function setMarkupEngine(PhabricatorMarkupEngine
$markup_engine) {
173 $this->markupEngine
= $markup_engine;
176 public function getMarkupEngine() {
177 return $this->markupEngine
;
180 public function setHandles(array $handles) {
181 assert_instances_of($handles, 'PhabricatorObjectHandle');
182 $this->handles
= $handles;
185 protected function getHandles() {
186 return $this->handles
;
189 public function setCodeCoverage($code_coverage) {
190 $this->codeCoverage
= $code_coverage;
193 protected function getCodeCoverage() {
194 return $this->codeCoverage
;
197 public function setHighlightNew($highlight_new) {
198 $this->highlightNew
= $highlight_new;
201 protected function getHighlightNew() {
202 return $this->highlightNew
;
205 public function setHighlightOld($highlight_old) {
206 $this->highlightOld
= $highlight_old;
209 protected function getHighlightOld() {
210 return $this->highlightOld
;
213 public function setNewAttachesToNewFile($attaches) {
214 $this->newAttachesToNewFile
= $attaches;
217 protected function getNewAttachesToNewFile() {
218 return $this->newAttachesToNewFile
;
221 public function setOldAttachesToNewFile($attaches) {
222 $this->oldAttachesToNewFile
= $attaches;
225 protected function getOldAttachesToNewFile() {
226 return $this->oldAttachesToNewFile
;
229 public function setNewChangesetID($new_changeset_id) {
230 $this->newChangesetID
= $new_changeset_id;
233 protected function getNewChangesetID() {
234 return $this->newChangesetID
;
237 public function setOldChangesetID($old_changeset_id) {
238 $this->oldChangesetID
= $old_changeset_id;
241 protected function getOldChangesetID() {
242 return $this->oldChangesetID
;
245 public function setDocumentEngine(PhabricatorDocumentEngine
$engine) {
246 $this->documentEngine
= $engine;
250 public function getDocumentEngine() {
251 return $this->documentEngine
;
254 public function setDocumentEngineBlocks(
255 PhabricatorDocumentEngineBlocks
$blocks) {
256 $this->documentEngineBlocks
= $blocks;
260 public function getDocumentEngineBlocks() {
261 return $this->documentEngineBlocks
;
264 public function setNewComments(array $new_comments) {
265 foreach ($new_comments as $line_number => $comments) {
266 assert_instances_of($comments, 'PhabricatorInlineComment');
268 $this->newComments
= $new_comments;
271 protected function getNewComments() {
272 return $this->newComments
;
275 public function setOldComments(array $old_comments) {
276 foreach ($old_comments as $line_number => $comments) {
277 assert_instances_of($comments, 'PhabricatorInlineComment');
279 $this->oldComments
= $old_comments;
282 protected function getOldComments() {
283 return $this->oldComments
;
286 public function setNewLines(array $new_lines) {
287 $this->newLines
= $new_lines;
290 protected function getNewLines() {
291 return $this->newLines
;
294 public function setOldLines(array $old_lines) {
295 $this->oldLines
= $old_lines;
298 protected function getOldLines() {
299 return $this->oldLines
;
302 public function setHunkStartLines(array $hunk_start_lines) {
303 $this->hunkStartLines
= $hunk_start_lines;
307 protected function getHunkStartLines() {
308 return $this->hunkStartLines
;
311 public function setUser(PhabricatorUser
$user) {
315 protected function getUser() {
319 public function setChangeset(DifferentialChangeset
$changeset) {
320 $this->changeset
= $changeset;
323 protected function getChangeset() {
324 return $this->changeset
;
327 public function setRenderingReference($rendering_reference) {
328 $this->renderingReference
= $rendering_reference;
331 protected function getRenderingReference() {
332 return $this->renderingReference
;
335 public function setRenderPropertyChangeHeader($should_render) {
336 $this->renderPropertyChangeHeader
= $should_render;
340 private function shouldRenderPropertyChangeHeader() {
341 return $this->renderPropertyChangeHeader
;
344 public function setIsTopLevel($is) {
345 $this->isTopLevel
= $is;
349 private function getIsTopLevel() {
350 return $this->isTopLevel
;
353 public function setCanMarkDone($can_mark_done) {
354 $this->canMarkDone
= $can_mark_done;
358 public function getCanMarkDone() {
359 return $this->canMarkDone
;
362 public function setObjectOwnerPHID($phid) {
363 $this->objectOwnerPHID
= $phid;
367 public function getObjectOwnerPHID() {
368 return $this->objectOwnerPHID
;
371 final public function renderChangesetTable($content) {
373 if ($this->shouldRenderPropertyChangeHeader()) {
374 $props = $this->renderPropertyChangeHeader();
378 if ($this->getIsTopLevel()) {
379 $force = (!$content && !$props);
381 // If we have DocumentEngine messages about the blocks, assume they
382 // explain why there's no content.
383 $blocks = $this->getDocumentEngineBlocks();
385 if ($blocks->getMessages()) {
390 $notice = $this->renderChangeTypeHeader($force);
394 if ($this->getIsUndershield()) {
395 $undershield = $this->renderUndershieldHeader();
405 return hsprintf('%s', $result);
408 abstract public function isOneUpRenderer();
409 abstract public function renderTextChange(
414 public function renderDocumentEngineBlocks(
415 PhabricatorDocumentEngineBlocks
$blocks,
417 $new_changeset_key) {
421 abstract protected function renderChangeTypeHeader($force);
422 abstract protected function renderUndershieldHeader();
424 protected function didRenderChangesetTableContents($contents) {
429 * Render a "shield" over the diff, with a message like "This file is
430 * generated and does not need to be reviewed." or "This file was completely
431 * deleted." This UI element hides unimportant text so the reviewer doesn't
432 * need to scroll past it.
434 * The shield includes a link to view the underlying content. This link
435 * may force certain rendering modes when the link is clicked:
437 * - `"default"`: Render the diff normally, as though it was not
438 * shielded. This is the default and appropriate if the underlying
439 * diff is a normal change, but was hidden for reasons of not being
440 * important (e.g., generated code).
441 * - `"text"`: Force the text to be shown. This is probably only relevant
442 * when a file is not changed.
443 * - `"none"`: Don't show the link (e.g., text not available).
445 * @param string Message explaining why the diff is hidden.
446 * @param string|null Force mode, see above.
447 * @return string Shield markup.
449 abstract public function renderShield($message, $force = 'default');
451 abstract protected function renderPropertyChangeHeader();
453 protected function buildPrimitives($range_start, $range_len) {
454 $primitives = array();
456 $hunk_starts = $this->getHunkStartLines();
458 $mask = $this->getMask();
459 $gaps = $this->getGaps();
461 $old = $this->getOldLines();
462 $new = $this->getNewLines();
463 $old_render = $this->getOldRender();
464 $new_render = $this->getNewRender();
465 $old_comments = $this->getOldComments();
466 $new_comments = $this->getNewComments();
469 for ($ii = $range_start; $ii < $range_start +
$range_len; $ii++
) {
470 if (empty($mask[$ii])) {
471 list($top, $len) = array_pop($gaps);
472 $primitives[] = array(
502 if (isset($old[$ii])) {
503 $ospec['line'] = (int)$old[$ii]['line'];
504 $nspec['oline'] = (int)$old[$ii]['line'];
505 $ospec['htype'] = $old[$ii]['type'];
506 if (isset($old_render[$ii])) {
507 $ospec['render'] = $old_render[$ii];
508 } else if ($ospec['htype'] === '\\') {
509 $ospec['render'] = $old[$ii]['text'];
513 if (isset($new[$ii])) {
514 $nspec['line'] = (int)$new[$ii]['line'];
515 $ospec['oline'] = (int)$new[$ii]['line'];
516 $nspec['htype'] = $new[$ii]['type'];
517 if (isset($new_render[$ii])) {
518 $nspec['render'] = $new_render[$ii];
519 } else if ($nspec['htype'] === '\\') {
520 $nspec['render'] = $new[$ii]['text'];
524 if (isset($hunk_starts[$ospec['line']])) {
525 $primitives[] = array(
526 'type' => 'no-context',
530 $primitives[] = $ospec;
531 $primitives[] = $nspec;
533 if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
534 foreach ($old_comments[$ospec['line']] as $comment) {
535 $primitives[] = array(
537 'comment' => $comment,
543 if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
544 foreach ($new_comments[$nspec['line']] as $comment) {
545 $primitives[] = array(
547 'comment' => $comment,
553 if ($hunk_starts && ($ii == $size - 1)) {
554 $primitives[] = array(
555 'type' => 'no-context',
560 if ($this->isOneUpRenderer()) {
561 $primitives = $this->processPrimitivesForOneUp($primitives);
567 private function processPrimitivesForOneUp(array $primitives) {
568 // Primitives come out of buildPrimitives() in two-up format, because it
569 // is the most general, flexible format. To put them into one-up format,
570 // we need to filter and reorder them. In particular:
572 // - We discard unchanged lines in the old file; in one-up format, we
573 // render them only once.
574 // - We group contiguous blocks of old-modified and new-modified lines, so
575 // they render in "block of old, block of new" order instead of
576 // alternating old and new lines.
582 foreach ($primitives as $primitive) {
583 $type = $primitive['type'];
585 if ($type == 'old') {
586 if (!$primitive['htype']) {
587 // This is a line which appears in both the old file and the new
588 // file, or the spacer corresponding to a line added in the new file.
589 // Ignore it when rendering a one-up diff.
592 $old_buf[] = $primitive;
593 } else if ($type == 'new') {
594 if ($primitive['line'] === null) {
595 // This is an empty spacer corresponding to a line removed from the
596 // old file. Ignore it when rendering a one-up diff.
599 if (!$primitive['htype']) {
600 // If this line is the same in both versions of the file, put it in
601 // the old line buffer. This makes sure inlines on old, unchanged
602 // lines end up in the right place.
604 // First, we need to flush the line buffers if they're not empty.
613 $old_buf[] = $primitive;
615 $new_buf[] = $primitive;
617 } else if ($type == 'context' ||
$type == 'no-context') {
622 $out[] = array($primitive);
623 } else if ($type == 'inline') {
625 // If this inline is on the left side, put it after the old lines.
626 if (!$primitive['right']) {
628 $out[] = array($primitive);
633 $out[] = array($primitive);
639 throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
645 $out = array_mergev($out);
650 protected function getChangesetProperties($changeset) {
651 $old = $changeset->getOldProperties();
652 $new = $changeset->getNewProperties();
654 // If a property has been changed, but is not present on one side of the
655 // change and has an uninteresting default value on the other, remove it.
656 // This most commonly happens when a change adds or removes a file: the
657 // side of the change with the file has a "100644" filemode in Git.
660 'unix:filemode' => '100644',
663 foreach ($defaults as $default_key => $default_value) {
664 $old_value = idx($old, $default_key, $default_value);
665 $new_value = idx($new, $default_key, $default_value);
667 $old_default = ($old_value === $default_value);
668 $new_default = ($new_value === $default_value);
670 if ($old_default && $new_default) {
671 unset($old[$default_key]);
672 unset($new[$default_key]);
676 $metadata = $changeset->getMetadata();
678 if ($this->hasOldFile()) {
679 $file = $this->getOldFile();
680 if ($file->getImageWidth()) {
681 $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
682 $old['file:dimensions'] = $dimensions;
684 $old['file:mimetype'] = $file->getMimeType();
685 $old['file:size'] = phutil_format_bytes($file->getByteSize());
687 $old['file:mimetype'] = idx($metadata, 'old:file:mime-type');
688 $size = idx($metadata, 'old:file:size');
689 if ($size !== null) {
690 $old['file:size'] = phutil_format_bytes($size);
694 if ($this->hasNewFile()) {
695 $file = $this->getNewFile();
696 if ($file->getImageWidth()) {
697 $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
698 $new['file:dimensions'] = $dimensions;
700 $new['file:mimetype'] = $file->getMimeType();
701 $new['file:size'] = phutil_format_bytes($file->getByteSize());
703 $new['file:mimetype'] = idx($metadata, 'new:file:mime-type');
704 $size = idx($metadata, 'new:file:size');
705 if ($size !== null) {
706 $new['file:size'] = phutil_format_bytes($size);
710 return array($old, $new);
713 public function renderUndoTemplates() {
715 'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),
716 'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),
719 foreach ($views as $key => $view) {
720 $scaffold = $this->getRowScaffoldForInline($view);
722 $scaffold->setIsUndoTemplate(true);
724 $views[$key] = id(new PHUIDiffInlineCommentTableScaffold())
725 ->addRowScaffold($scaffold);
731 final protected function getScopeEngine() {
732 if ($this->scopeEngine
=== false) {
733 $hunk_starts = $this->getHunkStartLines();
735 // If this change is missing context, don't try to identify scopes, since
736 // we won't really be able to get anywhere.
737 $has_multiple_hunks = (count($hunk_starts) > 1);
739 $has_offset_hunks = false;
741 $has_offset_hunks = (head_key($hunk_starts) != 1);
744 $missing_context = ($has_multiple_hunks ||
$has_offset_hunks);
746 if ($missing_context) {
747 $scope_engine = null;
749 $line_map = $this->getNewLineTextMap();
750 $scope_engine = id(new PhabricatorDiffScopeEngine())
751 ->setLineTextMap($line_map);
754 $this->scopeEngine
= $scope_engine;
757 return $this->scopeEngine
;
760 private function getNewLineTextMap() {
761 $new = $this->getNewLines();
764 foreach ($new as $new_line) {
765 if (!isset($new_line['line'])) {
768 $text_map[$new_line['line']] = $new_line['text'];