Files
log/tools/worklog-pending.sh
T
jiantw83 e762cf986e fix(工作日誌暫存): 收編舊名目錄,並掃出定址不到的孤兒
雜湊規則從 8 碼改成完整 40 碼那一刻,還沒寫進 wiki 的暫存留在舊名底下。
現行程式拿 40 碼去查,查不到那些目錄,裡面的條目就再也寫不出去。

沒人發現是因為兩種狀態長得一樣:「真的沒有待寫」與「待寫卡在舊名底下」
對呼叫端都是成功。技能於是照常回報完成,日誌卻一筆都沒有。

兩道處置。推得出對映的(舊名等於 H 加新名前 7 碼)由 list、cat、merge
一併收編,clear 與 commit 也連舊目錄一起清——commit 不放行的話,已經寫進
wiki 的暫存清不掉,下一輪會整批重複寫一次。推不出對映的交給新的 orphans
子命令掃出來回報,要有人看到才處理得掉。
2026-09-02 15:27:03 +08:00

282 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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