merge(pr-留言): 把議題支援套到 pr-threads 的共用結構上
#48 把留言讀取抽成 pr-threads 給 pr-watch 與 pr-comments 共用,本分支則在修「純議題讀不了」 與「已處理的判定有兩份」。兩邊動到同一塊,但要的其實是同一件事——#48 的檔頭就寫著 「規則寫兩份遲早會各自演化,一邊認自己打的 +1、另一邊認任何人的」,而那正是本分支在 lib 與 pr-comments 之間發現的那個 bug。 合併的方向是保留 pr-threads 的結構,把修正套進去: - readGeneral 改名 readGeneralComments 並導出,pr-comments 判斷出是純議題時只叫它。 先讀 /issues/{index} 再決定要不要翻 review——每個 PR 都是議題,反過來不成立。 - pr-threads 自己那份 markedByMe 拿掉,改用 lib 的 mergedByMe。抽取契約數未整併則數 用的是同一條規則,現在三處共用一份,#48 擔心的分歧不會再發生。 - pr-comments 的試跑改印 commonRequests:它收得下兩種輸入,而試跑階段還沒讀過議題、 不知道是哪一種。與其假設是 PR 而列出五個(對純議題有三個根本不會發),不如只列一定 會發的,其餘交給 note。pr-watch 的輸入一定是 PR,繼續用 plannedRequests。 lib.js 與 sdlc-feat.md 兩邊改的是不同區域,三方合併無衝突。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-148
@@ -10,39 +10,28 @@
|
||||
* 看它有沒有 `pull_request` 才決定要不要去翻 review;反過來先打 `/pulls/{index}`,
|
||||
* 對純議題會 404,整個流程在讀到第一則留言之前就斷了。
|
||||
*
|
||||
* 這一支只讀不寫,分類(必改/建議、是不是決策)由讀到內容的人判斷。
|
||||
*
|
||||
* 「已處理」在三類上的機制不同:
|
||||
* - 一般留言、review 總評 → 自己打的 `+1` reaction
|
||||
* - 行內留言 → 有沒有被 resolve(只有 review comment 有 resolve 端點)
|
||||
*
|
||||
* **總評的 reaction 掛在它的 issue comment id 上,不是 review id。** Gitea 的 review
|
||||
* 總評在 issue comment 表裡也有一份,兩個 id 不同命名空間——拿 review id 去打
|
||||
* reaction 會 404。那一份的 id 由 timeline 給(`type: 'review'` 的項目帶 `review_id`)。
|
||||
*
|
||||
* reaction 要是**自己**打的才算已處理:reviewer 對留言按讚是「我同意」,不是
|
||||
* 「這則我處理過了」,把它當成已處理會讓那一則被靜靜跳過。
|
||||
* 讀取本身與「已處理」的判定在 `pr-threads.js`——`pr-watch` 要數同一件事,
|
||||
* 規則寫兩份遲早會各自演化。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/pr-comments.js --repo owner/name --index 45 [--host <網址>] [--dry-run]
|
||||
*/
|
||||
import {
|
||||
expectOk,
|
||||
fetchIssue,
|
||||
giteaRequest,
|
||||
listIssueComments,
|
||||
main,
|
||||
mergedByMe,
|
||||
pages,
|
||||
parseFlags,
|
||||
parseIndex,
|
||||
parseRepo,
|
||||
preflight,
|
||||
resolveLogin,
|
||||
} from './lib.js';
|
||||
|
||||
/** 還沒送出的 review:reviewer 自己都還看不到,不該被當成意見 */
|
||||
const DRAFT = 'PENDING';
|
||||
import {
|
||||
ISSUE_OR_PULL_NOTE,
|
||||
commonRequests,
|
||||
readGeneralComments,
|
||||
readPullComments,
|
||||
unhandledCount,
|
||||
} from './pr-threads.js';
|
||||
|
||||
main(async () => {
|
||||
const flags = parseFlags(process.argv.slice(2), {
|
||||
@@ -52,21 +41,14 @@ main(async () => {
|
||||
});
|
||||
const repo = parseRepo(flags.repo);
|
||||
const index = parseIndex(flags.index);
|
||||
const issuePath = `/repos/${repo}/issues/${index}`;
|
||||
const pullPath = `/repos/${repo}/pulls/${index}`;
|
||||
|
||||
if (flags['dry-run']) {
|
||||
return {
|
||||
dryRun: true,
|
||||
repo,
|
||||
index,
|
||||
requests: [
|
||||
{ method: 'GET', path: issuePath },
|
||||
{ method: 'GET', path: `${issuePath}/comments` },
|
||||
],
|
||||
note:
|
||||
'每則留言還會各查一次 reaction;是 PR 的話還會再讀 review 清單、每個 review 的' +
|
||||
'行內留言與 timeline。次數取決於留言數,事前無法列舉。',
|
||||
requests: commonRequests(repo, index),
|
||||
note: ISSUE_OR_PULL_NOTE,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,11 +59,9 @@ main(async () => {
|
||||
// 先讀議題:PR 也是議題,反過來不成立。這一步同時決定要不要去翻 review
|
||||
const issue = await fetchIssue(login, repo, index);
|
||||
const isPull = issue.pull_request != null;
|
||||
|
||||
const 留言 = [
|
||||
...(await readGeneral(login, repo, index, me)),
|
||||
...(isPull ? await readReviews(login, repo, index, pullPath, me) : []),
|
||||
];
|
||||
const 留言 = isPull
|
||||
? await readPullComments(login, repo, index, me)
|
||||
: await readGeneralComments(login, repo, index, me);
|
||||
|
||||
return {
|
||||
repo,
|
||||
@@ -91,120 +71,7 @@ main(async () => {
|
||||
url: issue.html_url,
|
||||
state: issue.state,
|
||||
留言,
|
||||
未處理數: 留言.filter((comment) => !comment.已處理).length,
|
||||
未處理數: unhandledCount(留言),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 一般留言。PR 在 Gitea 裡也是 issue,所以走 issue 的留言端點。
|
||||
* 內容是空的那些多半是狀態變更的系統紀錄(指派、改標題),不是意見。
|
||||
*/
|
||||
async function readGeneral(login, repo, index, me) {
|
||||
const 留言 = [];
|
||||
|
||||
for await (const comment of listIssueComments(login, repo, index)) {
|
||||
if ((comment.body ?? '').trim() === '') continue;
|
||||
留言.push({
|
||||
id: comment.id,
|
||||
review: null,
|
||||
類型: '一般',
|
||||
作者: comment.user?.login ?? '',
|
||||
內容: comment.body,
|
||||
已處理: await mergedByMe(login, repo, comment.id, me),
|
||||
可標記: true,
|
||||
});
|
||||
}
|
||||
return 留言;
|
||||
}
|
||||
|
||||
/**
|
||||
* review 的總評與它底下的行內留言。
|
||||
*
|
||||
* 總評的 id 要用它在 issue comment 表裡的那一份(timeline 給),reaction 才打得上去;
|
||||
* 行內留言則要用 review 自己的 id 去查。兩個 id 都要,所以兩邊都讀。
|
||||
*/
|
||||
async function readReviews(login, repo, index, pullPath, me) {
|
||||
const path = `${pullPath}/reviews`;
|
||||
const commentIds = await reviewCommentIds(login, repo, index);
|
||||
const 留言 = [];
|
||||
|
||||
for await (const reviews of pages(login, path, {
|
||||
limitCode: 'REVIEW_LIMIT',
|
||||
limitHint: `${path} 的 review 太多,讀不完整份清單`,
|
||||
})) {
|
||||
for (const review of reviews) {
|
||||
if (review.state === DRAFT) continue;
|
||||
|
||||
const commentId = commentIds.get(review.id);
|
||||
if ((review.body ?? '').trim() !== '') {
|
||||
留言.push({
|
||||
id: commentId ?? review.id,
|
||||
review: review.id,
|
||||
類型: '總評',
|
||||
作者: review.user?.login ?? '',
|
||||
內容: review.body,
|
||||
// 找不到它在 issue comment 表裡的那一份就標不了——那時如實說,不要假裝可以
|
||||
已處理: commentId === undefined ? false : await mergedByMe(login, repo, commentId, me),
|
||||
可標記: commentId !== undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const commentsPath = `${path}/${review.id}/comments`;
|
||||
for await (const comments of pages(login, commentsPath, {
|
||||
limitCode: 'REVIEW_COMMENT_LIMIT',
|
||||
limitHint: `${commentsPath} 的行內留言太多,讀不完整份清單`,
|
||||
})) {
|
||||
for (const comment of comments) 留言.push(inlineComment(comment, review.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
return 留言;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一則行內留言。
|
||||
*
|
||||
* 位置分兩側:留在新檔那一側用 `position`,留在被刪掉的那一行用 `original_position`,
|
||||
* Gitea 只會填其中一個。只讀 position 的話,留在刪除行的留言會得到 undefined,
|
||||
* 回覆時位置就送錯欄位、落到別的地方去。
|
||||
*/
|
||||
function inlineComment(comment, reviewId) {
|
||||
const onNew = (comment.position ?? 0) > 0;
|
||||
return {
|
||||
id: comment.id,
|
||||
review: reviewId,
|
||||
類型: '行內',
|
||||
作者: comment.user?.login ?? '',
|
||||
內容: comment.body,
|
||||
檔案: comment.path,
|
||||
行: onNew ? comment.position : comment.original_position,
|
||||
側: onNew ? '新' : '舊',
|
||||
// 帶上 diff 片段:沒有它,agent 只看得到「這裡少了錯誤處理」而不知道哪裡
|
||||
diff: comment.diff_hunk ?? '',
|
||||
// 回覆要落在同一個 commit 上,否則 PR 之後又推了新 commit 時行號對不上
|
||||
commit: comment.commit_id ?? comment.original_commit_id ?? null,
|
||||
已處理: comment.resolver != null,
|
||||
可標記: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* review id → 它在 issue comment 表裡的那一則 id。
|
||||
* 總評的 reaction 掛在後者上,而 reviews 端點只給得出前者。
|
||||
*/
|
||||
async function reviewCommentIds(login, repo, index) {
|
||||
const path = `/repos/${repo}/issues/${index}/timeline`;
|
||||
const ids = new Map();
|
||||
|
||||
for await (const entries of pages(login, path, {
|
||||
limitCode: 'TIMELINE_LIMIT',
|
||||
limitHint: `${path} 的項目太多,對不齊總評的 reaction`,
|
||||
})) {
|
||||
for (const entry of entries) {
|
||||
if (entry.type === 'review' && entry.review_id) ids.set(entry.review_id, entry.id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user