@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 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}