diff --git a/tools/build-test.sh b/tools/build-test.sh index d7fcfea..9072e79 100755 --- a/tools/build-test.sh +++ b/tools/build-test.sh @@ -1,25 +1,37 @@ #!/usr/bin/env sh # build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。 -# 用法:build-test.sh +# 用法: +# build-test.sh 偵測後實際執行 +# build-test.sh --detect 只偵測,唯讀,不執行任何建置或測試 # ecosystem:nodejs | python | dotnet # 偵測方式: # nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有) # python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py # dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet -# 輸出:底層指令的原始輸出。 +# 為什麼要有 --detect:推不出指令(5)本來要等所有版本都改寫完才發現,使用者又答不出指令時, +# 前面全數作廢還要走還原。--detect 讓呼叫方在動任何檔案之前先問出結果,工作區完全沒被動過。 +# --detect 只做偵測與前置檢查,一個檔案都不寫。 +# 輸出:底層指令的原始輸出;--detect 模式輸出偵測到的指令名稱,一行一個(nodejs 是 build、test, +# python 是 pytest,dotnet 是 dotnet build、dotnet test)。 # 結束碼(慣例見 README「工具」章的結束碼總表): -# 0 建置與測試都通過 +# 0 建置與測試都通過;--detect 模式是推得出指令 # 1 參數個數不對 # 2 ecosystem 不認識 # 4 需要的指令不存在(npm、python3、pytest、dotnet) # 5 推不出任何建置或測試指令——呼叫方要改問使用者 # 6 找不到專案目錄——呼叫方停手 -# 其他 建置或測試失敗,帶回失敗指令的結束碼 +# 其他 建置或測試失敗,帶回失敗指令的結束碼;--detect 模式不會回這一類 # 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。 set -u +DETECT=0 +if [ "${1:-}" = "--detect" ]; then + DETECT=1 + shift +fi + if [ "$#" -ne 2 ]; then - echo "用法:build-test.sh " >&2 + echo "用法:build-test.sh [--detect] " >&2 exit 1 fi @@ -49,6 +61,10 @@ EOF ) [ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } + if [ "$DETECT" -eq 1 ]; then + printf '%s\n' "$scripts" + exit 0 + fi for s in $scripts; do ( cd "$DIR" && npm run --if-present "$s" ) || exit $? done @@ -67,9 +83,17 @@ EOF fi [ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } if command -v pytest >/dev/null 2>&1; then + if [ "$DETECT" -eq 1 ]; then + echo "pytest" + exit 0 + fi ( cd "$DIR" && pytest ) exit $? elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then + if [ "$DETECT" -eq 1 ]; then + echo "python3 -m pytest" + exit 0 + fi ( cd "$DIR" && python3 -m pytest ) exit $? else @@ -82,6 +106,11 @@ EOF projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null) [ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } + if [ "$DETECT" -eq 1 ]; then + echo "dotnet build" + echo "dotnet test" + exit 0 + fi ( cd "$DIR" && dotnet build ) || exit $? ( cd "$DIR" && dotnet test ) || exit $? exit 0 diff --git a/tools/git-guard.sh b/tools/git-guard.sh new file mode 100755 index 0000000..36048f0 --- /dev/null +++ b/tools/git-guard.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env sh +# git-guard.sh — 還原路線的前提把關與還原本身,兩個子指令一支腳本。 +# 用法: +# git-guard.sh check +# 確認目標是 git 工作樹,而且工作區乾淨。任何一步改檔案之前先跑這支。 +# git-guard.sh revert --confirm-destructive +# 還原工作區:git checkout -- . 之後 git clean -fd,最後複驗乾淨。 +# +# 為什麼還原要多一個 --confirm-destructive: +# revert 會跑 git clean -fd,未追蹤檔案刪掉就沒有 reflog 可救。把「確實是 git 工作樹」與 +# 「呼叫端真的要還原」兩個前提寫進腳本,誤觸就退回 exit 1 或 exit 5,工作區一個位元組都不動。 +# 前提逐條擋在動作之前,順序是:目錄存在 → git 指令存在 → 確實是 git 工作樹(不是裸存取庫) +# → 旗標給了 → 目標不是檔案系統根目錄。五條全過才會執行第一個破壞性指令。 +# +# 範圍:check 的 status --porcelain 是整個存取庫(比較嚴,髒在別的目錄也擋得住); +# revert 的 checkout 與 clean 只作用在 以下(比較窄,不會波及上層目錄)。 +# clean 用 -fd 不用 -fdx:被 gitignore 的 node_modules、__pycache__、bin、obj 要留著。 +# +# 輸出: +# 成功 → stdout 最後一行是 `check ok ` 或 `revert ok `; +# revert 會先帶出 git clean 自己的 Removing 清單,那是刪掉哪些檔案的紀錄,要留給呼叫方回報。 +# 前提不成立 → stderr 第一行是原因(not a git repository、working tree not clean、 +# still dirty after revert),工作區不乾淨時後面接 git status --porcelain 的原始輸出。 +# +# 結束碼(慣例見 README「工具」章的結束碼總表): +# 0 check:是 git 工作樹而且乾淨/revert:還原完成而且乾淨 +# 1 參數個數不對,或 revert 少了 --confirm-destructive——本工具不動任何檔案 +# 2 子指令不認識(只收 check、revert) +# 4 需要的指令不存在(git) +# 5 前提不成立。check 是「不是 git 工作樹」或「工作區不乾淨」;revert 是「不是 git 工作樹」 +# 或「還原後仍不乾淨」。呼叫方停手,stderr 第一行講明是哪一種 +# 6 找不到專案目錄——呼叫方停手 +# 其他 底層 git 指令的結束碼 +set -u + +usage() { + echo "用法:git-guard.sh check " >&2 + echo " git-guard.sh revert --confirm-destructive" >&2 +} + +[ "$#" -ge 1 ] || { usage; exit 1; } + +cmd="$1" + +case "$cmd" in + check) [ "$#" -eq 2 ] || { usage; exit 1; } ;; + revert) [ "$#" -eq 3 ] || { usage; exit 1; } ;; + *) echo "unknown subcommand: $cmd" >&2; usage; exit 2 ;; +esac + +DIR="$2" + +[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; } +command -v git >/dev/null 2>&1 || { echo "git not found" >&2; exit 4; } + +# 確實是 git 工作樹才有還原路線。裸存取庫沒有工作樹,checkout 與 clean 都無從跑起。 +inside=$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null || true) +[ "$inside" = "true" ] || { echo "not a git repository: $DIR" >&2; exit 5; } + +if [ "$cmd" = "check" ]; then + dirty=$(git -C "$DIR" status --porcelain 2>/dev/null) + rc=$? + [ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; } + if [ -n "$dirty" ]; then + # 未追蹤的 ?? 行也算髒:還原會跑 git clean -fd,那些檔案刪掉沒有還原路徑。 + echo "working tree not clean: $DIR" >&2 + printf '%s\n' "$dirty" >&2 + exit 5 + fi + echo "check ok $DIR" + exit 0 +fi + +# 以下只有 revert 走得到。破壞性指令的最後兩道前提。 +[ "$3" = "--confirm-destructive" ] || { + echo "revert 會執行 git clean -fd,必須明確傳入 --confirm-destructive" >&2 + usage + exit 1 +} + +# 目標是檔案系統根目錄時直接拒絕:路徑組錯的時候,這是唯一擋得住的地方。 +abs=$(CDPATH= cd -- "$DIR" 2>/dev/null && pwd) || { echo "project dir not found: $DIR" >&2; exit 6; } +[ "$abs" != "/" ] || { echo "refusing to revert the filesystem root" >&2; exit 5; } + +git -C "$DIR" checkout -- . || exit $? +git -C "$DIR" clean -fd || exit $? + +left=$(git -C "$DIR" status --porcelain 2>/dev/null) +rc=$? +[ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; } +if [ -n "$left" ]; then + echo "still dirty after revert: $DIR" >&2 + printf '%s\n' "$left" >&2 + exit 5 +fi +echo "revert ok $DIR" +exit 0