diff --git a/tools/base-branch.sh b/tools/base-branch.sh index 29e8a3e..69feb91 100755 --- a/tools/base-branch.sh +++ b/tools/base-branch.sh @@ -1,27 +1,51 @@ #!/usr/bin/env sh # base-branch.sh — 決定 PR 的基底分支。 # 用法: base-branch.sh [caller-base] -# 規則: 呼叫方傳入的分支最優先;呼叫方沒傳,才依序試 develop、main、master。 -# 先 git fetch --prune origin,再確認選中的分支存在於遠端。 +# 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。 +# 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]" >&2 + echo "用法: base-branch.sh [caller-base] 或 base-branch.sh --derive [branch]" >&2 exit 2 fi ARGC=$# -CALLER=${1:-} +ARG=${1:-} -if [ "$ARGC" -eq 1 ] && [ -z "$CALLER" ]; then - echo "錯誤: 呼叫方傳入空字串當基底分支。請確認來源分支變數有值,或整個參數不要傳。" >&2 +if [ "$ARGC" -eq 1 ] && [ -z "$ARG" ]; then + echo "錯誤: 傳入空字串當分支名。請確認分支變數有值,或整個參數不要傳。" >&2 exit 6 fi @@ -34,21 +58,107 @@ remote_has() { [ -n "$(git ls-remote --heads origin "refs/heads/$1" 2>/dev/null)" ] } -if [ "$ARGC" -eq 1 ]; then - if remote_has "$CALLER"; then - printf '%s\n' "$CALLER" - exit 0 +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 - echo "錯誤: 呼叫方指定的基底分支 $CALLER 不在 origin 上。請確認分支名,不要自行改用其他分支。" >&2 - exit 4 + + 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 -for candidate in develop main master; do - if remote_has "$candidate"; then - printf '%s\n' "$candidate" - exit 0 - fi -done +# 以下是 --derive 模式。 +BRANCH=$ARG +if [ -z "$BRANCH" ]; then + BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true) +fi -echo "錯誤: origin 上找不到 develop、main、master。請由呼叫方指定基底分支。" >&2 -exit 5 +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