feat(timer): 碼錶移出領取,等工作樹建成之後才起

原本的順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立
失敗會中止整個領取——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,
而工時要準正是工時報表的立足點。

claim 只留領取鎖的兩件事(assignee 與標籤),起錶交給新的 timer.js,由流程
正本排在 branch-prep 之後。timer 已經跑在這顆議題上時什麼都不做:中斷後重跑
是它最常見的處境,重新起錶會把已經累積的時間切成兩段;跑在別顆上則照舊擋下,
不代勞停錶。

三支腳本讀碼錶的那段各留一份,趁這次收進 lib。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 16:27:43 +08:00
co-authored by Claude Opus 5
parent 5b740c236b
commit 74b4ca130e
5 changed files with 248 additions and 30 deletions
+10 -11
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env node
/**
* 領取一顆工作包:上鎖、貼標籤、起錶。
* 領取一顆工作包:上鎖、貼標籤。
*
* 鎖用 assignee 加標籤,不用碼錶——Gitea 只讓人讀自己的碼錶(`/user/stopwatches`),
* 看不到別人的錶,拿它當鎖會漏判。碼錶在這裡只有一個用途:發現自己忘了停掉上一顆。
*
* **錶不在這一步起**。它等工作樹建好之後才由 timer.js 起動(見 branch-prep.js):
* 工作樹建立失敗會中止整個領取,錶要是先起了,使用者就被計了一段什麼都沒做的時間。
*
* 四種狀態的處置:
* - 他人已認領 → 擋。不會兩個人做同一件事。
* - 自己的錶跑在本議題 → 擋。這顆你已經在做了,別重複起錶。
@@ -24,12 +27,14 @@ import {
fetchIssue,
giteaRequest,
listLabels,
listStopwatches,
main,
parseFlags,
parseIndex,
parseRepo,
preflight,
resolveLogin,
stopwatchOnIssue,
} from './lib.js';
/** 領取鎖的另一半。本 plugin 不自動建立標籤,這個名字要在 repo 上先存在。 */
@@ -84,9 +89,6 @@ main(async () => {
});
labels.push(IN_PROGRESS);
}
// 錶最後才起:前面任一步失敗時,不該留下一顆還在跑的碼錶
planned.push({ method: 'POST', path: `${issuePath}/stopwatch/start`, body: {} });
if (dryRun) {
return { dryRun: true, repo, index, title: issue.title, requests: planned, 已認領過 };
}
@@ -102,7 +104,8 @@ main(async () => {
url: issue.html_url,
assignee: me,
labels,
碼錶中: true,
// 鎖上好了,錶還沒起:它等工作樹建好之後才由 timer.js 起動
碼錶中: false,
已認領過,
};
});
@@ -128,14 +131,10 @@ function checkClaimable(assignees, me, index) {
* 跑在別的議題是「你忘了停掉那一顆」。
*/
async function checkNoStopwatch(login, repo, index) {
const path = '/user/stopwatches';
const watches = expectOk(await giteaRequest(login, 'GET', path), `GET ${path}`) ?? [];
const watches = await listStopwatches(login);
if (watches.length === 0) return;
const here = watches.find(
(watch) => `${watch.repo_owner_name}/${watch.repo_name}` === repo && watch.issue_index === index,
);
if (here) {
if (stopwatchOnIssue(watches, repo, index)) {
throw new ScriptError(
'STOPWATCH_ON_THIS_ISSUE',
`你的碼錶已經跑在議題 #${index} 上,這顆你正在做;` +