feat(git): PR 分支階梯推導與既有 PR 校準 #11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-git",
|
"name": "jsc-git",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "Commit 分組認可與 Push Request 建立",
|
"description": "Commit 分組認可與 Push Request 建立",
|
||||||
"skills": "./skills",
|
"skills": "./skills",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-git",
|
"name": "jsc-git",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "Commit 分組認可與 Push Request 建立",
|
"description": "Commit 分組認可與 Push Request 建立",
|
||||||
"skills": "./skills"
|
"skills": "./skills"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,21 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
| 檔案 | 用途 |
|
| 檔案 | 用途 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `tools/base-branch.sh` | 決定 PR 基底分支:呼叫方傳入的分支最優先,沒傳才依序試 develop、main、master;一律確認分支存在於遠端,找不到就回傳非零。「沒傳」看參數個數,傳入空字串算錯誤,不會退回 develop |
|
| `tools/base-branch.sh` | 決定 PR 基底分支。兩種模式:不帶旗標時,呼叫方傳入的分支最優先,沒傳才依序試 develop、main、master;`--derive [分支]` 從分支名推出階梯的上一階。一律確認分支存在於遠端,找不到就回傳非零。「沒傳」看參數個數,傳入空字串算錯誤,不會退回 develop |
|
||||||
| `tools/slugify.sh` | 把類型與英文短語組成 ASCII 分支名 `{type}/{slug}`;輸入含非 ASCII 或 slug 化後為空,就回傳非零並要求先翻譯成英文短語 |
|
| `tools/slugify.sh` | 把類型與英文短語組成 ASCII 分支名 `{type}/{slug}`;輸入含非 ASCII 或 slug 化後為空,就回傳非零並要求先翻譯成英文短語。第一個參數可以帶斜線,所以連叫兩次就組得出 `feat/{功能}/{子功能}` |
|
||||||
|
|
||||||
|
## PR 階梯
|
||||||
|
|
||||||
|
每個 PR 只往上一階開,禁止越級。基底一律由 `tools/base-branch.sh --derive` 推導,不手挑。
|
||||||
|
|
||||||
|
| Commit 類型 | 階梯 |
|
||||||
|
| --- | --- |
|
||||||
|
| feat、docs、style、refactor、perf、test、chore、revert | `{類型}/{功能}/{子功能}` → `{類型}/{功能}/main` → `develop` → `master` |
|
||||||
|
| fix | `fix/{修改}` → `develop` → `master` |
|
||||||
|
|
||||||
|
`{子功能}` 可以多層(例:`feat/a/b/c`),推導一律把最後一段換成 `main`。推不出唯一合法基底就回傳 7 並中止,由呼叫端問使用者,不猜也不退回 develop。功能主幹 `{類型}/{功能}/main` 不在遠端時,自動以 develop 為起點建立並推上去,再把建立了哪一條分支印到 stderr。
|
||||||
|
|
||||||
|
分支名只允許 ASCII(小寫、數字、連字號、斜線)。中文簡述先過 `tools/slugify.sh`,`--derive` 不收非 ASCII 分支名。
|
||||||
|
|
||||||
## Skills 目錄
|
## Skills 目錄
|
||||||
|
|
||||||
@@ -33,11 +46,11 @@ Marketplace 統一為 `jsc`(https://gitea.jsc.idv.tw/plugins/meta.git),安
|
|||||||
|
|
||||||
### `commit`
|
### `commit`
|
||||||
|
|
||||||
追蹤所有檔案變更,依 Commit 格式 `{類型}({需求 or 功能}): {訊息}` 將同類型與同需求的變更認可在一起。認可前先跑 `jsc-hooks` 的 `comment-scope.sh sweep` 掃過工作區,攔下夾帶文件相關資訊的註解;腳本不在本機就跳過並在回報中說明,不中止認可。訊息格式三選一:完整版(What/Why/How/Who)、簡易版(依 git diff 總結一句)、自訂。
|
追蹤所有檔案變更,依 Commit 格式 `{類型}({需求 or 功能}): {訊息}` 將同類型與同需求的變更認可在一起。認可前先跑 `jsc-hooks` 的 `comment-scope.sh sweep` 掃過工作區,攔下夾帶文件相關資訊的註解;腳本不在本機就跳過並在回報中說明,不中止認可。訊息格式三選一:完整版(What/Why/How/Who)、簡易版(依 git diff 總結一句)、自訂。認可完成後,目前分支若已有開啟中的 PR,就交給 `pr` 校準標題、描述與前置 PR 依賴。
|
||||||
|
|
||||||
### `pr`
|
### `pr`
|
||||||
|
|
||||||
先認可所有變更,再建立目標分支、push、以範本描述建立 Gitea PR。基底分支優先採用呼叫方傳入的分支;呼叫方沒傳,才依序退回 develop、main、master。分支名只允許 ASCII:類型取 commit 優先度最高者,標題先翻譯成英文短語再 slug 化。
|
先認可所有變更,再依階梯命名目標分支、push、以範本描述建立 Gitea PR。基底分支由 `base-branch.sh --derive` 從分支名推出上一階;呼叫方傳入的基底與推導結果不同,就當成越級擋下並說明正確階梯,不會悄悄改目標。分支名只允許 ASCII:類型取 commit 優先度最高者,功能與標題先翻譯成英文短語再 slug 化。分支已有開啟中的 PR 時不重開,改成比對標題、描述、前置 PR 依賴三項,只有不一樣的那幾項才送出 API 呼叫。
|
||||||
|
|
||||||
<!-- JSC-SKILLS:END -->
|
<!-- JSC-SKILLS:END -->
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jsc-git",
|
"name": "jsc-git",
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"description": "Commit 分組認可與 Push Request 建立",
|
"description": "Commit 分組認可與 Push Request 建立",
|
||||||
"skills": "./skills/"
|
"skills": "./skills/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: commit
|
name: commit
|
||||||
description: Group all pending file changes by conventional type and feature, then commit each group as {type}({scope}): {message}. Before committing, sweep the working tree with jsc-hooks/hooks/comment-scope.sh so no comment carrying document tracking information enters a commit. Message style is full (What/Why/How/Who), brief (one line from git diff), or custom, chosen via decision tree. Use whenever changes must be committed; not for push or PR creation.
|
description: Group all pending file changes by conventional type and feature, then commit each group as {type}({scope}): {message}. Before committing, sweep the working tree with jsc-hooks/hooks/comment-scope.sh so no comment carrying document tracking information enters a commit. Message style is full (What/Why/How/Who), brief (one line from git diff), or custom, chosen via decision tree. Once the commits land, an open PR on the current branch gets its title, description, and prerequisite dependency calibrated through jsc-git:pr. Use whenever changes must be committed; not for push or PR creation.
|
||||||
---
|
---
|
||||||
|
|
||||||
# commit — group and commit file changes
|
# commit — group and commit file changes
|
||||||
@@ -15,6 +15,7 @@ description: Group all pending file changes by conventional type and feature, th
|
|||||||
- Done when one of these is true and reported: the sweep exited 0, or every remaining warning is reported with the reason it is a false positive, or the script was not found on this machine.
|
- Done when one of these is true and reported: the sweep exited 0, or every remaining warning is reported with the reason it is a false positive, or the script was not found on this machine.
|
||||||
3. Group the changes by same type plus same requirement or feature. One group is one commit. Done when each group carries one type and one requirement or feature, and no path sits in two groups.
|
3. Group the changes by same type plus same requirement or feature. One group is one commit. Done when each group carries one type and one requirement or feature, and no path sits in two groups.
|
||||||
4. Commit each group with the format `{type}({requirement or feature}): {message}`. Done when `git status --porcelain` returns empty; report done only then.
|
4. Commit each group with the format `{type}({requirement or feature}): {message}`. Done when `git status --porcelain` returns empty; report done only then.
|
||||||
|
5. Calibrate the open PR of this branch, when there is one: `jsc-gitea/tools/gitea.sh api GET /repos/{owner}/{repo}/pulls?state=open` and match `head.ref` against the current branch. A match → hand the PR number to `jsc-git:pr` step 9, which compares title, description, and prerequisite dependency and updates only what differs. No match → nothing to do here. Done when either the branch has no open PR, or `jsc-git:pr` reported each of the three items as matched or updated.
|
||||||
|
|
||||||
## Type table
|
## Type table
|
||||||
|
|
||||||
@@ -42,4 +43,4 @@ Ask the user per the `jsc-ask:ask` rules. Skip the question when the question re
|
|||||||
|
|
||||||
1. The grouping and message drafting details **MUST run as a sub agent**. The main agent only confirms the grouping and runs the commits.
|
1. The grouping and message drafting details **MUST run as a sub agent**. The main agent only confirms the grouping and runs the commits.
|
||||||
2. Write messages in UTF-8 Traditional Chinese (the type and scope stay in English), per the STE100 output rule.
|
2. Write messages in UTF-8 Traditional Chinese (the type and scope stay in English), per the STE100 output rule.
|
||||||
3. Never push. Push and PR belong to `jsc-git:pr`.
|
3. Never push, and never create a PR. Step 5 only calibrates a PR that already exists; pushing and creating PRs belong to `jsc-git:pr`.
|
||||||
|
|||||||
+36
-11
@@ -1,31 +1,56 @@
|
|||||||
---
|
---
|
||||||
name: pr
|
name: pr
|
||||||
description: Commit all changes via jsc-git:commit, create a target branch named from the highest-priority commit type plus a summarized title, push, then open a Gitea PR with the templated description against the base branch the caller passed in (falling back to develop, main, master only when none was given). Branch priority is revert > fix > feat > perf > refactor > test > docs > style > chore. Use when work is ready for review; not for plain commits.
|
description: Commit all changes via jsc-git:commit, name a ladder-shaped target branch, derive the base with tools/base-branch.sh (sub-feature → feat/{feature}/main → develop → master; fix → develop → master, no level skipping), push, then open a Gitea PR with the templated description. When the branch already has an open PR, compare its title, description, and prerequisite dependency, and update only the items that differ. Use when work is ready for review, or when an open PR needs re-syncing after new commits; not for plain commits.
|
||||||
---
|
---
|
||||||
|
|
||||||
# pr — create a Push Request
|
# pr — create a Push Request
|
||||||
|
|
||||||
|
## PR ladder
|
||||||
|
|
||||||
|
Every PR targets the next rung up. Skipping a rung is forbidden.
|
||||||
|
|
||||||
|
| Commit type | Ladder |
|
||||||
|
| --- | --- |
|
||||||
|
| feat, docs, style, refactor, perf, test, chore, revert | `{type}/{feature}/{sub-feature}` → `{type}/{feature}/main` → `develop` → `master` |
|
||||||
|
| fix | `fix/{change}` → `develop` → `master` |
|
||||||
|
|
||||||
|
`{sub-feature}` may hold several levels (`feat/a/b/c`); the base is always the branch name with its last segment replaced by `main`. `tools/base-branch.sh --derive` owns this derivation — never hand-pick a base.
|
||||||
|
|
||||||
## Steps
|
## Steps
|
||||||
|
|
||||||
1. Call `jsc-git:commit` to commit every file change first. Done when `git status --porcelain` prints nothing.
|
1. Call `jsc-git:commit` to commit every file change first. Done when `git status --porcelain` prints nothing.
|
||||||
2. Resolve the base branch: `tools/base-branch.sh {base branch the caller passed in, omitted when the caller passed none}`. **A base branch passed in by the caller wins over everything else** — `jsc-sdlc:implement` passes the analysis page's source branch, and silently retargeting that PR at `develop` would merge a single work package straight past the feature branch it belongs to. Done when the script prints one branch name; a non-zero exit → report and stop, never fall back silently.
|
2. Build the target branch name in ladder shape:
|
||||||
3. Create the target branch from the remote base branch:
|
|
||||||
1. Take the type with the highest priority across all commits: `revert > fix > feat > perf > refactor > test > docs > style > chore`. Done when exactly one type is picked.
|
1. Take the type with the highest priority across all commits: `revert > fix > feat > perf > refactor > test > docs > style > chore`. Done when exactly one type is picked.
|
||||||
2. Summarize one title from all commit messages. Done when one title line covers every commit in the range.
|
2. Summarize one title from all commit messages. Done when one title line covers every commit in the range.
|
||||||
3. Translate the requirement and the title into a short English phrase, then run `tools/slugify.sh {type} {phrase}` to build the target branch name `{type}/{requirement or feature}-{title}` (example: a non-English request to export order reports becomes the phrase `order export report`, then `feat/order-export-report`). Done when the script prints one slug on stdout; exit 2 (non-ASCII input) or exit 3 (empty slug) → re-translate the title into an English phrase and retry, never hand-build the branch name.
|
3. Translate the feature and the title into short English phrases, then build the name with `tools/slugify.sh`. `fix` takes one call: `tools/slugify.sh fix {change phrase}` → `fix/order-export-crash`. Every other type takes two calls, feeding the first result back in as the type: `tools/slugify.sh feat {feature phrase}` → `feat/order-export`, then `tools/slugify.sh feat/order-export {sub-feature phrase}` → `feat/order-export/report-filter`. Done when the name has the shape its ladder row requires; exit 2 (non-ASCII input) or exit 3 (empty slug) → re-translate into an English phrase and retry, never hand-build the branch name.
|
||||||
4. Run `git checkout -b {target branch} origin/{base branch}` so the branch starts at the remote base, then bring step 1's commits onto it (`git cherry-pick` them when they landed on another branch). Done when `git branch --show-current` prints the target branch and `git log --oneline origin/{base branch}..HEAD` lists exactly step 1's commits.
|
3. Resolve the base branch:
|
||||||
4. Run `git push -u origin {target branch}`. Done when `git ls-remote --heads origin` shows the target branch's ref.
|
- Run `tools/base-branch.sh --derive {target branch}`. Done when the script prints one branch name.
|
||||||
5. Create the PR: `jsc-gitea/tools/gitea.sh pr-create {owner}/{repo} {target branch} {base branch} "{branch name}" {description file}`.
|
- The caller passed a base in (`jsc-sdlc:implement` passes the analysis page's source branch): run `tools/base-branch.sh {caller base}` as well. Both print the same name → use it. They differ → the pair skips a rung, so stop, report the caller base, the derived base, and the ladder row that applies, and ask the user per the `jsc-ask:ask` rules whether to rename the branch or correct the caller base. Never silently retarget the PR.
|
||||||
|
- Exit 7 (no unique legal base), 8 (derived base missing on origin), or 9 (auto-create failed) → report the script's message and stop. Never fall back to `develop`.
|
||||||
|
- The feature trunk missing on origin is not an error: the script creates it from `develop`, pushes it, and prints 「已自動從 develop 建立功能主幹 {分支}」 to stderr. Capture that line; step 10 has to report it.
|
||||||
|
4. Run `git checkout -b {target branch} origin/{base branch}` so the branch starts at the remote base, then bring step 1's commits onto it (`git cherry-pick` them when they landed on another branch). Done when `git branch --show-current` prints the target branch and `git log --oneline origin/{base branch}..HEAD` lists exactly step 1's commits.
|
||||||
|
5. Run `git push -u origin {target branch}`. Done when `git ls-remote --heads origin` shows the target branch's ref.
|
||||||
|
6. Find out whether the target branch already has an open PR: `jsc-gitea/tools/gitea.sh api GET /repos/{owner}/{repo}/pulls?state=open` and match `head.ref` against the target branch. Done when you hold either one PR number or the fact that there is none; a PR number → skip step 7 and step 8 and go to step 9, because the PR already exists and only needs calibrating.
|
||||||
|
7. Create the PR: `jsc-gitea/tools/gitea.sh pr-create {owner}/{repo} {target branch} {base branch} "{branch name}" {description file}`.
|
||||||
- Title = branch name.
|
- Title = branch name.
|
||||||
- Write the description into a temp file first, using `templates/pr-description.md` (a Traditional Chinese template; the generated description stays in Traditional Chinese per the STE100 output rule).
|
- Write the description into a temp file first, using `templates/pr-description.md` (a Traditional Chinese template; the generated description stays in Traditional Chinese per the STE100 output rule).
|
||||||
- Done when the command prints a PR URL; no URL → report the error and stop.
|
- Done when the command prints a PR URL; no URL → report the error and stop.
|
||||||
6. **Prerequisite PR blocking**: when the description lists a prerequisite Push Request, run
|
8. **Prerequisite PR blocking**: when the description lists a prerequisite Push Request, run
|
||||||
`jsc-gitea/tools/gitea.sh pr-depend {owner}/{repo} {this PR number} {prerequisite owner}/{repo} {prerequisite PR number}`
|
`jsc-gitea/tools/gitea.sh pr-depend {owner}/{repo} {this PR number} {prerequisite owner}/{repo} {prerequisite PR number}`
|
||||||
to add the dependency. Gitea then blocks merging until the prerequisite PR closes. On exit 4 (the dependency API call failed) degrade: prefix the PR title with `WIP:`, which Gitea blocks merging on natively, and record in the PR description that the prefix comes off once the prerequisite PR closes. Done when `pr-depend` printed `OK {owner}/{repo}#{number} depends on ...`, or the PR title starts with `WIP:` and the description names the prerequisite PR.
|
to add the dependency. Gitea then blocks merging until the prerequisite PR closes. On exit 4 (the dependency API call failed) degrade: prefix the PR title with `WIP:`, which Gitea blocks merging on natively, and record in the PR description that the prefix comes off once the prerequisite PR closes. Done when `pr-depend` printed `OK {owner}/{repo}#{number} depends on ...`, or the PR title starts with `WIP:` and the description names the prerequisite PR. A PR created here goes straight to step 10.
|
||||||
7. Report the PR URL. Done when the reported URL is the one step 5 printed, together with the base branch and the target branch it was opened against.
|
9. **Calibrate an existing PR** (step 6 found a PR number): read it once with `jsc-gitea/tools/gitea.sh pr-get {owner}/{repo} {pr number}` — line 1 is `title<TAB>{title}`, line 2 is `base<TAB>{base}`, line 3 is the marker `body`, and line 4 onward is the description. Compare three items against what this run produced, and send an API call only for the ones that differ:
|
||||||
|
1. Title against the target branch name.
|
||||||
|
2. Description against a freshly drafted description from `templates/pr-description.md`.
|
||||||
|
3. Prerequisite PR dependency against the one the description names.
|
||||||
|
- Title or description differs → write the new description to a temp file and run `jsc-gitea/tools/gitea.sh pr-edit {owner}/{repo} {pr number} "{title}" {description file}`, which carries both items in one call.
|
||||||
|
- The dependency differs → run `pr-depend` as in step 8.
|
||||||
|
- All three match → change nothing. Every needless edit notifies every reviewer, so silence is the correct outcome here.
|
||||||
|
- Done when each of the three items is reported as either matched or updated, and the number of API calls made equals the number of items that differed.
|
||||||
|
10. Report the PR URL, the base branch, the target branch, which of the three calibration items were updated, and the feature trunk the script auto-created when step 3 printed that line. Done when the report names all of them.
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. No template section may stay empty: fill the literal 「無」 when there is no plan page, analyze page, or prerequisite PR.
|
1. No template section may stay empty: fill the literal 「無」 when there is no plan page, analyze page, or prerequisite PR.
|
||||||
2. Read the `jsc-sdlc` plan page and analyze page through `jsc-gitea:wiki`, which resolves `JSC_WIKI_REPO_PLAN` for the plan page and `JSC_WIKI_REPO_ANALYZE` for the analyze page from the inherited environment, falls back to `JSC_WIKI_REPO`, and asks only when neither is set. Each page type reads its own variable only; a page type never borrows another type's variable. Take both links from the pages read this way; the analyze link must point at the work package heading anchor.
|
2. Read the `jsc-sdlc` plan page and analyze page through `jsc-gitea:wiki`, which resolves `JSC_WIKI_REPO_PLAN` for the plan page and `JSC_WIKI_REPO_ANALYZE` for the analyze page from the inherited environment, falls back to `JSC_WIKI_REPO`, and asks only when neither is set. Each page type reads its own variable only; a page type never borrows another type's variable. Take both links from the pages read this way; the analyze link must point at the work package heading anchor.
|
||||||
3. Branch title summarization (step 3.2) and description drafting MUST run as a sub agent.
|
3. Branch title summarization (step 2.2) and description drafting MUST run as a sub agent.
|
||||||
|
4. Branch names are ASCII only (`a-z0-9`, `/`, `-`). Traditional Chinese phrases go through `tools/slugify.sh` first; `tools/base-branch.sh --derive` rejects anything else.
|
||||||
|
|||||||
+126
-16
@@ -1,27 +1,51 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# base-branch.sh — 決定 PR 的基底分支。
|
# base-branch.sh — 決定 PR 的基底分支。
|
||||||
# 用法: base-branch.sh [caller-base]
|
# 用法: base-branch.sh [caller-base]
|
||||||
# 規則: 呼叫方傳入的分支最優先;呼叫方沒傳,才依序試 develop、main、master。
|
# base-branch.sh --derive [branch]
|
||||||
# 先 git fetch --prune origin,再確認選中的分支存在於遠端。
|
#
|
||||||
|
# 模式一(既有): 呼叫方傳入的分支最優先;呼叫方沒傳,才依序試 develop、main、master。
|
||||||
# 「有沒有傳」看參數個數,不看參數內容:呼叫方寫 base-branch.sh "$SOURCE_BRANCH"
|
# 「有沒有傳」看參數個數,不看參數內容:呼叫方寫 base-branch.sh "$SOURCE_BRANCH"
|
||||||
# 而變數沒設定時,那是空字串,不是沒傳。若當成沒傳就會悄悄退回 develop,
|
# 而變數沒設定時,那是空字串,不是沒傳。若當成沒傳就會悄悄退回 develop,
|
||||||
# 把單一工作包直接合進 develop——這支腳本存在的目的就是擋這件事。
|
# 把單一工作包直接合進 develop——這支腳本存在的目的就是擋這件事。
|
||||||
|
#
|
||||||
|
# 模式二(--derive): 從分支名推出唯一合法的上一階基底,禁止越級。
|
||||||
|
# 沒傳 branch 就取目前分支。階梯如下:
|
||||||
|
# feat、docs、style、refactor、perf、test、chore、revert:
|
||||||
|
# {類型}/{功能}/{子功能} → {類型}/{功能}/main → develop → master
|
||||||
|
# {子功能} 可以多層(例:feat/a/b/c),推導一律「去掉最後一段後接 /main」。
|
||||||
|
# fix:
|
||||||
|
# fix/{修改} → develop → master
|
||||||
|
# 推不出唯一合法基底就中止(退出碼 7),由呼叫端問使用者,不猜、也不退回 develop。
|
||||||
|
# 功能主幹 {類型}/{功能}/main 不在 origin 時,自動以 origin/develop 為起點建立並推上去,
|
||||||
|
# 再把建立了哪一條分支印到 stderr。
|
||||||
|
#
|
||||||
|
# 分支名只允許 ASCII(a-z0-9 與 /、-)。中文簡述先交給同目錄的 slugify.sh 轉成 ASCII slug,
|
||||||
|
# 再組成分支名,這支腳本不接受非 ASCII 分支名。
|
||||||
|
#
|
||||||
# 輸出: 選中的分支名(一行)。
|
# 輸出: 選中的分支名(一行)。
|
||||||
# 護欄: 參數過多回傳 2;抓不到遠端回傳 3;呼叫方指定的分支不在遠端回傳 4;
|
# 護欄: 參數過多回傳 2;抓不到遠端回傳 3;呼叫方指定的分支不在遠端回傳 4;
|
||||||
# develop、main、master 都不在遠端回傳 5;呼叫方傳入空字串回傳 6。
|
# develop、main、master 都不在遠端回傳 5;傳入空字串回傳 6;
|
||||||
|
# 推不出唯一合法基底回傳 7;推導出的基底不在遠端又不能自動建立回傳 8;
|
||||||
|
# 自動建立功能主幹失敗回傳 9。
|
||||||
# 錯誤訊息一律印繁中到 stderr。
|
# 錯誤訊息一律印繁中到 stderr。
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
|
MODE=caller
|
||||||
|
if [ "$#" -ge 1 ] && [ "$1" = "--derive" ]; then
|
||||||
|
MODE=derive
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$#" -gt 1 ]; then
|
if [ "$#" -gt 1 ]; then
|
||||||
echo "用法: base-branch.sh [caller-base]" >&2
|
echo "用法: base-branch.sh [caller-base] 或 base-branch.sh --derive [branch]" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ARGC=$#
|
ARGC=$#
|
||||||
CALLER=${1:-}
|
ARG=${1:-}
|
||||||
|
|
||||||
if [ "$ARGC" -eq 1 ] && [ -z "$CALLER" ]; then
|
if [ "$ARGC" -eq 1 ] && [ -z "$ARG" ]; then
|
||||||
echo "錯誤: 呼叫方傳入空字串當基底分支。請確認來源分支變數有值,或整個參數不要傳。" >&2
|
echo "錯誤: 傳入空字串當分支名。請確認分支變數有值,或整個參數不要傳。" >&2
|
||||||
exit 6
|
exit 6
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -34,21 +58,107 @@ remote_has() {
|
|||||||
[ -n "$(git ls-remote --heads origin "refs/heads/$1" 2>/dev/null)" ]
|
[ -n "$(git ls-remote --heads origin "refs/heads/$1" 2>/dev/null)" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "$ARGC" -eq 1 ]; then
|
if [ "$MODE" = "caller" ]; then
|
||||||
if remote_has "$CALLER"; then
|
if [ "$ARGC" -eq 1 ]; then
|
||||||
printf '%s\n' "$CALLER"
|
if remote_has "$ARG"; then
|
||||||
|
printf '%s\n' "$ARG"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "錯誤: 呼叫方指定的基底分支 $CALLER 不在 origin 上。請確認分支名,不要自行改用其他分支。" >&2
|
echo "錯誤: 呼叫方指定的基底分支 $ARG 不在 origin 上。請確認分支名,不要自行改用其他分支。" >&2
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for candidate in develop main master; do
|
for candidate in develop main master; do
|
||||||
if remote_has "$candidate"; then
|
if remote_has "$candidate"; then
|
||||||
printf '%s\n' "$candidate"
|
printf '%s\n' "$candidate"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "錯誤: origin 上找不到 develop、main、master。請由呼叫方指定基底分支。" >&2
|
echo "錯誤: origin 上找不到 develop、main、master。請由呼叫方指定基底分支。" >&2
|
||||||
exit 5
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 以下是 --derive 模式。
|
||||||
|
BRANCH=$ARG
|
||||||
|
if [ -z "$BRANCH" ]; then
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
|
||||||
|
echo "錯誤: 取不到目前分支名(可能在斷頭狀態)。請先切到分支,或直接把分支名當參數傳進來。" >&2
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s' "$BRANCH" | LC_ALL=C grep -q '[^a-z0-9/-]'; then
|
||||||
|
echo "錯誤: 分支名 $BRANCH 含分支名不允許的字元。分支名只允許 ASCII 小寫、數字、連字號與斜線;中文簡述請先交給 slugify.sh。" >&2
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
TYPE=${BRANCH%%/*}
|
||||||
|
SEGMENTS=$(printf '%s' "$BRANCH" | tr '/' '\n' | grep -c '')
|
||||||
|
LAST=${BRANCH##*/}
|
||||||
|
HEAD_PART=${BRANCH%/*}
|
||||||
|
|
||||||
|
ladder_type() {
|
||||||
|
case "$1" in
|
||||||
|
feat|docs|style|refactor|perf|test|chore|revert) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE=""
|
||||||
|
TRUNK_AUTOCREATE=no
|
||||||
|
|
||||||
|
if [ "$BRANCH" = "develop" ]; then
|
||||||
|
BASE=master
|
||||||
|
elif [ "$BRANCH" = "master" ]; then
|
||||||
|
echo "錯誤: master 已經是階梯頂端,推不出上一階基底分支。請確認是不是站錯分支。" >&2
|
||||||
|
exit 7
|
||||||
|
elif [ "$TYPE" = "fix" ]; then
|
||||||
|
if [ "$SEGMENTS" -eq 2 ]; then
|
||||||
|
BASE=develop
|
||||||
|
else
|
||||||
|
echo "錯誤: 分支 $BRANCH 不符合 fix 階梯。fix 只有 fix/{修改} → develop → master 一條路,不接受多層分支名。" >&2
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
elif ladder_type "$TYPE"; then
|
||||||
|
if [ "$SEGMENTS" -lt 3 ]; then
|
||||||
|
echo "錯誤: 分支 $BRANCH 少了功能層,推不出唯一合法基底。$TYPE 階梯為 $TYPE/{功能}/{子功能} → $TYPE/{功能}/main → develop → master,請補上功能層再重試。" >&2
|
||||||
|
exit 7
|
||||||
|
elif [ "$LAST" = "main" ]; then
|
||||||
|
BASE=develop
|
||||||
|
else
|
||||||
|
BASE="$HEAD_PART/main"
|
||||||
|
TRUNK_AUTOCREATE=yes
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "錯誤: 分支 $BRANCH 的類型 $TYPE 不在階梯表內,推不出唯一合法基底。請改用 feat、fix、docs、style、refactor、perf、test、chore、revert 其中一種類型。" >&2
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
if remote_has "$BASE"; then
|
||||||
|
printf '%s\n' "$BASE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TRUNK_AUTOCREATE" = "no" ]; then
|
||||||
|
echo "錯誤: 推導出的基底分支 $BASE 不在 origin 上,而且它不是可以自動建立的功能主幹。請先建立 $BASE,再重試。" >&2
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 功能主幹不存在就自動補一條:直接把 origin/develop 推成新分支,
|
||||||
|
# 不動本機工作區,省下切分支與切回來的來回。
|
||||||
|
if ! remote_has develop; then
|
||||||
|
echo "錯誤: origin 上沒有 develop,無法自動建立功能主幹 $BASE。請先建立 develop,再重試。" >&2
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git push origin "refs/remotes/origin/develop:refs/heads/$BASE" >/dev/null 2>&1; then
|
||||||
|
echo "錯誤: 自動建立功能主幹 $BASE 失敗。請確認遠端推送權限,或手動從 develop 建立 $BASE。" >&2
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "已自動從 develop 建立功能主幹 $BASE,並推上 origin。" >&2
|
||||||
|
printf '%s\n' "$BASE"
|
||||||
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user