#!/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; }