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

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 }