@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 recaptime-dev/main 650 lines 20 kB view raw
1<?php 2 3abstract class DifferentialChangesetHTMLRenderer 4 extends DifferentialChangesetRenderer { 5 6 public static function getHTMLRendererByKey($key) { 7 switch ($key) { 8 case '1up': 9 return new DifferentialChangesetOneUpRenderer(); 10 case '2up': 11 default: 12 return new DifferentialChangesetTwoUpRenderer(); 13 } 14 throw new Exception(pht('Unknown HTML renderer "%s"!', $key)); 15 } 16 17 abstract protected function getRendererTableClass(); 18 abstract public function getRowScaffoldForInline( 19 PHUIDiffInlineCommentView $view); 20 21 protected function renderChangeTypeHeader($force) { 22 $changeset = $this->getChangeset(); 23 24 $change = $changeset->getChangeType(); 25 $file = $changeset->getFileType(); 26 27 $messages = array(); 28 switch ($change) { 29 30 case DifferentialChangeType::TYPE_ADD: 31 switch ($file) { 32 case DifferentialChangeType::FILE_TEXT: 33 $messages[] = pht('This file was added.'); 34 break; 35 case DifferentialChangeType::FILE_IMAGE: 36 $messages[] = pht('This image was added.'); 37 break; 38 case DifferentialChangeType::FILE_DIRECTORY: 39 $messages[] = pht('This directory was added.'); 40 break; 41 case DifferentialChangeType::FILE_BINARY: 42 $messages[] = pht('This binary file was added.'); 43 break; 44 case DifferentialChangeType::FILE_SYMLINK: 45 $messages[] = pht('This symlink was added.'); 46 break; 47 case DifferentialChangeType::FILE_SUBMODULE: 48 $messages[] = pht('This submodule was added.'); 49 break; 50 } 51 break; 52 53 case DifferentialChangeType::TYPE_DELETE: 54 switch ($file) { 55 case DifferentialChangeType::FILE_TEXT: 56 $messages[] = pht('This file was deleted.'); 57 break; 58 case DifferentialChangeType::FILE_IMAGE: 59 $messages[] = pht('This image was deleted.'); 60 break; 61 case DifferentialChangeType::FILE_DIRECTORY: 62 $messages[] = pht('This directory was deleted.'); 63 break; 64 case DifferentialChangeType::FILE_BINARY: 65 $messages[] = pht('This binary file was deleted.'); 66 break; 67 case DifferentialChangeType::FILE_SYMLINK: 68 $messages[] = pht('This symlink was deleted.'); 69 break; 70 case DifferentialChangeType::FILE_SUBMODULE: 71 $messages[] = pht('This submodule was deleted.'); 72 break; 73 } 74 break; 75 76 case DifferentialChangeType::TYPE_MOVE_HERE: 77 $from = phutil_tag('strong', array(), $changeset->getOldFile()); 78 switch ($file) { 79 case DifferentialChangeType::FILE_TEXT: 80 $messages[] = pht('This file was moved from %s.', $from); 81 break; 82 case DifferentialChangeType::FILE_IMAGE: 83 $messages[] = pht('This image was moved from %s.', $from); 84 break; 85 case DifferentialChangeType::FILE_DIRECTORY: 86 $messages[] = pht('This directory was moved from %s.', $from); 87 break; 88 case DifferentialChangeType::FILE_BINARY: 89 $messages[] = pht('This binary file was moved from %s.', $from); 90 break; 91 case DifferentialChangeType::FILE_SYMLINK: 92 $messages[] = pht('This symlink was moved from %s.', $from); 93 break; 94 case DifferentialChangeType::FILE_SUBMODULE: 95 $messages[] = pht('This submodule was moved from %s.', $from); 96 break; 97 } 98 break; 99 100 case DifferentialChangeType::TYPE_COPY_HERE: 101 $from = phutil_tag('strong', array(), $changeset->getOldFile()); 102 switch ($file) { 103 case DifferentialChangeType::FILE_TEXT: 104 $messages[] = pht('This file was copied from %s.', $from); 105 break; 106 case DifferentialChangeType::FILE_IMAGE: 107 $messages[] = pht('This image was copied from %s.', $from); 108 break; 109 case DifferentialChangeType::FILE_DIRECTORY: 110 $messages[] = pht('This directory was copied from %s.', $from); 111 break; 112 case DifferentialChangeType::FILE_BINARY: 113 $messages[] = pht('This binary file was copied from %s.', $from); 114 break; 115 case DifferentialChangeType::FILE_SYMLINK: 116 $messages[] = pht('This symlink was copied from %s.', $from); 117 break; 118 case DifferentialChangeType::FILE_SUBMODULE: 119 $messages[] = pht('This submodule was copied from %s.', $from); 120 break; 121 } 122 break; 123 124 case DifferentialChangeType::TYPE_MOVE_AWAY: 125 $paths = phutil_tag( 126 'strong', 127 array(), 128 implode(', ', $changeset->getAwayPaths())); 129 switch ($file) { 130 case DifferentialChangeType::FILE_TEXT: 131 $messages[] = pht('This file was moved to %s.', $paths); 132 break; 133 case DifferentialChangeType::FILE_IMAGE: 134 $messages[] = pht('This image was moved to %s.', $paths); 135 break; 136 case DifferentialChangeType::FILE_DIRECTORY: 137 $messages[] = pht('This directory was moved to %s.', $paths); 138 break; 139 case DifferentialChangeType::FILE_BINARY: 140 $messages[] = pht('This binary file was moved to %s.', $paths); 141 break; 142 case DifferentialChangeType::FILE_SYMLINK: 143 $messages[] = pht('This symlink was moved to %s.', $paths); 144 break; 145 case DifferentialChangeType::FILE_SUBMODULE: 146 $messages[] = pht('This submodule was moved to %s.', $paths); 147 break; 148 } 149 break; 150 151 case DifferentialChangeType::TYPE_COPY_AWAY: 152 $paths = phutil_tag( 153 'strong', 154 array(), 155 implode(', ', $changeset->getAwayPaths())); 156 switch ($file) { 157 case DifferentialChangeType::FILE_TEXT: 158 $messages[] = pht('This file was copied to %s.', $paths); 159 break; 160 case DifferentialChangeType::FILE_IMAGE: 161 $messages[] = pht('This image was copied to %s.', $paths); 162 break; 163 case DifferentialChangeType::FILE_DIRECTORY: 164 $messages[] = pht('This directory was copied to %s.', $paths); 165 break; 166 case DifferentialChangeType::FILE_BINARY: 167 $messages[] = pht('This binary file was copied to %s.', $paths); 168 break; 169 case DifferentialChangeType::FILE_SYMLINK: 170 $messages[] = pht('This symlink was copied to %s.', $paths); 171 break; 172 case DifferentialChangeType::FILE_SUBMODULE: 173 $messages[] = pht('This submodule was copied to %s.', $paths); 174 break; 175 } 176 break; 177 178 case DifferentialChangeType::TYPE_MULTICOPY: 179 $paths = phutil_tag( 180 'strong', 181 array(), 182 implode(', ', $changeset->getAwayPaths())); 183 switch ($file) { 184 case DifferentialChangeType::FILE_TEXT: 185 $messages[] = pht( 186 'This file was deleted after being copied to %s.', 187 $paths); 188 break; 189 case DifferentialChangeType::FILE_IMAGE: 190 $messages[] = pht( 191 'This image was deleted after being copied to %s.', 192 $paths); 193 break; 194 case DifferentialChangeType::FILE_DIRECTORY: 195 $messages[] = pht( 196 'This directory was deleted after being copied to %s.', 197 $paths); 198 break; 199 case DifferentialChangeType::FILE_BINARY: 200 $messages[] = pht( 201 'This binary file was deleted after being copied to %s.', 202 $paths); 203 break; 204 case DifferentialChangeType::FILE_SYMLINK: 205 $messages[] = pht( 206 'This symlink was deleted after being copied to %s.', 207 $paths); 208 break; 209 case DifferentialChangeType::FILE_SUBMODULE: 210 $messages[] = pht( 211 'This submodule was deleted after being copied to %s.', 212 $paths); 213 break; 214 } 215 break; 216 217 default: 218 switch ($file) { 219 case DifferentialChangeType::FILE_TEXT: 220 // This is the default case, so we only render this header if 221 // forced to since it's not very useful. 222 if ($force) { 223 $messages[] = pht('This file was not modified.'); 224 } 225 break; 226 case DifferentialChangeType::FILE_IMAGE: 227 $messages[] = pht('This is an image.'); 228 break; 229 case DifferentialChangeType::FILE_DIRECTORY: 230 $messages[] = pht('This is a directory.'); 231 break; 232 case DifferentialChangeType::FILE_BINARY: 233 $messages[] = pht('This is a binary file.'); 234 break; 235 case DifferentialChangeType::FILE_SYMLINK: 236 $messages[] = pht('This is a symlink.'); 237 break; 238 case DifferentialChangeType::FILE_SUBMODULE: 239 $messages[] = pht('This is a submodule.'); 240 break; 241 } 242 break; 243 } 244 245 return $this->formatHeaderMessages($messages); 246 } 247 248 protected function renderUndershieldHeader() { 249 $messages = array(); 250 251 $changeset = $this->getChangeset(); 252 253 $file = $changeset->getFileType(); 254 255 // If this is a text file with at least one hunk, we may have converted 256 // the text encoding. In this case, show a note. 257 $show_encoding = ($file == DifferentialChangeType::FILE_TEXT) && 258 ($changeset->getHunks()); 259 260 if ($show_encoding) { 261 $encoding = $this->getOriginalCharacterEncoding(); 262 if ($encoding != 'utf8') { 263 if ($encoding) { 264 $messages[] = pht( 265 'This file was converted from %s for display.', 266 phutil_tag('strong', array(), $encoding)); 267 } else { 268 $messages[] = pht('This file uses an unknown character encoding.'); 269 } 270 } 271 } 272 273 $blocks = $this->getDocumentEngineBlocks(); 274 if ($blocks) { 275 foreach ($blocks->getMessages() as $message) { 276 $messages[] = $message; 277 } 278 } else { 279 if ($this->getHighlightingDisabled()) { 280 $byte_limit = DifferentialChangesetParser::HIGHLIGHT_BYTE_LIMIT; 281 $byte_limit = phutil_format_bytes($byte_limit); 282 $messages[] = pht( 283 'This file is larger than %s, so syntax highlighting is '. 284 'disabled by default.', 285 $byte_limit); 286 } 287 } 288 289 return $this->formatHeaderMessages($messages); 290 } 291 292 private function formatHeaderMessages(array $messages) { 293 if (!$messages) { 294 return null; 295 } 296 297 foreach ($messages as $key => $message) { 298 $messages[$key] = phutil_tag('li', array(), $message); 299 } 300 301 return phutil_tag( 302 'ul', 303 array( 304 'class' => 'differential-meta-notice', 305 ), 306 $messages); 307 } 308 309 protected function renderPropertyChangeHeader() { 310 $changeset = $this->getChangeset(); 311 list($old, $new) = $this->getChangesetProperties($changeset); 312 313 // If we don't have any property changes, don't render this table. 314 if ($old === $new) { 315 return null; 316 } 317 318 $keys = array_keys($old + $new); 319 sort($keys); 320 321 $key_map = array( 322 'unix:filemode' => pht('File Mode'), 323 'file:dimensions' => pht('Image Dimensions'), 324 'file:mimetype' => pht('MIME Type'), 325 'file:size' => pht('File Size'), 326 ); 327 328 $rows = array(); 329 foreach ($keys as $key) { 330 $oval = idx($old, $key); 331 $nval = idx($new, $key); 332 if ($oval !== $nval) { 333 if ($oval === null) { 334 $oval = phutil_tag('em', array(), 'null'); 335 } else { 336 $oval = phutil_escape_html_newlines($oval); 337 } 338 339 if ($nval === null) { 340 $nval = phutil_tag('em', array(), 'null'); 341 } else { 342 $nval = phutil_escape_html_newlines($nval); 343 } 344 345 $readable_key = idx($key_map, $key, $key); 346 347 $row = array( 348 $readable_key, 349 $oval, 350 $nval, 351 ); 352 $rows[] = $row; 353 354 } 355 } 356 357 $classes = array('', 'oval', 'nval'); 358 $headers = array( 359 pht('Property'), 360 pht('Old Value'), 361 pht('New Value'), 362 ); 363 $table = id(new AphrontTableView($rows)) 364 ->setHeaders($headers) 365 ->setColumnClasses($classes); 366 return phutil_tag( 367 'div', 368 array( 369 'class' => 'differential-property-table', 370 ), 371 $table); 372 } 373 374 public function renderShield($message, $force = 'default') { 375 $end = count($this->getOldLines()); 376 $reference = $this->getRenderingReference(); 377 378 if ($force !== 'text' && 379 $force !== 'none' && 380 $force !== 'default') { 381 throw new Exception( 382 pht( 383 "Invalid '%s' parameter '%s'!", 384 'force', 385 $force)); 386 } 387 388 $range = "0-{$end}"; 389 if ($force == 'text') { 390 // If we're forcing text, force the whole file to be rendered. 391 $range = "{$range}/0-{$end}"; 392 } 393 394 $meta = array( 395 'ref' => $reference, 396 'range' => $range, 397 ); 398 399 $content = array(); 400 $content[] = $message; 401 if ($force !== 'none') { 402 $content[] = ' '; 403 $content[] = javelin_tag( 404 'a', 405 array( 406 'mustcapture' => true, 407 'sigil' => 'show-more', 408 'class' => 'complete', 409 'href' => '#', 410 'meta' => $meta, 411 ), 412 pht('Show File Contents')); 413 } 414 415 return $this->wrapChangeInTable( 416 javelin_tag( 417 'tr', 418 array( 419 'sigil' => 'context-target', 420 ), 421 phutil_tag( 422 'td', 423 array( 424 'class' => 'differential-shield', 425 'colspan' => 6, 426 ), 427 $content))); 428 } 429 430 abstract protected function renderColgroup(); 431 432 433 protected function wrapChangeInTable($content) { 434 if (!$content) { 435 return null; 436 } 437 438 $classes = array(); 439 $classes[] = 'differential-diff'; 440 $classes[] = 'remarkup-code'; 441 $classes[] = 'PhabricatorMonospaced'; 442 $classes[] = $this->getRendererTableClass(); 443 444 $sigils = array(); 445 $sigils[] = 'differential-diff'; 446 foreach ($this->getTableSigils() as $sigil) { 447 $sigils[] = $sigil; 448 } 449 450 return javelin_tag( 451 'table', 452 array( 453 'class' => implode(' ', $classes), 454 'sigil' => implode(' ', $sigils), 455 ), 456 array( 457 $this->renderColgroup(), 458 $content, 459 )); 460 } 461 462 protected function getTableSigils() { 463 return array(); 464 } 465 466 protected function buildInlineComment( 467 PhabricatorInlineComment $comment, 468 $on_right = false) { 469 470 $viewer = $this->getUser(); 471 $edit = $viewer && 472 ($comment->getAuthorPHID() == $viewer->getPHID()) && 473 ($comment->isDraft()) 474 && $this->getShowEditAndReplyLinks(); 475 $allow_reply = (bool)$viewer && $this->getShowEditAndReplyLinks(); 476 $allow_done = !$comment->isDraft() && $this->getCanMarkDone(); 477 478 return id(new PHUIDiffInlineCommentDetailView()) 479 ->setViewer($viewer) 480 ->setInlineComment($comment) 481 ->setIsOnRight($on_right) 482 ->setHandles($this->getHandles()) 483 ->setMarkupEngine($this->getMarkupEngine()) 484 ->setEditable($edit) 485 ->setAllowReply($allow_reply) 486 ->setCanMarkDone($allow_done) 487 ->setObjectOwnerPHID($this->getObjectOwnerPHID()); 488 } 489 490 491 /** 492 * Build links which users can click to show more context in a changeset. 493 * 494 * @param int $top Beginning of the line range to build links for. 495 * @param int $len Length of the line range to build links for. 496 * @param int $changeset_length Total number of lines in the changeset. 497 * @param bool $is_blocks (optional) 498 * @return string Rendered links. 499 */ 500 protected function renderShowContextLinks( 501 $top, 502 $len, 503 $changeset_length, 504 $is_blocks = false) { 505 506 $block_size = 20; 507 $end = ($top + $len) - $block_size; 508 509 // If this is a large block, such that the "top" and "bottom" ranges are 510 // non-overlapping, we'll provide options to show the top, bottom or entire 511 // block. For smaller blocks, we only provide an option to show the entire 512 // block, since it would be silly to show the bottom 20 lines of a 25-line 513 // block. 514 $is_large_block = ($len > ($block_size * 2)); 515 516 $links = array(); 517 518 $block_display = new PhutilNumber($block_size); 519 520 if ($is_large_block) { 521 $is_first_block = ($top == 0); 522 if ($is_first_block) { 523 if ($is_blocks) { 524 $text = pht('Show First %s Block(s)', $block_display); 525 } else { 526 $text = pht('Show First %s Line(s)', $block_display); 527 } 528 } else { 529 if ($is_blocks) { 530 $text = pht("\xE2\x96\xB2 Show %s Block(s)", $block_display); 531 } else { 532 $text = pht("\xE2\x96\xB2 Show %s Line(s)", $block_display); 533 } 534 } 535 536 $links[] = $this->renderShowContextLink( 537 false, 538 "{$top}-{$len}/{$top}-20", 539 $text); 540 } 541 542 if ($is_blocks) { 543 $text = pht('Show All %s Block(s)', new PhutilNumber($len)); 544 } else { 545 $text = pht('Show All %s Line(s)', new PhutilNumber($len)); 546 } 547 548 $links[] = $this->renderShowContextLink( 549 true, 550 "{$top}-{$len}/{$top}-{$len}", 551 $text); 552 553 if ($is_large_block) { 554 $is_last_block = (($top + $len) >= $changeset_length); 555 if ($is_last_block) { 556 if ($is_blocks) { 557 $text = pht('Show Last %s Block(s)', $block_display); 558 } else { 559 $text = pht('Show Last %s Line(s)', $block_display); 560 } 561 } else { 562 if ($is_blocks) { 563 $text = pht("\xE2\x96\xBC Show %s Block(s)", $block_display); 564 } else { 565 $text = pht("\xE2\x96\xBC Show %s Line(s)", $block_display); 566 } 567 } 568 569 $links[] = $this->renderShowContextLink( 570 false, 571 "{$top}-{$len}/{$end}-20", 572 $text); 573 } 574 575 return phutil_implode_html(" \xE2\x80\xA2 ", $links); 576 } 577 578 579 /** 580 * Build a link that shows more context in a changeset. 581 * 582 * See @{method:renderShowContextLinks}. 583 * 584 * @param bool $is_all Does this link show all context when clicked? 585 * @param string $range Range specification for lines to show. 586 * @param string $text Text of the link. 587 * @return string Rendered link. 588 */ 589 private function renderShowContextLink($is_all, $range, $text) { 590 $reference = $this->getRenderingReference(); 591 592 return javelin_tag( 593 'a', 594 array( 595 'href' => '#', 596 'mustcapture' => true, 597 'sigil' => 'show-more', 598 'meta' => array( 599 'type' => ($is_all ? 'all' : null), 600 'range' => $range, 601 ), 602 ), 603 $text); 604 } 605 606 /** 607 * Build the prefixes for line IDs used to track inline comments. 608 * 609 * @return array{string|null,string|null} Left and right prefixes. 610 */ 611 protected function getLineIDPrefixes() { 612 // These look like "C123NL45", which means the line is line 45 on the 613 // "new" side of the file in changeset 123. 614 615 // The "C" stands for "changeset", and is followed by a changeset ID. 616 617 // "N" stands for "new" and means the comment should attach to the new file 618 // when stored. "O" stands for "old" and means the comment should attach to 619 // the old file. These are important because either the old or new part 620 // of a file may appear on the left or right side of the diff in the 621 // diff-of-diffs view. 622 623 // The "L" stands for "line" and is followed by the line number. 624 625 if ($this->getOldChangesetID()) { 626 $left_prefix = array(); 627 $left_prefix[] = 'C'; 628 $left_prefix[] = $this->getOldChangesetID(); 629 $left_prefix[] = $this->getOldAttachesToNewFile() ? 'N' : 'O'; 630 $left_prefix[] = 'L'; 631 $left_prefix = implode('', $left_prefix); 632 } else { 633 $left_prefix = null; 634 } 635 636 if ($this->getNewChangesetID()) { 637 $right_prefix = array(); 638 $right_prefix[] = 'C'; 639 $right_prefix[] = $this->getNewChangesetID(); 640 $right_prefix[] = $this->getNewAttachesToNewFile() ? 'N' : 'O'; 641 $right_prefix[] = 'L'; 642 $right_prefix = implode('', $right_prefix); 643 } else { 644 $right_prefix = null; 645 } 646 647 return array($left_prefix, $right_prefix); 648 } 649 650}