@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3abstract class DifferentialChangesetRenderer extends Phobject {
4
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;
38
39 private $documentEngine;
40 private $documentEngineBlocks;
41
42 private $oldFile = false;
43 private $newFile = false;
44
45 abstract public function getRendererKey();
46
47 public function setShowEditAndReplyLinks($bool) {
48 $this->showEditAndReplyLinks = $bool;
49 return $this;
50 }
51
52 public function getShowEditAndReplyLinks() {
53 return $this->showEditAndReplyLinks;
54 }
55
56 public function setHighlightingDisabled($highlighting_disabled) {
57 $this->highlightingDisabled = $highlighting_disabled;
58 return $this;
59 }
60
61 public function getHighlightingDisabled() {
62 return $this->highlightingDisabled;
63 }
64
65 public function setOriginalCharacterEncoding($original_character_encoding) {
66 $this->originalCharacterEncoding = $original_character_encoding;
67 return $this;
68 }
69
70 public function getOriginalCharacterEncoding() {
71 return $this->originalCharacterEncoding;
72 }
73
74 public function setIsUndershield($is_undershield) {
75 $this->isUndershield = $is_undershield;
76 return $this;
77 }
78
79 public function getIsUndershield() {
80 return $this->isUndershield;
81 }
82
83 public function setMask($mask) {
84 $this->mask = $mask;
85 return $this;
86 }
87 protected function getMask() {
88 return $this->mask;
89 }
90
91 public function setGaps($gaps) {
92 $this->gaps = $gaps;
93 return $this;
94 }
95 protected function getGaps() {
96 return $this->gaps;
97 }
98
99 public function setDepthOnlyLines(array $lines) {
100 $this->depthOnlyLines = $lines;
101 return $this;
102 }
103
104 public function getDepthOnlyLines() {
105 return $this->depthOnlyLines;
106 }
107
108 public function attachOldFile(?PhabricatorFile $old = null) {
109 $this->oldFile = $old;
110 return $this;
111 }
112
113 public function getOldFile() {
114 if ($this->oldFile === false) {
115 throw new PhabricatorDataNotAttachedException($this);
116 }
117 return $this->oldFile;
118 }
119
120 public function hasOldFile() {
121 return (bool)$this->oldFile;
122 }
123
124 public function attachNewFile(?PhabricatorFile $new = null) {
125 $this->newFile = $new;
126 return $this;
127 }
128
129 public function getNewFile() {
130 if ($this->newFile === false) {
131 throw new PhabricatorDataNotAttachedException($this);
132 }
133 return $this->newFile;
134 }
135
136 public function hasNewFile() {
137 return (bool)$this->newFile;
138 }
139
140 public function setOriginalNew($original_new) {
141 $this->originalNew = $original_new;
142 return $this;
143 }
144 protected function getOriginalNew() {
145 return $this->originalNew;
146 }
147
148 public function setOriginalOld($original_old) {
149 $this->originalOld = $original_old;
150 return $this;
151 }
152 protected function getOriginalOld() {
153 return $this->originalOld;
154 }
155
156 public function setNewRender($new_render) {
157 $this->newRender = $new_render;
158 return $this;
159 }
160 protected function getNewRender() {
161 return $this->newRender;
162 }
163
164 public function setOldRender($old_render) {
165 $this->oldRender = $old_render;
166 return $this;
167 }
168 protected function getOldRender() {
169 return $this->oldRender;
170 }
171
172 public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) {
173 $this->markupEngine = $markup_engine;
174 return $this;
175 }
176 public function getMarkupEngine() {
177 return $this->markupEngine;
178 }
179
180 /**
181 * @param array<PhabricatorObjectHandle> $handles
182 */
183 public function setHandles(array $handles) {
184 assert_instances_of($handles, PhabricatorObjectHandle::class);
185 $this->handles = $handles;
186 return $this;
187 }
188 protected function getHandles() {
189 return $this->handles;
190 }
191
192 public function setCodeCoverage($code_coverage) {
193 $this->codeCoverage = $code_coverage;
194 return $this;
195 }
196 protected function getCodeCoverage() {
197 return $this->codeCoverage;
198 }
199
200 public function setHighlightNew($highlight_new) {
201 $this->highlightNew = $highlight_new;
202 return $this;
203 }
204 protected function getHighlightNew() {
205 return $this->highlightNew;
206 }
207
208 public function setHighlightOld($highlight_old) {
209 $this->highlightOld = $highlight_old;
210 return $this;
211 }
212 protected function getHighlightOld() {
213 return $this->highlightOld;
214 }
215
216 public function setNewAttachesToNewFile($attaches) {
217 $this->newAttachesToNewFile = $attaches;
218 return $this;
219 }
220 protected function getNewAttachesToNewFile() {
221 return $this->newAttachesToNewFile;
222 }
223
224 public function setOldAttachesToNewFile($attaches) {
225 $this->oldAttachesToNewFile = $attaches;
226 return $this;
227 }
228 protected function getOldAttachesToNewFile() {
229 return $this->oldAttachesToNewFile;
230 }
231
232 public function setNewChangesetID($new_changeset_id) {
233 $this->newChangesetID = $new_changeset_id;
234 return $this;
235 }
236 protected function getNewChangesetID() {
237 return $this->newChangesetID;
238 }
239
240 public function setOldChangesetID($old_changeset_id) {
241 $this->oldChangesetID = $old_changeset_id;
242 return $this;
243 }
244 protected function getOldChangesetID() {
245 return $this->oldChangesetID;
246 }
247
248 public function setDocumentEngine(PhabricatorDocumentEngine $engine) {
249 $this->documentEngine = $engine;
250 return $this;
251 }
252
253 public function getDocumentEngine() {
254 return $this->documentEngine;
255 }
256
257 public function setDocumentEngineBlocks(
258 PhabricatorDocumentEngineBlocks $blocks) {
259 $this->documentEngineBlocks = $blocks;
260 return $this;
261 }
262
263 public function getDocumentEngineBlocks() {
264 return $this->documentEngineBlocks;
265 }
266
267 /**
268 * @param array<PhabricatorInlineComment> $new_comments
269 */
270 public function setNewComments(array $new_comments) {
271 foreach ($new_comments as $line_number => $comments) {
272 assert_instances_of($comments, PhabricatorInlineComment::class);
273 }
274 $this->newComments = $new_comments;
275 return $this;
276 }
277
278 protected function getNewComments() {
279 return $this->newComments;
280 }
281
282 /**
283 * @param array<PhabricatorInlineComment> $old_comments
284 */
285 public function setOldComments(array $old_comments) {
286 foreach ($old_comments as $line_number => $comments) {
287 assert_instances_of($comments, PhabricatorInlineComment::class);
288 }
289 $this->oldComments = $old_comments;
290 return $this;
291 }
292 protected function getOldComments() {
293 return $this->oldComments;
294 }
295
296 public function setNewLines(array $new_lines) {
297 $this->newLines = $new_lines;
298 return $this;
299 }
300 protected function getNewLines() {
301 return $this->newLines;
302 }
303
304 public function setOldLines(array $old_lines) {
305 $this->oldLines = $old_lines;
306 return $this;
307 }
308 protected function getOldLines() {
309 return $this->oldLines;
310 }
311
312 public function setHunkStartLines(array $hunk_start_lines) {
313 $this->hunkStartLines = $hunk_start_lines;
314 return $this;
315 }
316
317 protected function getHunkStartLines() {
318 return $this->hunkStartLines;
319 }
320
321 public function setUser(PhabricatorUser $user) {
322 $this->user = $user;
323 return $this;
324 }
325 protected function getUser() {
326 return $this->user;
327 }
328
329 public function setChangeset(DifferentialChangeset $changeset) {
330 $this->changeset = $changeset;
331 return $this;
332 }
333 protected function getChangeset() {
334 return $this->changeset;
335 }
336
337 public function setRenderingReference($rendering_reference) {
338 $this->renderingReference = $rendering_reference;
339 return $this;
340 }
341 protected function getRenderingReference() {
342 return $this->renderingReference;
343 }
344
345 public function setRenderPropertyChangeHeader($should_render) {
346 $this->renderPropertyChangeHeader = $should_render;
347 return $this;
348 }
349
350 private function shouldRenderPropertyChangeHeader() {
351 return $this->renderPropertyChangeHeader;
352 }
353
354 public function setIsTopLevel($is) {
355 $this->isTopLevel = $is;
356 return $this;
357 }
358
359 private function getIsTopLevel() {
360 return $this->isTopLevel;
361 }
362
363 public function setCanMarkDone($can_mark_done) {
364 $this->canMarkDone = $can_mark_done;
365 return $this;
366 }
367
368 public function getCanMarkDone() {
369 return $this->canMarkDone;
370 }
371
372 public function setObjectOwnerPHID($phid) {
373 $this->objectOwnerPHID = $phid;
374 return $this;
375 }
376
377 public function getObjectOwnerPHID() {
378 return $this->objectOwnerPHID;
379 }
380
381 final public function renderChangesetTable($content) {
382 $props = null;
383 if ($this->shouldRenderPropertyChangeHeader()) {
384 $props = $this->renderPropertyChangeHeader();
385 }
386
387 $notice = null;
388 if ($this->getIsTopLevel()) {
389 $force = (!$content && !$props);
390
391 // If we have DocumentEngine messages about the blocks, assume they
392 // explain why there's no content.
393 $blocks = $this->getDocumentEngineBlocks();
394 if ($blocks) {
395 if ($blocks->getMessages()) {
396 $force = false;
397 }
398 }
399
400 $notice = $this->renderChangeTypeHeader($force);
401 }
402
403 $undershield = null;
404 if ($this->getIsUndershield()) {
405 $undershield = $this->renderUndershieldHeader();
406 }
407
408 $result = array(
409 $notice,
410 $props,
411 $undershield,
412 $content,
413 );
414
415 return hsprintf('%s', $result);
416 }
417
418 abstract public function isOneUpRenderer();
419 abstract public function renderTextChange(
420 $range_start,
421 $range_len,
422 $rows);
423
424 public function renderDocumentEngineBlocks(
425 PhabricatorDocumentEngineBlocks $blocks,
426 $old_changeset_key,
427 $new_changeset_key) {
428 return null;
429 }
430
431 abstract protected function renderChangeTypeHeader($force);
432 abstract protected function renderUndershieldHeader();
433
434 protected function didRenderChangesetTableContents($contents) {
435 return $contents;
436 }
437
438 /**
439 * Render a "shield" over the diff, with a message like "This file is
440 * generated and does not need to be reviewed." or "This file was completely
441 * deleted." This UI element hides unimportant text so the reviewer doesn't
442 * need to scroll past it.
443 *
444 * The shield includes a link to view the underlying content. This link
445 * may force certain rendering modes when the link is clicked:
446 *
447 * - `"default"`: Render the diff normally, as though it was not
448 * shielded. This is the default and appropriate if the underlying
449 * diff is a normal change, but was hidden for reasons of not being
450 * important (e.g., generated code).
451 * - `"text"`: Force the text to be shown. This is probably only relevant
452 * when a file is not changed.
453 * - `"none"`: Don't show the link (e.g., text not available).
454 *
455 * @param string $message Message explaining why the diff is hidden.
456 * @param string|null $force Force mode, see above.
457 * @return string|null Shield markup.
458 */
459 abstract public function renderShield($message, $force = 'default');
460
461 abstract protected function renderPropertyChangeHeader();
462
463 protected function buildPrimitives($range_start, $range_len) {
464 $primitives = array();
465
466 $hunk_starts = $this->getHunkStartLines();
467
468 $mask = $this->getMask();
469 $gaps = $this->getGaps();
470
471 $old = $this->getOldLines();
472 $new = $this->getNewLines();
473 $old_render = $this->getOldRender();
474 $new_render = $this->getNewRender();
475 $old_comments = $this->getOldComments();
476 $new_comments = $this->getNewComments();
477
478 $size = count($old);
479 for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) {
480 if (empty($mask[$ii])) {
481 list($top, $len) = array_pop($gaps);
482 $primitives[] = array(
483 'type' => 'context',
484 'top' => $top,
485 'len' => $len,
486 );
487
488 $ii += ($len - 1);
489 continue;
490 }
491
492 $ospec = array(
493 'type' => 'old',
494 'htype' => null,
495 'cursor' => $ii,
496 'line' => null,
497 'oline' => null,
498 'render' => null,
499 );
500
501 $nspec = array(
502 'type' => 'new',
503 'htype' => null,
504 'cursor' => $ii,
505 'line' => null,
506 'oline' => null,
507 'render' => null,
508 'copy' => null,
509 'coverage' => null,
510 );
511
512 if (isset($old[$ii])) {
513 $ospec['line'] = (int)$old[$ii]['line'];
514 $nspec['oline'] = (int)$old[$ii]['line'];
515 $ospec['htype'] = $old[$ii]['type'];
516 if (isset($old_render[$ii])) {
517 $ospec['render'] = $old_render[$ii];
518 } else if ($ospec['htype'] === '\\') {
519 $ospec['render'] = $old[$ii]['text'];
520 }
521 }
522
523 if (isset($new[$ii])) {
524 $nspec['line'] = (int)$new[$ii]['line'];
525 $ospec['oline'] = (int)$new[$ii]['line'];
526 $nspec['htype'] = $new[$ii]['type'];
527 if (isset($new_render[$ii])) {
528 $nspec['render'] = $new_render[$ii];
529 } else if ($nspec['htype'] === '\\') {
530 $nspec['render'] = $new[$ii]['text'];
531 }
532 }
533
534 if ($ospec['line'] !== null && isset($hunk_starts[$ospec['line']])) {
535 $primitives[] = array(
536 'type' => 'no-context',
537 );
538 }
539
540 $primitives[] = $ospec;
541 $primitives[] = $nspec;
542
543 if ($ospec['line'] !== null && isset($old_comments[$ospec['line']])) {
544 foreach ($old_comments[$ospec['line']] as $comment) {
545 $primitives[] = array(
546 'type' => 'inline',
547 'comment' => $comment,
548 'right' => false,
549 );
550 }
551 }
552
553 if ($nspec['line'] !== null && isset($new_comments[$nspec['line']])) {
554 foreach ($new_comments[$nspec['line']] as $comment) {
555 $primitives[] = array(
556 'type' => 'inline',
557 'comment' => $comment,
558 'right' => true,
559 );
560 }
561 }
562
563 if ($hunk_starts && ($ii == $size - 1)) {
564 $primitives[] = array(
565 'type' => 'no-context',
566 );
567 }
568 }
569
570 if ($this->isOneUpRenderer()) {
571 $primitives = $this->processPrimitivesForOneUp($primitives);
572 }
573
574 return $primitives;
575 }
576
577 private function processPrimitivesForOneUp(array $primitives) {
578 // Primitives come out of buildPrimitives() in two-up format, because it
579 // is the most general, flexible format. To put them into one-up format,
580 // we need to filter and reorder them. In particular:
581 //
582 // - We discard unchanged lines in the old file; in one-up format, we
583 // render them only once.
584 // - We group contiguous blocks of old-modified and new-modified lines, so
585 // they render in "block of old, block of new" order instead of
586 // alternating old and new lines.
587
588 $out = array();
589
590 $old_buf = array();
591 $new_buf = array();
592 foreach ($primitives as $primitive) {
593 $type = $primitive['type'];
594
595 if ($type == 'old') {
596 if (!$primitive['htype']) {
597 // This is a line which appears in both the old file and the new
598 // file, or the spacer corresponding to a line added in the new file.
599 // Ignore it when rendering a one-up diff.
600 continue;
601 }
602 $old_buf[] = $primitive;
603 } else if ($type == 'new') {
604 if ($primitive['line'] === null) {
605 // This is an empty spacer corresponding to a line removed from the
606 // old file. Ignore it when rendering a one-up diff.
607 continue;
608 }
609 if (!$primitive['htype']) {
610 // If this line is the same in both versions of the file, put it in
611 // the old line buffer. This makes sure inlines on old, unchanged
612 // lines end up in the right place.
613
614 // First, we need to flush the line buffers if they're not empty.
615 if ($old_buf) {
616 $out[] = $old_buf;
617 $old_buf = array();
618 }
619 if ($new_buf) {
620 $out[] = $new_buf;
621 $new_buf = array();
622 }
623 $old_buf[] = $primitive;
624 } else {
625 $new_buf[] = $primitive;
626 }
627 } else if ($type == 'context' || $type == 'no-context') {
628 $out[] = $old_buf;
629 $out[] = $new_buf;
630 $old_buf = array();
631 $new_buf = array();
632 $out[] = array($primitive);
633 } else if ($type == 'inline') {
634
635 // If this inline is on the left side, put it after the old lines.
636 if (!$primitive['right']) {
637 $out[] = $old_buf;
638 $out[] = array($primitive);
639 $old_buf = array();
640 } else {
641 $out[] = $old_buf;
642 $out[] = $new_buf;
643 $out[] = array($primitive);
644 $old_buf = array();
645 $new_buf = array();
646 }
647
648 } else {
649 throw new Exception(pht("Unknown primitive type '%s'!", $primitive));
650 }
651 }
652
653 $out[] = $old_buf;
654 $out[] = $new_buf;
655 $out = array_mergev($out);
656
657 return $out;
658 }
659
660 protected function getChangesetProperties($changeset) {
661 $old = $changeset->getOldProperties();
662 $new = $changeset->getNewProperties();
663
664 // If a property has been changed, but is not present on one side of the
665 // change and has an uninteresting default value on the other, remove it.
666 // This most commonly happens when a change adds or removes a file: the
667 // side of the change with the file has a "100644" filemode in Git.
668
669 $defaults = array(
670 'unix:filemode' => '100644',
671 );
672
673 foreach ($defaults as $default_key => $default_value) {
674 $old_value = idx($old, $default_key, $default_value);
675 $new_value = idx($new, $default_key, $default_value);
676
677 $old_default = ($old_value === $default_value);
678 $new_default = ($new_value === $default_value);
679
680 if ($old_default && $new_default) {
681 unset($old[$default_key]);
682 unset($new[$default_key]);
683 }
684 }
685
686 $metadata = $changeset->getMetadata();
687
688 if ($this->hasOldFile()) {
689 $file = $this->getOldFile();
690 if ($file->getImageWidth()) {
691 $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
692 $old['file:dimensions'] = $dimensions;
693 }
694 $old['file:mimetype'] = $file->getMimeType();
695 $old['file:size'] = phutil_format_bytes($file->getByteSize());
696 } else {
697 $old['file:mimetype'] = idx($metadata, 'old:file:mime-type');
698 $size = idx($metadata, 'old:file:size');
699 if ($size !== null) {
700 $old['file:size'] = phutil_format_bytes($size);
701 }
702 }
703
704 if ($this->hasNewFile()) {
705 $file = $this->getNewFile();
706 if ($file->getImageWidth()) {
707 $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
708 $new['file:dimensions'] = $dimensions;
709 }
710 $new['file:mimetype'] = $file->getMimeType();
711 $new['file:size'] = phutil_format_bytes($file->getByteSize());
712 } else {
713 $new['file:mimetype'] = idx($metadata, 'new:file:mime-type');
714 $size = idx($metadata, 'new:file:size');
715 if ($size !== null) {
716 $new['file:size'] = phutil_format_bytes($size);
717 }
718 }
719
720 return array($old, $new);
721 }
722
723 public function renderUndoTemplates() {
724 $views = array(
725 'l' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(false),
726 'r' => id(new PHUIDiffInlineCommentUndoView())->setIsOnRight(true),
727 );
728
729 foreach ($views as $key => $view) {
730 $scaffold = $this->getRowScaffoldForInline($view);
731
732 $scaffold->setIsUndoTemplate(true);
733
734 $views[$key] = id(new PHUIDiffInlineCommentTableScaffold())
735 ->addRowScaffold($scaffold);
736 }
737
738 return $views;
739 }
740
741 final protected function getScopeEngine() {
742 if ($this->scopeEngine === false) {
743 $hunk_starts = $this->getHunkStartLines();
744
745 // If this change is missing context, don't try to identify scopes, since
746 // we won't really be able to get anywhere.
747 $has_multiple_hunks = (count($hunk_starts) > 1);
748
749 $has_offset_hunks = false;
750 if ($hunk_starts) {
751 $has_offset_hunks = (head_key($hunk_starts) != 1);
752 }
753
754 $missing_context = ($has_multiple_hunks || $has_offset_hunks);
755
756 if ($missing_context) {
757 $scope_engine = null;
758 } else {
759 $line_map = $this->getNewLineTextMap();
760 $scope_engine = id(new PhabricatorDiffScopeEngine())
761 ->setLineTextMap($line_map);
762 }
763
764 $this->scopeEngine = $scope_engine;
765 }
766
767 return $this->scopeEngine;
768 }
769
770 private function getNewLineTextMap() {
771 $new = $this->getNewLines();
772
773 $text_map = array();
774 foreach ($new as $new_line) {
775 if (!isset($new_line['line'])) {
776 continue;
777 }
778 $text_map[$new_line['line']] = $new_line['text'];
779 }
780
781 return $text_map;
782 }
783
784}