同一段 /^[1-9]\d*$/ 與 BAD_INDEX 已經抄了四份。lib 本來就放著同性質的 parseRepo, 這一支該待在它旁邊。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
3.1 KiB
JavaScript
106 lines
3.1 KiB
JavaScript
#!/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,
|
|
parseIndex,
|
|
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 };
|
|
});
|
|
|
|
|
|
/**
|
|
* 解析逗號分隔的議題編號。
|
|
* 自己依賴自己在 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;
|
|
}
|