110 lines
3.6 KiB
JavaScript
110 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 領取一顆工作包:上鎖、貼標籤。
|
|
*
|
|
* 鎖用 assignee 加標籤,不用碼錶;碼錶與耗時統計已移除。
|
|
*
|
|
* 他人已認領時擋下,自己已認領時可冪等重跑;所有判斷都在寫入前完成。
|
|
*
|
|
* 所有會擋的判斷都做在任何寫入之前:擋下來卻已經改了一半,比直接放行更難收拾。
|
|
* `--dry-run` 走的是同一條路,只是停在寫入之前——它印出的是這一顆此刻真正缺的那幾步,
|
|
* 而不是一份手寫的固定清單。
|
|
*
|
|
* 用法:
|
|
* node scripts/claim.js --repo owner/name --index 11 [--host <網址>] [--dry-run]
|
|
*/
|
|
import {
|
|
ScriptError,
|
|
expectOk,
|
|
fetchIssue,
|
|
giteaRequest,
|
|
listLabels,
|
|
main,
|
|
parseFlags,
|
|
parseIndex,
|
|
parseRepo,
|
|
preflight,
|
|
resolveLogin,
|
|
} from './lib.js';
|
|
|
|
/** 領取鎖的另一半。本 plugin 不自動建立標籤,這個名字要在 repo 上先存在。 */
|
|
const IN_PROGRESS = '進行中';
|
|
|
|
main(async () => {
|
|
const flags = parseFlags(process.argv.slice(2), {
|
|
required: ['repo', 'index'],
|
|
optional: ['host'],
|
|
booleans: ['dry-run'],
|
|
});
|
|
const repo = parseRepo(flags.repo);
|
|
const index = parseIndex(flags.index);
|
|
const issuePath = `/repos/${repo}/issues/${index}`;
|
|
const dryRun = flags['dry-run'] === true;
|
|
|
|
const login = resolveLogin({ host: flags.host });
|
|
// 試跑照樣讀現況:手寫一份固定的清單會跟實作走鐘,也說不出「這顆已經是你的了」。
|
|
// 前置檢查第二層本來就問過「我是誰」,實跑時直接用它的答案,不再問第二次。
|
|
const checked = dryRun ? null : await preflight(login, repo);
|
|
const me = (checked?.user ?? expectOk(await giteaRequest(login, 'GET', '/user'), 'GET /user')).login;
|
|
const issue = await fetchIssue(login, repo, index);
|
|
|
|
const assignees = (issue.assignees ?? []).map((user) => user.login);
|
|
const labels = (issue.labels ?? []).map((label) => label.name);
|
|
|
|
checkClaimable(assignees, me, index);
|
|
// 本 plugin 不建標籤,缺了就整件事不做,不要只設一半的鎖
|
|
const inProgress = (await listLabels(login, repo)).find((label) => label.name === IN_PROGRESS);
|
|
if (!inProgress) {
|
|
throw new ScriptError(
|
|
'LABEL_NOT_FOUND',
|
|
`repo ${repo} 上沒有「${IN_PROGRESS}」標籤,領取鎖貼不上去;` +
|
|
'請先在該 repo 的 Issues → Labels 建立它(本工具不自動建立標籤)',
|
|
);
|
|
}
|
|
|
|
const 已認領過 = assignees.includes(me);
|
|
const planned = [];
|
|
if (!已認領過) {
|
|
planned.push({ method: 'PATCH', path: issuePath, body: { assignees: [me] } });
|
|
}
|
|
if (!labels.includes(IN_PROGRESS)) {
|
|
planned.push({
|
|
method: 'POST',
|
|
path: `${issuePath}/labels`,
|
|
body: { labels: [inProgress.id] },
|
|
});
|
|
labels.push(IN_PROGRESS);
|
|
}
|
|
if (dryRun) {
|
|
return { dryRun: true, repo, index, title: issue.title, requests: planned, 已認領過 };
|
|
}
|
|
|
|
for (const { method, path, body } of planned) {
|
|
expectOk(await giteaRequest(login, method, path, { body }), `${method} ${path}`);
|
|
}
|
|
|
|
return {
|
|
repo,
|
|
index: issue.number,
|
|
title: issue.title,
|
|
url: issue.html_url,
|
|
assignee: me,
|
|
labels,
|
|
已認領過,
|
|
};
|
|
});
|
|
|
|
|
|
/** 他人已認領就擋。自己也在裡面但還有別人時一樣擋——那是兩個人都以為自己在做。 */
|
|
function checkClaimable(assignees, me, index) {
|
|
const others = assignees.filter((login) => login !== me);
|
|
if (others.length > 0) {
|
|
throw new ScriptError(
|
|
'CLAIMED_BY_OTHER',
|
|
`議題 #${index} 已由 ${others.join('、')} 認領;` +
|
|
'請改領別顆,或先與對方確認後由對方取消認領',
|
|
);
|
|
}
|
|
}
|
|
|