352d7a0b81
雲端 session 開出來的工作分支天生沒有 upstream,內容卻等於遠端 main (薄殼 HEAD 081c547 = GitHub 遠端 main 081c547,同一顆) ⇒ 每個雲端 session、每次收工都被攔一次,08-27 一個 session 五次全是誤報。 三件: · 判準改成問遠端(git ls-remote,快取 120 秒)。本機只答得準「有」, 答「沒有」時才打網路——實測本 session 的 origin/main 落後遠端 4 天。 · 三態:遠端有/遠端沒有/問不到。**問不到一律不報**,不拿離線當罪證。 · 散落分支的基準也一起換成遠端實際那顆——基準過期會把已經在遠端 main 上的 分支整批報成散落(包含雲端 session 自己那條)。 順手補雲端那格接線:`$CLAUDE_PROJECT_DIR` 在雲端是薄殼根,真身在 `$TOP/InkStoneCo/`。 舊的掃描清單四個路徑一個都不存在 ⇒ 真身有東西沒推,這支閘一輩子不會知道。 測試 hooks/tests/unpushed-police.test.sh:10/10(全離線,本機 bare repo 當遠端)。 拿舊版跑同一份:A 群 5 條全紅、B 群 4 條全綠 ⇒ 測試有鑑別力,不是改到寬鬆。
138 lines
6.2 KiB
Bash
Executable File
138 lines
6.2 KiB
Bash
Executable File
#!/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 <repo> <期望 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
|