feat/sdlc-fix-worktree/main #53
+18
-6
@@ -16,21 +16,33 @@ reviewer 留完意見,跑這一段,意見被逐條處理並回覆,不漏
|
|||||||
先問一次現況,再決定要不要動手:
|
先問一次現況,再決定要不要動手:
|
||||||
|
|
||||||
```
|
```
|
||||||
node scripts/pr-watch.js --repo <owner/name> --index <PR 編號>
|
node scripts/pr-watch.js --repo <owner/name> --index <PR 編號> --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
**`terminal` 為 `true`(PR 已合併或已關閉)時就停下來。** 那顆工作包已經結束,在一棵
|
**這裡要帶 `--dry-run`**:`pr-watch` 在 PR 已終止時會順手清掉工作樹,而這一步只是要
|
||||||
該被清掉的工作樹上處理留言是白做工。把 `suggestedAction` 的意思講給使用者聽:
|
知道現況——清不清理是使用者的決定,不該由「我想看一下留言」這個動作順便做掉。
|
||||||
`nothing-to-do` 是工作樹也清掉了;`blocked-dirty` 是那裡還有沒提交的東西,要他自己處理。
|
|
||||||
使用者堅持要繼續就繼續,但要說清楚 PR 已經結束了。
|
**`terminal` 為 `true`(PR 已合併或已關閉)時就停下來,不要繼續處理留言。** 那顆工作包
|
||||||
|
已經結束,在一棵該被清掉的工作樹上改東西是白做工,而且那些改動不會進到任何 PR 裡。
|
||||||
|
把 `suggestedAction` 的意思講給使用者聽,讓他決定下一步:
|
||||||
|
|
||||||
|
| 值 | 意思 |
|
||||||
|
| --- | --- |
|
||||||
|
| `nothing-to-do` | 沒事了;工作樹不在或已經清掉 |
|
||||||
|
| `cleanup` | 工作樹還在,可以用 `worktree-remove` 清掉 |
|
||||||
|
| `blocked-dirty` | 那棵工作樹裡還有沒提交的東西,要他自己處理 |
|
||||||
|
|
||||||
PR 還開著就定位工作樹。輸入是 `pr-watch` 給的 `branch`——路徑由「哪顆工作包」推導,
|
PR 還開著就定位工作樹。輸入是 `pr-watch` 給的 `branch`——路徑由「哪顆工作包」推導,
|
||||||
不必也不該由使用者自己去記那串雜湊目錄名:
|
不必也不該由使用者自己去記那串雜湊目錄名:
|
||||||
|
|
||||||
```
|
```
|
||||||
node scripts/worktree-ensure.js --repo <owner/name> --branch <pr-watch 給的 branch> --dry-run
|
node scripts/worktree-ensure.js --repo <owner/name> --path <目標專案路徑> \
|
||||||
|
--branch <pr-watch 給的 branch> --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--path` 是目標專案在本機的位置(工作包 `repos` 列的那一顆),不給就用當前目錄——
|
||||||
|
而當前目錄多半不是它,這正是這一步要解決的問題。
|
||||||
|
|
||||||
試跑會印出推導出的路徑與將執行的 git 指令;確認無誤後拿掉旗標再跑一次。
|
試跑會印出推導出的路徑與將執行的 git 指令;確認無誤後拿掉旗標再跑一次。
|
||||||
|
|
||||||
**推導出的路徑不存在時它會重建,那是常態不是例外**:進度完全不寫在本機,換一台機器
|
**推導出的路徑不存在時它會重建,那是常態不是例外**:進度完全不寫在本機,換一台機器
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
parseFlags,
|
parseFlags,
|
||||||
parseRepo,
|
parseRepo,
|
||||||
planWorktree,
|
planWorktree,
|
||||||
|
requireOrigin,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
} from './lib.js';
|
} from './lib.js';
|
||||||
|
|
||||||
@@ -91,12 +92,7 @@ main(async () => {
|
|||||||
const worktree = worktreePath(repo, branch);
|
const worktree = worktreePath(repo, branch);
|
||||||
|
|
||||||
const git = openGitRepo(path);
|
const git = openGitRepo(path);
|
||||||
if (!git('remote').split('\n').includes('origin')) {
|
requireOrigin(git, path, '工作樹的起點一律取自 origin/{來源分支}');
|
||||||
throw new ScriptError(
|
|
||||||
'NO_ORIGIN',
|
|
||||||
`${path} 沒有 origin 遠端;工作樹的起點一律取自 origin/{來源分支},請先設定 origin`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = planWorktree(git, { source, branch, worktree });
|
const plan = planWorktree(git, { source, branch, worktree });
|
||||||
const 報告 = { path, repo, source, branch, worktree, 分支: { 動作: plan.動作 } };
|
const 報告 = { path, repo, source, branch, worktree, 分支: { 動作: plan.動作 } };
|
||||||
|
|||||||
+26
-5
@@ -1,19 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* tea-sdlc 所有腳本的共用地基。
|
* tea-sdlc 所有腳本的共用地基。
|
||||||
*
|
*
|
||||||
* 這一層負責六件事,其餘腳本只寫自己的業務:
|
* 這一層負責七件事,其餘腳本只寫自己的業務:
|
||||||
* 1. 具名 flag 解析與單行 JSON 輸出({ok, data, error:{code, message}})
|
* 1. 具名 flag 解析與單行 JSON 輸出({ok, data, error:{code, message}})
|
||||||
* 2. Gitea API 呼叫 —— 全專案唯一的 HTTP 出口
|
* 2. Gitea API 呼叫 —— 全專案唯一的 HTTP 出口
|
||||||
* 3. git 執行 —— 全專案唯一的子行程出口
|
* 3. git 執行 —— 全專案唯一的子行程出口
|
||||||
* 4. 四層前置檢查
|
* 4. 工作樹的一生 —— 路徑推導、建立(含重建)、現況、移除
|
||||||
* 5. 冪等查重
|
* 5. 四層前置檢查
|
||||||
* 6. 兩支抽取腳本共用的議題讀取
|
* 6. 冪等查重
|
||||||
|
* 7. 兩支抽取腳本共用的議題讀取
|
||||||
*
|
*
|
||||||
* 外部相依集中在 giteaRequest 與 runGit 兩個函式,測試才有地方替身。
|
* 外部相依集中在 giteaRequest 與 runGit 兩個函式,測試才有地方替身。
|
||||||
*/
|
*/
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
import {
|
||||||
|
accessSync,
|
||||||
|
constants,
|
||||||
|
existsSync,
|
||||||
|
readFileSync,
|
||||||
|
realpathSync,
|
||||||
|
rmSync,
|
||||||
|
statSync,
|
||||||
|
} from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -430,6 +439,18 @@ export function runGit(args, { cwd } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 要求目標專案有 origin 遠端。
|
||||||
|
* 工作樹的起點一律取自遠端,沒有 origin 就什麼都做不了——兩支腳本擋的是同一件事,
|
||||||
|
* 只有「為什麼需要它」那一句不同,所以那一句由呼叫端給。
|
||||||
|
* @param {string} 用途 出現在訊息裡的理由,例如「工作樹的起點一律取自 origin/{來源分支}」
|
||||||
|
*/
|
||||||
|
export function requireOrigin(git, path, 用途) {
|
||||||
|
if (!git('remote').split('\n').includes('origin')) {
|
||||||
|
throw new ScriptError('NO_ORIGIN', `${path} 沒有 origin 遠端;${用途},請先設定 origin`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 開一個目標專案的 git repo,回傳綁在它身上的執行器。
|
* 開一個目標專案的 git repo,回傳綁在它身上的執行器。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,13 +21,13 @@
|
|||||||
* [--path <目標專案>] [--dry-run]
|
* [--path <目標專案>] [--dry-run]
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ScriptError,
|
|
||||||
createWorktree,
|
createWorktree,
|
||||||
main,
|
main,
|
||||||
openGitRepo,
|
openGitRepo,
|
||||||
parseFlags,
|
parseFlags,
|
||||||
parseRepo,
|
parseRepo,
|
||||||
planWorktree,
|
planWorktree,
|
||||||
|
requireOrigin,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
} from './lib.js';
|
} from './lib.js';
|
||||||
|
|
||||||
@@ -43,12 +43,7 @@ main(async () => {
|
|||||||
const worktree = worktreePath(repo, branch);
|
const worktree = worktreePath(repo, branch);
|
||||||
|
|
||||||
const git = openGitRepo(path);
|
const git = openGitRepo(path);
|
||||||
if (!git('remote').split('\n').includes('origin')) {
|
requireOrigin(git, path, `重建工作樹要先能讀到 origin/${branch}`);
|
||||||
throw new ScriptError(
|
|
||||||
'NO_ORIGIN',
|
|
||||||
`${path} 沒有 origin 遠端;重建工作樹要先能讀到 origin/${branch},請先設定 origin`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不給 source:這裡只重建既有分支的工作樹,沒有「從來源長一支新的」那條路,
|
// 不給 source:這裡只重建既有分支的工作樹,沒有「從來源長一支新的」那條路,
|
||||||
// 那是 branch-prep 的事——在這裡憑空開一支新分支,等於把 PR 的進度扔掉
|
// 那是 branch-prep 的事——在這裡憑空開一支新分支,等於把 PR 的進度扔掉
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ test('工作樹建不起來時,不留下那一支已經建好的分支', async
|
|||||||
false,
|
false,
|
||||||
'也不該留下工作樹的中繼資料',
|
'也不該留下工作樹的中繼資料',
|
||||||
);
|
);
|
||||||
|
assert.equal(existsSync(join(blocked, 'worktrees')), false, '半途建出來的目錄也不該留下來');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── 目標分支已存在:接上去,不覆蓋 ─────────────────────────────────
|
// ── 目標分支已存在:接上去,不覆蓋 ─────────────────────────────────
|
||||||
|
|||||||
@@ -17,11 +17,25 @@ const locate = steps.slice(0, steps.indexOf('## 2.'));
|
|||||||
test('第一步先看 PR 現況,終止狀態就停下來不白做工', () => {
|
test('第一步先看 PR 現況,終止狀態就停下來不白做工', () => {
|
||||||
assert.match(locate, /pr-watch\.js/);
|
assert.match(locate, /pr-watch\.js/);
|
||||||
assert.match(locate, /terminal/);
|
assert.match(locate, /terminal/);
|
||||||
|
assert.match(locate, /不要繼續處理留言/);
|
||||||
assert.match(locate, /白做工/, '要說明為什麼停:那棵工作樹已經該被清掉了');
|
assert.match(locate, /白做工/, '要說明為什麼停:那棵工作樹已經該被清掉了');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('查現況帶 --dry-run,看一下留言不該順手清掉工作樹', () => {
|
||||||
|
const 指令 = locate.slice(locate.indexOf('pr-watch.js'));
|
||||||
|
assert.match(指令.slice(0, 120), /--dry-run/);
|
||||||
|
assert.match(locate, /清不清理是使用者的決定/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('三個建議動作的意思都交代了,不留一個讓 agent 自由發揮', () => {
|
||||||
|
for (const 值 of ['nothing-to-do', 'cleanup', 'blocked-dirty']) {
|
||||||
|
assert.match(locate, new RegExp(值));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('工作樹由工作包推導,不要求使用者自己切目錄', () => {
|
test('工作樹由工作包推導,不要求使用者自己切目錄', () => {
|
||||||
assert.match(locate, /worktree-ensure\.js/);
|
assert.match(locate, /worktree-ensure\.js/);
|
||||||
|
assert.match(locate, /--path/, '目標專案不是當前目錄時要指得出來,否則會找錯 repo');
|
||||||
assert.match(locate, /branch/, '輸入是 pr-watch 給的分支名');
|
assert.match(locate, /branch/, '輸入是 pr-watch 給的分支名');
|
||||||
assert.match(locate, /不必也不該由使用者自己去記/);
|
assert.match(locate, /不必也不該由使用者自己去記/);
|
||||||
assert.match(locate, /都在那棵工作樹裡做/);
|
assert.match(locate, /都在那棵工作樹裡做/);
|
||||||
|
|||||||
Reference in New Issue
Block a user