Files
git/tools/base-branch.sh
T
jiantw83 cc23722f31 feat(base-branch): 新增 --derive 從分支名推出階梯上一階
What:`tools/base-branch.sh` 新增 `--derive [分支]` 模式,從分支名推出唯一合法的上一階基底。`feat` 這一類走 `{類型}/{功能}/{子功能}` → `{類型}/{功能}/main` → `develop` → `master`,`fix` 走 `fix/{修改}` → `develop` → `master`。新增退出碼 `7`(推不出唯一合法基底)、`8`(推導出的基底不在遠端又不能自動建立)、`9`(自動建立功能主幹失敗)。既有的呼叫方模式一字未改。

Why:PR 階梯禁止越級,但基底原本靠呼叫端自己挑,挑錯就是把一個子功能直接併進 `develop`,中間那層功能主幹整個被跳過。判斷寫進程式,越級就不會發生在「這次剛好沒注意」的時候。

How:`{子功能}` 可以多層,推導一律把最後一段換成 `main`。推不出唯一合法基底就中止並回 `7`,由呼叫端問使用者,不猜、也不退回 `develop`——退回 `develop` 正是這支腳本要擋的那件事。功能主幹不在 origin 時,直接把 `origin/develop` 推成新分支自動補一條,不動本機工作區,再把建立了哪一條分支印到 stderr。分支名只收 ASCII 小寫、數字、連字號與斜線,中文簡述請先過 `tools/slugify.sh`。

Who:`jsc-git:pr` 決定 PR base 的那一步,以及所有經由它開 PR 的技能。
2026-08-27 11:20:27 +08:00

165 lines
6.0 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# base-branch.sh — 決定 PR 的基底分支。
# 用法: base-branch.sh [caller-base]
# base-branch.sh --derive [branch]
#
# 模式一(既有): 呼叫方傳入的分支最優先;呼叫方沒傳,才依序試 develop、main、master。
# 「有沒有傳」看參數個數,不看參數內容:呼叫方寫 base-branch.sh "$SOURCE_BRANCH"
# 而變數沒設定時,那是空字串,不是沒傳。若當成沒傳就會悄悄退回 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;
# develop、main、master 都不在遠端回傳 5;傳入空字串回傳 6;
# 推不出唯一合法基底回傳 7;推導出的基底不在遠端又不能自動建立回傳 8;
# 自動建立功能主幹失敗回傳 9。
# 錯誤訊息一律印繁中到 stderr。
set -u
MODE=caller
if [ "$#" -ge 1 ] && [ "$1" = "--derive" ]; then
MODE=derive
shift
fi
if [ "$#" -gt 1 ]; then
echo "用法: base-branch.sh [caller-base] 或 base-branch.sh --derive [branch]" >&2
exit 2
fi
ARGC=$#
ARG=${1:-}
if [ "$ARGC" -eq 1 ] && [ -z "$ARG" ]; then
echo "錯誤: 傳入空字串當分支名。請確認分支變數有值,或整個參數不要傳。" >&2
exit 6
fi
if ! git fetch --prune origin >/dev/null 2>&1; then
echo "錯誤: 無法向 origin 取得遠端分支。請確認遠端可以連線,再重試。" >&2
exit 3
fi
remote_has() {
[ -n "$(git ls-remote --heads origin "refs/heads/$1" 2>/dev/null)" ]
}
if [ "$MODE" = "caller" ]; then
if [ "$ARGC" -eq 1 ]; then
if remote_has "$ARG"; then
printf '%s\n' "$ARG"
exit 0
fi
echo "錯誤: 呼叫方指定的基底分支 $ARG 不在 origin 上。請確認分支名,不要自行改用其他分支。" >&2
exit 4
fi
for candidate in develop main master; do
if remote_has "$candidate"; then
printf '%s\n' "$candidate"
exit 0
fi
done
echo "錯誤: origin 上找不到 develop、main、master。請由呼叫方指定基底分支。" >&2
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