feat(assistant): 存取庫掃描接上 {repo} 代入點
新增 tools/scan-repos.sh:掃出工作目錄底下的存取庫,交出 {repo} 要代的
那份清單,順便對出每一個存取庫的 REPO_{HASH} 盤點頁頁名(雜湊規則與
盤點頁那一支相同,實測值一致)。
掃描起點刻意沒有預設值。給一個預設就等於猜,而猜錯的後果不是掃不到,
是在猜錯的那些目錄底下跑指令——往上一層是家目錄、再往上是整台機器,
而那一輪沒有人看得到它跑到哪裡去了。沒設就回 3 並說要設哪一個變數。
深度預設一層、上限三層;點開頭的目錄、符號連結、路徑帶空白或殼層特殊
字元的三種一律不交出去,但逐個印出來——那是真的存在卻沒被盤點到的
存取庫,只印一個總數會讀成整批都掃過了。
run-due.sh 改成兩個代入點都代:{cli} 取偵測到的 CLI 代號、{repo} 取
掃到的存取庫,兩個都帶的那一筆目標數是乘積。待辦簿那一筆的 repo 欄
有值時只代那一個,值可以是路徑也可以是 {owner}/{repo};對不出來就
印 held=,不退回全部存取庫——那一筆指名了一個目標。代入之後再驗一次
禁止字元,因為代進去的值是這一輪現場算出來的。
schedule.sh 把 JSC_ASSIST_SCAN_ROOT 與 JSC_ASSIST_SCAN_EXCLUDE 快照進
排程條目。少了掃描起點,帶 {repo} 的內建項每一輪都代不出目標,而那一輪
只印一行 held=,看起來像這一批還沒接上、不像一個變數沒進條目。
同時補兩個只有真的執行才發現得了的缺陷:
一、cron 一行的長度上限只在寫入那一刻由 crontab 自己擋,--dry-run 那一路
根本不碰它,所以預演每一次都過。實測踩過一次:整條 PATH 快照進條目那一批
預演全綠、安裝回結束碼 4。改成建好條目就量,兩個模式都印 entry_len= 與
上限,達到上限當場拒絕並指出長度是從哪幾個快照變數來的。
二、run-due.sh 截錯誤訊息用的 cut -c 數的是位元組不是字元,中文字剛好被
截在中間就在報告上留下一個替代字元。改成截完再過一次 iconv -c 丟掉那個
不完整的序列。這一種不會有人來報:亂碼不影響結束碼。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+273
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env sh
|
||||
# scan-repos.sh — 掃出工作目錄底下的存取庫,交出 {repo} 代入點要用的那份清單。
|
||||
#
|
||||
# 用法:
|
||||
# scan-repos.sh list [--scan-root {字面絕對路徑}] [--depth {0..3}] [--root {字面絕對根目錄}]
|
||||
# scan-repos.sh paths [同上]
|
||||
#
|
||||
# list 一行一個存取庫,帶工作目錄、{owner}/{repo} 與對應的 REPO_{HASH} 頁名
|
||||
# paths 只印工作目錄,一行一條。給 {repo} 代入點直接讀
|
||||
#
|
||||
# 環境變數:
|
||||
# JSC_ASSIST_SCAN_ROOT 掃描起點。--scan-root 優先於它
|
||||
# JSC_ASSIST_SCAN_EXCLUDE 排除清單,空白分隔,比對目錄名(不是整條路徑),可用萬用字元
|
||||
# JSC_ASSIST_HASH_ID hash-id 路徑覆寫;找不到並排存取庫時才設
|
||||
#
|
||||
# 結束碼:
|
||||
# 0 掃到至少一個存取庫,而且每一個都對得出 REPO_{HASH}、路徑也都送得進殼
|
||||
# 1 掃到了,但有幾個對不出頁名或路徑被拒。可用的那幾個照印,被拒的逐行印在 rejected=
|
||||
# 2 用法錯誤:不認得的子命令或選項、選項缺值、--scan-root 不是絕對路徑、--depth 超出範圍
|
||||
# 3 掃描起點沒設定,或那條路徑不是存在的目錄——**什麼都沒掃**,不等於「這台機器沒有存取庫」
|
||||
# 4 掃過了,一個 git 存取庫都沒有
|
||||
#
|
||||
# --- 為什麼掃描起點不給預設值 ---
|
||||
#
|
||||
# 這份清單的用途是把存取庫路徑代進待辦簿的指令,然後在無人值守那一輪送進殼執行。
|
||||
# 猜錯掃描起點的後果不是掃不到東西,是**在猜錯的那些目錄底下跑指令**,而那一輪沒有人
|
||||
# 看得到它跑到哪裡去了。往上猜一層就從工作目錄變成家目錄,再往上就是整台機器。
|
||||
# 所以沒設就回結束碼 3 並說清楚要設哪一個變數,不退回任何一條路徑。
|
||||
# 排程那一輪的值由 schedule.sh install 當下從殼裡快照進條目,跟 wiki 存取庫那幾個變數同一條路。
|
||||
#
|
||||
# --- 為什麼預設只掃一層 ---
|
||||
#
|
||||
# 工作目錄的版面是「一個存取庫一個子目錄」,一層就夠。往下遞歸會踩到三種東西:
|
||||
# 存取庫自己的 node_modules 與 vendor 底下的第三方存取庫、封存目錄裡的舊版存取庫、
|
||||
# 還有 worktree 的複本。三種都會被當成現役存取庫盤點,而盤點結果讀起來完全正常。
|
||||
# --depth 最多收到 3:真的有嵌套版面時夠用,而再深就不是「工作目錄那一層」了,
|
||||
# 那種版面該把掃描起點指到那一層,不是把界線調鬆。
|
||||
#
|
||||
# --- 三種東西一定跳過,不看排除清單 ---
|
||||
#
|
||||
# 一、名稱以點開頭的目錄。快取、封存、暫存都藏在那裡,掃進去會把封存版當現役版盤點。
|
||||
# 二、符號連結。連結會把掃描帶出掃描起點,而那條界線一旦被繞過就沒有第二道。
|
||||
# 三、路徑裡有空白或殼層特殊字元的存取庫。執行那一支是用 `sh -c` 送出指令的,那條路徑
|
||||
# 會被殼再解一次——帶空白的路徑會被切成兩個參數,帶金錢符號的會被展開成別的東西。
|
||||
# 這種一律列進 rejected= 不交出去,不是靜靜跳過:那是一個真的存在的存取庫,
|
||||
# 它沒有被盤點到這件事要有人知道。
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo 'usage: scan-repos.sh {list|paths} [--scan-root 絕對路徑] [--depth 0..3] [--root 絕對路徑]' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
die() { _c=$1; shift; printf '[jsc][助理掃描][ERR]:%s\n' "$*" >&2; exit "$_c"; }
|
||||
# 逐項的判定行與收尾統計。paths 那個模式的標準輸出要是純路徑——呼叫端拿它當代入點來源,
|
||||
# 混一行 unnamed= 進去就會被代成一條不存在的路徑然後送進殼。所以那個模式一律改走標準錯誤。
|
||||
emit() { if [ "$MODE" = 'paths' ]; then printf '%s\n' "$*" >&2; else printf '%s\n' "$*"; fi; }
|
||||
note() { printf '[jsc][助理掃描]:%s\n' "$*" >&2; }
|
||||
warn() { printf '[jsc][助理掃描][WARN]:%s\n' "$*" >&2; }
|
||||
|
||||
[ "$#" -ge 1 ] || usage
|
||||
MODE=$1; shift
|
||||
case "$MODE" in
|
||||
list|paths) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
SCAN_ROOT=''
|
||||
OPT_ROOT=''
|
||||
DEPTH=1
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--scan-root) [ "$#" -ge 2 ] || usage; SCAN_ROOT=$2; shift 2 ;;
|
||||
--root) [ "$#" -ge 2 ] || usage; OPT_ROOT=$2; shift 2 ;;
|
||||
--depth) [ "$#" -ge 2 ] || usage; DEPTH=$2; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$DEPTH" in
|
||||
0|1|2|3) ;;
|
||||
*) die 2 "--depth 只收 0 到 3,收到的是「$DEPTH」。0 只看掃描起點自己,1 是它底下那一層。要掃更深的版面就把掃描起點指到那一層。" ;;
|
||||
esac
|
||||
|
||||
case "$OPT_ROOT" in
|
||||
''|/*) ;;
|
||||
*) die 2 "--root 要給字面絕對路徑,收到的是「$OPT_ROOT」。" ;;
|
||||
esac
|
||||
|
||||
[ -n "$SCAN_ROOT" ] || SCAN_ROOT="${JSC_ASSIST_SCAN_ROOT:-}"
|
||||
if [ -z "$SCAN_ROOT" ]; then
|
||||
die 3 '掃描起點沒設定。這一支不猜:猜錯的後果是在猜錯的那些目錄底下跑指令,而無人值守那一輪沒有人看得到它跑到哪裡去了。請帶 --scan-root,或在殼裡設 JSC_ASSIST_SCAN_ROOT 之後重跑一次 schedule.sh install patrol,把值快照進排程條目。'
|
||||
fi
|
||||
case "$SCAN_ROOT" in
|
||||
/*) ;;
|
||||
*) die 2 "掃描起點要是字面絕對路徑,收到的是「$SCAN_ROOT」。權限層比對的是還沒展開的指令字面,帶變數或波浪號的路徑進不了允許清單。" ;;
|
||||
esac
|
||||
SCAN_ROOT=${SCAN_ROOT%/}
|
||||
# 去掉尾斜線之後空字串就是根目錄。掃根目錄那一層等於把整台機器的第一層當成工作目錄,
|
||||
# 那不是設定錯誤就是打錯字,兩種都不該照著跑。
|
||||
[ -n "$SCAN_ROOT" ] || die 2 '掃描起點是根目錄。掃根目錄那一層等於把整台機器的第一層當成工作目錄,這一支不接。'
|
||||
[ -d "$SCAN_ROOT" ] || die 3 "掃描起點不是存在的目錄:$SCAN_ROOT。什麼都沒掃,跟「這台機器沒有存取庫」不是同一件事——後者要回 4。"
|
||||
|
||||
TAB=$(printf '\t')
|
||||
TMPD=$(mktemp -d 2>/dev/null) || die 3 '暫存目錄建不起來。'
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
# --- 找 hash-id ---
|
||||
|
||||
HERE=$(CDPATH= cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
ROOT="$OPT_ROOT"
|
||||
[ -n "$ROOT" ] || ROOT=$(CDPATH= cd -- "$HERE/../.." 2>/dev/null && pwd -L) || ROOT=''
|
||||
|
||||
hash_id_sh() {
|
||||
if [ -n "${JSC_ASSIST_HASH_ID:-}" ] && [ -f "$JSC_ASSIST_HASH_ID" ]; then
|
||||
printf '%s' "$JSC_ASSIST_HASH_ID"; return 0
|
||||
fi
|
||||
for _d in "jsc-gitea" "gitea"; do
|
||||
[ -n "$ROOT" ] && [ -f "$ROOT/$_d/tools/hash-id" ] && { printf '%s' "$ROOT/$_d/tools/hash-id"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
HASH_ID=$(hash_id_sh) || HASH_ID=''
|
||||
|
||||
# 頁名的雜湊規則跟 jsc-sdlc 盤點頁是同一條:對那個存取庫自己的 {owner}/{repo} 取完整四十碼
|
||||
# 大寫 SHA-1。共用那一支找不到就退回本機的 sha1sum——同一條規則、同一個值,只是少一層共用。
|
||||
# 兩邊都算不出來就不硬湊一個頁名:湊出來的頁名指向沒有人讀的那一頁。
|
||||
#
|
||||
# 用哪一種先決定好,不在每一筆裡各判一次。算雜湊那一段跑在命令替換裡,也就是子殼裡,
|
||||
# 在那裡設「這一輪用了退路」的旗標,回到外面就沒了——那句提示會永遠印不出來。
|
||||
HASH_MODE='none'
|
||||
if [ -n "$HASH_ID" ] && [ -n "$(printf '%s' probe | "$HASH_ID" 2>/dev/null)" ]; then
|
||||
HASH_MODE='shared'
|
||||
elif command -v sha1sum >/dev/null 2>&1; then
|
||||
HASH_MODE='sha1sum'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
HASH_MODE='shasum'
|
||||
fi
|
||||
hash40() { # $1=要算的字串
|
||||
case "$HASH_MODE" in
|
||||
shared) printf '%s' "$1" | "$HASH_ID" 2>/dev/null ;;
|
||||
sha1sum) printf '%s' "$1" | sha1sum | awk '{print toupper($1)}' ;;
|
||||
shasum) printf '%s' "$1" | shasum -a 1 | awk '{print toupper($1)}' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- 找候選目錄 ---
|
||||
|
||||
# 排除清單比對目錄名,不比對整條路徑:整條路徑會隨掃描起點變,同一份清單換一台機器就失效。
|
||||
EXCLUDED_NAMES="${JSC_ASSIST_SCAN_EXCLUDE:-}"
|
||||
excluded() { # $1=目錄名
|
||||
[ -n "$EXCLUDED_NAMES" ] || return 1
|
||||
for _pat in $EXCLUDED_NAMES; do
|
||||
# shellcheck disable=SC2254
|
||||
case "$1" in
|
||||
$_pat) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
CAND="$TMPD/cand.txt"
|
||||
: >"$CAND"
|
||||
LINKS="$TMPD/links.txt"
|
||||
: >"$LINKS"
|
||||
|
||||
# find 預設不跟符號連結走,所以連結目錄不會出現在 -type d 的結果裡。這裡另外撈一次 -type l
|
||||
# 是為了把它們印出來:一個被跳過的連結跟一個不存在的目錄,在報告上看起來不能是同一件事。
|
||||
#
|
||||
# 排除清單不寫進 find 的剪枝條件,改在下面逐個判定時濾掉。理由是剪掉的目錄就不會出現在
|
||||
# 結果裡,也就報不出「這個被排除了」——而一份沒說自己排除了什麼的清單,讀起來跟沒有排除
|
||||
# 清單一樣。代價是深度兩層以上時會走進被排除的目錄裡再一層,而深度上限是 3,所以最多一層。
|
||||
if [ "$DEPTH" -ge 1 ]; then
|
||||
find "$SCAN_ROOT" -mindepth 1 -maxdepth "$DEPTH" \
|
||||
-name '.*' -prune -o -type l -print 2>/dev/null >"$LINKS"
|
||||
find "$SCAN_ROOT" -mindepth 1 -maxdepth "$DEPTH" \
|
||||
-name '.*' -prune -o -type d -print 2>/dev/null >"$CAND"
|
||||
fi
|
||||
# depth 0 與更深的版面都要把掃描起點自己算進來:工作目錄本身也可能就是一個存取庫。
|
||||
printf '%s\n' "$SCAN_ROOT" >>"$CAND"
|
||||
sort -u -o "$CAND" "$CAND"
|
||||
|
||||
# --- 逐個判定 ---
|
||||
|
||||
N_REPO=0; N_REJ=0; N_UNNAMED=0; N_EXCL=0; N_LINK=0
|
||||
OUT="$TMPD/out.txt"
|
||||
: >"$OUT"
|
||||
|
||||
while IFS= read -r d; do
|
||||
[ -n "$d" ] || continue
|
||||
_name=$(basename -- "$d")
|
||||
if excluded "$_name"; then
|
||||
N_EXCL=$((N_EXCL + 1))
|
||||
emit "excluded=$d reason=在排除清單上"
|
||||
continue
|
||||
fi
|
||||
# .git 可能是目錄(一般存取庫),也可能是檔案(worktree 與子模組)。兩種都算存取庫。
|
||||
[ -e "$d/.git" ] || continue
|
||||
|
||||
case "$d" in
|
||||
*[!A-Za-z0-9/._-]*)
|
||||
N_REJ=$((N_REJ + 1))
|
||||
emit "rejected=$d reason=路徑裡有空白或殼層特殊字元,送進 sh -c 會被再解一次"
|
||||
continue ;;
|
||||
esac
|
||||
|
||||
_url=$(git -C "$d" remote get-url origin 2>/dev/null) || _url=''
|
||||
_slug=''
|
||||
if [ -n "$_url" ]; then
|
||||
# 兩種形狀:git@host:owner/repo.git 與 https://host/owner/repo.git。都只取最後兩段。
|
||||
_s=${_url%.git}
|
||||
_s=${_s#*://}
|
||||
_s=${_s#*@}
|
||||
case "$_s" in
|
||||
*:*) _s=${_s#*:} ;;
|
||||
esac
|
||||
_s=${_s#/}
|
||||
_owner=''
|
||||
_repo=${_s##*/}
|
||||
_rest=${_s%/*}
|
||||
[ "$_rest" != "$_s" ] && _owner=${_rest##*/}
|
||||
[ -n "$_owner" ] && [ -n "$_repo" ] && _slug="$_owner/$_repo"
|
||||
fi
|
||||
|
||||
_page='-'
|
||||
if [ -n "$_slug" ]; then
|
||||
_h=$(hash40 "$_slug") || _h=''
|
||||
[ -n "$_h" ] && _page="REPO_$_h"
|
||||
fi
|
||||
if [ "$_page" = '-' ]; then
|
||||
N_UNNAMED=$((N_UNNAMED + 1))
|
||||
if [ -z "$_slug" ]; then
|
||||
emit "unnamed=$d reason=沒有 origin 遠端,或網址對不出 {owner}/{repo},算不出盤點頁頁名"
|
||||
else
|
||||
emit "unnamed=$d slug=$_slug reason=這台機器算不出 SHA-1,算不出盤點頁頁名"
|
||||
fi
|
||||
fi
|
||||
|
||||
N_REPO=$((N_REPO + 1))
|
||||
printf '%s%s%s%s%s\n' "$d" "$TAB" "${_slug:--}" "$TAB" "$_page" >>"$OUT"
|
||||
done <"$CAND"
|
||||
|
||||
while IFS= read -r l; do
|
||||
[ -n "$l" ] || continue
|
||||
N_LINK=$((N_LINK + 1))
|
||||
emit "skipped_link=$l reason=符號連結,跟著走會把掃描帶出掃描起點"
|
||||
done <"$LINKS"
|
||||
|
||||
if [ "$MODE" = 'paths' ]; then
|
||||
cut -f1 "$OUT"
|
||||
else
|
||||
while IFS="$TAB" read -r p s g; do
|
||||
printf 'repo=%s slug=%s page=%s\n' "$p" "$s" "$g"
|
||||
done <"$OUT"
|
||||
fi
|
||||
|
||||
emit "scan_root=$SCAN_ROOT depth=$DEPTH repos=$N_REPO unnamed=$N_UNNAMED rejected=$N_REJ excluded=$N_EXCL skipped_links=$N_LINK"
|
||||
|
||||
case "$HASH_MODE" in
|
||||
sha1sum|shasum) note '找不到共用那一支 hash-id(jsc-gitea 的 tools/hash-id),這一輪的頁名改用本機的 SHA-1 算。兩者是同一條規則、值相同,只是少了一層共用;--root 餵對就會用共用那一支。' ;;
|
||||
none) warn '這台機器既沒有共用那一支 hash-id,也沒有 sha1sum 與 shasum,這一輪一個盤點頁頁名都算不出來。存取庫路徑照樣交出去代 {repo},唯讀盤點跑得動;盤點結果沒有頁可以寫回去。' ;;
|
||||
esac
|
||||
[ "$N_REJ" -gt 0 ] && warn "有 $N_REJ 個存取庫的路徑送不進殼,這一份清單裡沒有它們,逐個印在上面的 rejected= 那幾行。那幾個是真的存在的存取庫,只是路徑帶了空白或殼層特殊字元——要盤點得到就得換路徑。"
|
||||
[ "$N_UNNAMED" -gt 0 ] && warn "有 $N_UNNAMED 個存取庫對不出 REPO_{HASH} 盤點頁頁名,逐個印在上面的 unnamed= 那幾行。它們的路徑照樣交出去代 {repo},唯讀盤點跑得動;但盤點結果沒有頁可以寫回去。"
|
||||
|
||||
if [ "$N_REPO" -eq 0 ]; then
|
||||
die 4 "掃過 $SCAN_ROOT 底下 $DEPTH 層,一個 git 存取庫都沒有。掃描起點指錯一層就會長這樣——工作目錄的版面是「一個存取庫一個子目錄」,指到某一個存取庫裡面就只剩它自己那一個,指到家目錄就一個都沒有。"
|
||||
fi
|
||||
if [ "$N_REJ" -gt 0 ] || [ "$N_UNNAMED" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user