Files
log/tools/worklog-pending.sh
T
jiantw83 aee17e45c7 feat(wiki): 三個目錄頁改走專用存取庫,比對鍵改用裸 HASH
What:LOG_CONTENTS、LEARN_CONTENTS、REPORT_CONTENTS 改由 wiki-repo CONTENTS
解析並透過 wiki-contents.sh upsert 寫入,內容頁仍各走自己的型別。LOG 與 REPORT
的目錄頁新增一欄裸 HASH 當比對鍵。

Why:比對鍵原本是含網址的儲存格,換主機、換存取庫或 URL 編碼有差就比對不到,
upsert 會走附加分支,同一頁多出第二列而舊列永遠不再更新。REPORT 更脆:存取庫一換,
雜湊與網址同時變,四個期間的列會一次全部重複。

How:worklog-pending.sh 的 valid_hash 放寬成 40 碼、8 碼與 H 加 7 碼三種形狀。
放寬的是長度不是字元集——先剝字元再比長度的順序保留,路徑穿越與換行注入照樣擋下。
年報不得沿用日誌存取庫那條規則收斂到內容頁,目錄頁同住一庫是另一回事。

Who:jsc-log
2026-09-02 11:02:21 +08:00

220 lines
10 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 沒有暫存內容
# merge 沒有暫存內容時仍然 exit 0:合併檔至少有本次條目,呼叫端不必分兩條路走。
#
# 陷阱:
# - 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
}
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
hash="${1:-}"; [ -n "$hash" ] || usage
valid_hash "$hash"
dir="$ROOT/$hash"
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)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
printf '%s\n' "$f"
found=1
done
[ "$found" -eq 1 ] || exit 3
;;
cat)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
cat "$f"
printf '\n'
found=1
done
[ "$found" -eq 1 ] || exit 3
;;
clear)
[ -d "$dir" ] || exit 3
rm -rf "$dir" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$dir」。" >&2; exit 1; }
printf '已清除暫存:%s\n' "$dir"
;;
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
if [ -d "$dir" ]; then
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
cat "$f" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:讀不到「$f」。" >&2; exit 1; }
printf '\n' >> "$merged"
printf '%s\n' "$f" >> "$claim"
taken=$((taken + 1))
done
fi
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
case "$f" in
"$dir"/*) ;;
*) echo "[jsc][工作日誌暫存][ERR]:併入清單裡的「$f」不在「$dir」底下,不清。" >&2; exit 2 ;;
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
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