#!/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..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 [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 # 若 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 # 要求使用者重新輸入完整字串才通過;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 }