@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
at upstream/main 784 lines 21 kB view raw
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}