diff --git a/scripts/issue-extract.js b/scripts/issue-extract.js index 6665c94..8306eac 100644 --- a/scripts/issue-extract.js +++ b/scripts/issue-extract.js @@ -10,11 +10,10 @@ * node scripts/issue-extract.js --repo owner/name --index 7 [--host <網址>] [--dry-run] */ import { - ScriptError, - expectOk, - giteaRequest, + UNMERGED_COMMENT_NOTE, + countUnmergedComments, + fetchIssue, main, - pages, parseFlags, parseIndex, parseRepo, @@ -43,14 +42,14 @@ main(async () => { { method: 'GET', path: issuePath }, { method: 'GET', path: commentsPath }, ], - note: '每則留言還會各查一次 reaction,用來數出未整併的則數;則數取決於留言數,事前無法列舉。', + note: UNMERGED_COMMENT_NOTE, }; } const login = resolveLogin({ host: flags.host }); await preflight(login, repo); - const issue = await fetchIssue(login, repo, index, issuePath); + const issue = await fetchIssue(login, repo, index); const sections = parseSections(issue.body); return { @@ -67,41 +66,7 @@ main(async () => { 驗收標準: listSection(sections, '驗收標準'), 影響範圍: listSection(sections, '影響範圍'), 未決事項: listSection(sections, '未決事項'), - 未處理留言數: await countUnmergedComments(login, repo, commentsPath), + 未處理留言數: await countUnmergedComments(login, repo, index), }; }); - -async function fetchIssue(login, repo, index, path) { - const response = await giteaRequest(login, 'GET', path); - if (response.status === 404) { - throw new ScriptError('ISSUE_NOT_FOUND', `${repo} 沒有編號 ${index} 的議題`); - } - if (response.status === 403) { - throw new ScriptError('NO_READ_ACCESS', `目前的帳號沒有 ${repo} 議題 ${index} 的讀取權`); - } - return expectOk(response, `GET ${path}`); -} - -/** - * 數出尚未被整併回描述的留言則數。 - * - * 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記),而 Gitea 的留言物件 - * 不含 reaction,所以只能逐則再查一次。留言多時請求數會跟著長,但這個數字要準 - * ——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,讀不完寧可報錯。 - */ -async function countUnmergedComments(login, repo, commentsPath) { - let unmerged = 0; - - for await (const comments of pages(login, commentsPath, { - limitCode: 'COMMENT_LIMIT', - limitHint: `${commentsPath} 的留言太多,數不完未整併的則數`, - })) { - for (const comment of comments) { - const path = `/repos/${repo}/issues/comments/${comment.id}/reactions`; - const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? []; - if (!reactions.some((reaction) => reaction.content === '+1')) unmerged += 1; - } - } - return unmerged; -} diff --git a/scripts/lib.js b/scripts/lib.js index 24b8d5c..378f2d7 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1,12 +1,13 @@ /** * tea-sdlc 所有腳本的共用地基。 * - * 這一層負責五件事,其餘腳本只寫自己的業務: + * 這一層負責六件事,其餘腳本只寫自己的業務: * 1. 具名 flag 解析與單行 JSON 輸出({ok, data, error:{code, message}}) * 2. Gitea API 呼叫 —— 全專案唯一的 HTTP 出口 * 3. git 執行 —— 全專案唯一的子行程出口 * 4. 四層前置檢查 * 5. 冪等查重 + * 6. 兩支抽取腳本共用的議題讀取 * * 外部相依集中在 giteaRequest 與 runGit 兩個函式,測試才有地方替身。 */ @@ -431,6 +432,66 @@ function checkTimeTracker(info) { } } +// ── 議題讀取:兩支抽取腳本共用 ──────────────────────────────────── + +/** + * 讀一顆議題。「不存在」與「沒有讀取權」要分得開——前者是編號打錯, + * 後者是權限沒開,兩種的下一步完全不同。 + * @param {{base: string, token: string}} login + * @param {string} repo owner/name + * @param {number} index + * @returns {Promise} + */ +export async function fetchIssue(login, repo, index) { + const path = `/repos/${repo}/issues/${index}`; + const response = await giteaRequest(login, 'GET', path); + + if (response.status === 404) { + throw new ScriptError('ISSUE_NOT_FOUND', `${repo} 沒有編號 ${index} 的議題`); + } + if (response.status === 403) { + throw new ScriptError('NO_READ_ACCESS', `目前的帳號沒有 ${repo} 議題 ${index} 的讀取權`); + } + return expectOk(response, `GET ${path}`); +} + +/** + * 抽取腳本 `--dry-run` 的共同附註:留言的 reaction 要逐則查,事前列不出來。 + * 與 countUnmergedComments 同進退——說明的是它發出的那些請求。 + */ +export const UNMERGED_COMMENT_NOTE = + '每則留言還會各查一次 reaction,用來數出未整併的則數;則數取決於留言數,事前無法列舉。'; + +/** + * 數出尚未被整併回描述的留言則數。 + * + * 抽取契約只讀 body 不讀留言,這個數字是下游判斷「手上的描述是不是過期了」的唯一依據。 + * 已整併的留言會被打上 `+1` reaction(由 sdlc-sync 負責標記),而 Gitea 的留言物件 + * 不含 reaction,所以只能逐則再查一次。留言多時請求數會跟著長,但這個數字要準 + * ——它決定下游會不會拿著過期的描述做事,所以留言也要逐頁讀完,讀不完寧可報錯。 + * + * @param {{base: string, token: string}} login + * @param {string} repo owner/name + * @param {number} index + * @returns {Promise} + */ +export async function countUnmergedComments(login, repo, index) { + const commentsPath = `/repos/${repo}/issues/${index}/comments`; + let unmerged = 0; + + for await (const comments of pages(login, commentsPath, { + limitCode: 'COMMENT_LIMIT', + limitHint: `${commentsPath} 的留言太多,數不完未整併的則數`, + })) { + for (const comment of comments) { + const path = `/repos/${repo}/issues/comments/${comment.id}/reactions`; + const reactions = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? []; + if (!reactions.some((reaction) => reaction.content === '+1')) unmerged += 1; + } + } + return unmerged; +} + // ── 標籤 ─────────────────────────────────────────────────────────── /**