Files
pkg/tools/git-guard.sh
jiantw83 338c2da89c feat(tools): 新增 git 還原閘門腳本,建置測試加上唯讀偵測模式
What:新增 `tools/git-guard.sh`,把工作區乾淨檢查與還原序列從技能內文搬進腳本。`tools/build-test.sh` 加上 `--detect` 唯讀模式,只偵測建置與測試指令,不執行。

Why:還原會執行 `git clean -fd`。這個指令刪掉未追蹤的檔案,沒有 reflog 可救,也沒有任何救回的路徑。前提原本只寫在技能內文,讀的人漏掉一行,就可能對著一個根本不該還原的目錄動手,把整個工作區的未追蹤檔案清光。前提要擋得住,就得寫成程式碼。另一件事是「推不出建置與測試指令」,原本要等所有版本來源檔案都改寫完才發現,使用者又答不出指令時,前面全部作廢,還得走一次還原。

How:`revert` 在跑第一個破壞性指令之前擋五道——目錄存在、git 指令存在、確實是 git 工作樹(不是裸存取庫)、呼叫端明確帶 `--confirm-destructive`、目標不是檔案系統根目錄。任何一道不過就直接結束,工作區一個位元組都不動。`check` 的乾淨判定看整個存取庫,`revert` 只作用在專案目錄以下,判定從嚴、動手從窄。`clean` 用 `-fd` 不用 `-fdx`,被 gitignore 的安裝產物要留著。`--detect` 只做偵測與前置檢查,一個檔案都不寫,結束碼與路由跟執行模式完全一致。

Who:套件批次更新的前置檢查與還原路線。
2026-08-31 11:09:01 +08:00

98 lines
4.4 KiB
Bash
Executable File
Raw Permalink 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
# git-guard.sh — 還原路線的前提把關與還原本身,兩個子指令一支腳本。
# 用法:
# git-guard.sh check <project-dir>
# 確認目標是 git 工作樹,而且工作區乾淨。任何一步改檔案之前先跑這支。
# git-guard.sh revert <project-dir> --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 只作用在 <project-dir> 以下(比較窄,不會波及上層目錄)。
# clean 用 -fd 不用 -fdx:被 gitignore 的 node_modules、__pycache__、bin、obj 要留著。
#
# 輸出:
# 成功 → stdout 最後一行是 `check ok <project-dir>` 或 `revert ok <project-dir>`;
# 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 <project-dir>" >&2
echo " git-guard.sh revert <project-dir> --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