兩份參考文件跟實際判定互相矛盾。照著做會漏掉收尾,或是擋錯階段。 - 模型閘門非零退出時,該回合仍要跑階段回報。提前停下來也要留紀錄,不然使用者看不到停在哪裡。 - 工作包閘門只擋分析與維護:規劃只印提醒就放行,實作一律放行。實作是結清那支 PR 的唯一路徑,擋了閘門就自己鎖死。 - 順帶把指向實作技能的步驟引用改成新的順序。
164 lines
14 KiB
Markdown
164 lines
14 KiB
Markdown
# 分支規則 — SDLC 各階段動工前的分支確認
|
||
|
||
分析要讀對分支的程式碼,實作要寫進對的分支。兩件事都不能從「目前 checkout 的分支」推定使用者的意圖。
|
||
|
||
## 總則:一律以遠端為準,不吃本地分支
|
||
|
||
SDLC 各階段引用的參考分支與來源分支,**一律指遠端的 `origin/{branch}`**。本地分支不可作為基準。
|
||
|
||
理由:本地分支可能落後遠端、可能有沒推上去的 commit、也可能是別的工作留下的狀態。以本地為基準,分析會對著過時的程式碼做,實作會從錯誤的起點長出來,而且錯了不會有任何徵兆。
|
||
|
||
| 項目 | 規則 |
|
||
| --- | --- |
|
||
| 動作之前 | 先 `git fetch --prune origin`。**沒 fetch 就用 `origin/{branch}` 等於用快取**,遠端追蹤參照可能已經過時,連分支被刪掉都看不出來 |
|
||
| 列分支 | `git branch -r`,不看 `git branch` |
|
||
| 基準與比對 | 一律寫 `origin/{branch}`,例如 `git rev-list --count origin/master..origin/develop` |
|
||
| 建立 worktree | 從 `origin/{source-branch}` 建,不從本地同名分支建 |
|
||
| 判定預設分支 | `refs/remotes/origin/HEAD`(見〔判定遠端預設分支〕) |
|
||
|
||
**本地與遠端不一致時一律停下回報**,不自行 `pull`、不自行 `reset`、不自行切換。要不要同步是使用者的決定。
|
||
|
||
`origin` 以外的遠端名稱(例如 `upstream`):先問使用者以哪個遠端為準,不臆測。
|
||
|
||
### 來源分支在遠端找不到:停下回報,不退回預設分支
|
||
|
||
已指定的來源分支,`origin/{source-branch}` 在遠端不存在時,**回報並停止**。不改用 `develop`、不改用 `master`、不套用任何後備分支,也不自行建立同名分支。這條沒有例外。
|
||
|
||
理由:來源分支是那份分析的功能分支。悄悄退回 `develop`,單一工作包就會越過自己的功能分支直接進 `develop`,而且沒有任何徵兆看得出來。
|
||
|
||
**這是停止條件,不是後備條件。**〔判定遠端預設分支〕那節的後備順序只用在「還沒有指定分支、要挑一條基準」的情況;指定過的來源分支找不到,一律套用本節。
|
||
|
||
## 先確認,再動工
|
||
|
||
| 階段 | 要確認的分支 | 確認時機 |
|
||
| --- | --- | --- |
|
||
| `analyze` | **來源分支**——哪條分支的程式碼算現況 | 讀任何程式碼之前 |
|
||
| `implement` | **來源分支**——worktree 的基準,也是每個工作包的 PR 目標 | 寫任何檔案之前 |
|
||
|
||
確認方式:
|
||
|
||
1. 先 `git fetch --prune origin`,再把事實攤開:目前分支、工作區是否乾淨、遠端有哪些分支(`git branch -r`,不看本地)、下節判定出的遠端預設分支。
|
||
2. 依 `jsc-ask:ask` 的決策樹詢問,每個選項都要標明影響範圍。分析錯分支會產出對不上程式碼的工作包;來源分支錯了則 worktree 從錯的起點長出來,PR 也會開到錯的地方。
|
||
3. 把確認結果寫進產出(分析頁的來源分支欄),後續步驟一律沿用同一個答案,不再自行改判。
|
||
4. 使用者沒回答就**不預設** `develop`/`master`;預設值只在使用者明確同意後才成立。
|
||
|
||
## 判定遠端預設分支
|
||
|
||
1. 先用 `git symbolic-ref --quiet refs/remotes/origin/HEAD`,結果形如 `refs/remotes/origin/<預設分支>`。
|
||
2. 取不到時退而用 `git remote show origin`,找輸出中的 `HEAD branch:` 那行。
|
||
3. 兩者都取不到 → 回報並停止,**不臆測** `master`/`main`。
|
||
|
||
還沒有指定分支、需要挑一條基準或後備分支時(例如維護階段要落腳的分支):`origin/develop` 存在就用 `develop`;否則 `origin/master` 存在就用 `master`;兩者皆無則回報並停止。
|
||
|
||
這條後備順序**不適用於已指定的來源分支**。指定過的來源分支在遠端找不到,一律回報並停止,見總則的〔來源分支在遠端找不到〕。
|
||
|
||
## 只讀階段不動工作區
|
||
|
||
`analyze` 是 logic-only 階段:
|
||
|
||
- **工作目錄的 HEAD 必須與 `origin/{source-branch}` 指向同一個 commit**。落後、超前或分歧都代表讀到的不是遠端現況——停下來回報差距(`git rev-list --left-right --count origin/{source-branch}...HEAD`),請使用者自己處理。
|
||
- 需要換分支才能讀到正確現況時,**停下來請使用者自己切換**。
|
||
- 不代為 `switch`、不 `stash`、不動工作區、不建分支。
|
||
|
||
## 分支階梯與 base 推導
|
||
|
||
階梯表本身(哪一種類型往上接哪一條、`{子功能}` 怎麼組、base 為什麼一律由 `jsc-git/tools/base-branch.sh --derive` 推導)的唯一來源,在 `jsc-meta` 的 [`references/guidelines.md`](https://gitea.jsc.idv.tw/plugins/meta/src/branch/develop/references/guidelines.md)「PR 分支階梯」那一節。本檔不抄第二份,只寫 sdlc 拿到腳本回應之後要做什麼:
|
||
|
||
| 腳本回應 | 這個階段要做的事 |
|
||
| --- | --- |
|
||
| 印出一條分支名(結束碼 `0`) | 拿它當 `jsc-git:pr` 的 base,不再自行改判 |
|
||
| 結束碼 `7`(推不出唯一合法基底) | **中止並問使用者**,不退回 `develop`——退回 `develop` 就是讓子功能越級直接進主線 |
|
||
| stderr 印出「已建立功能主幹」 | 在階段回報裡講明建了哪一條主幹,那是這次實作多長出來的一條分支 |
|
||
| 結束碼 `2`(分支名含非 ASCII) | 先把中文簡述翻成英文短語,過 `jsc-git/tools/slugify.sh {類型} {英文短語}` 再重組分支名 |
|
||
|
||
## 實作階段的分支選擇
|
||
|
||
實作在 worktree 內進行,工作分支從 `origin/{source-branch}` 長出來,做完再 PR 回同一條來源分支。
|
||
|
||
- **一個工作包一個 PR**,目標一律是 `origin/{source-branch}`。不把兩個完成的工作包併成一個 PR,也不把完成的工作包留到下一包一起送——拆工作包的意義就是各自能獨立審查。
|
||
- 呼叫 `jsc-git:pr` 時**必須明確帶入來源分支當 base**。該 skill 在沒收到 base 時會自行退回 `develop`,那會讓單一工作包越過它所屬的功能分支直接進 develop。
|
||
- 來源分支整條後續要進 `develop` 時,那是另一個獨立的 PR,不在本階段範圍。
|
||
- 新分支名稱要可讀且不覆蓋既有分支;本地或遠端已存在同名分支時,換一個時間戳或短 hash。
|
||
- 切換或建立分支屬不可忽略的狀態變更,要在輸出中講清楚原因與結果分支名稱。
|
||
|
||
## 實作一律在 worktree 內進行
|
||
|
||
`implement` 動任何程式碼之前,先 `git fetch --prune origin`,再從**分析頁記錄的來源分支的遠端版本**(`origin/{source-branch}`)建立 git worktree。所有修改都在 worktree 內,主工作目錄的分支與工作區完全不動。
|
||
|
||
### 路徑
|
||
|
||
```
|
||
{cwd}/.worktree/{analysis-HASH}/{wp-number}/{repo}
|
||
```
|
||
|
||
- `{analysis-HASH}`:`ANALYZE_{HASH}` 的 HASH 部分,不含 `ANALYZE_` 前綴。
|
||
- `{wp-number}`:這次要做的工作包編號(例如 `WP-06`)。**一定要有這一層**:同一份分析常有好幾個互不相依的工作包平行進行,路徑少了這一層,兩個工作包會搶同一個目錄——第二個到的會被「目標路徑已存在」擋下,因為它不是同一份工作的 worktree。
|
||
- `{repo}`:存取庫名稱,不含 owner(`HP/WebService.Buy` → `WebService.Buy`)。不同 owner 的同名存取庫同時出現時,才改用 `{owner}-{repo}` 避免蓋掉,並在輸出中說明。
|
||
- 一份分析涉及多個存取庫時,每個存取庫各一個 worktree,並列在同一個 `{wp-number}` 目錄下。
|
||
|
||
### 建立前先問分支怎麼處理
|
||
|
||
依 `jsc-ask:ask` 的決策樹詢問,每個選項標明影響範圍。固定兩個選項:
|
||
|
||
| 選項 | 指令 | 影響 |
|
||
| --- | --- | --- |
|
||
| 以來源分支為基準開新工作分支 | `git worktree add -b {work-branch} {path} "origin/{source-branch}"` | commit 落在新分支,來源分支不動 |
|
||
| 直接簽出來源分支 | `git worktree add --track -b {source-branch} {path} "origin/{source-branch}"` | 建立追蹤遠端的本地分支再簽出;本地已存在同名分支時指令會失敗,此時先確認它與遠端一致才可改用 `git worktree add {path} {source-branch}`,不一致就停下回報 |
|
||
|
||
**不得自行預設**,也不得跳過詢問。
|
||
|
||
### 建立時的鐵則
|
||
|
||
- **分支名與路徑一律加引號**:既有的來源分支可能含空白、多層斜線或非 ASCII 字元(例如 `feat/addr-lookup/query/full/P2` 這種多層命名,或更舊的分支帶空白),不加引號會被切斷。
|
||
- **jsc 自己開的分支只用 ASCII**(`a-z0-9` 與 `/`、`-`)。中文標題先翻譯成英文短語,再 slug 化當分支名。
|
||
- 找不到該存取庫的本地 clone → **停下來問使用者路徑**,不自行 clone、不臆測位置。
|
||
- 目標路徑已存在 → 不覆蓋。先確認它是不是同一份工作的 worktree(`git worktree list`),是就沿用,不是就回報並停止。
|
||
- 把 `.worktree/` 加進該存取庫的 `.git/info/exclude`(不動使用者的 `.gitignore`,那是專案共用檔)。
|
||
- 建立後在輸出中明確列出:worktree 路徑、簽出的分支、來源分支,以及 `origin/{source-branch}` 當下的 commit sha——那是這次實作的起點,要能事後追。
|
||
|
||
### 移除時機
|
||
|
||
**PR 合併之後才移除**:`git worktree remove {path}`,接著 `git worktree prune`。
|
||
|
||
不是 PR 建立後就移除——審查留言可能要求修改,而修改要回到同一個 worktree、推到同一條工作分支(PR 會自己更新,不開第二個 PR)。先移除就得為了改一行重建整個 worktree。
|
||
|
||
- PR 尚未合併 → 保留。PR 建立失敗 → 保留,讓使用者接手處理。
|
||
- worktree 內還有未提交變更時**不移除**,回報並停止——那些變更沒有進 PR,移除等於丟掉。
|
||
- 同一份分析的多個 worktree 全部移除後,若 `.worktree/{HASH}/` 已空就一併刪掉那層目錄。
|
||
|
||
### 一個工作包的 PR 沒合併,擋的是相依於它的包,不是整份分析
|
||
|
||
一個工作包的 PR 還沒合併,這一包就沒結清:修正一律回原 worktree、推同一條工作分支,**不為同一包開第二個 PR**。
|
||
|
||
但「這一包沒結清」不等於「不得開始下一包」——SDLC 實作可能有好幾個互不相依的工作包平行進行,各自的 worktree、各自的 PR。真正被擋住的只有**相依於它的**工作包;跟它無關的工作包不受影響,可以另開一個工作階段(或另一個 worktree)同時進行。
|
||
|
||
這條規則在程式層強制,不靠內文自律,分工是兩層:
|
||
|
||
| 層 | 誰執行 | 做什麼 |
|
||
| --- | --- | --- |
|
||
| 工具閘門 | `tools/wp-gate.sh` | 唯一去問 Gitea 的一方,也是唯一有權解鎖的一方。`check` 查合併狀態、印出全部留言、印 `latest=` 時間戳,只管那一支 PR 自己;`check-deps` 查某個候選工作包能不能挑,活抓分析頁的相依欄逐一核對;`claim` 在領包當下登錄歸屬;`lock` 在 PR 開好後記下未結清並把 PR 掛到工作包名下;`owns` 比對某支 PR 是不是自己這一包的。狀態檔的寫入一律轉呼叫 `sdlc-gate.sh`。結束碼 0 放行、1 擋住、2 用法錯誤、3 查不到(查不到就擋,不放行) |
|
||
| hook 提醒 | `jsc-hooks` 的 `sdlc-gate.sh wp-check` | 只讀 `$JSC_HOME/wp/` 的狀態檔,不打網路。`prompt` 模式注入提醒但**不擋提示**;`skill` 模式擋掉 `analyze` 與 `maintain`,對 `plan` 只印提醒就放行,對 `implement` 一律放行——擋住的是「跳去做別的階段」,不是「回來把這一包做完」。`implement` 放行是閘門不自鎖的關鍵:結清那支 PR 的唯一路徑就是它,擋了就沒有人解得開。`plan` 降成提醒之後,放棄的是「手上工作包沒結清就別開新計畫」這道在製品上限,`analyze` 那道仍在,上限只是晚一個階段才生效。這一層是整個存取庫共用的粗粒度提醒,不是逐工作包判斷,細粒度的相依判斷交給 `check-deps`,歸屬比對交給 `owns` |
|
||
|
||
狀態檔不綁 session:PR 沒合併就是沒合併,開新對話照樣擋。
|
||
|
||
### 工作包隔離:一個工作階段只碰自己領的那一包
|
||
|
||
同一份分析常有好幾包平行進行,每包各自的 worktree 與 PR。動任何一支 PR 之前先確認它是自己這一包的,靠的是狀態檔而不是記憶——記憶跨不了工作階段。
|
||
|
||
| 項目 | 規則 |
|
||
| --- | --- |
|
||
| 歸屬依據 | **分析頁上的工作包代號**。不看分支名、不看 worktree 目錄名,那兩個都可能被改,改了也沒有徵兆。`WP-03`、`WP-3`、`3` 視為同一包 |
|
||
| 狀態檔 | 領取檔 `$JSC_HOME/wp/{owner}-{repo}.claim`(欄位 `repo`、`wp`、`pr`、`analyze`、`claimed`,一個存取庫一支)與鎖檔 `$JSC_HOME/wp/{owner}-{repo}-{index}.pr`(欄位 `repo`、`index`、`wp`、`locked`)。兩者都是純文字 key=value,一行一欄 |
|
||
| 誰寫 | 只有 `jsc-hooks` 的 `sdlc-gate.sh`:領包時 `wp-claim`、開完 PR 時 `wp-lock` 的第四個參數帶工作包代號、結清或交回時 `wp-unclaim` 與 `wp-unlock`。`wp-gate.sh` 一律轉呼叫,**不自己拼檔案**——兩邊各拼各的,格式一改就對不上,而且誰寫壞了看不出來 |
|
||
| 誰讀 | `wp-gate.sh owns` 與 `sdlc-gate.sh wp-check`,兩邊都只讀不改 |
|
||
| 查無歸屬 | **放行只提醒**:沒有分析頁、查不到工作包代號、領取檔不存在、工作包還沒掛上 PR,一律 `status=unowned` 並 exit 0。這跟「查不到就擋」不衝突——那條講的是查得到卻查失敗,這條講的是根本還沒有歸屬可查,拿不存在的歸屬擋人會讓沒登錄過的工作包全部動不了 |
|
||
| 逃生門 | `JSC_WP_GATE=off`,由 hook 那一層認 |
|
||
|
||
查驗程序的細節(何時帶 `--since`、怎麼等 PR 合併、留言逐筆的處理結果、修不動的留言怎麼辦、時間戳寫回哪裡)由 `skills/implement/SKILL.md` 步驟 2 與步驟 11 擁有,本檔不重複。
|
||
|
||
## 不破壞既有工作
|
||
|
||
- 工作區有未提交變更時,先提醒使用者 commit 或備份,**絕不**強制丟棄。
|
||
- 未提交變更導致切換分支或 pull 失敗 → 停止並回報,請使用者處理。
|
||
- **絕不** `reset --hard`/`checkout -f`/`clean`。
|