未設時讀 config.env(行為不變);設了讀 config.<profile>.env。 .gitignore 新增 config.*.env 但保留 config.example.env。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
203 lines
6.3 KiB
Bash
203 lines
6.3 KiB
Bash
#!/usr/bin/env bash
|
||
# gitea-cli 共用 lib。
|
||
# 使用方式(呼叫端):
|
||
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# source "${SCRIPT_DIR}/lib/common.sh"
|
||
# load_config
|
||
#
|
||
# 重要:所有 API 呼叫走 gitea_api;所有 push/clone 走 git_push_with_token / git_clone_with_token。
|
||
# token 絕不允許出現在 .git/config、remote URL、log 中。
|
||
|
||
set -euo pipefail
|
||
|
||
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
_SKILL_DIR="$(cd "${_LIB_DIR}/../.." && pwd)"
|
||
# GITEA_PROFILE 未設 → 讀 config.env;設了 → 讀 config.<profile>.env
|
||
if [[ -n "${GITEA_PROFILE:-}" ]]; then
|
||
CONFIG_FILE="${_SKILL_DIR}/config.${GITEA_PROFILE}.env"
|
||
else
|
||
CONFIG_FILE="${_SKILL_DIR}/config.env"
|
||
fi
|
||
|
||
GITEA_API_RESP_FILE=""
|
||
_cleanup_common() {
|
||
if [[ -n "${GITEA_API_RESP_FILE:-}" && -f "${GITEA_API_RESP_FILE}" ]]; then
|
||
rm -f "${GITEA_API_RESP_FILE}"
|
||
fi
|
||
}
|
||
trap _cleanup_common EXIT
|
||
|
||
# === logging(全部寫 stderr;stdout 留給結構化輸出)===
|
||
log_info() { printf '▶ %s\n' "$*" >&2; }
|
||
log_ok() { printf '✓ %s\n' "$*" >&2; }
|
||
log_warn() { printf '⚠ %s\n' "$*" >&2; }
|
||
log_err() { printf '❌ %s\n' "$*" >&2; }
|
||
log_done() { printf '✅ %s\n' "$*" >&2; }
|
||
|
||
# === 必要指令檢查 ===
|
||
require_cmd() {
|
||
local missing=()
|
||
for c in "$@"; do
|
||
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
|
||
done
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
log_err "缺少指令:${missing[*]}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# === config 載入 ===
|
||
load_config() {
|
||
require_cmd curl git
|
||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||
log_err "找不到設定檔:${CONFIG_FILE}"
|
||
{
|
||
echo ""
|
||
echo "請先建立:"
|
||
echo " cp ${_SKILL_DIR}/config.example.env ${CONFIG_FILE}"
|
||
echo ""
|
||
echo "然後編輯 ${CONFIG_FILE} 填入 GITEA_URL / GITEA_USER / GITEA_TOKEN"
|
||
if [[ -n "${GITEA_PROFILE:-}" ]]; then
|
||
echo ""
|
||
echo "(目前使用 profile:${GITEA_PROFILE})"
|
||
fi
|
||
} >&2
|
||
exit 1
|
||
fi
|
||
# shellcheck disable=SC1090
|
||
source "$CONFIG_FILE"
|
||
: "${GITEA_URL:?GITEA_URL 未設定(請編輯 ${CONFIG_FILE})}"
|
||
: "${GITEA_USER:?GITEA_USER 未設定(請編輯 ${CONFIG_FILE})}"
|
||
: "${GITEA_TOKEN:?GITEA_TOKEN 未設定(請編輯 ${CONFIG_FILE})}"
|
||
if [[ "$GITEA_TOKEN" == "請貼上你的_access_token" ]]; then
|
||
log_err "GITEA_TOKEN 仍是預設值,請編輯 ${CONFIG_FILE}"
|
||
exit 1
|
||
fi
|
||
if [[ "$GITEA_URL" == "http://your-gitea.example.com:3000" ]]; then
|
||
log_err "GITEA_URL 仍是預設值,請編輯 ${CONFIG_FILE}"
|
||
exit 1
|
||
fi
|
||
if [[ "$GITEA_USER" == "your-username" ]]; then
|
||
log_err "GITEA_USER 仍是預設值,請編輯 ${CONFIG_FILE}"
|
||
exit 1
|
||
fi
|
||
GITEA_URL="${GITEA_URL%/}"
|
||
export GITEA_URL GITEA_USER GITEA_TOKEN
|
||
}
|
||
|
||
# === Gitea API 呼叫 ===
|
||
# gitea_api <METHOD> <path-from-/api/v1> [json-body]
|
||
# 回傳後讀:$HTTP_CODE(狀態碼字串)、$GITEA_API_RESP_FILE(body 檔路徑)
|
||
gitea_api() {
|
||
# 清掉上一次的 tmpfile(防止多次呼叫時洩漏)
|
||
if [[ -n "${GITEA_API_RESP_FILE:-}" && -f "${GITEA_API_RESP_FILE}" ]]; then
|
||
rm -f "${GITEA_API_RESP_FILE}"
|
||
fi
|
||
local method="$1"
|
||
local path="$2"
|
||
local body="${3:-}"
|
||
local url="${GITEA_URL}/api/v1${path}"
|
||
|
||
GITEA_API_RESP_FILE="$(mktemp)"
|
||
|
||
local -a curl_args=(
|
||
-sS
|
||
-o "$GITEA_API_RESP_FILE"
|
||
-w '%{http_code}'
|
||
-X "$method"
|
||
-H "Authorization: token ${GITEA_TOKEN}"
|
||
)
|
||
if [[ -n "$body" ]]; then
|
||
curl_args+=(-H "Content-Type: application/json" -d "$body")
|
||
fi
|
||
HTTP_CODE="$(curl "${curl_args[@]}" "$url")"
|
||
export HTTP_CODE GITEA_API_RESP_FILE
|
||
}
|
||
|
||
# 一次性帶 token 的 git push / clone
|
||
# 注意:token 透過 -c 參數傳入,在 git 子程序存活期間短暫出現於 process 列表
|
||
# (/proc/PID/cmdline)。這是此模式固有取捨,不會持久化到 .git/config / remote URL / log。
|
||
_git_with_token() {
|
||
git \
|
||
-c 'credential.helper=' \
|
||
-c "credential.helper=!f() { echo username=${GITEA_USER}; echo password=${GITEA_TOKEN}; }; f" \
|
||
"$@"
|
||
}
|
||
|
||
git_push_with_token() {
|
||
local branch="$1"
|
||
_git_with_token push -u origin "$branch"
|
||
}
|
||
|
||
git_push_tag_with_token() {
|
||
local tag="$1"
|
||
_git_with_token push origin "$tag"
|
||
}
|
||
|
||
git_clone_with_token() {
|
||
local clean_url="$1"
|
||
local target_dir="$2"
|
||
_git_with_token clone "$clean_url" "$target_dir"
|
||
# 再確保 origin 是乾淨 URL(clone 本來就是用 clean_url,這裡是防呆)
|
||
git -C "$target_dir" remote set-url origin "$clean_url"
|
||
}
|
||
|
||
# 從 cwd origin 推斷 repo 名稱
|
||
# 成功時 echo "owner/repo"(例:timmy/my-project)並回傳 0;失敗回傳非 0
|
||
# 呼叫端若只需要 repo 名稱:repo_name="${result##*/}"
|
||
# 只支援 http(s):// 形式 URL;ssh (git@) 不支援(刻意 YAGNI)
|
||
infer_repo_from_cwd() {
|
||
local url
|
||
if ! url="$(git remote get-url origin 2>/dev/null)"; then
|
||
return 1
|
||
fi
|
||
# 支援 http(s)://host[:port]/owner/repo[.git]
|
||
local path="${url#*://}"
|
||
path="${path#*/}"
|
||
path="${path%.git}"
|
||
[[ -z "$path" ]] && return 1
|
||
echo "$path"
|
||
}
|
||
|
||
# === 確認提示 ===
|
||
# confirm_destructive <prompt>
|
||
# 若 GITEA_YES=1 則直接通過;否則 y/N 互動
|
||
confirm_destructive() {
|
||
local prompt="$1"
|
||
if [[ "${GITEA_YES:-0}" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
if ! { true < /dev/tty; } 2>/dev/null; then
|
||
log_err "需要確認但無 TTY;請加 -y / --yes 或設定 GITEA_YES=1"
|
||
exit 1
|
||
fi
|
||
local ans
|
||
printf '%s [y/N] ' "$prompt" >&2
|
||
read -r ans < /dev/tty
|
||
case "$ans" in
|
||
y|Y|yes|YES) return 0 ;;
|
||
*) log_err "取消"; exit 1 ;;
|
||
esac
|
||
}
|
||
|
||
# confirm_exact_match <expected>
|
||
# 要求使用者重新輸入完整字串才通過;GITEA_YES=1 可略過
|
||
confirm_exact_match() {
|
||
local expected="$1"
|
||
if [[ "${GITEA_YES:-0}" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
if ! { true < /dev/tty; } 2>/dev/null; then
|
||
log_err "需要確認但無 TTY;請加 -y / --yes 或設定 GITEA_YES=1"
|
||
exit 1
|
||
fi
|
||
local ans
|
||
printf '請完整輸入 "%s" 以確認:' "$expected" >&2
|
||
read -r ans < /dev/tty
|
||
if [[ "$ans" == "$expected" ]]; then
|
||
return 0
|
||
fi
|
||
log_err "輸入不符合,取消"
|
||
exit 1
|
||
}
|