From b1025168c0ba27637ab8bbef1e8123b87d7ea9f2 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 06:06:04 +0000 Subject: [PATCH] =?UTF-8?q?feat(issue-link):=20=E5=BB=BA=E7=AB=8B=E8=AD=B0?= =?UTF-8?q?=E9=A1=8C=E4=B9=8B=E9=96=93=E7=9A=84=E9=98=BB=E6=93=8B=E8=88=87?= =?UTF-8?q?=E5=85=88=E6=B1=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --depends 是「這顆被誰擋住」,--blocks 是「這顆擋住誰」。先讀現況只補缺的那幾條, 重跑不會在 Gitea 上堆出重複的相依。 請求必須帶 owner 與 repo:Gitea 的相依端點少了它們會回 404,而且訊息是「repository does not exist」,不看文件會以為是路徑寫錯。 自己依賴自己擋在發出請求之前——Gitea 會接受,但那會讓後續的拓撲排序永遠排不完。 Co-Authored-By: Claude Opus 5 (1M context) --- scripts/issue-link.js | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 scripts/issue-link.js diff --git a/scripts/issue-link.js b/scripts/issue-link.js new file mode 100644 index 0000000..722ec28 --- /dev/null +++ b/scripts/issue-link.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node +/** + * 建立議題之間的相依。 + * + * --depends 這顆被誰擋住(它的先決) + * --blocks 這顆擋住誰 + * + * 冪等:先讀現況,只補缺的那幾條。重跑不會在 Gitea 上堆出重複的相依。 + * + * `--dry-run` 不寫入但會讀——要列出「將建立哪幾條」就得先知道哪幾條已經存在。 + * + * 用法: + * node scripts/issue-link.js --repo owner/name --index 12 + * [--depends 10,11] [--blocks 13] [--host <網址>] [--dry-run] + */ +import { + ScriptError, + expectOk, + giteaRequest, + main, + parseFlags, + parseRepo, + preflight, + resolveLogin, +} from './lib.js'; + +/** 兩種相依各自的端點與意義 */ +const KINDS = { + depends: { path: 'dependencies', label: '先決' }, + blocks: { path: 'blocks', label: '阻擋' }, +}; + +main(async () => { + const flags = parseFlags(process.argv.slice(2), { + required: ['repo', 'index'], + optional: ['depends', 'blocks', 'host'], + booleans: ['dry-run'], + }); + const repo = parseRepo(flags.repo); + const [owner, name] = repo.split('/'); + const index = parseIndex(flags.index, '--index'); + const wanted = { + depends: parseIndexList(flags.depends, index), + blocks: parseIndexList(flags.blocks, index), + }; + + if (wanted.depends.length === 0 && wanted.blocks.length === 0) { + throw new ScriptError('NOTHING_TO_LINK', '至少要指定 --depends 或 --blocks 其中一個'); + } + + const login = resolveLogin({ host: flags.host }); + if (!flags['dry-run']) await preflight(login, repo); + + const added = {}; + const skipped = {}; + const planned = []; + + for (const [kind, { path }] of Object.entries(KINDS)) { + const endpoint = `/repos/${repo}/issues/${index}/${path}`; + const existing = new Set( + (expectOk(await giteaRequest(login, 'GET', endpoint), `GET ${endpoint}`) ?? []).map( + (issue) => issue.number, + ), + ); + + added[kind] = wanted[kind].filter((target) => !existing.has(target)); + skipped[kind] = wanted[kind].filter((target) => existing.has(target)); + + for (const target of added[kind]) { + const body = { owner, repo: name, index: target }; + if (flags['dry-run']) { + planned.push({ method: 'POST', path: endpoint, body }); + continue; + } + expectOk(await giteaRequest(login, 'POST', endpoint, { body }), `POST ${endpoint}`); + } + } + + if (flags['dry-run']) { + return { dryRun: true, repo, index, requests: planned, added, skipped }; + } + return { repo, index, added, skipped }; +}); + +function parseIndex(value, what) { + if (!/^[1-9]\d*$/.test(String(value))) { + throw new ScriptError('BAD_INDEX', `${what} 需為正整數,收到的是 ${value}`); + } + return Number(value); +} + +/** + * 解析逗號分隔的議題編號。 + * 自己依賴自己在 Gitea 上會被接受但毫無意義,而且會讓後續的拓撲排序永遠排不完, + * 所以擋在發出請求之前。 + */ +function parseIndexList(value, self) { + if (value === undefined) return []; + + const indexes = value + .split(',') + .map((item) => item.trim()) + .filter((item) => item !== '') + .map((item) => parseIndex(item, '相依編號')); + + if (indexes.includes(self)) { + throw new ScriptError('SELF_DEPENDENCY', `議題 #${self} 不能是自己的先決或阻擋對象`); + } + return indexes; +}