fix(pr-comments): 議題也讀得了,不再先打 PR 端點
每個 PR 都是議題,議題不一定是 PR——原本的註解把這句話講反了,程式也照著反過來寫:
先打 /pulls/{index},對純議題回 404,於是 /sdlc-sync 在讀到第一則留言之前就斷了。
改成先讀 /issues/{index}(兩種都有),看它有沒有 pull_request 才決定要不要去翻 review。
輸出加上「類型」讓下游知道拿到的是議題還是 PR。
This commit is contained in:
+42
-59
@@ -1,9 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 讀 PR 上的三類留言:一般留言、review 總評、行內留言。
|
||||
* 讀 PR 或議題上的留言。
|
||||
*
|
||||
* 三類分散在三個端點,漏掉任何一類就會有 reviewer 的意見沒被處理——而那正是
|
||||
* `/sdlc-fix` 存在的理由。這一支只讀不寫,分類(必改/建議)由讀到內容的人判斷。
|
||||
* PR 有三類:一般留言、review 總評、行內留言,分散在三個端點——漏掉任何一類就會有
|
||||
* reviewer 的意見沒被處理,而那正是 `/sdlc-fix` 存在的理由。
|
||||
* 純議題只有一般留言,`/sdlc-sync` 要的就是那一份。
|
||||
*
|
||||
* **每個 PR 都是議題,但議題不一定是 PR。** 所以先讀 `/issues/{index}`(兩種都有),
|
||||
* 看它有沒有 `pull_request` 才決定要不要去翻 review;反過來先打 `/pulls/{index}`,
|
||||
* 對純議題會 404,整個流程在讀到第一則留言之前就斷了。
|
||||
*
|
||||
* 這一支只讀不寫,分類(必改/建議、是不是決策)由讀到內容的人判斷。
|
||||
*
|
||||
* 「已處理」在三類上的機制不同:
|
||||
* - 一般留言、review 總評 → 自己打的 `+1` reaction
|
||||
@@ -20,10 +27,12 @@
|
||||
* node scripts/pr-comments.js --repo owner/name --index 45 [--host <網址>] [--dry-run]
|
||||
*/
|
||||
import {
|
||||
ScriptError,
|
||||
expectOk,
|
||||
fetchIssue,
|
||||
giteaRequest,
|
||||
listIssueComments,
|
||||
main,
|
||||
mergedByMe,
|
||||
pages,
|
||||
parseFlags,
|
||||
parseIndex,
|
||||
@@ -43,6 +52,7 @@ 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']) {
|
||||
@@ -51,74 +61,59 @@ main(async () => {
|
||||
repo,
|
||||
index,
|
||||
requests: [
|
||||
{ method: 'GET', path: '/user' },
|
||||
{ method: 'GET', path: pullPath },
|
||||
{ method: 'GET', path: `/repos/${repo}/issues/${index}/comments` },
|
||||
{ method: 'GET', path: `${pullPath}/reviews` },
|
||||
{ method: 'GET', path: `/repos/${repo}/issues/${index}/timeline` },
|
||||
{ method: 'GET', path: issuePath },
|
||||
{ method: 'GET', path: `${issuePath}/comments` },
|
||||
],
|
||||
note:
|
||||
'每則一般留言還會各查一次 reaction、每個 review 還會各查一次它的行內留言;' +
|
||||
'次數取決於留言數,事前無法列舉。',
|
||||
'每則留言還會各查一次 reaction;是 PR 的話還會再讀 review 清單、每個 review 的' +
|
||||
'行內留言與 timeline。次數取決於留言數,事前無法列舉。',
|
||||
};
|
||||
}
|
||||
|
||||
const login = resolveLogin({ host: flags.host });
|
||||
await preflight(login, repo);
|
||||
const { user } = await preflight(login, repo);
|
||||
const me = user.login;
|
||||
|
||||
// 先讀議題:PR 也是議題,反過來不成立。這一步同時決定要不要去翻 review
|
||||
const issue = await fetchIssue(login, repo, index);
|
||||
const isPull = issue.pull_request != null;
|
||||
|
||||
const me = expectOk(await giteaRequest(login, 'GET', '/user'), 'GET /user').login;
|
||||
const pull = await fetchPull(login, repo, index, pullPath);
|
||||
const 留言 = [
|
||||
...(await readGeneral(login, repo, index, me)),
|
||||
...(await readReviews(login, repo, index, pullPath, me)),
|
||||
...(isPull ? await readReviews(login, repo, index, pullPath, me) : []),
|
||||
];
|
||||
|
||||
return {
|
||||
repo,
|
||||
index: pull.number,
|
||||
title: pull.title,
|
||||
url: pull.html_url,
|
||||
state: pull.state,
|
||||
index: issue.number,
|
||||
類型: isPull ? 'PR' : '議題',
|
||||
title: issue.title,
|
||||
url: issue.html_url,
|
||||
state: issue.state,
|
||||
留言,
|
||||
未處理數: 留言.filter((comment) => !comment.已處理).length,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
async function fetchPull(login, repo, index, path) {
|
||||
const response = await giteaRequest(login, 'GET', path);
|
||||
if (response.status === 404) {
|
||||
throw new ScriptError('PULL_NOT_FOUND', `${repo} 沒有編號 ${index} 的 PR`);
|
||||
}
|
||||
if (response.status === 403) {
|
||||
throw new ScriptError('NO_READ_ACCESS', `目前的帳號沒有 ${repo} 的 PR ${index} 的讀取權`);
|
||||
}
|
||||
return expectOk(response, `GET ${path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 一般留言。PR 在 Gitea 裡也是 issue,所以走 issue 的留言端點。
|
||||
* 內容是空的那些多半是狀態變更的系統紀錄(指派、改標題),不是意見。
|
||||
*/
|
||||
async function readGeneral(login, repo, index, me) {
|
||||
const path = `/repos/${repo}/issues/${index}/comments`;
|
||||
const 留言 = [];
|
||||
|
||||
for await (const comments of pages(login, path, {
|
||||
limitCode: 'COMMENT_LIMIT',
|
||||
limitHint: `${path} 的留言太多,讀不完整份清單`,
|
||||
})) {
|
||||
for (const comment of comments) {
|
||||
if ((comment.body ?? '').trim() === '') continue;
|
||||
留言.push({
|
||||
id: comment.id,
|
||||
類型: '一般',
|
||||
作者: comment.user?.login ?? '',
|
||||
內容: comment.body,
|
||||
已處理: await markedByMe(login, repo, comment.id, me),
|
||||
可標記: true,
|
||||
});
|
||||
}
|
||||
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 留言;
|
||||
}
|
||||
@@ -150,7 +145,7 @@ async function readReviews(login, repo, index, pullPath, me) {
|
||||
作者: review.user?.login ?? '',
|
||||
內容: review.body,
|
||||
// 找不到它在 issue comment 表裡的那一份就標不了——那時如實說,不要假裝可以
|
||||
已處理: commentId === undefined ? false : await markedByMe(login, repo, commentId, me),
|
||||
已處理: commentId === undefined ? false : await mergedByMe(login, repo, commentId, me),
|
||||
可標記: commentId !== undefined,
|
||||
});
|
||||
}
|
||||
@@ -213,15 +208,3 @@ async function reviewCommentIds(login, repo, index) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 這一則是不是「我」標記過已處理。
|
||||
*
|
||||
* Gitea 的留言物件不含 reaction,只能逐則再查一次。認的是自己打的 `+1`:
|
||||
* reviewer 對留言按讚是「我同意」,當成已處理會讓那一則被靜靜跳過。
|
||||
*/
|
||||
async function markedByMe(login, repo, id, me) {
|
||||
const path = `/repos/${repo}/issues/comments/${id}/reactions`;
|
||||
const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? [];
|
||||
|
||||
return reactions.some((reaction) => reaction.content === '+1' && reaction.user?.login === me);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user