thunderbird-patch-review
Easy-to-use patch review interface for Thunderbird
Contribute: ~marcc/thunderbird-review-plugin@lists.sr.ht
git clone git://mccd.space/thunderbird-patch-review
| Log | Files | Refs | README | LICENSE |
reply-format.js (2978B)
1 // Turn draft comments into a plain-text, interleaved mailing-list review:
2 // the commented regions of the patch are quoted with "> " and each comment
3 // sits unquoted below the line it targets — kernel-style.
4
5 import { parseLocator, GENERAL } from "./review-store.js";
6
7 const CONTEXT_BEFORE = 3; // quoted lines shown above the first comment in a hunk
8
9 /**
10 * @param {{commitMessage: string, files: Array, bodyLines: string[]}} parsed
11 * output of parsePatchEmail() for this patch
12 * @param {Record<string, {text: string}>} comments locator -> comment
13 * @returns {string} reply body ("" when there are no comments)
14 */
15 export function formatReview(parsed, comments) {
16 const entries = Object.entries(comments || {});
17 if (entries.length === 0) {
18 return "";
19 }
20
21 const out = [];
22
23 const general = comments[GENERAL];
24 if (general) {
25 out.push(general.text, "");
26 }
27
28 // Group line comments by file/hunk, in diff order.
29 const byHunk = new Map(); // "f:h" -> [{lineIndex, text}]
30 for (const [loc, comment] of entries) {
31 if (loc === GENERAL) {
32 continue;
33 }
34 const { fileIndex, hunkIndex, lineIndex } = parseLocator(loc);
35 const k = `${fileIndex}:${hunkIndex}`;
36 if (!byHunk.has(k)) {
37 byHunk.set(k, []);
38 }
39 byHunk.get(k).push({ fileIndex, hunkIndex, lineIndex, text: comment.text });
40 }
41
42 const orderedHunks = [...byHunk.values()]
43 .map((list) => list.sort((a, b) => a.lineIndex - b.lineIndex))
44 .sort((a, b) => a[0].fileIndex - b[0].fileIndex || a[0].hunkIndex - b[0].hunkIndex);
45
46 let lastFileIndex = -1;
47 for (const hunkComments of orderedHunks) {
48 const { fileIndex, hunkIndex } = hunkComments[0];
49 const file = parsed.files[fileIndex];
50 const hunk = file && file.hunks[hunkIndex];
51 if (!hunk) {
52 continue;
53 }
54
55 if (fileIndex !== lastFileIndex) {
56 out.push(`> diff --git a/${file.oldPath} b/${file.newPath}`);
57 lastFileIndex = fileIndex;
58 }
59 out.push(`> ${hunk.header}`);
60
61 // Quote from a little context above the first commented line through the
62 // last commented line, inserting comments after their targets.
63 const firstTarget = hunkComments[0].lineIndex;
64 const lastTarget = hunkComments[hunkComments.length - 1].lineIndex;
65 const start = Math.max(0, firstTarget - CONTEXT_BEFORE);
66 if (start > 0) {
67 out.push("> [...]");
68 }
69
70 let next = 0; // index into hunkComments
71 for (let i = start; i <= lastTarget && i < hunk.lines.length; i++) {
72 out.push(`> ${hunk.lines[i].raw}`);
73 while (next < hunkComments.length && hunkComments[next].lineIndex === i) {
74 out.push("", hunkComments[next].text, "");
75 next++;
76 }
77 }
78 out.push("");
79 }
80
81 while (out.length && out[out.length - 1] === "") {
82 out.pop();
83 }
84 out.push("");
85 return out.join("\n");
86 }
87
88 /** Comment count for a patch, general comment included. */
89 export function commentCount(comments) {
90 return Object.keys(comments || {}).length;
91 }