#!/usr/bin/env bash # unpushed-police.sh 的迴歸測試(inkstone/ISEP#90) # # 這支存在的理由是一個**穩定重現**的誤報:雲端 session 開出來的工作分支 # 天生沒有 upstream,內容卻等於遠端 main ⇒ 每個雲端 session、每次收工都被攔一次。 # 2026-08-27 一個 session 攔五次,五次全是誤報。 # # 兩個方向都要有證據,而且 **A 群(不該報)比 B 群更重要**: # 永遠在響的警報=訓練人忽略這個警報,下一條真的失蹤的分支會混在雜訊裡。 # # 🔴 全程離線:用本機 bare repo 當「遠端」,`git ls-remote` 打的是檔案路徑。 # 不碰任何真 repo、不打網路、不寫 /tmp 的正式快取(走 UNPUSHED_CACHE_DIR)。 set -u HOOK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/unpushed-police.sh}" TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT export GIT_CONFIG_GLOBAL="$TMP/gitconfig"; : > "$GIT_CONFIG_GLOBAL" export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t PASS=0; FAIL=0; N=0 # mk <名字> → 造一個「遠端 bare + 本機 clone」的場,印出本機 repo 路徑 mk() { d="$TMP/$1"; mkdir -p "$d" git init -q --bare "$d/remote.git" git init -q -b main "$d/work" ( cd "$d/work" echo a > a.txt; git add a.txt; git commit -qm first git remote add origin "$d/remote.git" git push -q origin main git branch --unset-upstream 2>/dev/null || true ) >/dev/null 2>&1 printf '%s\n' "$d/work" } # fire <期望 exit> <說明> [期望訊息片段] fire() { repo="$1"; want="$2"; desc="$3"; want_msg="${4:-}" N=$((N+1)) out=$(printf '{}' | CLAUDE_PROJECT_DIR="$repo" UNPUSHED_CACHE_DIR="$TMP/cache-$N" \ bash "$HOOK" 2>&1); rc=$? mkdir -p "$TMP/cache-$N" 2>/dev/null ok=1 [ "$rc" -eq "$want" ] || ok=0 if [ -n "$want_msg" ] && ! printf '%s' "$out" | grep -qF "$want_msg"; then ok=0; fi if [ "$ok" = 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1)) else printf ' ❌ %s —— 期望 exit=%s%s,實得 exit=%s\n' "$desc" "$want" \ "${want_msg:+ 且訊息含「$want_msg」}" "$rc" printf '%s\n' "$out" | sed -n '1,14p' | sed 's/^/ /' FAIL=$((FAIL+1)) fi } # 每個案例先把快取目錄準備好(fire 用的是自己那一格,互不污染) for i in $(seq 1 40); do mkdir -p "$TMP/cache-$i"; done echo "── A 群:不該報(誤攔比漏擋更該修)──────────────────────────" # ① 這就是雲端那個現場:分支沒有 upstream,但它那顆 commit 就是遠端 main 那顆 r=$(mk cloud) ( cd "$r" && git checkout -q -b claude/session-xyz && git branch -D main -q 2>/dev/null git remote set-head origin -d 2>/dev/null; git update-ref -d refs/remotes/origin/main ) >/dev/null 2>&1 fire "$r" 0 "① 沒 upstream,但遠端就有這顆 commit(雲端每個 session 的現場)" # ② HEAD 是遠端 main 的**祖先**(遠端已經走在前面)——同樣不是「沒推」 r=$(mk ancestor) ( cd "$r" echo b > b.txt && git add b.txt && git commit -qm second && git push -q origin main git checkout -q -b work HEAD~1 git update-ref -d refs/remotes/origin/main ) >/dev/null 2>&1 fire "$r" 0 "② HEAD 是遠端 main 的祖先 → 遠端已經有它" # ③ 本機的 remote-tracking ref **過期**(08-28 實測落後遠端 4 天的那一格) r=$(mk stale) ( cd "$r" old=$(git rev-parse HEAD) echo b > b.txt && git add b.txt && git commit -qm second && git push -q origin main git update-ref refs/remotes/origin/main "$old" # 本機那份停在舊的 git checkout -q -b claude/session-abc ) >/dev/null 2>&1 fire "$r" 0 "③ 本機 origin/main 過期,但遠端真的有這顆 → 不報" # ④ 完全問不到遠端(沒有任何 remote)→ **不當成沒推** r="$TMP/noremote"; git init -q -b main "$r" ( cd "$r" && echo a > a.txt && git add a.txt && git commit -qm only ) >/dev/null 2>&1 fire "$r" 0 "④ 沒有任何 remote=問不到 → 不報(不拿離線當罪證)" # ⑤ 基準過期時,已經在遠端 main 上的分支不算散落 r=$(mk straystale) ( cd "$r" old=$(git rev-parse HEAD) git checkout -q -b feat/done echo b > b.txt && git add b.txt && git commit -qm done git push -q origin feat/done:main # 遠端 main 已經含它 git update-ref refs/remotes/origin/main "$old" ) >/dev/null 2>&1 fire "$r" 0 "⑤ 分支已在遠端 main 上,只是本機基準舊 → 不算散落" # ⑥ stop_hook_active → 一律放行 r=$(mk stopactive) ( cd "$r" && echo x >> a.txt ) >/dev/null 2>&1 N=$((N+1)) out=$(printf '{"stop_hook_active":true}' | CLAUDE_PROJECT_DIR="$r" \ UNPUSHED_CACHE_DIR="$TMP/cache-$N" bash "$HOOK" 2>&1); rc=$? if [ "$rc" -eq 0 ]; then printf ' ✅ %s\n' "⑥ stop_hook_active=true → 放行"; PASS=$((PASS+1)) else printf ' ❌ %s(exit=%s)\n' "⑥ stop_hook_active=true → 放行" "$rc"; FAIL=$((FAIL+1)); fi echo "── B 群:該報(不能為了不吵就變成放行)────────────────────" # ⑦ 真的有遠端沒有的 commit = 真的沒推 r=$(mk unpushed) ( cd "$r" git checkout -q -b feat/real echo b > b.txt && git add b.txt && git commit -qm "沒推的那筆" ) >/dev/null 2>&1 fire "$r" 2 "⑦ 分支有遠端沒有的 commit → 要報" "從未推過" # ⑧ 有改動沒 commit r=$(mk dirty) ( cd "$r" && echo more >> a.txt ) >/dev/null 2>&1 fire "$r" 2 "⑧ 工作區有未 commit 的改動 → 要報" "未 commit" # ⑨ 散落分支:本地分支有遠端 main 沒有的東西 r=$(mk stray) ( cd "$r" git checkout -q -b feat/lost echo c > c.txt && git add c.txt && git commit -qm "躺著的那筆" git checkout -q main ) >/dev/null 2>&1 fire "$r" 2 "⑨ 有分支沒併回、遠端也沒有 → 要報散落" "feat/lost" # ⑩ 判準沒有滑回「有沒有 upstream」:有 upstream 但真的領先,照樣要報 r=$(mk ahead) ( cd "$r" git branch --set-upstream-to=origin/main main echo b > b.txt && git add b.txt && git commit -qm "領先一筆" ) >/dev/null 2>&1 fire "$r" 2 "⑩ 有 upstream 且真的領先 → 照樣要報" "領先" echo printf '通過 %s 條,失敗 %s 條(共 %s 條)\n' "$PASS" "$FAIL" "$N" [ "$FAIL" = 0 ] || exit 1