1823 lines
53 KiB
Markdown
1823 lines
53 KiB
Markdown
# gitea-cli 擴充實作計畫
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 把 `gitea-cli` 從單一 `publish.sh` 擴充成 10 支腳本的 Gitea 操作工具集;所有腳本共用 `scripts/lib/common.sh`。
|
||
|
||
**Architecture:** 多支獨立 bash 腳本 + 單一共用 lib。每支腳本 `source` lib、呼叫 `load_config`,以 `gitea_api` / `git_*_with_token` 等 function 操作;token 只在 push/clone 當下透過一次性 credential helper 注入,永遠不落地到 `.git/config`。
|
||
|
||
**Tech Stack:** bash、curl、jq、git、自架 Gitea(API v1)
|
||
|
||
**測試策略:** 依 spec「無自動化測試,手動驗收」。每個 task 的驗證步驟包含:`bash -n` 語法檢查、可離線跑的 unit-like 驗證、需要時標註「需要 live Gitea」的 smoke test。
|
||
|
||
---
|
||
|
||
## 前置條件
|
||
|
||
在開始 Task 1 之前確認以下:
|
||
|
||
- [ ] 已讀過 `docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md`
|
||
- [ ] `/Users/timmy/Projects/Personal/gitea-cli/` 為 cwd
|
||
- [ ] `config.env` 已存在且 `GITEA_URL` / `GITEA_USER` / `GITEA_TOKEN` 皆已填值(需要 live smoke test 時會用到)
|
||
- [ ] 系統上已安裝 `curl`、`git`、`jq`;`column` 指令可用
|
||
- 檢查:`command -v curl git jq column`;缺任何一個停下來先裝
|
||
- [ ] 已 `git status`,工作區乾淨
|
||
|
||
## 關鍵不變量(每個 task 都要守)
|
||
|
||
1. Token 只能出現在:`config.env`、呼叫過程的環境變數、`gitea_api` / `git_*_with_token` 函式內部
|
||
2. Token **絕不**進入:`.git/config`、remote URL、任何 log(stdout/stderr)
|
||
3. `origin` remote 永遠是乾淨 URL(push/clone 完成後)
|
||
4. `publish.sh` 既有呼叫方式(`publish.sh [repo] [public|private]`)行為完全不變
|
||
5. 每支腳本都要 `set -euo pipefail`;用到 `mktemp` 時要 `trap` 清
|
||
|
||
---
|
||
|
||
## Task 1:建立 `scripts/lib/common.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/lib/common.sh`
|
||
|
||
**Context:** 所有後續腳本的基礎。把重複的 config 載入、API 呼叫、一次性 token 注入、確認提示、log 都集中在這裡。後續任務會 `source` 它。
|
||
|
||
- [ ] **Step 1: 建立檔案並寫入 lib 內容**
|
||
|
||
```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)"
|
||
CONFIG_FILE="${_SKILL_DIR}/config.env"
|
||
|
||
GITEA_API_RESP_FILE=""
|
||
_cleanup_common() {
|
||
[[ -n "${GITEA_API_RESP_FILE:-}" && -f "${GITEA_API_RESP_FILE}" ]] && rm -f "${GITEA_API_RESP_FILE}"
|
||
}
|
||
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"
|
||
} >&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" || -z "$GITEA_TOKEN" ]]; then
|
||
log_err "GITEA_TOKEN 仍是預設值,請編輯 ${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() {
|
||
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 ===
|
||
_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" 並回傳 0;失敗回傳非 0
|
||
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 [[ ! -r /dev/tty ]]; 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 [[ ! -r /dev/tty ]]; 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
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/lib/common.sh`
|
||
Expected: 無輸出、exit 0
|
||
|
||
- [ ] **Step 3: Smoke test —— lib 可被 source 且 function 存在**
|
||
|
||
Run:
|
||
```bash
|
||
bash -c '
|
||
set -euo pipefail
|
||
source scripts/lib/common.sh
|
||
for fn in log_info log_ok log_warn log_err log_done require_cmd load_config gitea_api git_push_with_token git_push_tag_with_token git_clone_with_token infer_repo_from_cwd confirm_destructive confirm_exact_match; do
|
||
declare -F "$fn" >/dev/null || { echo "MISSING: $fn"; exit 1; }
|
||
done
|
||
echo OK
|
||
'
|
||
```
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Smoke test —— log function 確實寫 stderr**
|
||
|
||
Run: `bash -c 'source scripts/lib/common.sh; log_info hi' 2>/tmp/err 1>/tmp/out && [[ -z "$(cat /tmp/out)" ]] && grep -q '▶ hi' /tmp/err && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/lib/common.sh
|
||
git commit -m "feat(lib): add shared common.sh for gitea-cli scripts"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2:重構 `publish.sh` 使用 common.sh(外部行為不變)
|
||
|
||
**Files:**
|
||
- Modify: `scripts/publish.sh`(整個重寫,但對外行為與既有版本相同)
|
||
|
||
**Context:** 把原本 `publish.sh` 裡的 config 載入、API 呼叫、credential helper 改成用 common.sh 的對應 function。此階段**不**新增 `-m` 選項;-m 在 Task 3 加。
|
||
|
||
- [ ] **Step 1: 重寫 `scripts/publish.sh`**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
REPO_NAME="${1:-$(basename "$PWD")}"
|
||
VISIBILITY="${2:-public}"
|
||
|
||
case "$VISIBILITY" in
|
||
public) PRIVATE=false ;;
|
||
private) PRIVATE=true ;;
|
||
*)
|
||
log_err "可見性只能是 public 或 private,收到:${VISIBILITY}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
CLEAN_URL="${GITEA_URL}/${GITEA_USER}/${REPO_NAME}.git"
|
||
WEB_URL="${GITEA_URL}/${GITEA_USER}/${REPO_NAME}"
|
||
|
||
log_info "建立 Gitea repo:${GITEA_USER}/${REPO_NAME} (${VISIBILITY})"
|
||
|
||
gitea_api POST "/user/repos" \
|
||
"{\"name\":\"${REPO_NAME}\",\"private\":${PRIVATE},\"auto_init\":false,\"default_branch\":\"main\"}"
|
||
|
||
case "$HTTP_CODE" in
|
||
201) log_ok "repo 已建立" ;;
|
||
409) log_warn "repo 已存在,略過建立步驟" ;;
|
||
401|403)
|
||
log_err "Gitea 授權失敗 (HTTP ${HTTP_CODE})"
|
||
{
|
||
echo "請確認 ${CONFIG_FILE} 的 GITEA_TOKEN 是否正確、權限是否包含 write:repository"
|
||
cat "$GITEA_API_RESP_FILE"
|
||
echo ""
|
||
} >&2
|
||
exit 1
|
||
;;
|
||
*)
|
||
log_err "建立 repo 失敗 (HTTP ${HTTP_CODE})"
|
||
{ cat "$GITEA_API_RESP_FILE"; echo ""; } >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
if [[ ! -d .git ]]; then
|
||
log_info "git init"
|
||
git init -b main >/dev/null
|
||
fi
|
||
|
||
if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||
git add -A
|
||
if git diff --cached --quiet; then
|
||
log_info "工作目錄沒有檔案,建立空的 initial commit"
|
||
git commit --allow-empty -m "Initial commit" >/dev/null
|
||
else
|
||
log_info "建立 initial commit"
|
||
git commit -m "Initial commit" >/dev/null
|
||
fi
|
||
else
|
||
git add -A
|
||
if ! git diff --cached --quiet; then
|
||
log_info "提交未提交的變更"
|
||
git commit -m "Update" >/dev/null
|
||
fi
|
||
fi
|
||
|
||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||
|
||
if git remote get-url origin >/dev/null 2>&1; then
|
||
git remote set-url origin "$CLEAN_URL"
|
||
else
|
||
git remote add origin "$CLEAN_URL"
|
||
fi
|
||
|
||
log_info "推送到 origin (${CLEAN_URL}) branch=${BRANCH}"
|
||
git_push_with_token "$BRANCH"
|
||
|
||
log_done "完成"
|
||
echo " Branch: ${BRANCH}" >&2
|
||
# stdout:最終結構化輸出
|
||
echo "$WEB_URL"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/publish.sh`
|
||
Expected: 無輸出、exit 0
|
||
|
||
- [ ] **Step 3: Smoke test(離線)—— `--help` 式錯誤輸入**
|
||
|
||
Run: `bash scripts/publish.sh foo bogus 2>&1 | grep -q '可見性只能是 public 或 private' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: 需要 live Gitea 的手動驗收**(可延到 Task 14 統一做)
|
||
|
||
在一個 tmp 目錄試推:
|
||
```bash
|
||
mkdir -p /tmp/gitea-cli-smoke-$(date +%s) && cd /tmp/gitea-cli-smoke-* && echo hi > hi.txt
|
||
URL="$(bash /path/to/gitea-cli/scripts/publish.sh gitea-cli-smoke-$(date +%s) private)"
|
||
echo "publish.sh stdout: $URL"
|
||
grep -q "${GITEA_TOKEN:-NEVER}" .git/config && { echo LEAK; exit 1; } || echo "no token leak"
|
||
git remote get-url origin # 應為乾淨 URL
|
||
```
|
||
Expected: stdout 印 web URL;`.git/config` 不含 token;remote origin 為乾淨 URL。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/publish.sh
|
||
git commit -m "refactor(publish): use shared common.sh lib"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3:`publish.sh` 新增 `-m "msg"` 選項
|
||
|
||
**Files:**
|
||
- Modify: `scripts/publish.sh`(換成支援 `-m` 的 arg parsing)
|
||
|
||
**Context:** 讓使用者可指定 commit 訊息取代預設的 `Initial commit` / `Update`。既有位置參數必須繼續運作。
|
||
|
||
- [ ] **Step 1: 替換 arg parsing 與 commit 訊息邏輯**
|
||
|
||
把 Task 2 的 `REPO_NAME="${1:-...}" / VISIBILITY="${2:-...}"` 段落換成以下 while 迴圈,並把寫 commit 的兩處改用 `${COMMIT_MSG:-...}`:
|
||
|
||
```bash
|
||
REPO_NAME=""
|
||
VISIBILITY="public"
|
||
COMMIT_MSG=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-m)
|
||
[[ $# -ge 2 ]] || { log_err "-m 需要一個訊息參數"; exit 1; }
|
||
COMMIT_MSG="$2"
|
||
shift 2
|
||
;;
|
||
public|private)
|
||
VISIBILITY="$1"
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
cat >&2 <<'EOF'
|
||
Usage: publish.sh [repo] [public|private] [-m "commit message"]
|
||
EOF
|
||
exit 0
|
||
;;
|
||
-*)
|
||
log_err "未知參數:$1"
|
||
exit 1
|
||
;;
|
||
*)
|
||
if [[ -z "$REPO_NAME" ]]; then
|
||
REPO_NAME="$1"
|
||
else
|
||
log_err "多餘參數:$1"
|
||
exit 1
|
||
fi
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
REPO_NAME="${REPO_NAME:-$(basename "$PWD")}"
|
||
```
|
||
|
||
然後把兩處 commit 呼叫改成:
|
||
|
||
```bash
|
||
# 第一處(沒任何 commit 時)
|
||
git commit --allow-empty -m "${COMMIT_MSG:-Initial commit}" >/dev/null
|
||
# ...另一分支
|
||
git commit -m "${COMMIT_MSG:-Initial commit}" >/dev/null
|
||
|
||
# 第二處(已有 commit、有新變更時)
|
||
git commit -m "${COMMIT_MSG:-Update}" >/dev/null
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/publish.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 既有呼叫方式不變**
|
||
|
||
Run: `bash scripts/publish.sh -h 2>&1 | grep -q 'Usage: publish.sh' && echo OK`
|
||
Expected: `OK`
|
||
|
||
Run: `bash scripts/publish.sh foo bogus 2>&1 | grep -q '可見性只能是 public 或 private' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add scripts/publish.sh
|
||
git commit -m "feat(publish): add -m option for custom commit message"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4:建立 `scripts/list.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/list.sh`
|
||
|
||
**Context:** 列出 `GITEA_USER` 名下所有 repo,支援可見性篩選與三種輸出格式。會自動翻頁。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
require_cmd jq column
|
||
|
||
FILTER=""
|
||
FORMAT="table"
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: list.sh [--private|--public] [--format table|tsv|name]
|
||
|
||
--private 只列出 private repo
|
||
--public 只列出 public repo
|
||
--format FORMAT table (default) | tsv | name
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--private) FILTER="private"; shift ;;
|
||
--public) FILTER="public"; shift ;;
|
||
--format)
|
||
[[ $# -ge 2 ]] || { log_err "--format 需要一個值"; exit 1; }
|
||
FORMAT="$2"; shift 2 ;;
|
||
-h|--help) usage ;;
|
||
*) log_err "未知參數:$1"; usage ;;
|
||
esac
|
||
done
|
||
|
||
case "$FORMAT" in
|
||
table|tsv|name) ;;
|
||
*) log_err "--format 只能是 table / tsv / name"; exit 1 ;;
|
||
esac
|
||
|
||
PAGE=1
|
||
LIMIT=50
|
||
ALL_ITEMS="$(mktemp)"
|
||
trap 'rm -f "$ALL_ITEMS"' EXIT
|
||
|
||
while :; do
|
||
gitea_api GET "/user/repos?limit=${LIMIT}&page=${PAGE}"
|
||
if [[ "$HTTP_CODE" != "200" ]]; then
|
||
log_err "GET /user/repos 失敗 (HTTP ${HTTP_CODE})"
|
||
cat "$GITEA_API_RESP_FILE" >&2
|
||
exit 1
|
||
fi
|
||
jq -c '.[]' < "$GITEA_API_RESP_FILE" >> "$ALL_ITEMS"
|
||
COUNT="$(jq 'length' < "$GITEA_API_RESP_FILE")"
|
||
[[ "$COUNT" -lt "$LIMIT" ]] && break
|
||
PAGE=$((PAGE + 1))
|
||
done
|
||
|
||
filter_jq="."
|
||
case "$FILTER" in
|
||
private) filter_jq='select(.private == true)' ;;
|
||
public) filter_jq='select(.private == false)' ;;
|
||
esac
|
||
|
||
TOTAL="$(jq -s "map($filter_jq) | length" < "$ALL_ITEMS")"
|
||
log_info "抓到 ${TOTAL} 個 repo"
|
||
|
||
case "$FORMAT" in
|
||
name)
|
||
jq -r "$filter_jq | .name" < "$ALL_ITEMS"
|
||
;;
|
||
tsv)
|
||
jq -r "$filter_jq | [.name, (if .private then \"private\" else \"public\" end), (.description // \"\")] | @tsv" < "$ALL_ITEMS"
|
||
;;
|
||
table)
|
||
{
|
||
printf 'NAME\tVISIBILITY\tDESCRIPTION\n'
|
||
jq -r "$filter_jq | [.name, (if .private then \"private\" else \"public\" end), (.description // \"\")] | @tsv" < "$ALL_ITEMS"
|
||
} | column -t -s $'\t'
|
||
;;
|
||
esac
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/list.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test(參數錯誤路徑)**
|
||
|
||
Run: `bash scripts/list.sh --format bogus 2>&1 | grep -q "--format 只能是" && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
Run: `bash scripts/list.sh --format name | head -5`
|
||
Expected: 印出 ≤5 個 repo 名稱(若帳號沒有任何 repo 就空白,也算成功)
|
||
|
||
Run: `bash scripts/list.sh --format tsv | head -3`
|
||
Expected: 每行三欄用 tab 分隔
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/list.sh
|
||
git commit -m "feat: add list.sh to list repos with filter & format options"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5:建立 `scripts/clone.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/clone.sh`
|
||
|
||
**Context:** clone 指定 repo 到本地,預設目標資料夾為 `./<repo>`,可指定替代路徑;clone 完成後 origin 仍是乾淨 URL。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: clone.sh <repo> [target-dir]
|
||
|
||
<repo> GITEA_USER 名下的 repo 名稱
|
||
[target-dir] clone 目的地(預設 ./<repo>)
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 && $# -le 2 ]] || usage
|
||
case "$1" in
|
||
-h|--help) usage ;;
|
||
esac
|
||
|
||
REPO="$1"
|
||
TARGET="${2:-./${REPO}}"
|
||
|
||
if [[ -e "$TARGET" ]]; then
|
||
# 空目錄 OK;其他情況拒絕
|
||
if [[ -d "$TARGET" ]]; then
|
||
if [[ -n "$(ls -A "$TARGET")" ]]; then
|
||
log_err "目標目錄已存在且非空:${TARGET}"
|
||
exit 1
|
||
fi
|
||
else
|
||
log_err "目標路徑已存在且不是目錄:${TARGET}"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
CLEAN_URL="${GITEA_URL}/${GITEA_USER}/${REPO}.git"
|
||
|
||
log_info "clone ${GITEA_USER}/${REPO} → ${TARGET}"
|
||
git_clone_with_token "$CLEAN_URL" "$TARGET"
|
||
|
||
ABS="$(cd "$TARGET" && pwd)"
|
||
log_done "完成"
|
||
echo "$ABS"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/clone.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test(參數錯誤)**
|
||
|
||
Run: `bash scripts/clone.sh 2>&1 | grep -q 'Usage: clone.sh' && echo OK`
|
||
Expected: `OK`
|
||
|
||
Run: `mkdir -p /tmp/ccc && echo x > /tmp/ccc/y; bash scripts/clone.sh anyname /tmp/ccc 2>&1 | grep -q '目標目錄已存在且非空' && echo OK; rm -rf /tmp/ccc`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
先用 `list.sh --format name | head -1` 取一個自己的 repo 名稱 `<R>`:
|
||
```bash
|
||
R="$(bash scripts/list.sh --format name | head -1)"
|
||
DST="$(mktemp -d)/${R}"
|
||
ABS="$(bash scripts/clone.sh "$R" "$DST")"
|
||
echo "abs: $ABS"
|
||
git -C "$DST" remote get-url origin | grep -q "${GITEA_TOKEN:-NEVER}" && echo LEAK || echo "no token leak"
|
||
rm -rf "$(dirname "$DST")"
|
||
```
|
||
Expected: `abs: ...`;`no token leak`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/clone.sh
|
||
git commit -m "feat: add clone.sh that clones without leaking token to origin"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6:建立 `scripts/rename.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/rename.sh`
|
||
|
||
**Context:** 改 repo 名稱;若 cwd 的 origin 正好指向舊 repo,順便 `set-url` 到新位置。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: rename.sh <old-name> <new-name>
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -eq 2 ]] || usage
|
||
|
||
OLD="$1"
|
||
NEW="$2"
|
||
|
||
log_info "改名 ${GITEA_USER}/${OLD} → ${GITEA_USER}/${NEW}"
|
||
gitea_api PATCH "/repos/${GITEA_USER}/${OLD}" "{\"name\":\"${NEW}\"}"
|
||
|
||
case "$HTTP_CODE" in
|
||
200|201) log_ok "改名完成" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${OLD}"; exit 1 ;;
|
||
422) log_err "新名稱可能已被使用或無效"; cat "$GITEA_API_RESP_FILE" >&2; echo "" >&2; exit 1 ;;
|
||
*)
|
||
log_err "改名失敗 (HTTP ${HTTP_CODE})"
|
||
{ cat "$GITEA_API_RESP_FILE"; echo ""; } >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# 若 cwd 的 origin 指到舊 repo,自動更新
|
||
if CURR="$(infer_repo_from_cwd 2>/dev/null)"; then
|
||
if [[ "$CURR" == "${GITEA_USER}/${OLD}" ]]; then
|
||
NEW_CLEAN_URL="${GITEA_URL}/${GITEA_USER}/${NEW}.git"
|
||
git remote set-url origin "$NEW_CLEAN_URL"
|
||
log_ok "cwd 的 origin 已更新為 ${NEW_CLEAN_URL}"
|
||
fi
|
||
fi
|
||
|
||
NEW_WEB_URL="${GITEA_URL}/${GITEA_USER}/${NEW}"
|
||
log_done "完成"
|
||
echo "$NEW_WEB_URL"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/rename.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test**
|
||
|
||
Run: `bash scripts/rename.sh 2>&1 | grep -q 'Usage: rename.sh' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
先用 `publish.sh` 在 tmp 目錄建一個測試 repo,再改名回來:
|
||
```bash
|
||
D="$(mktemp -d)"; cd "$D"; echo a > a.txt
|
||
NAME="gcli-rename-test-$(date +%s)"
|
||
bash /path/to/gitea-cli/scripts/publish.sh "$NAME" private >/dev/null
|
||
bash /path/to/gitea-cli/scripts/rename.sh "$NAME" "${NAME}-renamed"
|
||
git remote get-url origin # 應是 ...-renamed.git
|
||
bash /path/to/gitea-cli/scripts/rename.sh "${NAME}-renamed" "${NAME}-final"
|
||
cd / && rm -rf "$D"
|
||
# 清 gitea 側測試 repo:等 delete.sh 做完後再清,或手動從 Gitea UI 刪
|
||
```
|
||
Expected: stdout 為新的 web URL;origin remote 被順手更新到 `-renamed.git`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/rename.sh
|
||
git commit -m "feat: add rename.sh that also updates local origin if applicable"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7:建立 `scripts/visibility.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/visibility.sh`
|
||
|
||
**Context:** 切換 repo public / private。API 就是 `PATCH {"private": bool}`。不需要二次確認(非破壞性)。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: visibility.sh <repo> <public|private>
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -eq 2 ]] || usage
|
||
|
||
REPO="$1"
|
||
VIS="$2"
|
||
|
||
case "$VIS" in
|
||
public) PRIVATE=false ;;
|
||
private) PRIVATE=true ;;
|
||
*) log_err "可見性只能是 public 或 private"; exit 1 ;;
|
||
esac
|
||
|
||
log_info "切換 ${GITEA_USER}/${REPO} → ${VIS}"
|
||
gitea_api PATCH "/repos/${GITEA_USER}/${REPO}" "{\"private\":${PRIVATE}}"
|
||
|
||
case "$HTTP_CODE" in
|
||
200|201) log_ok "已更新" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${REPO}"; exit 1 ;;
|
||
*)
|
||
log_err "更新失敗 (HTTP ${HTTP_CODE})"
|
||
{ cat "$GITEA_API_RESP_FILE"; echo ""; } >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
log_done "完成"
|
||
echo "${GITEA_URL}/${GITEA_USER}/${REPO}"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/visibility.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test**
|
||
|
||
Run: `bash scripts/visibility.sh foo bogus 2>&1 | grep -q '可見性只能是' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
用 Task 6 的測試 repo 切一輪 public/private 回去。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/visibility.sh
|
||
git commit -m "feat: add visibility.sh to toggle public/private"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8:建立 `scripts/meta.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/meta.sh`
|
||
|
||
**Context:** 一支腳本處理 description 與 topics。Topics API 是獨立 endpoint 且是覆蓋式,要注意順序:先打 description(若指定),再打 topics(若指定)。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
require_cmd jq
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: meta.sh <repo> [--desc "description"] [--topics "a,b,c"]
|
||
|
||
--desc 設定 repo description
|
||
--topics 覆蓋式設定 topic list(逗號分隔)
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 ]] || usage
|
||
|
||
REPO=""
|
||
DESC=""
|
||
HAS_DESC=0
|
||
TOPICS_CSV=""
|
||
HAS_TOPICS=0
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--desc)
|
||
[[ $# -ge 2 ]] || { log_err "--desc 需要一個值"; exit 1; }
|
||
DESC="$2"; HAS_DESC=1; shift 2 ;;
|
||
--topics)
|
||
[[ $# -ge 2 ]] || { log_err "--topics 需要一個值"; exit 1; }
|
||
TOPICS_CSV="$2"; HAS_TOPICS=1; shift 2 ;;
|
||
-h|--help) usage ;;
|
||
-*) log_err "未知參數:$1"; usage ;;
|
||
*)
|
||
if [[ -z "$REPO" ]]; then REPO="$1"; else log_err "多餘參數:$1"; usage; fi
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
[[ -n "$REPO" ]] || usage
|
||
[[ $HAS_DESC -eq 1 || $HAS_TOPICS -eq 1 ]] || { log_err "必須至少提供 --desc 或 --topics"; usage; }
|
||
|
||
if [[ $HAS_DESC -eq 1 ]]; then
|
||
ESCAPED_DESC="$(printf '%s' "$DESC" | jq -Rs .)"
|
||
log_info "更新 description"
|
||
gitea_api PATCH "/repos/${GITEA_USER}/${REPO}" "{\"description\":${ESCAPED_DESC}}"
|
||
case "$HTTP_CODE" in
|
||
200|201) log_ok "description 已更新" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${REPO}"; exit 1 ;;
|
||
*) log_err "更新 description 失敗 (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
fi
|
||
|
||
if [[ $HAS_TOPICS -eq 1 ]]; then
|
||
TOPICS_JSON="$(printf '%s' "$TOPICS_CSV" | jq -R 'split(",") | map(select(length > 0))')"
|
||
log_info "覆蓋 topics:${TOPICS_JSON}"
|
||
gitea_api PUT "/repos/${GITEA_USER}/${REPO}/topics" "{\"topics\":${TOPICS_JSON}}"
|
||
case "$HTTP_CODE" in
|
||
200|204) log_ok "topics 已更新" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${REPO}"; exit 1 ;;
|
||
422) log_err "topics 格式錯誤"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
*) log_err "更新 topics 失敗 (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
fi
|
||
|
||
log_done "完成"
|
||
echo "${GITEA_URL}/${GITEA_USER}/${REPO}"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/meta.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test**
|
||
|
||
Run: `bash scripts/meta.sh 2>&1 | grep -q 'Usage: meta.sh' && echo OK`
|
||
Expected: `OK`
|
||
|
||
Run: `bash scripts/meta.sh foo 2>&1 | grep -q '至少提供' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
```bash
|
||
bash scripts/meta.sh <your-test-repo> --desc "smoke test $(date)" --topics "test,cli,smoke"
|
||
```
|
||
Expected: 兩個 API 都成功;上 Gitea UI 可看到。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/meta.sh
|
||
git commit -m "feat: add meta.sh for description and topics"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9:建立 `scripts/archive.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/archive.sh`
|
||
|
||
**Context:** archive / unarchive。破壞性中等(可逆但影響閱讀/push);預設需要二次確認,可用 `-y` 或 `GITEA_YES=1` 略過。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: archive.sh <repo> [--unarchive] [-y|--yes]
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 ]] || usage
|
||
|
||
REPO=""
|
||
ACTION="archive"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--unarchive) ACTION="unarchive"; shift ;;
|
||
-y|--yes) GITEA_YES=1; shift ;;
|
||
-h|--help) usage ;;
|
||
-*) log_err "未知參數:$1"; usage ;;
|
||
*)
|
||
if [[ -z "$REPO" ]]; then REPO="$1"; else log_err "多餘參數:$1"; usage; fi
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
[[ -n "$REPO" ]] || usage
|
||
|
||
if [[ "$ACTION" == "archive" ]]; then
|
||
confirm_destructive "確定要 archive ${GITEA_USER}/${REPO}?"
|
||
BODY='{"archived":true}'
|
||
MSG="archived"
|
||
else
|
||
confirm_destructive "確定要 unarchive ${GITEA_USER}/${REPO}?"
|
||
BODY='{"archived":false}'
|
||
MSG="unarchived"
|
||
fi
|
||
|
||
log_info "${ACTION} ${GITEA_USER}/${REPO}"
|
||
gitea_api PATCH "/repos/${GITEA_USER}/${REPO}" "$BODY"
|
||
|
||
case "$HTTP_CODE" in
|
||
200|201) log_ok "$MSG" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${REPO}"; exit 1 ;;
|
||
*) log_err "${ACTION} 失敗 (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
|
||
log_done "完成"
|
||
echo "${GITEA_URL}/${GITEA_USER}/${REPO}"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/archive.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test(確認機制)**
|
||
|
||
Run: `echo n | GITEA_YES=0 bash scripts/archive.sh some-repo 2>&1 | grep -q '取消' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
```bash
|
||
GITEA_YES=1 bash scripts/archive.sh <your-test-repo>
|
||
GITEA_YES=1 bash scripts/archive.sh <your-test-repo> --unarchive
|
||
```
|
||
Expected: 兩次都回 `archived` / `unarchived`。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/archive.sh
|
||
git commit -m "feat: add archive.sh with confirmation / -y bypass"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10:建立 `scripts/delete.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/delete.sh`
|
||
|
||
**Context:** 最高破壞性:互動模式必須**重新輸入完整 repo 名稱**才執行;`-y` / `GITEA_YES=1` 可略過。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: delete.sh <repo> [-y|--yes]
|
||
|
||
互動模式:會要求重新輸入完整 repo 名稱才執行。
|
||
-y / --yes 或 GITEA_YES=1:直接執行,不需輸入。
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 ]] || usage
|
||
|
||
REPO=""
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-y|--yes) GITEA_YES=1; shift ;;
|
||
-h|--help) usage ;;
|
||
-*) log_err "未知參數:$1"; usage ;;
|
||
*)
|
||
if [[ -z "$REPO" ]]; then REPO="$1"; else log_err "多餘參數:$1"; usage; fi
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
[[ -n "$REPO" ]] || usage
|
||
|
||
log_warn "即將刪除 ${GITEA_USER}/${REPO}(此操作不可復原)"
|
||
confirm_exact_match "$REPO"
|
||
|
||
log_info "刪除 ${GITEA_USER}/${REPO}"
|
||
gitea_api DELETE "/repos/${GITEA_USER}/${REPO}"
|
||
|
||
case "$HTTP_CODE" in
|
||
204) log_ok "已刪除" ;;
|
||
404) log_err "找不到 repo:${GITEA_USER}/${REPO}"; exit 1 ;;
|
||
*) log_err "刪除失敗 (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
|
||
log_done "完成"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/delete.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test(輸入錯誤名稱應被拒)**
|
||
|
||
Run: `echo "wrong-name" | bash scripts/delete.sh real-name </dev/tty 2>&1 || true`
|
||
|
||
> 注意:此測試因 `< /dev/tty` 需要真實 TTY,建議手動跑而非自動化。改用 GITEA_YES=0 的環境測試:
|
||
|
||
Run: `GITEA_YES=0 bash scripts/delete.sh some-repo </dev/null 2>&1 | grep -q '需要確認但無 TTY' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
用 Task 6 建的測試 repo:
|
||
```bash
|
||
# 互動模式(需手動輸入)
|
||
bash scripts/delete.sh <your-test-repo>
|
||
# 或一鍵:
|
||
GITEA_YES=1 bash scripts/delete.sh <your-test-repo>
|
||
```
|
||
Expected: 回 `已刪除`;Gitea UI 上 repo 消失。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/delete.sh
|
||
git commit -m "feat: add delete.sh with exact-name confirmation"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11:建立 `scripts/tag.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/tag.sh`
|
||
|
||
**Context:** 純 local git tag + push。不呼叫 Gitea API。若在 cwd 的 git repo 裡跑,會推該 repo 的 origin;origin 必須已經設好(由之前的 publish/clone)。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: tag.sh <tag> [-m "message"]
|
||
|
||
建立 annotated tag 並推到 origin。
|
||
需要在 cwd 已有 git repo 且 origin 已指到 Gitea。
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 ]] || usage
|
||
|
||
TAG=""
|
||
MSG=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-m) [[ $# -ge 2 ]] || { log_err "-m 需要一個值"; exit 1; }; MSG="$2"; shift 2 ;;
|
||
-h|--help) usage ;;
|
||
-*) log_err "未知參數:$1"; usage ;;
|
||
*)
|
||
if [[ -z "$TAG" ]]; then TAG="$1"; else log_err "多餘參數:$1"; usage; fi
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
[[ -n "$TAG" ]] || usage
|
||
MSG="${MSG:-Tag $TAG}"
|
||
|
||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||
log_err "cwd 不是 git repo"
|
||
exit 1
|
||
fi
|
||
|
||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||
log_err "tag 已存在:${TAG}"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "建立 tag ${TAG}"
|
||
git tag -a "$TAG" -m "$MSG"
|
||
|
||
log_info "推送 tag 到 origin"
|
||
git_push_tag_with_token "$TAG"
|
||
|
||
log_done "完成"
|
||
echo "$TAG"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/tag.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test**
|
||
|
||
Run: `cd /tmp && bash /path/to/gitea-cli/scripts/tag.sh v0 2>&1 | grep -q '不是 git repo' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
在已 publish 過的 tmp repo 裡:
|
||
```bash
|
||
cd <your-published-test-repo>
|
||
bash /path/to/gitea-cli/scripts/tag.sh "test-$(date +%s)" -m "smoke"
|
||
```
|
||
Expected: tag 推上去;`grep -q $GITEA_TOKEN .git/config` 為空(`echo $?` 為 1 表示沒找到)。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/tag.sh
|
||
git commit -m "feat: add tag.sh for annotated local tag + push"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12:建立 `scripts/release.sh`
|
||
|
||
**Files:**
|
||
- Create: `scripts/release.sh`
|
||
|
||
**Context:** 兩段 API:先建 release、再逐個上傳 asset。asset 失敗不中止其他 asset,但最終 exit 非零並報告。Repo 預設從 cwd origin 推斷,可用 `--repo` override。
|
||
|
||
- [ ] **Step 1: 建立檔案**
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
require_cmd jq
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: release.sh <tag> [--name NAME] [--notes TEXT] [--target BRANCH]
|
||
[--draft] [--prerelease] [--asset PATH]... [--repo NAME]
|
||
|
||
<tag> tag_name;若 tag 不存在 Gitea 會在 target 自動建
|
||
--name release 標題(預設同 tag)
|
||
--notes release 說明
|
||
--target target_commitish;預設 HEAD 所在分支
|
||
--draft 建成 draft
|
||
--prerelease 標記 prerelease
|
||
--asset PATH 要上傳的附件(可重複)
|
||
--repo NAME 指定 repo;省略則從 cwd origin 推斷
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
[[ $# -ge 1 ]] || usage
|
||
|
||
TAG=""
|
||
NAME=""
|
||
NOTES=""
|
||
TARGET=""
|
||
DRAFT=false
|
||
PRERELEASE=false
|
||
ASSETS=()
|
||
REPO_OVERRIDE=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--name) [[ $# -ge 2 ]] || { log_err "--name 需要值"; exit 1; }; NAME="$2"; shift 2 ;;
|
||
--notes) [[ $# -ge 2 ]] || { log_err "--notes 需要值"; exit 1; }; NOTES="$2"; shift 2 ;;
|
||
--target) [[ $# -ge 2 ]] || { log_err "--target 需要值"; exit 1; }; TARGET="$2"; shift 2 ;;
|
||
--draft) DRAFT=true; shift ;;
|
||
--prerelease) PRERELEASE=true; shift ;;
|
||
--asset) [[ $# -ge 2 ]] || { log_err "--asset 需要值"; exit 1; }; ASSETS+=("$2"); shift 2 ;;
|
||
--repo) [[ $# -ge 2 ]] || { log_err "--repo 需要值"; exit 1; }; REPO_OVERRIDE="$2"; shift 2 ;;
|
||
-h|--help) usage ;;
|
||
-*) log_err "未知參數:$1"; usage ;;
|
||
*)
|
||
if [[ -z "$TAG" ]]; then TAG="$1"; else log_err "多餘參數:$1"; usage; fi
|
||
shift ;;
|
||
esac
|
||
done
|
||
|
||
[[ -n "$TAG" ]] || usage
|
||
NAME="${NAME:-$TAG}"
|
||
|
||
if [[ -n "$REPO_OVERRIDE" ]]; then
|
||
OWNER_REPO="${GITEA_USER}/${REPO_OVERRIDE}"
|
||
else
|
||
if ! OWNER_REPO="$(infer_repo_from_cwd)"; then
|
||
log_err "無法從 cwd 推斷 repo;請加 --repo"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
if [[ -z "$TARGET" ]]; then
|
||
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||
TARGET="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
||
fi
|
||
fi
|
||
|
||
# 驗證 asset 路徑
|
||
for a in "${ASSETS[@]}"; do
|
||
if [[ ! -f "$a" ]]; then
|
||
log_err "找不到 asset 檔案:$a"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
# 組 release body
|
||
BODY="$(jq -n \
|
||
--arg tag "$TAG" \
|
||
--arg name "$NAME" \
|
||
--arg body "$NOTES" \
|
||
--arg target "$TARGET" \
|
||
--argjson draft "$DRAFT" \
|
||
--argjson prerelease "$PRERELEASE" \
|
||
'{tag_name:$tag, name:$name, body:$body, draft:$draft, prerelease:$prerelease}
|
||
+ (if $target == "" then {} else {target_commitish:$target} end)')"
|
||
|
||
log_info "建立 release ${OWNER_REPO} / ${TAG}"
|
||
gitea_api POST "/repos/${OWNER_REPO}/releases" "$BODY"
|
||
|
||
case "$HTTP_CODE" in
|
||
201) log_ok "release 已建立" ;;
|
||
404) log_err "找不到 repo:${OWNER_REPO}"; exit 1 ;;
|
||
*) log_err "建立 release 失敗 (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
|
||
RELEASE_ID="$(jq -r '.id' < "$GITEA_API_RESP_FILE")"
|
||
RELEASE_URL="$(jq -r '.html_url' < "$GITEA_API_RESP_FILE")"
|
||
|
||
# stdout 先印 release URL
|
||
echo "$RELEASE_URL"
|
||
|
||
# 上傳 asset
|
||
FAILED=()
|
||
for a in "${ASSETS[@]}"; do
|
||
local_name="$(basename "$a")"
|
||
log_info "上傳 asset:${local_name}"
|
||
UP_RESP="$(mktemp)"
|
||
UP_CODE="$(curl -sS -o "$UP_RESP" -w '%{http_code}' \
|
||
-X POST \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
-F "attachment=@${a};filename=${local_name}" \
|
||
"${GITEA_URL}/api/v1/repos/${OWNER_REPO}/releases/${RELEASE_ID}/assets?name=${local_name}")"
|
||
case "$UP_CODE" in
|
||
201)
|
||
ASSET_URL="$(jq -r '.browser_download_url' < "$UP_RESP")"
|
||
log_ok "$local_name"
|
||
echo "$ASSET_URL"
|
||
;;
|
||
*)
|
||
log_err "上傳 ${local_name} 失敗 (HTTP ${UP_CODE})"
|
||
cat "$UP_RESP" >&2
|
||
echo "" >&2
|
||
FAILED+=("$local_name")
|
||
;;
|
||
esac
|
||
rm -f "$UP_RESP"
|
||
done
|
||
|
||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||
log_err "部分 asset 上傳失敗:${FAILED[*]}"
|
||
exit 1
|
||
fi
|
||
|
||
log_done "完成"
|
||
```
|
||
|
||
- [ ] **Step 2: 語法檢查**
|
||
|
||
Run: `bash -n scripts/release.sh`
|
||
Expected: 無輸出
|
||
|
||
- [ ] **Step 3: 離線 smoke test**
|
||
|
||
Run: `bash scripts/release.sh 2>&1 | grep -q 'Usage: release.sh' && echo OK`
|
||
Expected: `OK`
|
||
|
||
Run: `cd /tmp && bash /path/to/gitea-cli/scripts/release.sh v0 2>&1 | grep -q '無法從 cwd 推斷 repo' && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 4: Live smoke test**
|
||
|
||
```bash
|
||
cd <your-published-test-repo>
|
||
echo "hello" > /tmp/asset1.txt
|
||
bash /path/to/gitea-cli/scripts/release.sh "v0.1.0-$(date +%s)" \
|
||
--name "Smoke test" --notes "hi" \
|
||
--asset /tmp/asset1.txt
|
||
```
|
||
Expected: stdout 依序為 release URL 與 asset URL;Gitea UI 上看得到 release 與 asset。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add scripts/release.sh
|
||
git commit -m "feat: add release.sh for release creation with asset upload"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13:更新 docs(SKILL.md / README.md / CLAUDE.md)
|
||
|
||
**Files:**
|
||
- Modify: `SKILL.md`
|
||
- Modify: `README.md`
|
||
- Modify: `CLAUDE.md`
|
||
|
||
**Context:** 把 docs 對齊擴充後的內容:skill description 擴寫、README 新增腳本總覽、CLAUDE.md 補共用 lib 的邊界說明與新腳本骨架模板。
|
||
|
||
- [ ] **Step 1: 更新 `SKILL.md`**
|
||
|
||
把整個檔案內容覆寫為:
|
||
|
||
```markdown
|
||
---
|
||
name: gitea-cli
|
||
description: 對自架 Gitea 做常見操作:建 repo 並推送、列出 repo、clone、改名、切換公開私有、archive、刪除、設定 description/topics、建 release 並上傳附件、打 tag。當使用者提到 Gitea 相關的 push / clone / list / rename / release / archive / delete / tag 等需求時使用。需先在 config.env 設定 Gitea URL、使用者名稱、access token。
|
||
---
|
||
|
||
# gitea-cli
|
||
|
||
把自架 Gitea 常見操作封裝成 bash 腳本。Token 絕不落地到 `.git/config`。
|
||
|
||
## 使用前準備(只需做一次)
|
||
|
||
1. 到自己的 Gitea 產生 access token(權限至少 `write:repository`)
|
||
2. 在本 skill 目錄下:
|
||
```bash
|
||
cp config.example.env config.env
|
||
# 編輯 config.env 填入 GITEA_URL / GITEA_USER / GITEA_TOKEN
|
||
```
|
||
3. `config.env` 已被 `.gitignore` 排除。
|
||
|
||
## 常見情境 → 對應腳本
|
||
|
||
`<skill-path>` 代表本 skill 所在路徑(clone 下來的位置或 `~/.claude/skills/gitea-cli` symlink)。
|
||
|
||
| 要做什麼 | 指令 |
|
||
|---|---|
|
||
| 建 repo 並把目前專案推上去 | `bash <skill-path>/scripts/publish.sh [repo] [public\|private] [-m "msg"]` |
|
||
| 列出自己所有 repo | `bash <skill-path>/scripts/list.sh [--private\|--public] [--format table\|tsv\|name]` |
|
||
| clone 某個 repo 到本地 | `bash <skill-path>/scripts/clone.sh <repo> [target-dir]` |
|
||
| 改 repo 名稱 | `bash <skill-path>/scripts/rename.sh <old> <new>` |
|
||
| 切換 public/private | `bash <skill-path>/scripts/visibility.sh <repo> <public\|private>` |
|
||
| 設定 description / topics | `bash <skill-path>/scripts/meta.sh <repo> [--desc "..."] [--topics "a,b,c"]` |
|
||
| archive / unarchive | `bash <skill-path>/scripts/archive.sh <repo> [--unarchive] [-y]` |
|
||
| 刪除 repo | `bash <skill-path>/scripts/delete.sh <repo> [-y]` |
|
||
| 打 tag 並推上去 | `bash <skill-path>/scripts/tag.sh <tag> [-m "msg"]` |
|
||
| 建 release(含 asset 上傳) | `bash <skill-path>/scripts/release.sh <tag> [--name ...] [--notes ...] [--asset PATH]... [--repo NAME]` |
|
||
|
||
## 注意
|
||
|
||
- `config.env` 絕對不要 commit
|
||
- `origin` remote 永遠是乾淨 URL;token 只在 push/clone 當下透過一次性 credential helper 注入
|
||
- 破壞性操作(`archive.sh` / `delete.sh`)預設需要互動確認;自動化場景請帶 `-y` / `--yes` 或設 `GITEA_YES=1`
|
||
- 401/403 → 檢查 `config.env` 的 token
|
||
|
||
## 回覆原則
|
||
|
||
- 執行前先確認 cwd 就是要操作的目標
|
||
- 執行後簡述結果(URL / branch / 刪除/建立的項目)
|
||
```
|
||
|
||
- [ ] **Step 2: 更新 `README.md`**
|
||
|
||
在檔案最後加上「腳本總覽」表格。找到目前 README 的「使用方式」段落(講 `publish.sh` 的那段),在其後插入:
|
||
|
||
```markdown
|
||
## 所有可用腳本
|
||
|
||
| 腳本 | 用法 |
|
||
|---|---|
|
||
| `publish.sh` | `publish.sh [repo] [public\|private] [-m "msg"]` — 建 repo + 推目前目錄 |
|
||
| `list.sh` | `list.sh [--private\|--public] [--format table\|tsv\|name]` — 列出自己的 repo |
|
||
| `clone.sh` | `clone.sh <repo> [target-dir]` — clone 到本地,origin 保持乾淨 URL |
|
||
| `rename.sh` | `rename.sh <old> <new>` — 改名;若 cwd origin 指到舊 repo 自動更新 |
|
||
| `visibility.sh` | `visibility.sh <repo> <public\|private>` — 切換可見性 |
|
||
| `meta.sh` | `meta.sh <repo> [--desc ...] [--topics a,b,c]` — 設定 description / topics |
|
||
| `archive.sh` | `archive.sh <repo> [--unarchive] [-y]` — archive / unarchive |
|
||
| `delete.sh` | `delete.sh <repo> [-y]` — 刪除(互動模式需重新輸入 repo 名) |
|
||
| `tag.sh` | `tag.sh <tag> [-m "msg"]` — 建 annotated tag 並 push origin |
|
||
| `release.sh` | `release.sh <tag> [--name ...] [--notes ...] [--asset PATH]... [--repo NAME]` — 建 release + 上傳 asset |
|
||
|
||
所有呼叫 API 的腳本共用 `scripts/lib/common.sh`,該 lib 負責 config 載入、API 呼叫、一次性 credential helper 注入、互動確認。
|
||
```
|
||
|
||
並在「安全設計」段落(`## 安全設計`)末端加一句:
|
||
|
||
```markdown
|
||
- `clone.sh` 完成後 origin 也是乾淨 URL,再次 `git pull` / `git push` 要重新帶 credential(跟 `publish.sh` 一致)。
|
||
```
|
||
|
||
- [ ] **Step 3: 更新 `CLAUDE.md`**
|
||
|
||
把整個檔案替換為:
|
||
|
||
```markdown
|
||
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## 專案性質
|
||
|
||
這是一個 **Claude Code skill**(根目錄的 `SKILL.md` 就是 skill 定義檔)。本身不是一般應用程式,產物是一組 bash 腳本(`scripts/*.sh`),針對自架 Gitea 做常見操作。`publish.sh` 是最早的腳本,其他腳本是逐步擴充而來。
|
||
|
||
## 常用指令
|
||
|
||
在本 repo 裡沒有測試/build/lint;開發流程就是:改 `scripts/*.sh` → `bash -n` 語法檢查 → 在另一個 tmp 目錄實際跑一次看看。所有腳本共用 `scripts/lib/common.sh`。
|
||
|
||
實際使用腳本時(在任何專案目錄下):
|
||
|
||
```bash
|
||
bash /path/to/gitea-cli/scripts/publish.sh [repo] [public|private] [-m "msg"]
|
||
bash /path/to/gitea-cli/scripts/list.sh [--private|--public] [--format table|tsv|name]
|
||
# ... 其他腳本見 SKILL.md / README.md
|
||
```
|
||
|
||
首次使用前要 `cp config.example.env config.env` 並填入 `GITEA_URL` / `GITEA_USER` / `GITEA_TOKEN`。
|
||
|
||
## 架構與關鍵不變量
|
||
|
||
### 安全模型:token 絕不落地
|
||
|
||
**修改任何腳本時都要守住**:
|
||
|
||
1. `origin` remote 永遠是**乾淨 URL**,不含 token
|
||
2. Token 只能出現在 `config.env`、呼叫過程的環境變數、以及 `scripts/lib/common.sh` 裡的 `gitea_api` / `git_*_with_token` 函式內
|
||
3. Token 絕不進入 `.git/config`、remote URL、任何 log(stdout/stderr)
|
||
4. `config.env` 被 `.gitignore` 排除
|
||
|
||
### 共用 lib `scripts/lib/common.sh`
|
||
|
||
所有 API / push / clone 都走 lib,腳本本身不寫 curl 也不直接操作 credential helper。lib 提供:
|
||
|
||
- `load_config`:讀 `config.env`,檢查三個變數都非空非預設
|
||
- `gitea_api <METHOD> <path> [json-body]`:單一 API 呼叫入口;回傳後讀 `$HTTP_CODE` 與 `$GITEA_API_RESP_FILE`
|
||
- `git_push_with_token <branch>` / `git_push_tag_with_token <tag>` / `git_clone_with_token <clean-url> <target>`:一次性帶 token 的 git 操作
|
||
- `infer_repo_from_cwd`:從 `git remote get-url origin` 推斷 `owner/repo`
|
||
- `confirm_destructive <prompt>` / `confirm_exact_match <expected>`:破壞性操作互動;`GITEA_YES=1` 或腳本處理的 `-y` 可略過
|
||
- `log_info` / `log_ok` / `log_warn` / `log_err` / `log_done`:統一前綴、**全部寫 stderr**
|
||
|
||
**什麼不放進 lib**(刻意留在各腳本):
|
||
- 各腳本自己的 arg 解析(每支不同)
|
||
- JSON body 組字串(放在呼叫處最直觀;複雜的用 jq 在呼叫端組)
|
||
- `publish.sh` 的三種 git 狀態分支(publish 獨有)
|
||
|
||
### stdout / stderr 分流
|
||
|
||
- **stderr**:`log_*` 進度訊息、錯誤
|
||
- **stdout**:最終結構化輸出
|
||
- `publish.sh` / `rename.sh` / `visibility.sh` / `archive.sh` / `meta.sh` → repo web URL
|
||
- `list.sh` → repo 清單
|
||
- `clone.sh` → clone 到的絕對路徑
|
||
- `release.sh` → release URL + 每個 asset URL
|
||
- `tag.sh` → tag 名
|
||
- `delete.sh` → 空
|
||
|
||
使用者可以 `URL=$(publish.sh)` 拿結果。
|
||
|
||
### 新腳本骨架(照抄)
|
||
|
||
寫新腳本時以這個為起點:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
# shellcheck source=lib/common.sh
|
||
source "${SCRIPT_DIR}/lib/common.sh"
|
||
|
||
load_config
|
||
# require_cmd jq # 若會用到
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
Usage: <script>.sh ...
|
||
EOF
|
||
exit 1
|
||
}
|
||
|
||
# arg parsing
|
||
# ...
|
||
|
||
log_info "..."
|
||
gitea_api METHOD "/path" "$BODY"
|
||
case "$HTTP_CODE" in
|
||
2??) log_ok "..." ;;
|
||
404) log_err "..."; exit 1 ;;
|
||
*) log_err "... (HTTP ${HTTP_CODE})"; { cat "$GITEA_API_RESP_FILE"; echo ""; } >&2; exit 1 ;;
|
||
esac
|
||
|
||
log_done "完成"
|
||
echo "<結構化結果>" # stdout
|
||
```
|
||
|
||
### 相容性限制
|
||
|
||
使用 **Gitea API v1**,Forgejo 相容;GitHub / GitLab **不相容**。要擴充到其他平台要重寫 API 呼叫處。
|
||
|
||
## 寫入偏好
|
||
|
||
- 腳本訊息與使用者文件皆為繁體中文(台灣用語)
|
||
- 沿用既有風格:▶ 進行中、✓ 成功、⚠ 警告、❌ 失敗、✅ 完成
|
||
- `SKILL.md` 是給 Claude 讀的 skill metadata + 簡短說明;`README.md` 是給人看的完整文件。修改功能時兩份都要同步
|
||
- 範例路徑用通用形式(`/path/to/gitea-cli`),不要硬編碼個人資訊
|
||
```
|
||
|
||
- [ ] **Step 4: 語法 / 拼寫檢查**
|
||
|
||
Run: `grep -E '(TODO|TBD|XXX|FIXME)' SKILL.md README.md CLAUDE.md && echo FOUND || echo clean`
|
||
Expected: `clean`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add SKILL.md README.md CLAUDE.md
|
||
git commit -m "docs: update SKILL.md / README.md / CLAUDE.md for full gitea-cli"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14:最終手動驗收
|
||
|
||
**Files:** 無修改,純驗證。
|
||
|
||
**Context:** 對應 spec「測試策略」段。做完此 task 後,實作階段才算完成。
|
||
|
||
- [ ] **Step 1: 基礎檢查——所有腳本語法正確**
|
||
|
||
Run: `for f in scripts/*.sh scripts/lib/*.sh; do bash -n "$f" && echo OK: "$f" || echo FAIL: "$f"; done`
|
||
Expected: 每行都 `OK:`
|
||
|
||
- [ ] **Step 2: 既有 `publish.sh` 用法回歸測試**
|
||
|
||
在 tmp 目錄跑三種 git 狀態:
|
||
```bash
|
||
# A. 全新目錄
|
||
D="$(mktemp -d)"; cd "$D"
|
||
NA="gcli-reg-a-$(date +%s)"
|
||
bash /path/to/gitea-cli/scripts/publish.sh "$NA" private
|
||
# B. 空目錄但已有 .git(無 commit)
|
||
cd "$(mktemp -d)"; git init -b main
|
||
NB="gcli-reg-b-$(date +%s)"
|
||
bash /path/to/gitea-cli/scripts/publish.sh "$NB" private
|
||
# C. 有 commit 且有未提交變更
|
||
cd "$(mktemp -d)"; git init -b main; echo 1 > 1.txt; git add .; git commit -m "x"
|
||
echo 2 > 2.txt
|
||
NC="gcli-reg-c-$(date +%s)"
|
||
bash /path/to/gitea-cli/scripts/publish.sh "$NC" private
|
||
```
|
||
Expected: 三次都成功;Gitea UI 上有三個 repo;每次 stdout 為對應 web URL。
|
||
|
||
- [ ] **Step 3: Token 不落地驗收**
|
||
|
||
接續 Step 2 的任一個目錄:
|
||
```bash
|
||
# 應該完全找不到 token
|
||
grep -r -F "$GITEA_TOKEN" .git/ 2>/dev/null && echo LEAK || echo "no token leak"
|
||
# origin 應為乾淨 URL(不含 @ 或 token 字樣)
|
||
git remote get-url origin
|
||
```
|
||
Expected: `no token leak`;origin URL 不含 `:token@` 或 token 字串
|
||
|
||
- [ ] **Step 4: `-m` 選項驗收**
|
||
|
||
```bash
|
||
D="$(mktemp -d)"; cd "$D"; echo x > a.txt
|
||
NM="gcli-m-$(date +%s)"
|
||
bash /path/to/gitea-cli/scripts/publish.sh "$NM" private -m "custom message"
|
||
git log -1 --pretty=%s # 應為 "custom message"
|
||
```
|
||
Expected: 最新 commit message 為 `custom message`
|
||
|
||
- [ ] **Step 5: 破壞性操作確認機制**
|
||
|
||
```bash
|
||
# delete.sh 無 TTY + 無 -y 應拒絕
|
||
bash /path/to/gitea-cli/scripts/delete.sh "$NA" </dev/null 2>&1 | grep -q '需要確認但無 TTY' && echo OK
|
||
# archive.sh 同理
|
||
bash /path/to/gitea-cli/scripts/archive.sh "$NB" </dev/null 2>&1 | grep -q '需要確認但無 TTY' && echo OK
|
||
```
|
||
Expected: 兩次都 `OK`
|
||
|
||
- [ ] **Step 6: 清測試 repo**
|
||
|
||
```bash
|
||
for N in "$NA" "$NB" "$NC" "$NM"; do
|
||
GITEA_YES=1 bash /path/to/gitea-cli/scripts/delete.sh "$N"
|
||
done
|
||
```
|
||
Expected: 四次都印 `已刪除`
|
||
|
||
- [ ] **Step 7: 檢查所有 grep TODO/TBD/FIXME 類待辦**
|
||
|
||
Run: `grep -rE '(TODO|TBD|XXX|FIXME)' scripts/ SKILL.md README.md CLAUDE.md && echo FOUND || echo clean`
|
||
Expected: `clean`
|
||
|
||
- [ ] **Step 8: Commit 驗收筆記(選用)**
|
||
|
||
若上述驗收全過,可打空 commit 做里程碑:
|
||
```bash
|
||
git commit --allow-empty -m "chore: manual acceptance passed per spec test strategy"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15(選用):清 git 歷史為單一 `Initial commit`
|
||
|
||
**Files:** 整個 `.git/` 目錄
|
||
|
||
**Context:** 使用者先前表明**最終要 re-init 整個 repo**,不想要中途 commit 歷史。此 task 把整個 repo 保留為最終檔案,但歷史重置。
|
||
|
||
> ⚠ 執行前再次確認使用者要這麼做。此動作會丟失實作過程中的所有 commits。
|
||
|
||
- [ ] **Step 1: 確認工作區乾淨**
|
||
|
||
Run: `git status`
|
||
Expected: `nothing to commit, working tree clean`
|
||
|
||
- [ ] **Step 2: 刪除 `.git/` 並重新 init**
|
||
|
||
```bash
|
||
rm -rf .git
|
||
git init -b main
|
||
git config user.email "timmy@automodules.com"
|
||
git config user.name "timmy"
|
||
```
|
||
|
||
- [ ] **Step 3: 明確 stage 所有檔案(不用 git add .)**
|
||
|
||
```bash
|
||
git add .gitignore CLAUDE.md README.md SKILL.md config.example.env
|
||
git add scripts/publish.sh scripts/list.sh scripts/clone.sh scripts/rename.sh scripts/visibility.sh scripts/meta.sh scripts/archive.sh scripts/delete.sh scripts/tag.sh scripts/release.sh
|
||
git add scripts/lib/common.sh
|
||
git add docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md
|
||
git add docs/superpowers/plans/2026-04-21-gitea-cli-expansion.md
|
||
```
|
||
|
||
- [ ] **Step 4: 驗證 `config.env` 與任何 token 都沒被 stage**
|
||
|
||
```bash
|
||
git diff --cached --name-only
|
||
git diff --cached | grep -E '^\+.*GITEA_TOKEN=' | grep -v '請貼上你的_access_token' && echo TOKEN_LEAK || echo clean
|
||
```
|
||
Expected: 檔名列表不含 `config.env`;第二行印 `clean`
|
||
|
||
- [ ] **Step 5: 做單一 `Initial commit`**
|
||
|
||
```bash
|
||
git commit -m "Initial commit"
|
||
git log --oneline # 應只有一行
|
||
```
|
||
Expected: 只有一個 commit
|
||
|
||
---
|
||
|
||
## Self-Review 結果
|
||
|
||
以下對照 spec(`docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md`)做自檢:
|
||
|
||
**Spec 覆蓋:**
|
||
- 共用 lib(`common.sh`)→ Task 1 ✅
|
||
- 重構 `publish.sh` + `-m`→ Task 2 + 3 ✅
|
||
- `list.sh` ✅ Task 4
|
||
- `clone.sh` ✅ Task 5
|
||
- `rename.sh` ✅ Task 6
|
||
- `visibility.sh` ✅ Task 7
|
||
- `meta.sh` ✅ Task 8
|
||
- `archive.sh` ✅ Task 9
|
||
- `delete.sh` ✅ Task 10
|
||
- `tag.sh` ✅ Task 11
|
||
- `release.sh` ✅ Task 12
|
||
- 文件更新(SKILL / README / CLAUDE)✅ Task 13
|
||
- Spec 的「測試策略」四項驗收 ✅ Task 14
|
||
- Spec 的「非目標」—— 無相關 task(正確的缺席)
|
||
|
||
**Placeholder 掃描:**
|
||
- 無 `TODO` / `TBD` / `fill in details` / `Add appropriate error handling` 等。所有 step 都有具體 code 或指令。
|
||
|
||
**Type 一致性:**
|
||
- lib function 名稱在各 script 引用處一致(`gitea_api` / `git_push_with_token` / `git_push_tag_with_token` / `git_clone_with_token` / `infer_repo_from_cwd` / `confirm_destructive` / `confirm_exact_match` / `log_*` / `require_cmd` / `load_config`)
|
||
- 全域變數 `HTTP_CODE` / `GITEA_API_RESP_FILE` / `GITEA_URL` / `GITEA_USER` / `GITEA_TOKEN` / `CONFIG_FILE` / `GITEA_YES` 命名一致
|