Initial commit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
config.env
|
||||||
109
CLAUDE.md
Normal file
109
CLAUDE.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 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`),不要硬編碼個人資訊
|
||||||
120
README.md
Normal file
120
README.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# gitea-cli
|
||||||
|
|
||||||
|
一組 bash 腳本,把自架 Gitea 的常見操作封裝成單一指令;目前實作「建立 repo → init → commit → push」,其他功能(list / clone / rename / release / …)依設計文件擴充中。
|
||||||
|
|
||||||
|
## 檔案結構
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-cli/
|
||||||
|
├── SKILL.md # Claude Code skill 定義
|
||||||
|
├── README.md # 本檔
|
||||||
|
├── CLAUDE.md # 給 Claude 的 repo 內指引
|
||||||
|
├── config.example.env # 設定範本(可提交)
|
||||||
|
├── config.env # 實際設定(內含 token,.gitignore 排除)
|
||||||
|
├── .gitignore
|
||||||
|
├── docs/
|
||||||
|
│ └── superpowers/specs/ # 擴充設計文件
|
||||||
|
└── scripts/
|
||||||
|
└── publish.sh # 建 repo + 推送
|
||||||
|
```
|
||||||
|
|
||||||
|
> 以下範例用 `/path/to/gitea-cli` 代表本 skill 實際所在位置(你 clone 到哪裡就是哪裡);也可以 symlink 到 `~/.claude/skills/gitea-cli` 讓 Claude Code 自動識別。
|
||||||
|
|
||||||
|
## 首次設定
|
||||||
|
|
||||||
|
1. **產生 Gitea access token**
|
||||||
|
登入 Gitea → 使用者設定 → Applications → Generate New Token
|
||||||
|
URL: `<你的 Gitea URL>/-/user/settings/applications`
|
||||||
|
權限至少勾選:**write:repository**
|
||||||
|
|
||||||
|
2. **複製設定檔並填入 token**
|
||||||
|
```bash
|
||||||
|
cd /path/to/gitea-cli
|
||||||
|
cp config.example.env config.env
|
||||||
|
# 用任一編輯器編輯 config.env,填入三個變數
|
||||||
|
```
|
||||||
|
|
||||||
|
`config.env` 範例(請替換為自己的值):
|
||||||
|
```env
|
||||||
|
GITEA_URL=http://your-gitea.example.com:3000
|
||||||
|
GITEA_USER=your-username
|
||||||
|
GITEA_TOKEN=你剛才產生的_token
|
||||||
|
```
|
||||||
|
|
||||||
|
3. (選用)讓 Claude Code 自動辨識這個 skill:
|
||||||
|
```bash
|
||||||
|
ln -s /path/to/gitea-cli ~/.claude/skills/gitea-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
在要推送的專案目錄下執行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 用當前資料夾名當 repo 名,建立 public repo
|
||||||
|
bash /path/to/gitea-cli/scripts/publish.sh
|
||||||
|
|
||||||
|
# 指定 repo 名
|
||||||
|
bash /path/to/gitea-cli/scripts/publish.sh my-project
|
||||||
|
|
||||||
|
# 建立 private repo
|
||||||
|
bash /path/to/gitea-cli/scripts/publish.sh my-project private
|
||||||
|
```
|
||||||
|
|
||||||
|
| 參數 | 說明 | 預設 |
|
||||||
|
|------|------|------|
|
||||||
|
| `$1` | repo 名稱 | 當前目錄名 |
|
||||||
|
| `$2` | 可見性:`public` / `private` | `public` |
|
||||||
|
|
||||||
|
## 所有可用腳本
|
||||||
|
|
||||||
|
| 腳本 | 用法 |
|
||||||
|
|---|---|
|
||||||
|
| `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 注入、互動確認。
|
||||||
|
|
||||||
|
## 腳本執行步驟
|
||||||
|
|
||||||
|
1. 讀取 `config.env`,驗證三個必要環境變數都有值
|
||||||
|
2. 呼叫 Gitea API `POST /api/v1/user/repos` 建立 repo
|
||||||
|
- `201` 建立成功
|
||||||
|
- `409` repo 已存在,略過
|
||||||
|
- `401/403` 授權錯誤,提示檢查 token
|
||||||
|
3. 若當前目錄沒有 `.git` → `git init -b main`
|
||||||
|
4. 若還沒有任何 commit → 自動建 initial commit(含空 commit 以支援空目錄)
|
||||||
|
5. 若有未提交變更 → `git add -A && git commit -m "Update"`
|
||||||
|
6. 設定 `origin` 為**不含 token** 的乾淨 URL
|
||||||
|
7. 用一次性 credential helper 帶 token 執行 `git push -u origin <branch>`
|
||||||
|
|
||||||
|
## 安全設計
|
||||||
|
|
||||||
|
- **`config.env` 內含 token,`.gitignore` 已排除**,不會被提交。
|
||||||
|
- **token 不寫入 `.git/config`**:`origin` 為乾淨 URL,push 時用 `git -c credential.helper=...` 注入,結束後即消失。
|
||||||
|
- Credential helper 以變數替換方式產生 shell 片段;命令列參數會短暫出現在 `ps` 中,僅限本機使用者看到(自架 LAN Gitea 場景可接受)。
|
||||||
|
- `clone.sh` 完成後 origin 也是乾淨 URL,再次 `git pull` / `git push` 要重新帶 credential(跟 `publish.sh` 一致)。
|
||||||
|
|
||||||
|
## 常見問題
|
||||||
|
|
||||||
|
**Q:推送完之後再跑 `git push`,為什麼失敗?**
|
||||||
|
A:origin 是乾淨 URL,再次 push 會要求認證。解法二選一:
|
||||||
|
- 再用本腳本 push(會重新帶 token)
|
||||||
|
- 自行設定 git credential helper 記住帳密
|
||||||
|
|
||||||
|
**Q:Token 失效/過期?**
|
||||||
|
A:重新到 Gitea 產生 token,覆蓋 `config.env` 裡的 `GITEA_TOKEN`。
|
||||||
|
|
||||||
|
**Q:要換成另一個 Gitea 站或使用者?**
|
||||||
|
A:直接改 `config.env` 的 `GITEA_URL` / `GITEA_USER`。
|
||||||
|
|
||||||
|
**Q:可以用在非 Gitea(例如 Gitea clone 產品)嗎?**
|
||||||
|
A:本腳本用的是 Gitea API v1,Forgejo 相容,GitHub/GitLab 不相容。
|
||||||
47
SKILL.md
Normal file
47
SKILL.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
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 / 刪除/建立的項目)
|
||||||
13
config.example.env
Normal file
13
config.example.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 複製此檔為 config.env 後填入實際值
|
||||||
|
# cp config.example.env config.env
|
||||||
|
|
||||||
|
# Gitea 站台網址(不要結尾 /)
|
||||||
|
GITEA_URL=http://your-gitea.example.com:3000
|
||||||
|
|
||||||
|
# Gitea 使用者名稱
|
||||||
|
GITEA_USER=your-username
|
||||||
|
|
||||||
|
# Gitea personal access token
|
||||||
|
# 產生位置: <GITEA_URL>/-/user/settings/applications
|
||||||
|
# 權限勾選: write:repository
|
||||||
|
GITEA_TOKEN=請貼上你的_access_token
|
||||||
1822
docs/superpowers/plans/2026-04-21-gitea-cli-expansion.md
Normal file
1822
docs/superpowers/plans/2026-04-21-gitea-cli-expansion.md
Normal file
File diff suppressed because it is too large
Load Diff
259
docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md
Normal file
259
docs/superpowers/specs/2026-04-21-gitea-cli-expansion-design.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# gitea-cli 擴充設計
|
||||||
|
|
||||||
|
**日期**:2026-04-21
|
||||||
|
**狀態**:待實作
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`gitea-cli` 目前只有一支 `scripts/publish.sh`,負責在自架 Gitea 建立 repo 並把目前工作目錄推上去。本 spec 定義將它擴充為完整的 Gitea 操作工具集。
|
||||||
|
|
||||||
|
## 目標
|
||||||
|
|
||||||
|
- 新增三大類功能:**repo 管理**、**取用既有 repo**、**推送進階(release / tag / 自訂 commit 訊息)**
|
||||||
|
- 抽出共用 lib(`scripts/lib/common.sh`),新舊腳本都使用
|
||||||
|
- **完全不破壞** 現有 `publish.sh` 的外部行為:既有呼叫方式仍然有效
|
||||||
|
- 維持既有安全不變量:token 絕不寫進 `.git/config` / remote URL / log
|
||||||
|
|
||||||
|
## 非目標(刻意 YAGNI)
|
||||||
|
|
||||||
|
以下情境**不**在本次範圍,留待真正需要時再加:
|
||||||
|
|
||||||
|
- 批次 clone 所有 repo(`clone.sh --all`)
|
||||||
|
- Fork / mirror / webhook / collaborator 管理
|
||||||
|
- 多站台切換 config(`GITEA_PROFILE` 等)
|
||||||
|
- 互動式 UI / 問答精靈(Claude 會代問,腳本維持純 CLI)
|
||||||
|
- GitHub / GitLab 相容
|
||||||
|
|
||||||
|
## 架構
|
||||||
|
|
||||||
|
### 組織方式:多支獨立腳本 + 共用 lib
|
||||||
|
|
||||||
|
選擇理由:腳本風格延續既有、每支單一職責、最不破壞現況;相對於單一子命令 CLI,shell subcommand 分派需要自己寫會讓單檔肥大。
|
||||||
|
|
||||||
|
### 檔案結構
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-cli/
|
||||||
|
├── SKILL.md
|
||||||
|
├── README.md
|
||||||
|
├── CLAUDE.md
|
||||||
|
├── config.example.env
|
||||||
|
├── config.env (gitignored)
|
||||||
|
├── .gitignore
|
||||||
|
├── docs/superpowers/specs/ (本 spec 所在)
|
||||||
|
└── scripts/
|
||||||
|
├── lib/
|
||||||
|
│ └── common.sh (所有腳本 source 此檔)
|
||||||
|
├── publish.sh (重構後 source lib;外部行為不變 + 新增 -m)
|
||||||
|
├── list.sh
|
||||||
|
├── clone.sh
|
||||||
|
├── rename.sh
|
||||||
|
├── visibility.sh
|
||||||
|
├── archive.sh
|
||||||
|
├── delete.sh
|
||||||
|
├── meta.sh
|
||||||
|
├── release.sh
|
||||||
|
└── tag.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### `scripts/lib/common.sh` 介面
|
||||||
|
|
||||||
|
**必須放進來**(跨腳本重複且短小):
|
||||||
|
|
||||||
|
| Function | 職責 |
|
||||||
|
|---|---|
|
||||||
|
| `load_config` | 找 `config.env` → `source` → 檢查 `GITEA_URL` / `GITEA_USER` / `GITEA_TOKEN` 非空且非預設值;失敗時統一錯誤訊息 |
|
||||||
|
| `gitea_api <METHOD> <path> [json-body]` | 封裝 curl;回傳 HTTP 狀態碼、把 body 寫入 `$GITEA_API_RESP_FILE`;呼叫端自行判斷狀態碼。所有新腳本**只透過這支呼叫 API**,不自己寫 curl |
|
||||||
|
| `git_push_with_token <branch>` | 封裝一次性 `credential.helper` 推送 |
|
||||||
|
| `git_clone_with_token <clean-url> <target-dir>` | 同上,用於 clone;clone 完自動把 origin 重設為乾淨 URL |
|
||||||
|
| `infer_repo_from_cwd` | 從 `git remote get-url origin` 推斷 repo 名;解析不到回非零 |
|
||||||
|
| `confirm_destructive <prompt>` | 互動 y/N;若 `--yes` flag 或 `GITEA_YES=1` 則直接通過 |
|
||||||
|
| `confirm_exact_match <expected>` | 要求使用者完整輸入指定字串才通過(給 `delete.sh` 用);同樣支援 `--yes` 略過 |
|
||||||
|
| `log_info` / `log_warn` / `log_err` | 統一輸出前綴(`▶ / ⚠ / ❌ / ✅ / ✓`),**全部寫到 stderr** |
|
||||||
|
|
||||||
|
**刻意不放**(留在各腳本):
|
||||||
|
|
||||||
|
- 各腳本的 arg 解析(幾行而已,抽共用反而難讀)
|
||||||
|
- 各 API endpoint 的 JSON body 組字串(放在呼叫處比較直觀)
|
||||||
|
- `publish.sh` 的三種 git 狀態分支判斷(是 publish 獨有)
|
||||||
|
|
||||||
|
### 不變量(review 時必守)
|
||||||
|
|
||||||
|
1. **Token 絕不落地**:token 僅允許出現在 `config.env`、環境變數、`gitea_api` / `git_*_with_token` 函式內部;不得寫進 `.git/config`、remote URL、log、`stderr`、`stdout`
|
||||||
|
2. **origin URL 永遠乾淨**:`publish.sh` 和 `clone.sh` 結束後 origin 都是不含 token 的 URL
|
||||||
|
3. **`publish.sh` 外部行為**:`publish.sh [repo] [public|private]` 原有參數位置與預設值完全不變;`-m "msg"` 是**新增**選項,不影響舊呼叫方式
|
||||||
|
4. **`set -euo pipefail` + `trap` 清臨時檔**:每支腳本都要
|
||||||
|
5. **stdout 只有結構化結果;stderr 放進度訊息**
|
||||||
|
|
||||||
|
## 腳本詳細設計
|
||||||
|
|
||||||
|
### 既有腳本(微調)
|
||||||
|
|
||||||
|
#### `publish.sh`
|
||||||
|
|
||||||
|
**用法**:`publish.sh [repo] [public|private] [-m "msg"]`
|
||||||
|
|
||||||
|
- 新增 `-m "msg"`:若指定,取代預設 commit 訊息(原本是 `Initial commit` / `Update`)
|
||||||
|
- 重構:`load_config` / `gitea_api` / `git_push_with_token` / `log_*` 改用 lib
|
||||||
|
- 三種 git 狀態分支保留在腳本內
|
||||||
|
- **stdout**:最後只印 repo web URL(方便 `URL=$(publish.sh)`)
|
||||||
|
- **stderr**:進度訊息
|
||||||
|
|
||||||
|
### 新腳本(方向 1:repo 管理)
|
||||||
|
|
||||||
|
#### `rename.sh`
|
||||||
|
|
||||||
|
**用法**:`rename.sh <old-name> <new-name>`
|
||||||
|
|
||||||
|
- `PATCH /repos/{GITEA_USER}/{old}` body `{"name": "<new>"}`
|
||||||
|
- 若 cwd 的 `git remote get-url origin` 指向舊 repo,順手 `git remote set-url origin <new-clean-url>`
|
||||||
|
- **stdout**:新 repo web URL
|
||||||
|
- 不需二次確認(重命名非破壞性)
|
||||||
|
|
||||||
|
#### `visibility.sh`
|
||||||
|
|
||||||
|
**用法**:`visibility.sh <repo> <public|private>`
|
||||||
|
|
||||||
|
- `PATCH /repos/{GITEA_USER}/{repo}` body `{"private": true|false}`
|
||||||
|
- 不需二次確認
|
||||||
|
|
||||||
|
#### `archive.sh`
|
||||||
|
|
||||||
|
**用法**:`archive.sh <repo> [--unarchive] [--yes|-y]`
|
||||||
|
|
||||||
|
- `PATCH /repos/{GITEA_USER}/{repo}` body `{"archived": true}`(`--unarchive` 則 `false`)
|
||||||
|
- 互動模式預設需 y/N 確認;`--yes` / `-y` / `GITEA_YES=1` 略過
|
||||||
|
|
||||||
|
#### `delete.sh`
|
||||||
|
|
||||||
|
**用法**:`delete.sh <repo> [--yes|-y]`
|
||||||
|
|
||||||
|
- `DELETE /repos/{GITEA_USER}/{repo}`
|
||||||
|
- 互動模式:用 `confirm_exact_match <repo>` 要求使用者**重新輸入完整 repo 名稱**才動(最高保險)
|
||||||
|
- `--yes` / `-y` / `GITEA_YES=1` 略過
|
||||||
|
|
||||||
|
#### `meta.sh`
|
||||||
|
|
||||||
|
**用法**:`meta.sh <repo> [--desc "..."] [--topics "a,b,c"]`
|
||||||
|
|
||||||
|
- `--desc`:`PATCH /repos/{owner}/{repo}` body `{"description": "..."}`
|
||||||
|
- `--topics`:`PUT /repos/{owner}/{repo}/topics` body `{"topics": ["a","b","c"]}`(覆蓋式,非增量)
|
||||||
|
- 兩者可同時給;都不給時印用法並非零退出
|
||||||
|
|
||||||
|
### 新腳本(方向 2:取用既有 repo)
|
||||||
|
|
||||||
|
#### `list.sh`
|
||||||
|
|
||||||
|
**用法**:`list.sh [--private|--public] [--format table|tsv|name]`
|
||||||
|
|
||||||
|
- `GET /user/repos`(自動翻頁,每頁 50)
|
||||||
|
- `--private` / `--public`:過濾可見性,預設顯示全部
|
||||||
|
- `--format`:
|
||||||
|
- `table`(預設):對齊欄位 `NAME VISIBILITY DESCRIPTION`
|
||||||
|
- `tsv`:`name\tvisibility\tdescription`,給管線 / Claude 用
|
||||||
|
- `name`:每行一個 repo 名,給 `xargs` 用
|
||||||
|
- **stdout**:清單內容
|
||||||
|
- **stderr**:進度(抓到幾頁、共幾個)
|
||||||
|
|
||||||
|
#### `clone.sh`
|
||||||
|
|
||||||
|
**用法**:`clone.sh <repo> [target-dir]`
|
||||||
|
|
||||||
|
- 預設 clone 到 `./<repo>`,可指定 target
|
||||||
|
- 目標目錄已存在且非空 → 失敗(不覆蓋)
|
||||||
|
- 用 `git_clone_with_token` 做 clone,完成後把 origin 重設為乾淨 URL
|
||||||
|
- **stdout**:clone 到的絕對路徑
|
||||||
|
|
||||||
|
### 新腳本(方向 3:推送進階)
|
||||||
|
|
||||||
|
#### `release.sh`
|
||||||
|
|
||||||
|
**用法**:`release.sh <tag> [--name "..."] [--notes "..."] [--target <branch>] [--draft] [--prerelease] [--asset <path>]...`
|
||||||
|
|
||||||
|
- 從 cwd 的 origin 推斷 repo;推不到強制要 `--repo <name>`
|
||||||
|
- **兩段 API**:
|
||||||
|
1. `POST /repos/{owner}/{repo}/releases` body 組合參數 → 取回 `id`
|
||||||
|
2. 對每個 `--asset` 做 `POST /repos/{owner}/{repo}/releases/{id}/assets?name=<basename>`(`multipart/form-data`)
|
||||||
|
- Tag 處理:Gitea release API 的 `tag_name` 若不存在會自動建(指到 `target_commitish`,預設 HEAD)。不強制先 `tag.sh`
|
||||||
|
- **錯誤處理**:
|
||||||
|
- 第 1 步失敗 → 直接結束
|
||||||
|
- 第 2 步某個 asset 失敗 → 繼續其他 asset,最後非零退出並列出失敗的
|
||||||
|
- **stdout**:第一行 release web URL,其餘每行一個 asset 下載 URL
|
||||||
|
|
||||||
|
#### `tag.sh`
|
||||||
|
|
||||||
|
**用法**:`tag.sh <tag> [-m "msg"]`
|
||||||
|
|
||||||
|
- 本地 `git tag -a <tag> -m "<msg>"`(預設 msg = `"Tag <tag>"`)
|
||||||
|
- `git_push_with_token` 推這個 tag(`git push origin <tag>`)
|
||||||
|
- **定位**:只打 tag 不發 release 的情境;與 `release.sh` 獨立
|
||||||
|
|
||||||
|
### Repo 名稱解析原則(跨腳本)
|
||||||
|
|
||||||
|
所有需要 repo 名的腳本,若參數省略,呼叫 `infer_repo_from_cwd` 嘗試從 `git remote get-url origin` 推斷。推斷不到就要求使用者帶參數。這樣在 repo 裡直接跑 `rename.sh newname`、`release.sh v1.0.0` 都能用。
|
||||||
|
|
||||||
|
## 錯誤處理與輸出格式
|
||||||
|
|
||||||
|
### 退出碼
|
||||||
|
|
||||||
|
- `0` 成功
|
||||||
|
- 非零 失敗(不細分子類;錯誤訊息本身會講清楚原因)
|
||||||
|
|
||||||
|
### stdout / stderr 分流
|
||||||
|
|
||||||
|
- **stderr**:所有 `▶ / ✓ / ⚠ / ❌ / ✅` 進度訊息與錯誤
|
||||||
|
- **stdout**:僅最終結構化輸出
|
||||||
|
- `publish.sh` / `rename.sh` → repo web URL
|
||||||
|
- `list.sh` → repo 清單
|
||||||
|
- `clone.sh` → 絕對路徑
|
||||||
|
- `release.sh` → release URL + 每個 asset URL
|
||||||
|
- 其他 → 空
|
||||||
|
|
||||||
|
**既有 `publish.sh` 變更**:原本進度訊息是 stdout,會改到 stderr。在終端機看起來一樣(兩者都會顯示),但若有人 pipe 其 stdout 會只看到最後的 URL。這是刻意設計,不視為 regression。
|
||||||
|
|
||||||
|
## 測試策略
|
||||||
|
|
||||||
|
無自動化測試。驗收方式:
|
||||||
|
|
||||||
|
1. 既有 `publish.sh` 使用情境(三種 git 狀態、public/private、已存在 repo)全部手動跑過,確認行為不變
|
||||||
|
2. 每支新腳本在測試用的 Gitea repo 上跑 happy path 一次
|
||||||
|
3. 破壞性操作(`delete.sh` / `archive.sh`)特別驗證:
|
||||||
|
- 不帶 `-y` 時會提示
|
||||||
|
- `delete.sh` 輸入錯 repo 名會被拒
|
||||||
|
- 帶 `-y` 時直接執行
|
||||||
|
4. Token 不落地驗收:跑完 `publish.sh` 後 `grep -r "$GITEA_TOKEN" .git/` 應為空
|
||||||
|
|
||||||
|
## 文件同步
|
||||||
|
|
||||||
|
### `SKILL.md`
|
||||||
|
|
||||||
|
- `description:` 擴寫以涵蓋所有擴充後的觸發情境(push / clone / list / rename / release / archive / delete / tag…)
|
||||||
|
- 正文改以「常見情境 → 對應腳本」方式列全部腳本,保留 `config.env` 一次性設定段落
|
||||||
|
|
||||||
|
### `README.md`
|
||||||
|
|
||||||
|
- 新增「腳本總覽」表格:每支腳本一行用法與一句說明
|
||||||
|
- 每支新腳本 1-2 段詳細說明 + 範例
|
||||||
|
- 「安全設計」段落補上「clone 完 origin 也是乾淨 URL」
|
||||||
|
|
||||||
|
### `CLAUDE.md`
|
||||||
|
|
||||||
|
- 補充「擴展為多腳本」的架構敘述
|
||||||
|
- 不變量改寫為**所有 push / clone 腳本**都必須守 token 不落地
|
||||||
|
- 新增「`scripts/lib/common.sh` 的用途與邊界」段落
|
||||||
|
- 新增「新腳本的檔案骨架」小範本(source lib → parse args → 呼叫 lib → 印結果),讓未來加新腳本的人照抄
|
||||||
|
|
||||||
|
### `config.example.env`
|
||||||
|
|
||||||
|
不變,仍是三個變數。
|
||||||
|
|
||||||
|
## 實作順序建議
|
||||||
|
|
||||||
|
1. `scripts/lib/common.sh`(所有後續腳本的底)
|
||||||
|
2. 重構 `publish.sh` 改用 lib,新增 `-m`;手動驗證外部行為不變
|
||||||
|
3. `list.sh` / `clone.sh`(方向 2:最常用)
|
||||||
|
4. `rename.sh` / `visibility.sh` / `meta.sh`(方向 1:非破壞)
|
||||||
|
5. `archive.sh` / `delete.sh`(方向 1:破壞性,需測確認機制)
|
||||||
|
6. `tag.sh` / `release.sh`(方向 3)
|
||||||
|
7. 更新 `SKILL.md` / `README.md` / `CLAUDE.md`
|
||||||
|
8. 整套驗收(上節「測試策略」)
|
||||||
56
scripts/archive.sh
Normal file
56
scripts/archive.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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}"
|
||||||
48
scripts/clone.sh
Normal file
48
scripts/clone.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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"
|
||||||
48
scripts/delete.sh
Normal file
48
scripts/delete.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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 "完成"
|
||||||
193
scripts/lib/common.sh
Normal file
193
scripts/lib/common.sh
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/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() {
|
||||||
|
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"
|
||||||
|
} >&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 [[ ! -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
|
||||||
|
}
|
||||||
82
scripts/list.sh
Executable file
82
scripts/list.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
load_config
|
||||||
|
require_cmd jq column
|
||||||
|
|
||||||
|
PAGE=1
|
||||||
|
LIMIT=50
|
||||||
|
ALL_ITEMS="$(mktemp)"
|
||||||
|
trap 'rm -f "$ALL_ITEMS"; _cleanup_common' 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
|
||||||
72
scripts/meta.sh
Normal file
72
scripts/meta.sh
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
load_config
|
||||||
|
require_cmd jq
|
||||||
|
|
||||||
|
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}"
|
||||||
123
scripts/publish.sh
Executable file
123
scripts/publish.sh
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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"
|
||||||
|
elif [[ "$VISIBILITY" == "public" ]]; then
|
||||||
|
VISIBILITY="$1"
|
||||||
|
else
|
||||||
|
log_err "多餘參數:$1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
REPO_NAME="${REPO_NAME:-$(basename "$PWD")}"
|
||||||
|
|
||||||
|
case "$VISIBILITY" in
|
||||||
|
public) PRIVATE=false ;;
|
||||||
|
private) PRIVATE=true ;;
|
||||||
|
*)
|
||||||
|
log_err "可見性只能是 public 或 private,收到:${VISIBILITY}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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 "${COMMIT_MSG:-Initial commit}" >/dev/null
|
||||||
|
else
|
||||||
|
log_info "建立 initial commit"
|
||||||
|
git commit -m "${COMMIT_MSG:-Initial commit}" >/dev/null
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
git add -A
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
log_info "提交未提交的變更"
|
||||||
|
git commit -m "${COMMIT_MSG:-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"
|
||||||
140
scripts/release.sh
Executable file
140
scripts/release.sh
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
load_config
|
||||||
|
require_cmd jq
|
||||||
|
|
||||||
|
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[@]+"${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[@]+"${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 "完成"
|
||||||
47
scripts/rename.sh
Normal file
47
scripts/rename.sh
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Usage: rename.sh <old-name> <new-name>
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $# -eq 2 ]] || usage
|
||||||
|
|
||||||
|
OLD="$1"
|
||||||
|
NEW="$2"
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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"
|
||||||
56
scripts/tag.sh
Normal file
56
scripts/tag.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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"
|
||||||
42
scripts/visibility.sh
Normal file
42
scripts/visibility.sh
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
load_config
|
||||||
|
|
||||||
|
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}"
|
||||||
Reference in New Issue
Block a user