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

Configure Feed

Select the types of activity you want to include in your feed.

Rough cut of inline comment context

Summary:
Ref T10694. This is still missing some pieces, but seems to get most of the data into the mail in a plausible format:

- When an inline remarks on code, show the patch inline in the mail body.
- When an inline replies to another inline, show that other inline in the mail body.
- Apply remarkup rendering to inline content.
- Apply basic styling to mail body blocks.

Not covered yet:

- Syntax highlighting.
- Diff highlighting.
- Maybe clearer style/layout hints to connect comments to what they reply to? Current approach might get messy with inlines that have blockquotes and code blocks inside them, for example.
- I probably want to cap the amount of diff context we ever show to ~7 lines, even if you drag over 200 lines of code.
- CSS is a generally a bit rough still.
- The `unified-comment-context` option is effectively always on now, and should be removed.
- Text section is getting indented right now but probably shouldn't be.
- Spacing, etc., might be a bit off.

Test Plan:
Rigged Home to render these things, got a plausible-looking render (top is text, bottom is HTML):

{F1259052}

Sent myself some inline comment mail, got a plausible result.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10694

Differential Revision: https://secure.phabricator.com/D15850

+387 -128
+2
src/__phutil_library_map__.php
··· 459 459 'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php', 460 460 'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php', 461 461 'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php', 462 + 'DifferentialInlineCommentMailView' => 'applications/differential/mail/DifferentialInlineCommentMailView.php', 462 463 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php', 463 464 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 464 465 'DifferentialJIRAIssuesField' => 'applications/differential/customfield/DifferentialJIRAIssuesField.php', ··· 4660 4661 'PhabricatorInlineCommentInterface', 4661 4662 ), 4662 4663 'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController', 4664 + 'DifferentialInlineCommentMailView' => 'Phobject', 4663 4665 'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 4664 4666 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 4665 4667 'DifferentialJIRAIssuesField' => 'DifferentialStoredCustomField',
+3 -128
src/applications/differential/editor/DifferentialTransactionEditor.php
··· 1374 1374 return $result; 1375 1375 } 1376 1376 1377 - protected function indentForMail(array $lines) { 1378 - $indented = array(); 1379 - foreach ($lines as $line) { 1380 - $indented[] = '> '.$line; 1381 - } 1382 - return $indented; 1383 - } 1384 - 1385 - protected function nestCommentHistory( 1386 - DifferentialTransactionComment $comment, array $comments_by_line_number, 1387 - array $users_by_phid) { 1388 - 1389 - $nested = array(); 1390 - $previous_comments = $comments_by_line_number[$comment->getChangesetID()] 1391 - [$comment->getLineNumber()]; 1392 - foreach ($previous_comments as $previous_comment) { 1393 - if ($previous_comment->getID() >= $comment->getID()) { 1394 - break; 1395 - } 1396 - $nested = $this->indentForMail( 1397 - array_merge( 1398 - $nested, 1399 - explode("\n", $previous_comment->getContent()))); 1400 - $user = idx($users_by_phid, $previous_comment->getAuthorPHID(), null); 1401 - if ($user) { 1402 - array_unshift($nested, pht('%s wrote:', $user->getUserName())); 1403 - } 1404 - } 1405 - 1406 - $nested = array_merge($nested, explode("\n", $comment->getContent())); 1407 - return implode("\n", $nested); 1408 - } 1409 - 1410 1377 private function renderInlineCommentsForMail( 1411 1378 PhabricatorLiskDAO $object, 1412 1379 array $inlines) { 1413 - 1414 - $context_key = 'metamta.differential.unified-comment-context'; 1415 - $show_context = PhabricatorEnv::getEnvConfig($context_key); 1416 - 1417 - $changeset_ids = array(); 1418 - $line_numbers_by_changeset = array(); 1419 - foreach ($inlines as $inline) { 1420 - $id = $inline->getComment()->getChangesetID(); 1421 - $changeset_ids[$id] = $id; 1422 - $line_numbers_by_changeset[$id][] = 1423 - $inline->getComment()->getLineNumber(); 1424 - } 1425 - 1426 - $changesets = id(new DifferentialChangesetQuery()) 1380 + return id(new DifferentialInlineCommentMailView()) 1427 1381 ->setViewer($this->getActor()) 1428 - ->withIDs($changeset_ids) 1429 - ->needHunks(true) 1430 - ->execute(); 1431 - 1432 - $inline_groups = DifferentialTransactionComment::sortAndGroupInlines( 1433 - $inlines, 1434 - $changesets); 1435 - 1436 - if ($show_context) { 1437 - $hunk_parser = new DifferentialHunkParser(); 1438 - $table = new DifferentialTransactionComment(); 1439 - $conn_r = $table->establishConnection('r'); 1440 - $queries = array(); 1441 - foreach ($line_numbers_by_changeset as $id => $line_numbers) { 1442 - $queries[] = qsprintf( 1443 - $conn_r, 1444 - '(changesetID = %d AND lineNumber IN (%Ld))', 1445 - $id, $line_numbers); 1446 - } 1447 - $all_comments = id(new DifferentialTransactionComment())->loadAllWhere( 1448 - 'transactionPHID IS NOT NULL AND (%Q)', implode(' OR ', $queries)); 1449 - $comments_by_line_number = array(); 1450 - foreach ($all_comments as $comment) { 1451 - $comments_by_line_number 1452 - [$comment->getChangesetID()] 1453 - [$comment->getLineNumber()] 1454 - [$comment->getID()] = $comment; 1455 - } 1456 - $author_phids = mpull($all_comments, 'getAuthorPHID'); 1457 - $authors = id(new PhabricatorPeopleQuery()) 1458 - ->setViewer($this->getActor()) 1459 - ->withPHIDs($author_phids) 1460 - ->execute(); 1461 - $authors_by_phid = mpull($authors, null, 'getPHID'); 1462 - } 1463 - 1464 - $section = new PhabricatorMetaMTAMailSection(); 1465 - foreach ($inline_groups as $changeset_id => $group) { 1466 - $changeset = idx($changesets, $changeset_id); 1467 - if (!$changeset) { 1468 - continue; 1469 - } 1470 - 1471 - foreach ($group as $inline) { 1472 - $comment = $inline->getComment(); 1473 - $file = $changeset->getFilename(); 1474 - $start = $comment->getLineNumber(); 1475 - $len = $comment->getLineLength(); 1476 - if ($len) { 1477 - $range = $start.'-'.($start + $len); 1478 - } else { 1479 - $range = $start; 1480 - } 1481 - 1482 - $inline_content = $comment->getContent(); 1483 - 1484 - if (!$show_context) { 1485 - $section->addFragment("{$file}:{$range} {$inline_content}"); 1486 - } else { 1487 - $patch = $hunk_parser->makeContextDiff( 1488 - $changeset->getHunks(), 1489 - $comment->getIsNewFile(), 1490 - $comment->getLineNumber(), 1491 - $comment->getLineLength(), 1492 - 1); 1493 - $nested_comments = $this->nestCommentHistory( 1494 - $inline->getComment(), $comments_by_line_number, $authors_by_phid); 1495 - 1496 - $section 1497 - ->addFragment('================') 1498 - ->addFragment(pht('Comment at: %s:%s', $file, $range)) 1499 - ->addPlaintextFragment($patch) 1500 - ->addHTMLFragment($this->renderPatchHTMLForMail($patch)) 1501 - ->addFragment('----------------') 1502 - ->addFragment($nested_comments) 1503 - ->addFragment(null); 1504 - } 1505 - } 1506 - } 1507 - 1508 - return $section; 1382 + ->setInlines($inlines) 1383 + ->buildMailSection(); 1509 1384 } 1510 1385 1511 1386 private function loadDiff($phid, $need_changesets = false) {
+382
src/applications/differential/mail/DifferentialInlineCommentMailView.php
··· 1 + <?php 2 + 3 + final class DifferentialInlineCommentMailView 4 + extends Phobject { 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 + $is_last_inline = ($inline_key == $last_inline_key); 63 + 64 + $context_text = null; 65 + $context_html = null; 66 + 67 + if ($parent_phid) { 68 + $parent = idx($parents, $parent_phid); 69 + if ($parent) { 70 + $context_text = $this->renderInline($parent, false, true); 71 + $context_html = $this->renderInline($parent, true, true); 72 + } 73 + } else { 74 + $patch = $this->getPatch($hunk_parser, $comment); 75 + $context_text = $this->renderPatch($comment, $patch, false); 76 + $context_html = $this->renderPatch($comment, $patch, true); 77 + } 78 + 79 + $render_text = $this->renderInline($comment, false, false); 80 + $render_html = $this->renderInline($comment, true, false); 81 + 82 + $section->addPlaintextFragment($context_text); 83 + $section->addHTMLFragment($context_html); 84 + 85 + $section->addPlaintextFragment($spacer_text); 86 + $section->addHTMLFragment($spacer_html); 87 + 88 + $section->addPlaintextFragment($render_text); 89 + $section->addHTMLFragment($render_html); 90 + 91 + if (!$is_last_group || !$is_last_inline) { 92 + $section->addPlaintextFragment($spacer_text); 93 + $section->addHTMLFragment($spacer_html); 94 + } 95 + } 96 + } 97 + 98 + return $section; 99 + } 100 + 101 + private function loadChangesets(array $comments) { 102 + if (!$comments) { 103 + return array(); 104 + } 105 + 106 + $ids = array(); 107 + foreach ($comments as $comment) { 108 + $ids[] = $comment->getChangesetID(); 109 + } 110 + 111 + $changesets = id(new DifferentialChangesetQuery()) 112 + ->setViewer($this->getViewer()) 113 + ->withIDs($ids) 114 + ->needHunks(true) 115 + ->execute(); 116 + 117 + return mpull($changesets, null, 'getID'); 118 + } 119 + 120 + private function loadParents(array $comments) { 121 + $viewer = $this->getViewer(); 122 + 123 + $phids = array(); 124 + foreach ($comments as $comment) { 125 + $parent_phid = $comment->getReplyToCommentPHID(); 126 + if (!$parent_phid) { 127 + continue; 128 + } 129 + $phids[] = $parent_phid; 130 + } 131 + 132 + if (!$phids) { 133 + return array(); 134 + } 135 + 136 + $parents = id(new DifferentialDiffInlineCommentQuery()) 137 + ->setViewer($viewer) 138 + ->withPHIDs($phids) 139 + ->execute(); 140 + 141 + return mpull($parents, null, 'getPHID'); 142 + } 143 + 144 + private function loadAuthors(array $comments) { 145 + $viewer = $this->getViewer(); 146 + 147 + $phids = array(); 148 + foreach ($comments as $comment) { 149 + $author_phid = $comment->getAuthorPHID(); 150 + if (!$author_phid) { 151 + continue; 152 + } 153 + $phids[] = $author_phid; 154 + } 155 + 156 + if (!$phids) { 157 + return array(); 158 + } 159 + 160 + return $viewer->loadHandles($phids); 161 + } 162 + 163 + private function groupInlines(array $inlines) { 164 + return DifferentialTransactionComment::sortAndGroupInlines( 165 + $inlines, 166 + $this->changesets); 167 + } 168 + 169 + private function renderInline( 170 + DifferentialTransactionComment $comment, 171 + $is_html, 172 + $is_quote) { 173 + 174 + $changeset = $this->getChangeset($comment->getChangesetID()); 175 + if (!$changeset) { 176 + return null; 177 + } 178 + 179 + $content = $comment->getContent(); 180 + $content = $this->renderRemarkupContent($content, $is_html); 181 + 182 + if ($is_quote) { 183 + $header = $this->renderHeader($comment, $is_html, true); 184 + } else { 185 + $header = null; 186 + } 187 + 188 + $parts = array( 189 + $header, 190 + "\n", 191 + $content, 192 + ); 193 + 194 + if (!$is_html) { 195 + $parts = implode('', $parts); 196 + $parts = trim($parts); 197 + } 198 + 199 + if ($is_quote) { 200 + if ($is_html) { 201 + $parts = $this->quoteHTML($parts); 202 + } else { 203 + $parts = $this->quoteText($parts); 204 + } 205 + } 206 + 207 + return $parts; 208 + } 209 + 210 + private function renderRemarkupContent($content, $is_html) { 211 + $viewer = $this->getViewer(); 212 + $production_uri = PhabricatorEnv::getProductionURI('/'); 213 + 214 + if ($is_html) { 215 + $mode = PhutilRemarkupEngine::MODE_HTML_MAIL; 216 + } else { 217 + $mode = PhutilRemarkupEngine::MODE_TEXT; 218 + } 219 + 220 + $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) 221 + ->setConfig('viewer', $viewer) 222 + ->setConfig('uri.base', $production_uri) 223 + ->setMode($mode); 224 + 225 + try { 226 + return $engine->markupText($content); 227 + } catch (Exception $ex) { 228 + return $content; 229 + } 230 + } 231 + 232 + private function getChangeset($id) { 233 + return idx($this->changesets, $id); 234 + } 235 + 236 + private function getAuthor($phid) { 237 + if (isset($this->authors[$phid])) { 238 + return $this->authors[$phid]; 239 + } 240 + return null; 241 + } 242 + 243 + private function quoteText($block) { 244 + $block = phutil_split_lines($block); 245 + foreach ($block as $key => $line) { 246 + $block[$key] = '> '.$line; 247 + } 248 + 249 + return implode('', $block); 250 + } 251 + 252 + private function quoteHTML($block) { 253 + $styles = array( 254 + 'padding: 4px 8px;', 255 + 'background: #F8F9FC;', 256 + 'border-left: 3px solid #a7b5bf;', 257 + ); 258 + 259 + $styles = implode(' ', $styles); 260 + 261 + return phutil_tag( 262 + 'div', 263 + array( 264 + 'style' => $styles, 265 + ), 266 + $block); 267 + } 268 + 269 + private function getPatch( 270 + DifferentialHunkParser $parser, 271 + DifferentialTransactionComment $comment) { 272 + 273 + $changeset = $this->getChangeset($comment->getChangesetID()); 274 + $hunks = $changeset->getHunks(); 275 + 276 + $is_new = $comment->getIsNewFile(); 277 + $start = $comment->getLineNumber(); 278 + $length = $comment->getLineLength(); 279 + 280 + $diff = $parser->makeContextDiff($hunks, $is_new, $start, $length, 1); 281 + 282 + return $diff; 283 + } 284 + 285 + private function renderPatch( 286 + DifferentialTransactionComment $comment, 287 + $patch, 288 + $is_html) { 289 + 290 + $patch = phutil_split_lines($patch); 291 + 292 + // Remove the "@@ -x,y +u,v @@" line. 293 + array_shift($patch); 294 + 295 + $patch = implode('', $patch); 296 + 297 + if ($is_html) { 298 + $style = array( 299 + 'font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;', 300 + 'padding: 0', 301 + 'margin: 0;', 302 + ); 303 + 304 + $style = implode(' ', $style); 305 + $patch = phutil_tag( 306 + 'pre', 307 + array( 308 + 'style' => $style, 309 + ), 310 + $patch); 311 + } 312 + 313 + $header = $this->renderHeader($comment, $is_html, false); 314 + 315 + $patch = array( 316 + $header, 317 + "\n", 318 + $patch, 319 + ); 320 + 321 + if (!$is_html) { 322 + $patch = implode('', $patch); 323 + $patch = $this->quoteText($patch); 324 + } else { 325 + $patch = $this->quoteHTML($patch); 326 + } 327 + 328 + return $patch; 329 + } 330 + 331 + private function renderHeader( 332 + DifferentialTransactionComment $comment, 333 + $is_html, 334 + $with_author) { 335 + 336 + $changeset = $this->getChangeset($comment->getChangesetID()); 337 + $path = $changeset->getFilename(); 338 + 339 + $start = $comment->getLineNumber(); 340 + $length = $comment->getLineLength(); 341 + if ($length) { 342 + $range = pht('%s-%s', $start, $start + $length); 343 + } else { 344 + $range = $start; 345 + } 346 + 347 + $header = "{$path}:{$range}"; 348 + if ($is_html) { 349 + $header = phutil_tag('strong', array(), $header); 350 + } 351 + 352 + if ($with_author) { 353 + $author = $this->getAuthor($comment->getAuthorPHID()); 354 + } else { 355 + $author = null; 356 + } 357 + 358 + if ($author) { 359 + $byline = '@'.$author->getName(); 360 + 361 + if ($is_html) { 362 + $byline = phutil_tag('strong', array(), $byline); 363 + } 364 + 365 + $header = pht('%s wrote in %s', $byline, $header); 366 + } else { 367 + $header = pht('In %s', $header); 368 + } 369 + 370 + if ($is_html) { 371 + $header = phutil_tag( 372 + 'div', 373 + array( 374 + 'style' => 'font-style: italic', 375 + ), 376 + $header); 377 + } 378 + 379 + return $header; 380 + } 381 + 382 + }