fix/pr-head-after-merge/main #51
+25
-5
@@ -64,13 +64,13 @@ main(async () => {
|
|||||||
const state = stateOf(pull);
|
const state = stateOf(pull);
|
||||||
const 未處理留言數 = unhandledCount(await readPullComments(login, repo, index, me));
|
const 未處理留言數 = unhandledCount(await readPullComments(login, repo, index, me));
|
||||||
|
|
||||||
// 工作樹由 PR 自己的 head 分支推導,不必另外給——同一顆工作包算出來的永遠是同一條路徑
|
// 工作樹由 PR 自己的來源分支推導,不必另外給——同一顆工作包算出來的永遠是同一條路徑
|
||||||
const branch = pull.head?.ref;
|
const branch = headBranch(pull);
|
||||||
if (!branch) {
|
if (branch === '') {
|
||||||
throw new ScriptError(
|
throw new ScriptError(
|
||||||
'PULL_HEAD_MISSING',
|
'PULL_HEAD_MISSING',
|
||||||
`PR #${index} 讀不到 head 分支(來源分支可能已經被刪掉),推導不出工作樹在哪;` +
|
`PR #${index} 讀不到來源分支名,推導不出工作樹在哪;` +
|
||||||
'請改用 worktree-remove --branch 指名要清哪一棵',
|
'請改用 worktree-remove --repo <owner/name> --branch <分支名> 指名要清哪一棵',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const worktree = worktreePath(repo, branch);
|
const worktree = worktreePath(repo, branch);
|
||||||
@@ -119,6 +119,26 @@ main(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PR 的來源分支名。
|
||||||
|
*
|
||||||
|
* **不能只看 `head.ref`。** PR 合併而來源分支被刪掉之後,Gitea 會把 `head.ref` 換成
|
||||||
|
* `refs/pull/{編號}/head`,分支名退到 `head.label`——而合併正是唯一該清理工作樹的時機。
|
||||||
|
* 拿那個 ref 去推導會算出一條根本不存在的路徑,然後回報「工作樹不在、沒事要做」:
|
||||||
|
* 自動清理於是在真實情況下從來不會成立,而且靜悄悄的,沒有人會發現。
|
||||||
|
*
|
||||||
|
* fork 來的 PR 的 label 是 `owner:branch`,只取分支那一段——工作樹是以分支名推導的。
|
||||||
|
*/
|
||||||
|
function headBranch(pull) {
|
||||||
|
const label = (pull.head?.label ?? '').trim();
|
||||||
|
const 分支 = label.includes(':') ? label.slice(label.indexOf(':') + 1) : label;
|
||||||
|
if (分支 !== '') return 分支;
|
||||||
|
|
||||||
|
// label 缺席時才退回 ref,而且 refs/pull/… 不是分支名,寧可說讀不到
|
||||||
|
const ref = (pull.head?.ref ?? '').trim();
|
||||||
|
return /^refs\/pull\//.test(ref) ? '' : ref;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PR 的四種狀態。
|
* PR 的四種狀態。
|
||||||
* Gitea 的 `state` 只有 open/closed,合併與草稿各是另一個布林值——三個欄位湊成一種
|
* Gitea 的 `state` 只有 open/closed,合併與草稿各是另一個布林值——三個欄位湊成一種
|
||||||
|
|||||||
+52
-2
@@ -28,7 +28,7 @@ const BRANCH = `feat/${SLUG}/main`;
|
|||||||
* 一顆 PR 的現場:狀態由 options 決定,留言預設沒有。
|
* 一顆 PR 的現場:狀態由 options 決定,留言預設沒有。
|
||||||
* 一般留言只有在沒有自己打的 `+1` 時才算未處理。
|
* 一般留言只有在沒有自己打的 `+1` 時才算未處理。
|
||||||
*/
|
*/
|
||||||
function routes({ state = 'open', merged = false, draft = false, general = [] } = {}) {
|
function routes({ state = 'open', merged = false, draft = false, general = [], head } = {}) {
|
||||||
const base = healthyRoutes(REPO, {
|
const base = healthyRoutes(REPO, {
|
||||||
[`GET /api/v1/repos/${REPO}/pulls/${INDEX}`]: {
|
[`GET /api/v1/repos/${REPO}/pulls/${INDEX}`]: {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -39,7 +39,9 @@ function routes({ state = 'open', merged = false, draft = false, general = [] }
|
|||||||
state,
|
state,
|
||||||
merged,
|
merged,
|
||||||
draft,
|
draft,
|
||||||
head: { ref: BRANCH },
|
// 預設是「PR 還開著」的樣子:head.ref 就是分支名。合併之後 Gitea 會換一種樣子,
|
||||||
|
// 那一種由測試自己指定。
|
||||||
|
head: head ?? { ref: BRANCH, label: BRANCH },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[`GET /api/v1/repos/${REPO}/issues/${INDEX}/comments`]: {
|
[`GET /api/v1/repos/${REPO}/issues/${INDEX}/comments`]: {
|
||||||
@@ -250,6 +252,54 @@ test('清不掉的路徑,--dry-run 不會預告一行實跑會拒絕的指令'
|
|||||||
assert.equal(json.data.工作樹.是工作樹, false);
|
assert.equal(json.data.工作樹.是工作樹, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 合併之後的 head ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('來源分支在合併時被刪掉,仍要推導出正確的工作樹並清掉它', async (t) => {
|
||||||
|
// Gitea 在這種情況下把 head.ref 換成 refs/pull/{編號}/head,分支名退到 head.label。
|
||||||
|
// 拿 ref 去推導會算出一條不存在的路徑,然後靜靜回報「沒事要做」——而合併正是唯一
|
||||||
|
// 該動手清理的時機,等於自動清理在真實情況下從來不會成立。
|
||||||
|
const { run, worktree } = await withScene(t, {
|
||||||
|
state: 'closed',
|
||||||
|
merged: true,
|
||||||
|
head: { ref: `refs/pull/${INDEX}/head`, label: BRANCH },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, json } = await run();
|
||||||
|
|
||||||
|
assert.equal(code, 0, json.error?.message);
|
||||||
|
assert.equal(json.data.branch, BRANCH, '分支名要取自 head.label');
|
||||||
|
assert.equal(json.data.工作樹.路徑, worktree);
|
||||||
|
assert.equal(json.data.cleaned, true);
|
||||||
|
assert.equal(existsSync(worktree), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fork 來的 PR:head.label 是 owner:branch,只取分支那一段', async (t) => {
|
||||||
|
const { run, worktree } = await withScene(t, {
|
||||||
|
state: 'closed',
|
||||||
|
merged: true,
|
||||||
|
head: { ref: `refs/pull/${INDEX}/head`, label: `someone:${BRANCH}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { json } = await run();
|
||||||
|
|
||||||
|
assert.equal(json.data.branch, BRANCH);
|
||||||
|
assert.equal(existsSync(worktree), false, '推導出來的仍是同一條路徑');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('分支名兩邊都取不到時明確中止,不拿空字串去推導路徑', async (t) => {
|
||||||
|
const { run } = await withScene(t, {
|
||||||
|
state: 'closed',
|
||||||
|
merged: true,
|
||||||
|
head: { ref: `refs/pull/${INDEX}/head` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { code, json } = await run();
|
||||||
|
|
||||||
|
assert.equal(code, 1);
|
||||||
|
assert.equal(json.error.code, 'PULL_HEAD_MISSING');
|
||||||
|
assert.match(json.error.message, /worktree-remove/, '要指出手動出口怎麼指名那一棵');
|
||||||
|
});
|
||||||
|
|
||||||
// ── 回報內容 ───────────────────────────────────────────────────────
|
// ── 回報內容 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
test('回報內容含 PR 狀態、未處理留言數與工作樹現況', async (t) => {
|
test('回報內容含 PR 狀態、未處理留言數與工作樹現況', async (t) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user