202 lines
8.0 KiB
Bash
Executable File
202 lines
8.0 KiB
Bash
Executable File
#!/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|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|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
|