雜湊規則從 8 碼改成完整 40 碼那一刻,還沒寫進 wiki 的暫存留在舊名底下。 現行程式拿 40 碼去查,查不到那些目錄,裡面的條目就再也寫不出去。 沒人發現是因為兩種狀態長得一樣:「真的沒有待寫」與「待寫卡在舊名底下」 對呼叫端都是成功。技能於是照常回報完成,日誌卻一筆都沒有。 兩道處置。推得出對映的(舊名等於 H 加新名前 7 碼)由 list、cat、merge 一併收編,clear 與 commit 也連舊目錄一起清——commit 不放行的話,已經寫進 wiki 的暫存清不掉,下一輪會整批重複寫一次。推不出對映的交給新的 orphans 子命令掃出來回報,要有人看到才處理得掉。
282 lines
14 KiB
Bash
Executable File
282 lines
14 KiB
Bash
Executable File
#!/usr/bin/env sh
|
||
# worklog-pending.sh — 待寫入工作日誌的暫存區(供 jsc-sdlc 階段回報與 jsc-log:worklog 使用)。
|
||
#
|
||
# 為什麼要有這支腳本:SDLC 每個階段結束都要回報工作日誌連結。階段跑完卻沒寫日誌時,
|
||
# 內容如果只留在對話裡,換一個工作階段就消失了,下次寫日誌也補不回來。所以暫存搬到檔案層:
|
||
# 階段回報發現沒有日誌,就把該階段的內容存進本區並警告使用者;下次 jsc-log:worklog 寫入時
|
||
# 先把本區的內容一併寫進去,寫完才清掉。
|
||
#
|
||
# 一次寫入的三段式(merge、commit、abort)解決同一件事:暫存只能在 wiki 寫入成功之後才清。
|
||
# merge 把暫存內容與本次條目合成一份要寫進 wiki 的檔案,同時記下這次併走了哪些暫存檔(併入清單)。
|
||
# commit wiki 寫入成功後呼叫,只刪掉併入清單上那幾個檔。
|
||
# abort wiki 寫入失敗後呼叫,暫存原封不動,只丟掉合併檔與併入清單。
|
||
# 只刪清單上的檔案,是為了保住 merge 之後、commit 之前另一個工作階段新存進來的內容。
|
||
#
|
||
# 用法:
|
||
# worklog-pending.sh add <hash> <file> 把一段待寫入的日誌內容存起來,印出存放路徑
|
||
# worklog-pending.sh list <hash> 列出該 hash 的暫存檔路徑(一行一個,依時間排序)
|
||
# worklog-pending.sh cat <hash> 依時間順序印出全部暫存內容
|
||
# worklog-pending.sh clear <hash> 清掉該 hash 的全部暫存(寫入日誌成功後才做)
|
||
# worklog-pending.sh merge <hash> <file> 合成「暫存內容+本次條目」,印出 MERGED= 與 CLAIM=
|
||
# worklog-pending.sh commit <hash> <claim> 寫入成功後清掉併入清單上的暫存檔
|
||
# worklog-pending.sh abort <hash> <claim> 寫入失敗後保留暫存,只丟掉合併檔與併入清單
|
||
#
|
||
# <hash> 為 jsc-gitea/tools/hash-id 算出的工作日誌 hash,也就是 LOG_{HASH} 的 HASH。
|
||
# 現行是完整 40 碼大寫十六進位;8 碼那種是舊規則留下的暫存檔,還沒遷移完之前一併收。
|
||
# 舊規則的首碼落在 0-9ABC 就改寫成 H 加原前 7 碼,十六個首碼有十三個會命中,所以既有的
|
||
# 8 碼暫存大多長成 H1A2B3C4。不收這種,遷移期間存進去的內容就再也取不回來。
|
||
# 存放位置: $JSC_HOME/worklog-pending/{hash}/{UTC 時間}-{pid}.md(JSC_HOME 預設 ~/.jsc)
|
||
# 合併檔: $JSC_HOME/worklog-pending/.merge/{hash}-{UTC 時間}-{pid}.md
|
||
# 併入清單: 同名換副檔名 .claim,一行一個被併走的暫存檔路徑
|
||
#
|
||
# 結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=該 hash 沒有暫存內容 4=orphans 掃到孤兒目錄
|
||
# merge 沒有暫存內容時仍然 exit 0:合併檔至少有本次條目,呼叫端不必分兩條路走。
|
||
#
|
||
# 舊目錄自動收編:
|
||
# 拿 40 碼來查時,一併收「H 加前 7 碼」那個舊目錄。雜湊規則從 8 碼改成 40 碼那一刻,
|
||
# 還沒寫進 wiki 的暫存會留在舊名底下;不收的話它查不到,而且沒有人會說查不到——
|
||
# 「沒有暫存」與「暫存卡在舊名底下」對呼叫端長得一模一樣,兩種都是成功。日誌就這樣消失。
|
||
# 只認「舊名等於 H 加新名前 7 碼」這一種對映,推導唯一,不會收到別的專案。
|
||
# list、cat、merge 讀兩個目錄,clear 清兩個,commit 也放行舊目錄的路徑——
|
||
# 放行漏掉的話,已經寫進 wiki 的暫存清不掉,下一輪會整批重複寫一次。
|
||
#
|
||
# orphans 子命令(不吃 hash):
|
||
# 掃暫存區根目錄,印出每一個不是 40 碼的目錄:{目錄名}<TAB>{檔數}<TAB>{路徑}。
|
||
# 沒有就 exit 0,有就 exit 4。上面那條自動收編只救得到推得出對映的舊目錄,
|
||
# 推不出來的要有人看到才處理得掉,所以另外給一支掃描。
|
||
#
|
||
# 陷阱:
|
||
# - clear 與 commit 都只在日誌確實寫進 wiki 之後才呼叫。先清再寫,寫失敗就兩邊都沒有。
|
||
# - commit 只認 merge 產出的併入清單,且清單上的路徑必須落在該 hash 的暫存目錄底下,
|
||
# 不然就當成用法錯誤擋下來——這道護欄擋的是拿別人的清單來刪檔。
|
||
# - 檔名帶 UTC 時間與 pid,同一秒內兩個工作階段各存各的,不會互相覆蓋。
|
||
set -eu
|
||
|
||
JSC_HOME="${JSC_HOME:-$HOME/.jsc}"
|
||
ROOT="$JSC_HOME/worklog-pending"
|
||
MERGE_DIR="$ROOT/.merge"
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
用法:
|
||
worklog-pending.sh add <hash> <file> 存一段待寫入的日誌內容
|
||
worklog-pending.sh list <hash> 列出暫存檔路徑
|
||
worklog-pending.sh cat <hash> 印出全部暫存內容
|
||
worklog-pending.sh clear <hash> 清掉全部暫存(寫入日誌成功後才做)
|
||
worklog-pending.sh merge <hash> <file> 合成「暫存內容+本次條目」,印出 MERGED= 與 CLAIM=
|
||
worklog-pending.sh commit <hash> <claim> 寫入成功後清掉併入清單上的暫存檔
|
||
worklog-pending.sh abort <hash> <claim> 寫入失敗後保留暫存,只丟掉合併檔與併入清單
|
||
結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=沒有暫存內容
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
# hash 會直接拼進暫存目錄路徑,這裡是整支腳本唯一的格式驗證,擋的是路徑穿越。
|
||
# 放寬長度不等於放寬字元集:先確認每一個字都是十六進位,再比對長度。字元集這一關先過,
|
||
# 「.」與「/」才進不了路徑;只看長度就會讓 ../ 那類值溜進來。
|
||
# 字元檢查逐字剝,不呼叫 grep 之類逐行比對的工具:值裡夾一個換行,逐行比對會拿第一行
|
||
# 當整個值判過,那正是路徑穿越要的缺口。
|
||
#
|
||
# 這份樣式與 jsc-gitea/tools/page-name.sh、jsc-hooks/hooks/comment-scope.sh 是三份各自
|
||
# 獨立的定義,故意不共用:hook 與這個暫存區都必須自足,執行期不能相依別的 plugin 的安裝
|
||
# 路徑——那個路徑每個 CLI 不一樣,也可能根本沒裝,抓不到就等於整道護欄失效。三份的一致性
|
||
# 改在稽核時比對。
|
||
valid_hash() {
|
||
raw="${1:-}"
|
||
# H 開頭先剝掉再驗其餘:舊規則把首碼落在 0-9ABC 的 hash 改寫成 H 加原前 7 碼,
|
||
# H 本身不是十六進位字元,不先剝就會被逐字剝那一關擋掉。剝的是固定字面 H,
|
||
# 不是一個字元集,所以放進來的字仍然只有 H 與十六進位這兩種。
|
||
case "$raw" in
|
||
H*) rest="${raw#H}"; hprefixed=1 ;;
|
||
*) rest="$raw"; hprefixed=0 ;;
|
||
esac
|
||
while :; do
|
||
case "$rest" in
|
||
[0-9A-F]*) rest="${rest#?}" ;;
|
||
*) break ;;
|
||
esac
|
||
done
|
||
if [ -z "$rest" ]; then
|
||
if [ "$hprefixed" -eq 1 ]; then
|
||
case "$raw" in
|
||
????????) return 0 ;;
|
||
esac
|
||
else
|
||
case "$raw" in
|
||
????????????????????????????????????????) return 0 ;;
|
||
????????) return 0 ;;
|
||
esac
|
||
fi
|
||
fi
|
||
echo "[jsc][工作日誌暫存][ERR]:hash 須為 40 碼大寫十六進位(現行),或 8 碼大寫十六進位、H 加 7 碼大寫十六進位(尚未遷移的舊暫存),收到「${1:-空值}」。" >&2
|
||
exit 2
|
||
}
|
||
|
||
claim_reject() { # 併入清單上出現不該清的路徑,一律當用法錯誤擋下
|
||
echo "[jsc][工作日誌暫存][ERR]:併入清單裡的「$1」不在這個 hash 的暫存目錄底下,不清。" >&2
|
||
exit 2
|
||
}
|
||
|
||
valid_claim() { # 併入清單只認 merge 產出的那一份,擋掉拿別處的清單來刪檔
|
||
case "$1" in
|
||
"$MERGE_DIR"/*.claim) ;;
|
||
*) echo "[jsc][工作日誌暫存][ERR]:併入清單須是 merge 產出的「$MERGE_DIR/*.claim」,收到「$1」。" >&2; exit 2 ;;
|
||
esac
|
||
[ -f "$1" ] || { echo "[jsc][工作日誌暫存][ERR]:找不到併入清單「$1」。" >&2; exit 1; }
|
||
}
|
||
|
||
cmd="${1:-}"; [ -n "$cmd" ] || usage
|
||
shift || true
|
||
|
||
# orphans 不吃 hash:它要掃的就是「算不出對應 hash」的那些目錄。
|
||
if [ "$cmd" = orphans ]; then
|
||
[ -d "$ROOT" ] || exit 0
|
||
found=0
|
||
for d in "$ROOT"/*/; do
|
||
[ -d "$d" ] || continue
|
||
name=$(basename "$d")
|
||
[ "$name" = .merge ] && continue
|
||
case "$name" in
|
||
????????????????????????????????????????) continue ;;
|
||
esac
|
||
n=0
|
||
for f in "$d"*.md; do [ -f "$f" ] && n=$((n + 1)); done
|
||
printf '%s\t%s\t%s\n' "$name" "$n" "$d"
|
||
found=1
|
||
done
|
||
[ "$found" -eq 0 ] && exit 0
|
||
echo "[jsc][工作日誌暫存][ERR]:上列暫存目錄不是現行的 40 碼頁名,現行程式查不到它們,裡面的日誌永遠寫不進 wiki。舊名是新名的「H 加前 7 碼」,把目錄改成完整 40 碼即可。" >&2
|
||
exit 4
|
||
fi
|
||
|
||
hash="${1:-}"; [ -n "$hash" ] || usage
|
||
valid_hash "$hash"
|
||
dir="$ROOT/$hash"
|
||
|
||
# 舊規則的同一個專案會落在「H 加前 7 碼」那個目錄。拿 40 碼來查時一併收,否則規則改版
|
||
# 那一刻還沒寫進 wiki 的暫存就成為孤兒——查不到、也沒有人會說查不到,日誌就這樣消失。
|
||
# 只認這一種對映:舊名完全等於 H 加新名前 7 碼,推導唯一,不會收錯別的專案。
|
||
legacy_dir=""
|
||
case "$hash" in
|
||
????????????????????????????????????????)
|
||
_cand="$ROOT/H$(printf '%s' "$hash" | cut -c1-7)"
|
||
[ -d "$_cand" ] && legacy_dir="$_cand" ;;
|
||
esac
|
||
|
||
# 兩個目錄的暫存檔一起排。檔名開頭是 UTC 時間戳,字典序等同時間序,所以跨目錄排完
|
||
# 仍然是「舊的在前」。分開接的話,舊目錄那幾筆會整批插到新目錄最舊那一筆前面。
|
||
pending_files() {
|
||
for _d in "$dir" "$legacy_dir"; do
|
||
[ -n "$_d" ] && [ -d "$_d" ] || continue
|
||
for _f in "$_d"/*.md; do
|
||
[ -f "$_f" ] || continue
|
||
# 先印檔名再印全路徑,排完再把檔名那一欄切掉:排序的鍵是檔名,不是目錄。
|
||
printf '%s\t%s\n' "$(basename "$_f")" "$_f"
|
||
done
|
||
done | sort | cut -f2-
|
||
}
|
||
|
||
case "$cmd" in
|
||
add)
|
||
src="${2:-}"
|
||
[ -n "$src" ] || usage
|
||
[ -f "$src" ] || { echo "[jsc][工作日誌暫存][ERR]:找不到內容檔「$src」。" >&2; exit 1; }
|
||
mkdir -p "$dir" || { echo "[jsc][工作日誌暫存][ERR]:建不出暫存目錄「$dir」。" >&2; exit 1; }
|
||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||
target="$dir/$stamp-$$.md"
|
||
cp "$src" "$target" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$target」。" >&2; exit 1; }
|
||
printf '%s\n' "$target"
|
||
;;
|
||
list)
|
||
found=0
|
||
pending_files | while IFS= read -r f; do printf '%s\n' "$f"; done
|
||
pending_files | grep -q . && found=1
|
||
[ "$found" -eq 1 ] || exit 3
|
||
;;
|
||
cat)
|
||
found=0
|
||
pending_files | while IFS= read -r f; do cat "$f"; printf '\n'; done
|
||
pending_files | grep -q . && found=1
|
||
[ "$found" -eq 1 ] || exit 3
|
||
;;
|
||
clear)
|
||
cleared=0
|
||
for d in "$dir" "$legacy_dir"; do
|
||
[ -n "$d" ] && [ -d "$d" ] || continue
|
||
rm -rf "$d" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$d」。" >&2; exit 1; }
|
||
printf '已清除暫存:%s\n' "$d"
|
||
cleared=1
|
||
done
|
||
[ "$cleared" -eq 1 ] || exit 3
|
||
;;
|
||
merge)
|
||
# 合成這一次要寫進 wiki 的內容:暫存的在前(依時間),本次條目在後。
|
||
# 這裡只讀不刪——刪檔一律等 commit,也就是等 wiki 寫入回報成功。
|
||
src="${2:-}"
|
||
[ -n "$src" ] || usage
|
||
[ -f "$src" ] || { echo "[jsc][工作日誌暫存][ERR]:找不到本次條目檔「$src」。" >&2; exit 1; }
|
||
mkdir -p "$MERGE_DIR" || { echo "[jsc][工作日誌暫存][ERR]:建不出合併目錄「$MERGE_DIR」。" >&2; exit 1; }
|
||
stamp=$(date -u +%Y%m%dT%H%M%SZ)
|
||
merged="$MERGE_DIR/$hash-$stamp-$$.md"
|
||
claim="$MERGE_DIR/$hash-$stamp-$$.claim"
|
||
: > "$merged" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$merged」。" >&2; exit 1; }
|
||
: > "$claim" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$claim」。" >&2; exit 1; }
|
||
taken=0
|
||
pending_files > "$claim" || { echo "[jsc][工作日誌暫存][ERR]:列不出暫存檔。" >&2; exit 1; }
|
||
while IFS= read -r f; do
|
||
[ -n "$f" ] || continue
|
||
cat "$f" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:讀不到「$f」。" >&2; exit 1; }
|
||
printf '\n' >> "$merged"
|
||
taken=$((taken + 1))
|
||
done < "$claim"
|
||
cat "$src" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:併不進本次條目「$src」。" >&2; exit 1; }
|
||
printf '\n' >> "$merged"
|
||
printf 'MERGED=%s\n' "$merged"
|
||
printf 'CLAIM=%s\n' "$claim"
|
||
printf 'PENDING=%s\n' "$taken"
|
||
;;
|
||
commit)
|
||
# 護欄:只有 wiki 寫入成功才輪到這一段。清單以外的暫存檔一律不動,
|
||
# 因為那是 merge 之後才存進來的內容,還沒被寫進任何一頁。
|
||
claim="${2:-}"
|
||
[ -n "$claim" ] || usage
|
||
valid_claim "$claim"
|
||
cleared=0
|
||
while IFS= read -r f; do
|
||
[ -n "$f" ] || continue
|
||
# 舊目錄也要放行:merge 會把「H 加前 7 碼」那個舊目錄的暫存一起併走,
|
||
# 這裡不認的話,寫進 wiki 的內容清不掉,下一輪會整批重複寫一次。
|
||
case "$f" in
|
||
"$dir"/*) ;;
|
||
*) if [ -n "$legacy_dir" ]; then
|
||
case "$f" in "$legacy_dir"/*) ;; *) claim_reject "$f" ;; esac
|
||
else claim_reject "$f"; fi ;;
|
||
esac
|
||
[ -e "$f" ] || continue
|
||
rm -f "$f" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$f」。" >&2; exit 1; }
|
||
cleared=$((cleared + 1))
|
||
done < "$claim"
|
||
rmdir "$dir" 2>/dev/null || true
|
||
[ -n "$legacy_dir" ] && rmdir "$legacy_dir" 2>/dev/null || true
|
||
rm -f "$claim" "${claim%.claim}.md"
|
||
printf '已清除暫存 %s 個檔案(wiki 寫入成功後才做)。\n' "$cleared"
|
||
;;
|
||
abort)
|
||
# wiki 寫入失敗時走這裡:暫存一個都不刪,下一次 merge 會再把它們併進去。
|
||
claim="${2:-}"
|
||
[ -n "$claim" ] || usage
|
||
valid_claim "$claim"
|
||
kept=0
|
||
while IFS= read -r f; do
|
||
[ -n "$f" ] || continue
|
||
if [ -e "$f" ]; then
|
||
kept=$((kept + 1))
|
||
fi
|
||
done < "$claim"
|
||
rm -f "$claim" "${claim%.claim}.md"
|
||
printf '暫存保留 %s 個檔案,合併檔與併入清單已丟掉。\n' "$kept"
|
||
;;
|
||
*)
|
||
usage ;;
|
||
esac
|