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:套件批次更新的前置檢查與還原路線。
98 lines
4.4 KiB
Bash
Executable File
98 lines
4.4 KiB
Bash
Executable File
#!/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
|