@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 519 lines 13 kB view raw
1<?php 2 3final class DifferentialInlineCommentMailView 4 extends DifferentialMailView { 5 6 private $viewer; 7 private $inlines; 8 private $changesets; 9 private $authors; 10 11 public function setViewer(PhabricatorUser $viewer) { 12 $this->viewer = $viewer; 13 return $this; 14 } 15 16 public function getViewer() { 17 return $this->viewer; 18 } 19 20 public function setInlines($inlines) { 21 $this->inlines = $inlines; 22 return $this; 23 } 24 25 public function getInlines() { 26 return $this->inlines; 27 } 28 29 public function buildMailSection() { 30 $inlines = $this->getInlines(); 31 32 $comments = mpull($inlines, 'getComment'); 33 $comments = mpull($comments, null, 'getPHID'); 34 $parents = $this->loadParents($comments); 35 $all_comments = $comments + $parents; 36 37 $this->changesets = $this->loadChangesets($all_comments); 38 $this->authors = $this->loadAuthors($all_comments); 39 $groups = $this->groupInlines($inlines); 40 41 $hunk_parser = new DifferentialHunkParser(); 42 43 $spacer_text = null; 44 $spacer_html = phutil_tag('br'); 45 46 $section = new PhabricatorMetaMTAMailSection(); 47 48 $last_group_key = last_key($groups); 49 foreach ($groups as $changeset_id => $group) { 50 $changeset = $this->getChangeset($changeset_id); 51 if (!$changeset) { 52 continue; 53 } 54 55 $is_last_group = ($changeset_id == $last_group_key); 56 57 $last_inline_key = last_key($group); 58 foreach ($group as $inline_key => $inline) { 59 $comment = $inline->getComment(); 60 $parent_phid = $comment->getReplyToCommentPHID(); 61 62 $inline_object = $comment->newInlineCommentObject(); 63 $document_engine_key = $inline_object->getDocumentEngineKey(); 64 65 $is_last_inline = ($inline_key == $last_inline_key); 66 67 $context_text = null; 68 $context_html = null; 69 70 if ($parent_phid) { 71 $parent = idx($parents, $parent_phid); 72 if ($parent) { 73 $context_text = $this->renderInline($parent, false, true); 74 $context_html = $this->renderInline($parent, true, true); 75 } 76 } else if ($document_engine_key !== null) { 77 // See T13513. If an inline was left on a rendered document, don't 78 // include the patch context. Document engines currently can not 79 // render to mail targets, and using the line numbers as raw source 80 // lines produces misleading context. 81 82 $patch_text = null; 83 $context_text = $this->renderPatch($comment, $patch_text, false); 84 85 $patch_html = null; 86 $context_html = $this->renderPatch($comment, $patch_html, true); 87 } else { 88 $patch_text = $this->getPatch($hunk_parser, $comment, false); 89 $context_text = $this->renderPatch($comment, $patch_text, false); 90 91 $patch_html = $this->getPatch($hunk_parser, $comment, true); 92 $context_html = $this->renderPatch($comment, $patch_html, true); 93 } 94 95 $render_text = $this->renderInline($comment, false, false); 96 $render_html = $this->renderInline($comment, true, false); 97 98 $section->addPlaintextFragment($context_text); 99 $section->addPlaintextFragment($spacer_text); 100 $section->addPlaintextFragment($render_text); 101 102 $html_fragment = $this->renderContentBox( 103 array( 104 $context_html, 105 $render_html, 106 )); 107 108 $section->addHTMLFragment($html_fragment); 109 110 if (!$is_last_group || !$is_last_inline) { 111 $section->addPlaintextFragment($spacer_text); 112 $section->addHTMLFragment($spacer_html); 113 } 114 } 115 } 116 117 return $section; 118 } 119 120 private function loadChangesets(array $comments) { 121 if (!$comments) { 122 return array(); 123 } 124 125 $ids = array(); 126 foreach ($comments as $comment) { 127 $ids[] = $comment->getChangesetID(); 128 } 129 130 $changesets = id(new DifferentialChangesetQuery()) 131 ->setViewer($this->getViewer()) 132 ->withIDs($ids) 133 ->needHunks(true) 134 ->execute(); 135 136 return mpull($changesets, null, 'getID'); 137 } 138 139 private function loadParents(array $comments) { 140 $viewer = $this->getViewer(); 141 142 $phids = array(); 143 foreach ($comments as $comment) { 144 $parent_phid = $comment->getReplyToCommentPHID(); 145 if (!$parent_phid) { 146 continue; 147 } 148 $phids[] = $parent_phid; 149 } 150 151 if (!$phids) { 152 return array(); 153 } 154 155 $parents = id(new DifferentialDiffInlineCommentQuery()) 156 ->setViewer($viewer) 157 ->withPHIDs($phids) 158 ->execute(); 159 160 return mpull($parents, null, 'getPHID'); 161 } 162 163 private function loadAuthors(array $comments) { 164 $viewer = $this->getViewer(); 165 166 $phids = array(); 167 foreach ($comments as $comment) { 168 $author_phid = $comment->getAuthorPHID(); 169 if (!$author_phid) { 170 continue; 171 } 172 $phids[] = $author_phid; 173 } 174 175 if (!$phids) { 176 return array(); 177 } 178 179 return $viewer->loadHandles($phids); 180 } 181 182 private function groupInlines(array $inlines) { 183 return DifferentialTransactionComment::sortAndGroupInlines( 184 $inlines, 185 $this->changesets); 186 } 187 188 private function renderInline( 189 DifferentialTransactionComment $comment, 190 $is_html, 191 $is_quote) { 192 193 $changeset = $this->getChangeset($comment->getChangesetID()); 194 if (!$changeset) { 195 return null; 196 } 197 198 $content = $comment->getContent(); 199 $content = $this->renderRemarkupContent($content, $is_html); 200 201 if ($is_quote) { 202 $header = $this->renderHeader($comment, $is_html, true); 203 } else { 204 $header = null; 205 } 206 207 if ($is_html) { 208 $style = array( 209 'margin: 8px 0;', 210 'padding: 0 12px;', 211 ); 212 213 if ($is_quote) { 214 $style[] = 'color: #74777D;'; 215 } 216 217 $content = phutil_tag( 218 'div', 219 array( 220 'style' => implode(' ', $style), 221 ), 222 $content); 223 } 224 225 $parts = array( 226 $header, 227 "\n", 228 $content, 229 ); 230 231 if (!$is_html) { 232 $parts = implode('', $parts); 233 $parts = trim($parts); 234 } 235 236 if ($is_quote) { 237 if ($is_html) { 238 $parts = $this->quoteHTML($parts); 239 } else { 240 $parts = $this->quoteText($parts); 241 } 242 } 243 244 return $parts; 245 } 246 247 private function renderRemarkupContent($content, $is_html) { 248 $viewer = $this->getViewer(); 249 $production_uri = PhabricatorEnv::getProductionURI('/'); 250 251 if ($is_html) { 252 $mode = PhutilRemarkupEngine::MODE_HTML_MAIL; 253 } else { 254 $mode = PhutilRemarkupEngine::MODE_TEXT; 255 } 256 257 $attributes = array( 258 'style' => 'padding: 0; margin: 8px;', 259 ); 260 261 $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) 262 ->setConfig('viewer', $viewer) 263 ->setConfig('uri.base', $production_uri) 264 ->setConfig('default.p.attributes', $attributes) 265 ->setMode($mode); 266 267 try { 268 return $engine->markupText($content); 269 } catch (Exception $ex) { 270 return $content; 271 } 272 } 273 274 private function getChangeset($id) { 275 return idx($this->changesets, $id); 276 } 277 278 private function getAuthor($phid) { 279 if (isset($this->authors[$phid])) { 280 return $this->authors[$phid]; 281 } 282 return null; 283 } 284 285 private function quoteText($block) { 286 $block = phutil_split_lines($block); 287 foreach ($block as $key => $line) { 288 $block[$key] = '> '.$line; 289 } 290 291 return implode('', $block); 292 } 293 294 private function quoteHTML($block) { 295 $styles = array( 296 'padding: 0;', 297 'background: #F7F7F7;', 298 'border-color: #e3e4e8;', 299 'border-style: solid;', 300 'border-width: 0 0 1px 0;', 301 'margin: 0;', 302 ); 303 304 $styles = implode(' ', $styles); 305 306 return phutil_tag( 307 'div', 308 array( 309 'style' => $styles, 310 ), 311 $block); 312 } 313 314 private function getPatch( 315 DifferentialHunkParser $parser, 316 DifferentialTransactionComment $comment, 317 $is_html) { 318 319 $changeset = $this->getChangeset($comment->getChangesetID()); 320 $is_new = $comment->getIsNewFile(); 321 $start = $comment->getLineNumber(); 322 $length = $comment->getLineLength(); 323 324 // By default, show one line of context around the target inline. 325 $context = 1; 326 327 // If the inline is at least 3 lines long, don't show any extra context. 328 if ($length >= 2) { 329 $context = 0; 330 } 331 332 // If the inline is more than 7 lines long, only show the first 7 lines. 333 if ($length >= 6) { 334 $length = 6; 335 } 336 337 if (!$is_html) { 338 $hunks = $changeset->getHunks(); 339 $patch = $parser->makeContextDiff( 340 $hunks, 341 $is_new, 342 $start, 343 $length, 344 $context); 345 $patch = phutil_split_lines($patch); 346 347 // Remove the "@@ -x,y +u,v @@" line. 348 array_shift($patch); 349 350 return implode('', $patch); 351 } 352 353 $viewer = $this->getViewer(); 354 $engine = new PhabricatorMarkupEngine(); 355 356 if ($is_new) { 357 $offset_mode = 'new'; 358 } else { 359 $offset_mode = 'old'; 360 } 361 362 // See PHI894. Use the parse cache since we can end up with a large 363 // rendering cost otherwise when users or bots leave hundreds of inline 364 // comments on diffs with long recipient lists. 365 $cache_key = $changeset->getID(); 366 367 $viewstate = new PhabricatorChangesetViewState(); 368 369 $parser = id(new DifferentialChangesetParser()) 370 ->setRenderCacheKey($cache_key) 371 ->setViewer($viewer) 372 ->setViewstate($viewstate) 373 ->setChangeset($changeset) 374 ->setOffsetMode($offset_mode) 375 ->setMarkupEngine($engine); 376 377 $parser->setRenderer(new DifferentialChangesetOneUpMailRenderer()); 378 379 return $parser->render( 380 $start - $context, 381 $length + (2 * $context), 382 array()); 383 } 384 385 private function renderPatch( 386 DifferentialTransactionComment $comment, 387 $patch, 388 $is_html) { 389 390 if ($is_html) { 391 if ($patch !== null) { 392 $patch = $this->renderCodeBlock($patch); 393 } 394 } 395 396 $header = $this->renderHeader($comment, $is_html, false); 397 398 if ($patch === null) { 399 $patch = array( 400 $header, 401 ); 402 } else { 403 $patch = array( 404 $header, 405 "\n", 406 $patch, 407 ); 408 } 409 410 if (!$is_html) { 411 $patch = implode('', $patch); 412 $patch = $this->quoteText($patch); 413 } else { 414 $patch = $this->quoteHTML($patch); 415 } 416 417 return $patch; 418 } 419 420 private function renderHeader( 421 DifferentialTransactionComment $comment, 422 $is_html, 423 $with_author) { 424 425 $changeset = $this->getChangeset($comment->getChangesetID()); 426 $path = $changeset->getFilename(); 427 428 // Only show the filename. 429 $path = basename($path); 430 431 $start = $comment->getLineNumber(); 432 $length = $comment->getLineLength(); 433 if ($length) { 434 $range = pht('%s-%s', $start, $start + $length); 435 } else { 436 $range = $start; 437 } 438 439 $header = "{$path}:{$range}"; 440 if ($is_html) { 441 $header = $this->renderHeaderBold($header); 442 } 443 444 if ($with_author) { 445 $author = $this->getAuthor($comment->getAuthorPHID()); 446 } else { 447 $author = null; 448 } 449 450 if ($author) { 451 $byline = $author->getName(); 452 453 if ($is_html) { 454 $byline = $this->renderHeaderBold($byline); 455 } 456 457 $header = pht('%s wrote in %s', $byline, $header); 458 } 459 460 if ($is_html) { 461 $link_href = $this->getInlineURI($comment); 462 if ($link_href) { 463 $link_style = array( 464 'float: right;', 465 'text-decoration: none;', 466 ); 467 468 $link = phutil_tag( 469 'a', 470 array( 471 'style' => implode(' ', $link_style), 472 'href' => $link_href, 473 ), 474 array( 475 pht('View Inline'), 476 477 // See PHI920. Add a space after the link so we render this into 478 // the document: 479 // 480 // View Inline filename.txt 481 // 482 // Otherwise, we render "Inlinefilename.txt" and double-clicking 483 // the file name selects the word "Inline" as well. 484 ' ', 485 )); 486 } else { 487 $link = null; 488 } 489 490 $header = $this->renderHeaderBlock(array($link, $header)); 491 } 492 493 return $header; 494 } 495 496 private function getInlineURI(DifferentialTransactionComment $comment) { 497 $changeset = $this->getChangeset($comment->getChangesetID()); 498 if (!$changeset) { 499 return null; 500 } 501 502 $diff = $changeset->getDiff(); 503 if (!$diff) { 504 return null; 505 } 506 507 $revision = $diff->getRevision(); 508 if (!$revision) { 509 return null; 510 } 511 512 $link_href = '/'.$revision->getMonogram().'#inline-'.$comment->getID(); 513 $link_href = PhabricatorEnv::getProductionURI($link_href); 514 515 return $link_href; 516 } 517 518 519}