Files
gitea/tools/gitea.sh
T
jiantw83andClaude Fable 5 653b619ea1 feat(wiki): wiki 頁型允許清單新增 LEARN
What:gitea.sh 的 wiki-repo 允許型別清單、wiki 技能 SKILL.md 的頁型清單,以及 check-wiki-rules.sh 的驗證案例,同步收錄 LEARN 頁型。
Why:jsc-log 新增 LEARN wiki 頁型,若允許清單未收錄,wiki-repo LEARN 會以 unknown type(exit 2)被擋下,學習紀錄頁無法解析存放位置。
How:resolve_wiki_repo 的 case 白名單加入 LEARN,解析順序維持 JSC_WIKI_REPO_LEARN 優先、退回 JSC_WIKI_REPO、不跨型別代用;check-wiki-rules.sh 補上 LEARN 專屬變數優先與退回共用預設兩個案例;SKILL.md 允許頁型清單同步更新。
Who:jsc-gitea wiki 技能與其呼叫方(jsc-log 等)的 wiki 頁型解析功能。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:36:49 +08:00

202 lines
8.0 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
# gitea.sh — Gitea API 工具(curl + GITEA_TOKEN)。
# 用法:
# gitea.sh owners # 列出可讀取的 owner(自己 + 組織)
# gitea.sh repos <owner> # 列出 owner 底下可讀取的 repo(全名)
# gitea.sh default-branch <owner>/<repo> # 印出預設分支
# gitea.sh clone-url <owner>/<repo> # 印出 clone URL
# gitea.sh hash-id <text> # 產生 8 碼大寫 SHA-1 hash;首碼為 0-9/A/B/C 時改成 Hxxxxxxx
# gitea.sh wiki-repo <TYPE> # 解析頁面類型的 wiki 位置:
# TYPE = QUESTION|PLAN|ANALYZE|MAINTAIN|REPO|LOG|LEARN|ERROR(即頁名前綴)
# 依序取 JSC_WIKI_REPO_{TYPE} > JSC_WIKI_REPO;不得跨類型代用;都未設定 exit 3
# gitea.sh wiki-list <owner>/<repo> # 列出 wiki 頁名
# gitea.sh wiki-get <owner>/<repo> <page> # 印出 wiki 頁 markdown;不存在時 exit 4
# gitea.sh wiki-put <owner>/<repo> <page> <file> # 建立或更新 wiki 頁(內容取自檔案)
# gitea.sh pr-create <owner>/<repo> <head> <base> <title> <body-file> # 建立 PR,印出 PR URL
# gitea.sh pr-depend <owner>/<repo> <pr-index> <dep-owner>/<dep-repo> <dep-index>
# 把 PR 掛上前置 PR 依賴;依賴未關閉前 Gitea 會阻擋合併
# gitea.sh repo-set <owner>/<repo> <description> [website] # 設定 repo 描述與網頁
# gitea.sh api <METHOD> <path> [json-file] # 原始 API 呼叫(path 以 /repos/... 起始)
# 環境變數: GITEA_HOST(例 https://gitea.jsc.idv.tw)、GITEA_TOKEN
# GITEA_TOKEN 未設定,或請求遇 401/403 時,自動退回 tea CLI 的登入 token
# (~/.config/tea/config.yml,優先取 default: true 的登入);兩者皆無才失敗。
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
resolve_wiki_repo() { # TYPE -> JSC_WIKI_REPO_{TYPE} -> JSC_WIKI_REPO
type=$(printf '%s' "${1:?TYPE required}" | tr a-z A-Z)
case "$type" in
QUESTION|PLAN|ANALYZE|MAINTAIN|REPO|LOG|LEARN|ERROR) ;;
*) echo "unknown wiki type: $type" >&2; exit 2 ;;
esac
eval "v=\${JSC_WIKI_REPO_${type}:-}"
[ -n "$v" ] || v="${JSC_WIKI_REPO:-}"
if [ -n "$v" ]; then printf '%s\n' "$v"; else
echo "no wiki repo configured for $type (set JSC_WIKI_REPO_$type or JSC_WIKI_REPO)" >&2; exit 3
fi
}
cmd="${1:?usage: gitea.sh <command> ...}"; shift
case "$cmd" in
hash-id)
exec "$script_dir/hash-id" "$@" ;;
wiki-repo)
resolve_wiki_repo "${1-}"
exit 0 ;;
esac
tea_token() { # 取 tea CLI 設定檔的 token(優先 default: true 的登入,否則第一個)
cfg="${HOME}/.config/tea/config.yml"
[ -f "$cfg" ] || return 1
t=$(awk '
/^ *- / { if (def && tok != "") { print tok; found=1; exit } tok=""; def=0 }
/^ *token: */ { line=$0; sub(/^ *token: */,"",line); gsub(/"/,"",line)
tok=line; if (first=="") first=line }
/^ *default: *true/ { def=1 }
END { if (!found) { if (def && tok != "") print tok; else print first } }
' "$cfg")
[ -n "$t" ] && printf '%s\n' "$t"
}
: "${GITEA_HOST:?GITEA_HOST is required}"
TEA_TOKEN=$(tea_token 2>/dev/null || true)
if [ -z "${GITEA_TOKEN:-}" ]; then
if [ -n "$TEA_TOKEN" ]; then GITEA_TOKEN="$TEA_TOKEN"; else
: "${GITEA_TOKEN:?GITEA_TOKEN is required}"
fi
fi
case "$GITEA_HOST" in http://*|https://*) HOST="$GITEA_HOST" ;; *) HOST="https://$GITEA_HOST" ;; esac
API="${HOST%/}/api/v1"
req() { # METHOD path [json-file] -> body(HTTP >= 400 時 exit 4;401/403 以 tea token 重試一次)
method="$1"; path="$2"; body_file="${3:-}"
retried=0
while :; do
if [ -n "$body_file" ]; then
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
-H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
--data-binary "@$body_file" "$API$path")
else
out=$(curl -sS -w '\n%{http_code}' -X "$method" \
-H "Authorization: token $GITEA_TOKEN" "$API$path")
fi
code=$(printf '%s' "$out" | tail -n1)
if [ "$retried" -eq 0 ] && { [ "$code" = 401 ] || [ "$code" = 403 ]; } \
&& [ -n "$TEA_TOKEN" ] && [ "$TEA_TOKEN" != "$GITEA_TOKEN" ]; then
GITEA_TOKEN="$TEA_TOKEN"; retried=1; continue
fi
break
done
printf '%s\n' "$out" | sed '$d'
[ "$code" -lt 400 ]
}
json_field() { # stdin JSON -> 每個物件的指定欄位一行(陣列或單一物件皆可)
python3 -c '
import json,sys
d=json.load(sys.stdin); k=sys.argv[1]
items=d if isinstance(d,list) else [d]
for i in items:
v=i.get(k,"")
if v!="" and v is not None: print(v)
' "$1"
}
case "$cmd" in
owners)
{ req GET "/user" | json_field login
req GET "/user/orgs?limit=50" | json_field username; } | sort -u ;;
repos)
owner="${1:?owner required}"
# 分頁抓 owner 的 repo(org 與 user 端點擇一成功)
page=1
while :; do
out=$(req GET "/orgs/$owner/repos?limit=50&page=$page" 2>/dev/null) \
|| out=$(req GET "/users/$owner/repos?limit=50&page=$page")
names=$(printf '%s' "$out" | json_field full_name)
[ -n "$names" ] || break
printf '%s\n' "$names"
page=$((page+1))
done ;;
default-branch)
or="${1:?owner/repo required}"
req GET "/repos/$or" | json_field default_branch ;;
clone-url)
or="${1:?owner/repo required}"
req GET "/repos/$or" | json_field clone_url ;;
wiki-list)
or="${1:?owner/repo required}"
req GET "/repos/$or/wiki/pages?limit=200" | json_field title ;;
wiki-get)
or="${1:?owner/repo required}"; page="${2:?page required}"
if ! out=$(req GET "/repos/$or/wiki/page/$page" 2>/dev/null); then
echo "wiki page not found: $page" >&2; exit 4
fi
printf '%s' "$out" | python3 -c '
import json,sys,base64
d=json.load(sys.stdin)
sys.stdout.write(base64.b64decode(d.get("content_base64","")).decode("utf-8"))
' ;;
wiki-put)
or="${1:?owner/repo required}"; page="${2:?page required}"; file="${3:?content file required}"
tmp=$(mktemp)
python3 -c '
import json,sys,base64
title,path=sys.argv[1],sys.argv[2]
content=open(path,"rb").read()
print(json.dumps({"title":title,"content_base64":base64.b64encode(content).decode()}))
' "$page" "$file" > "$tmp"
if req GET "/repos/$or/wiki/page/$page" >/dev/null 2>&1; then
req PATCH "/repos/$or/wiki/page/$page" "$tmp" >/dev/null
else
req POST "/repos/$or/wiki/new" "$tmp" >/dev/null
fi
rm -f "$tmp"
echo "OK $page" ;;
pr-create)
or="${1:?owner/repo required}"; head="${2:?head required}"; base="${3:?base required}"
title="${4:?title required}"; body_file="${5:?body file required}"
tmp=$(mktemp)
python3 -c '
import json,sys
print(json.dumps({"head":sys.argv[1],"base":sys.argv[2],"title":sys.argv[3],
"body":open(sys.argv[4],encoding="utf-8").read()}))
' "$head" "$base" "$title" "$body_file" > "$tmp"
req POST "/repos/$or/pulls" "$tmp" | json_field html_url
rm -f "$tmp" ;;
pr-depend)
or="${1:?owner/repo required}"; idx="${2:?pr index required}"
dep_or="${3:?dep owner/repo required}"; dep_idx="${4:?dep index required}"
tmp=$(mktemp)
python3 -c '
import json,sys
o,r=sys.argv[1].split("/",1)
print(json.dumps({"owner":o,"repo":r,"index":int(sys.argv[2])}))
' "$dep_or" "$dep_idx" > "$tmp"
if ! out=$(req POST "/repos/$or/issues/$idx/dependencies" "$tmp"); then
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
fi
rm -f "$tmp"
echo "OK $or#$idx depends on $dep_or#$dep_idx" ;;
repo-set)
or="${1:?owner/repo required}"; desc="${2:?description required}"; site="${3:-}"
tmp=$(mktemp)
python3 -c '
import json,sys
b={"description":sys.argv[1]}
if len(sys.argv)>2 and sys.argv[2]: b["website"]=sys.argv[2]
print(json.dumps(b))
' "$desc" "$site" > "$tmp"
if ! out=$(req PATCH "/repos/$or" "$tmp"); then
rm -f "$tmp"; printf '%s\n' "$out" >&2; exit 4
fi
rm -f "$tmp"
echo "OK $or" ;;
api)
method="${1:?METHOD}"; path="${2:?path}"; body="${3:-}"
req "$method" "$path" $body ;;
*)
echo "unknown command: $cmd" >&2; exit 2 ;;
esac