Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / differential / render / DifferentialChangesetRenderer.php
blob711d7d574be9e1b5cb9a71b492f003739902bead
1 <?php
3 abstract class DifferentialChangesetRenderer extends Phobject {
5 private $user;
6 private $changeset;
7 private $renderingReference;
8 private $renderPropertyChangeHeader;
9 private $isTopLevel;
10 private $isUndershield;
11 private $hunkStartLines;
12 private $oldLines;
13 private $newLines;
14 private $oldComments;
15 private $newComments;
16 private $oldChangesetID;
17 private $newChangesetID;
18 private $oldAttachesToNewFile;
19 private $newAttachesToNewFile;
20 private $highlightOld = array();
21 private $highlightNew = array();
22 private $codeCoverage;
23 private $handles;
24 private $markupEngine;
25 private $oldRender;
26 private $newRender;
27 private $originalOld;
28 private $originalNew;
29 private $gaps;
30 private $mask;
31 private $originalCharacterEncoding;
32 private $showEditAndReplyLinks;
33 private $canMarkDone;
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;
49 return $this;
52 public function getShowEditAndReplyLinks() {
53 return $this->showEditAndReplyLinks;
56 public function setHighlightingDisabled($highlighting_disabled) {
57 $this->highlightingDisabled = $highlighting_disabled;
58 return $this;
61 public function getHighlightingDisabled() {
62 return $this->highlightingDisabled;
65 public function setOriginalCharacterEncoding($original_character_encoding) {
66 $this->originalCharacterEncoding = $original_character_encoding;
67 return $this;
70 public function getOriginalCharacterEncoding() {
71 return $this->originalCharacterEncoding;
74 public function setIsUndershield($is_undershield) {
75 $this->isUndershield = $is_undershield;
76 return $this;
79 public function getIsUndershield() {
80 return $this->isUndershield;
83 public function setMask($mask) {
84 $this->mask = $mask;
85 return $this;
87 protected function getMask() {
88 return $this->mask;
91 public function setGaps($gaps) {
92 $this->gaps = $gaps;
93 return $this;
95 protected function getGaps() {
96 return $this->gaps;
99 public function setDepthOnlyLines(array $lines) {
100 $this->depthOnlyLines = $lines;
101 return $this;
104 public function getDepthOnlyLines() {
105 return $this->depthOnlyLines;
108 public function attachOldFile(PhabricatorFile $old = null) {
109 $this->oldFile = $old;
110 return $this;
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;
126 return $this;
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;
142 return $this;
144 protected function getOriginalNew() {
145 return $this->originalNew;
148 public function setOriginalOld($original_old) {
149 $this->originalOld = $original_old;
150 return $this;
152 protected function getOriginalOld() {
153 return $this->originalOld;
156 public function setNewRender($new_render) {
157 $this->newRender = $new_render;
158 return $this;
160 protected function getNewRender() {
161 return $this->newRender;
164 public function setOldRender($old_render) {
165 $this->oldRender = $old_render;
166 return $this;
168 protected function getOldRender() {
169 return $this->oldRender;
172 public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
173 $this->markupEngine = $markup_engine;
174 return $this;
176 public function getMarkupEngine() {
177 return $this->markupEngine;
180 public function setHandles(array $handles) {
181 assert_instances_of($handles, 'PhabricatorObjectHandle');
182 $this->handles = $handles;
183 return $this;
185 protected function getHandles() {
186 return $this->handles;
189 public function setCodeCoverage($code_coverage) {
190 $this->codeCoverage = $code_coverage;
191 return $this;
193 protected function getCodeCoverage() {
194 return $this->codeCoverage;
197 public function setHighlightNew($highlight_new) {
198 $this->highlightNew = $highlight_new;
199 return $this;
201 protected function getHighlightNew() {
202 return $this->highlightNew;
205 public function setHighlightOld($highlight_old) {
206 $this->highlightOld = $highlight_old;
207 return $this;
209 protected function getHighlightOld() {
210 return $this->highlightOld;
213 public function setNewAttachesToNewFile($attaches) {
214 $this->newAttachesToNewFile = $attaches;
215 return $this;
217 protected function getNewAttachesToNewFile() {
218 return $this->newAttachesToNewFile;
221 public function setOldAttachesToNewFile($attaches) {
222 $this->oldAttachesToNewFile = $attaches;
223 return $this;
225 protected function getOldAttachesToNewFile() {
226 return $this->oldAttachesToNewFile;
229 public function setNewChangesetID($new_changeset_id) {
230 $this->newChangesetID = $new_changeset_id;
231 return $this;
233 protected function getNewChangesetID() {
234 return $this->newChangesetID;
237 public function setOldChangesetID($old_changeset_id) {
238 $this->oldChangesetID = $old_changeset_id;
239 return $this;
241 protected function getOldChangesetID() {
242 return $this->oldChangesetID;
245 public function setDocumentEngine(PhabricatorDocumentEngine $engine) {
246 $this->documentEngine = $engine;
247 return $this;
250 public function getDocumentEngine() {
251 return $this->documentEngine;
254 public function setDocumentEngineBlocks(
255 PhabricatorDocumentEngineBlocks $blocks) {
256 $this->documentEngineBlocks = $blocks;
257 return $this;
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;
269 return $this;
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;
280 return $this;
282 protected function getOldComments() {
283 return $this->oldComments;
286 public function setNewLines(array $new_lines) {
287 $this->newLines = $new_lines;
288 return $this;
290 protected function getNewLines() {
291 return $this->newLines;
294 public function setOldLines(array $old_lines) {
295 $this->oldLines = $old_lines;
296 return $this;
298 protected function getOldLines() {
299 return $this->oldLines;
302 public function setHunkStartLines(array $hunk_start_lines) {
303 $this->hunkStartLines = $hunk_start_lines;
304 return $this;
307 protected function getHunkStartLines() {
308 return $this->hunkStartLines;
311 public function setUser(PhabricatorUser $user) {
312 $this->user = $user;
313 return $this;
315 protected function getUser() {
316 return $this->user;
319 public function setChangeset(DifferentialChangeset $changeset) {
320 $this->changeset = $changeset;
321 return $this;
323 protected function getChangeset() {
324 return $this->changeset;
327 public function setRenderingReference($rendering_reference) {
328 $this->renderingReference = $rendering_reference;
329 return $this;
331 protected function getRenderingReference() {
332 return $this->renderingReference;
335 public function setRenderPropertyChangeHeader($should_render) {
336 $this->renderPropertyChangeHeader = $should_render;
337 return $this;
340 private function shouldRenderPropertyChangeHeader() {
341 return $this->renderPropertyChangeHeader;
344 public function setIsTopLevel($is) {
345 $this->isTopLevel = $is;
346 return $this;
349 private function getIsTopLevel() {
350 return $this->isTopLevel;
353 public function setCanMarkDone($can_mark_done) {
354 $this->canMarkDone = $can_mark_done;
355 return $this;
358 public function getCanMarkDone() {
359 return $this->canMarkDone;
362 public function setObjectOwnerPHID($phid) {
363 $this->objectOwnerPHID = $phid;
364 return $this;
367 public function getObjectOwnerPHID() {
368 return $this->objectOwnerPHID;
371 final public function renderChangesetTable($content) {
372 $props = null;
373 if ($this->shouldRenderPropertyChangeHeader()) {
374 $props = $this->renderPropertyChangeHeader();
377 $notice = null;
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();
384 if ($blocks) {
385 if ($blocks->getMessages()) {
386 $force = false;
390 $notice = $this->renderChangeTypeHeader($force);
393 $undershield = null;
394 if ($this->getIsUndershield()) {
395 $undershield = $this->renderUndershieldHeader();
398 $result = array(
399 $notice,
400 $props,
401 $undershield,
402 $content,
405 return hsprintf('%s', $result);
408 abstract public function isOneUpRenderer();
409 abstract public function renderTextChange(
410 $range_start,
411 $range_len,
412 $rows);
414 public function renderDocumentEngineBlocks(
415 PhabricatorDocumentEngineBlocks $blocks,
416 $old_changeset_key,
417 $new_changeset_key) {
418 return null;
421 abstract protected function renderChangeTypeHeader($force);
422 abstract protected function renderUndershieldHeader();
424 protected function didRenderChangesetTableContents($contents) {
425 return $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();
468 $size = count($old);
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(
473 'type' => 'context',
474 'top' => $top,
475 'len' => $len,
478 $ii += ($len - 1);
479 continue;
482 $ospec = array(
483 'type' => 'old',
484 'htype' => null,
485 'cursor' => $ii,
486 'line' => null,
487 'oline' => null,
488 'render' => null,
491 $nspec = array(
492 'type' => 'new',
493 'htype' => null,
494 'cursor' => $ii,
495 'line' => null,
496 'oline' => null,
497 'render' => null,
498 'copy' => null,
499 'coverage' => null,
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(
536 'type' => 'inline',
537 'comment' => $comment,
538 'right' => false,
543 if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
544 foreach ($new_comments[$nspec['line']] as $comment) {
545 $primitives[] = array(
546 'type' => 'inline',
547 'comment' => $comment,
548 'right' => true,
553 if ($hunk_starts && ($ii == $size - 1)) {
554 $primitives[] = array(
555 'type' => 'no-context',
560 if ($this->isOneUpRenderer()) {
561 $primitives = $this->processPrimitivesForOneUp($primitives);
564 return $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.
578 $out = array();
580 $old_buf = array();
581 $new_buf = array();
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.
590 continue;
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.
597 continue;
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.
605 if ($old_buf) {
606 $out[] = $old_buf;
607 $old_buf = array();
609 if ($new_buf) {
610 $out[] = $new_buf;
611 $new_buf = array();
613 $old_buf[] = $primitive;
614 } else {
615 $new_buf[] = $primitive;
617 } else if ($type == 'context' || $type == 'no-context') {
618 $out[] = $old_buf;
619 $out[] = $new_buf;
620 $old_buf = array();
621 $new_buf = array();
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']) {
627 $out[] = $old_buf;
628 $out[] = array($primitive);
629 $old_buf = array();
630 } else {
631 $out[] = $old_buf;
632 $out[] = $new_buf;
633 $out[] = array($primitive);
634 $old_buf = array();
635 $new_buf = array();
638 } else {
639 throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
643 $out[] = $old_buf;
644 $out[] = $new_buf;
645 $out = array_mergev($out);
647 return $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.
659 $defaults = array(
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());
686 } else {
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());
702 } else {
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() {
714 $views = array(
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);
728 return $views;
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;
740 if ($hunk_starts) {
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;
748 } else {
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();
763 $text_map = array();
764 foreach ($new as $new_line) {
765 if (!isset($new_line['line'])) {
766 continue;
768 $text_map[$new_line['line']] = $new_line['text'];
771 return $text_map;