822 lines
26 KiB
Bash
Executable file
822 lines
26 KiB
Bash
Executable file
#!/bin/bash
|
||
# fei — feiCode (Forgejo) CLI 工具
|
||
# 用法: fei <命令> [参数...]
|
||
set -euo pipefail
|
||
|
||
FEI_VERSION="1.1.0"
|
||
FEI_HOST="feicode.com"
|
||
FEI_API="https://${FEI_HOST}/api/v1"
|
||
FEI_SSH_PORT=2222
|
||
FEI_TOKEN="${FORGEJO_TOKEN:-}"
|
||
|
||
# ── 工具函数 ──
|
||
|
||
_fei_check_token() {
|
||
if [[ -z "$FEI_TOKEN" ]]; then
|
||
echo "错误: FORGEJO_TOKEN 未设置" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
_fei_api() {
|
||
local method="$1" path="$2"
|
||
shift 2
|
||
local tmpfile http_code
|
||
tmpfile=$(mktemp)
|
||
http_code=$(curl -s -o "$tmpfile" -w '%{http_code}' -X "$method" \
|
||
-H "Authorization: token $FEI_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
"$@" "${FEI_API}${path}") || { rm -f "$tmpfile"; echo "错误: 网络请求失败" >&2; return 1; }
|
||
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
|
||
cat "$tmpfile"
|
||
rm -f "$tmpfile"
|
||
else
|
||
local msg
|
||
msg=$(jq -r '.message // empty' < "$tmpfile" 2>/dev/null)
|
||
rm -f "$tmpfile"
|
||
echo "错误: HTTP ${http_code}${msg:+ — $msg}" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
_fei_get() { _fei_api GET "$@"; }
|
||
_fei_post() { local p="$1"; shift; _fei_api POST "$p" "$@"; }
|
||
_fei_patch() { local p="$1"; shift; _fei_api PATCH "$p" "$@"; }
|
||
_fei_delete() { _fei_api DELETE "$@"; }
|
||
|
||
# 从当前 git 仓库提取 org/repo
|
||
_fei_repo_from_git() {
|
||
local url
|
||
url=$(git remote get-url origin 2>/dev/null) || { echo "错误: 不在 git 仓库中或无 origin remote" >&2; return 1; }
|
||
# 支持 ssh://git@host:port/org/repo.git 和 git@host:org/repo.git 和 https
|
||
url="${url%.git}"
|
||
if [[ "$url" == ssh://* ]]; then
|
||
# ssh://git@feicode.com:2222/Org/repo
|
||
echo "$url" | sed -E 's#^ssh://[^/]*:[0-9]+/##; s#^ssh://[^/]*/##'
|
||
elif [[ "$url" == *@* ]]; then
|
||
# git@feicode.com:Org/repo
|
||
echo "$url" | sed -E 's#^[^:]+:##'
|
||
else
|
||
# https://feicode.com/Org/repo
|
||
echo "$url" | sed -E 's#^https?://[^/]+/##'
|
||
fi
|
||
}
|
||
|
||
_fei_fmt_time() {
|
||
# 简化时间显示
|
||
echo "$1" | sed -E 's/T/ /; s/\+.*//' | cut -c1-16
|
||
}
|
||
|
||
# ── 命令实现 ──
|
||
|
||
cmd_help() {
|
||
cat << 'EOF'
|
||
fei — feiCode CLI 工具
|
||
|
||
仓库操作:
|
||
fei clone <org/repo> 克隆仓库 (SSH)
|
||
fei clone --https <org/repo> 克隆仓库 (HTTPS)
|
||
fei repos [org] 列出仓库
|
||
fei search <关键词> 搜索仓库
|
||
fei info [org/repo] 仓库详情 (默认当前仓库)
|
||
fei web [org/repo] 浏览器打开仓库页面
|
||
fei fork <org/repo> Fork 仓库到当前用户
|
||
|
||
分支操作:
|
||
fei branch list [org/repo] 分支列表
|
||
fei branch create <名称> [org/repo] 创建分支 (基于默认分支)
|
||
fei branch delete <名称> [org/repo] 删除分支
|
||
fei diff <分支1> <分支2> [org/repo] 比较两个分支差异
|
||
fei log [org/repo] 最近提交记录
|
||
|
||
PR 操作:
|
||
fei pr list [org/repo] PR 列表
|
||
fei pr create <标题> [正文] 创建 PR (当前分支 → main)
|
||
fei pr create <标题> -b <base> 指定目标分支
|
||
fei pr view <编号> 查看 PR 详情
|
||
fei pr merge <编号> 合并 PR
|
||
|
||
Issue 操作:
|
||
fei issue list [org/repo] Issue 列表
|
||
fei issue create <标题> [正文] 创建 Issue
|
||
fei issue view <编号> 查看 Issue 详情
|
||
fei issue close <编号> 关闭 Issue
|
||
|
||
Release 操作:
|
||
fei release list [org/repo] Release 列表
|
||
fei release create <tag> <标题> 创建 Release
|
||
fei release create <tag> <标题> --asset <文件> 创建并上传附件
|
||
|
||
标签操作:
|
||
fei label list [org/repo] 标签列表
|
||
fei label create <名称> <颜色> 创建标签 (颜色如 #ff0000)
|
||
|
||
通知:
|
||
fei notify 未读通知列表
|
||
fei notify count 未读通知数量
|
||
|
||
文件操作:
|
||
fei cat <文件路径> [org/repo] 查看文件内容
|
||
fei ls [目录] [org/repo] 列出目录内容
|
||
|
||
推送 (带敏感文件过滤):
|
||
fei push [branch] [remote] 推送分支 (自动排除 .git/push-exclude 中的路径)
|
||
fei push --tag <tag> [remote] 推送 tag
|
||
|
||
管理 (需要管理员权限):
|
||
fei admin users 用户列表
|
||
fei admin dump 触发 Forgejo 备份
|
||
fei admin doctor Forgejo 健康检查
|
||
|
||
Pages:
|
||
fei pages create <站点名> 创建 Pages 站点仓库 (自带部署 workflow)
|
||
fei pages list 列出所有 Pages 站点
|
||
fei pages open <站点名> 浏览器打开站点
|
||
|
||
其他:
|
||
fei whoami 当前认证信息
|
||
fei orgs 列出组织
|
||
fei help 显示帮助
|
||
fei --version 显示版本号
|
||
EOF
|
||
}
|
||
|
||
cmd_whoami() {
|
||
_fei_check_token || return 1
|
||
_fei_get "/user" | jq -r '"用户: \(.login)\n邮箱: \(.email)\n全名: \(.full_name // "未设置")\n管理员: \(.is_admin)"'
|
||
}
|
||
|
||
cmd_orgs() {
|
||
_fei_check_token || return 1
|
||
_fei_get "/user/orgs" | jq -r '.[] | "\(.username)\t\(.description // "")"' | column -t -s$'\t'
|
||
}
|
||
|
||
cmd_clone() {
|
||
local use_https=false
|
||
if [[ "${1:-}" == "--https" ]]; then
|
||
use_https=true
|
||
shift
|
||
fi
|
||
local repo="${1:?用法: fei clone [--https] <org/repo>}"
|
||
if $use_https; then
|
||
git clone "https://${FEI_HOST}/${repo}.git"
|
||
else
|
||
git clone "ssh://git@${FEI_HOST}:${FEI_SSH_PORT}/${repo}.git"
|
||
fi
|
||
}
|
||
|
||
cmd_repos() {
|
||
_fei_check_token || return 1
|
||
local org="" limit=50
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--limit) limit="$2"; shift 2 ;;
|
||
*) org="$1"; shift ;;
|
||
esac
|
||
done
|
||
|
||
local page=1 path
|
||
|
||
if [[ -n "$org" ]]; then
|
||
path="/orgs/${org}/repos"
|
||
else
|
||
path="/user/repos"
|
||
fi
|
||
|
||
while true; do
|
||
local result
|
||
result=$(_fei_get "${path}?limit=${limit}&page=${page}&sort=updated")
|
||
local count
|
||
count=$(echo "$result" | jq 'length')
|
||
[[ "$count" == "0" ]] && break
|
||
|
||
echo "$result" | jq -r '.[] | "\(.full_name)\t\(.description // "-" | .[0:50])\t\(if .mirror then "镜像" else "" end)"' | column -t -s$'\t'
|
||
[[ "$count" -lt "$limit" ]] && break
|
||
((page++))
|
||
done
|
||
}
|
||
|
||
cmd_search() {
|
||
_fei_check_token || return 1
|
||
local query="" limit=50
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--limit) limit="$2"; shift 2 ;;
|
||
*) query="$1"; shift ;;
|
||
esac
|
||
done
|
||
[[ -z "$query" ]] && { echo "用法: fei search <关键词> [--limit N]" >&2; return 1; }
|
||
_fei_get "/repos/search?q=$(printf '%s' "$query" | jq -sRr @uri)&limit=${limit}" | \
|
||
jq -r '.data[] | "\(.full_name)\t\(.description // "-" | .[0:50])\t⭐\(.stars_count)"' | column -t -s$'\t'
|
||
}
|
||
|
||
cmd_info() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}" | jq -r '
|
||
"仓库: \(.full_name)",
|
||
"描述: \(.description // "无")",
|
||
"网站: \(.website // "无")",
|
||
"镜像: \(if .mirror then "是" else "否" end)",
|
||
"星标: \(.stars_count) Fork: \(.forks_count) Issue: \(.open_issues_count)",
|
||
"默认分支: \(.default_branch)",
|
||
"创建: \(.created_at[:10])",
|
||
"SSH: ssh://git@'"${FEI_HOST}:${FEI_SSH_PORT}"'/\(.full_name).git",
|
||
"HTTP: \(.clone_url)"'
|
||
}
|
||
|
||
cmd_web() {
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
local url="https://${FEI_HOST}/${repo}"
|
||
echo "$url"
|
||
xdg-open "$url" 2>/dev/null || open "$url" 2>/dev/null || echo "(请手动打开)"
|
||
}
|
||
|
||
cmd_fork() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:?用法: fei fork <org/repo>}"
|
||
_fei_post "/repos/${repo}/forks" -d '{}' | jq -r '"Fork 完成: \(.full_name)\nSSH: ssh://git@'"${FEI_HOST}:${FEI_SSH_PORT}"'/\(.full_name).git"'
|
||
}
|
||
|
||
# ── 分支 ──
|
||
|
||
cmd_branch() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
list) _branch_list "$@" ;;
|
||
create) _branch_create "$@" ;;
|
||
delete) _branch_delete "$@" ;;
|
||
*) echo "用法: fei branch {list|create|delete}" ;;
|
||
esac
|
||
}
|
||
|
||
_branch_list() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/branches?limit=50" | \
|
||
jq -r '.[] | "\(.name)\t\(.commit.id[:8])\t\(.commit.message | split("\n")[0] | .[0:60])"' | column -t -s$'\t'
|
||
}
|
||
|
||
_branch_create() {
|
||
_fei_check_token || return 1
|
||
local name="${1:?用法: fei branch create <名称> [org/repo]}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
local default_branch
|
||
default_branch=$(_fei_get "/repos/${repo}" | jq -r '.default_branch')
|
||
_fei_post "/repos/${repo}/branches" \
|
||
-d "$(jq -n --arg n "$name" --arg b "$default_branch" '{new_branch_name: $n, old_branch_name: $b}')" | \
|
||
jq -r '"分支已创建: \(.name) (基于 '"$default_branch"')"'
|
||
}
|
||
|
||
_branch_delete() {
|
||
_fei_check_token || return 1
|
||
local name="${1:?用法: fei branch delete <名称> [org/repo]}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
local http_code
|
||
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
|
||
-H "Authorization: token $FEI_TOKEN" \
|
||
"${FEI_API}/repos/${repo}/branches/${name}")
|
||
if [[ "$http_code" == "204" ]]; then
|
||
echo "分支已删除: $name"
|
||
else
|
||
echo "删除失败: HTTP $http_code" >&2; return 1
|
||
fi
|
||
}
|
||
|
||
cmd_diff() {
|
||
_fei_check_token || return 1
|
||
local base="${1:?用法: fei diff <分支1> <分支2> [org/repo]}"
|
||
local head="${2:?用法: fei diff <分支1> <分支2> [org/repo]}"
|
||
local repo="${3:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/compare/${base}...${head}" | \
|
||
jq -r '"比较: \(.base_commit.id[:8])...\(.merge_base_commit.id[:8])",
|
||
"提交数: \(.total_commits)",
|
||
"文件变更: \(.files | length)",
|
||
"---",
|
||
(.files[] | "\(.status)\t\(.filename)\t+\(.additions) -\(.deletions)")' | column -t -s$'\t'
|
||
}
|
||
|
||
cmd_log() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/commits?limit=15" | \
|
||
jq -r '.[] | "\(.sha[:8])\t\(.commit.author.date[:10])\t\(.commit.author.name)\t\(.commit.message | split("\n")[0] | .[0:60])"' | column -t -s$'\t'
|
||
}
|
||
|
||
# ── 标签 ──
|
||
|
||
cmd_label() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
list) _label_list "$@" ;;
|
||
create) _label_create "$@" ;;
|
||
*) echo "用法: fei label {list|create}" ;;
|
||
esac
|
||
}
|
||
|
||
_label_list() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/labels?limit=50" | \
|
||
jq -r '.[] | "\(.name)\t#\(.color)\t\(.description // "")"' | column -t -s$'\t'
|
||
}
|
||
|
||
_label_create() {
|
||
_fei_check_token || return 1
|
||
local name="${1:?用法: fei label create <名称> <颜色>}"
|
||
local color="${2:?用法: fei label create <名称> <颜色>}"
|
||
local repo="${3:-$(_fei_repo_from_git)}"
|
||
color="${color#\#}"
|
||
_fei_post "/repos/${repo}/labels" \
|
||
-d "$(jq -n --arg n "$name" --arg c "$color" '{name: $n, color: ("#" + $c)}')" | \
|
||
jq -r '"标签已创建: \(.name) (#\(.color))"'
|
||
}
|
||
|
||
# ── 通知 ──
|
||
|
||
cmd_notify() {
|
||
_fei_check_token || return 1
|
||
local sub="${1:-list}"
|
||
case "$sub" in
|
||
count)
|
||
local count
|
||
count=$(_fei_get "/notifications/new" | jq -r '.new // 0')
|
||
echo "未读通知: ${count}" ;;
|
||
list|*)
|
||
_fei_get "/notifications?limit=20" | \
|
||
jq -r '.[] | "\(if .unread then "●" else "○" end) \(.subject.type)\t\(.subject.title | .[0:60])\t\(.repository.full_name)\t\(.updated_at[:10])"' | column -t -s$'\t'
|
||
[[ "$sub" != "list" ]] && true ;;
|
||
esac
|
||
}
|
||
|
||
# ── 管理 ──
|
||
|
||
cmd_admin() {
|
||
local sub="${1:-help}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
users) _admin_users "$@" ;;
|
||
dump) _admin_dump "$@" ;;
|
||
doctor) _admin_doctor "$@" ;;
|
||
*) echo "用法: fei admin {users|dump|doctor}" ;;
|
||
esac
|
||
}
|
||
|
||
_admin_users() {
|
||
_fei_check_token || return 1
|
||
_fei_get "/admin/users?limit=50" | \
|
||
jq -r '.[] | "\(.id)\t\(.login)\t\(.email)\t\(if .is_admin then "管理员" else "" end)\t\(if .prohibit_login then "禁用" else "活跃" end)\t\(.last_login[:10] // "从未")"' | column -t -s$'\t'
|
||
}
|
||
|
||
_admin_dump() {
|
||
echo "触发 Forgejo 备份..."
|
||
ssh -o ConnectTimeout=5 feicode-prod \
|
||
"docker exec forgejo_FeiCode-1 forgejo dump -f /data/forgejo-dump-\$(date +%Y%m%d-%H%M%S).zip --quiet" 2>&1
|
||
echo "备份完成,文件在 feicode-prod 容器 /data/ 目录"
|
||
}
|
||
|
||
_admin_doctor() {
|
||
echo "运行 Forgejo 健康检查..."
|
||
ssh -o ConnectTimeout=10 feicode-prod \
|
||
"docker exec forgejo_FeiCode-1 forgejo doctor check" 2>&1
|
||
}
|
||
|
||
# ── PR ──
|
||
|
||
cmd_pr() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
list) _pr_list "$@" ;;
|
||
create) _pr_create "$@" ;;
|
||
view) _pr_view "$@" ;;
|
||
merge) _pr_merge "$@" ;;
|
||
*) echo "用法: fei pr {list|create|view|merge}" ;;
|
||
esac
|
||
}
|
||
|
||
_pr_list() {
|
||
_fei_check_token || return 1
|
||
local repo="" state="open" limit=50
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--limit) limit="$2"; shift 2 ;;
|
||
*) [[ -z "$repo" ]] && repo="$1" || state="$1"; shift ;;
|
||
esac
|
||
done
|
||
repo="${repo:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/pulls?state=${state}&limit=${limit}" | \
|
||
jq -r '.[] | "#\(.number)\t\(.title | .[0:50])\t\(.user.login)\t\(.head.label)→\(.base.label)\t\(.created_at[:10])"' | column -t -s$'\t'
|
||
}
|
||
|
||
_pr_create() {
|
||
_fei_check_token || return 1
|
||
local title="${1:?用法: fei pr create <标题> [正文] [-b <base>]}"
|
||
shift
|
||
local body="" base="main"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-b|--base) base="$2"; shift 2 ;;
|
||
*) body="$1"; shift ;;
|
||
esac
|
||
done
|
||
|
||
local repo head
|
||
repo=$(_fei_repo_from_git)
|
||
head=$(git branch --show-current)
|
||
|
||
local payload
|
||
payload=$(jq -n --arg t "$title" --arg b "$body" --arg h "$head" --arg base "$base" \
|
||
'{title: $t, body: $b, head: $h, base: $base}')
|
||
|
||
_fei_post "/repos/${repo}/pulls" -d "$payload" | \
|
||
jq -r '"PR #\(.number) 已创建: \(.html_url)"'
|
||
}
|
||
|
||
_pr_view() {
|
||
_fei_check_token || return 1
|
||
local num="${1:?用法: fei pr view <编号>}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/pulls/${num}" | jq -r '
|
||
"PR #\(.number): \(.title)",
|
||
"状态: \(.state) 可合并: \(.mergeable)",
|
||
"作者: \(.user.login) 分支: \(.head.label) → \(.base.label)",
|
||
"创建: \(.created_at[:10])",
|
||
"",
|
||
(.body // "无描述")'
|
||
}
|
||
|
||
_pr_merge() {
|
||
_fei_check_token || return 1
|
||
local num="${1:?用法: fei pr merge <编号>}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
_fei_post "/repos/${repo}/pulls/${num}/merge" -d '{"Do":"merge"}' | \
|
||
jq -r '// "PR #'"$num"' 已合并"'
|
||
}
|
||
|
||
# ── Issue ──
|
||
|
||
cmd_issue() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
list) _issue_list "$@" ;;
|
||
create) _issue_create "$@" ;;
|
||
view) _issue_view "$@" ;;
|
||
close) _issue_close "$@" ;;
|
||
*) echo "用法: fei issue {list|create|view|close}" ;;
|
||
esac
|
||
}
|
||
|
||
_issue_list() {
|
||
_fei_check_token || return 1
|
||
local repo="" state="open" limit=50
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--limit) limit="$2"; shift 2 ;;
|
||
*) [[ -z "$repo" ]] && repo="$1" || state="$1"; shift ;;
|
||
esac
|
||
done
|
||
repo="${repo:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/issues?state=${state}&type=issues&limit=${limit}" | \
|
||
jq -r '.[] | "#\(.number)\t\(.title | .[0:50])\t\(.user.login)\t\(.labels | map(.name) | join(",") | .[0:20])\t\(.created_at[:10])"' | column -t -s$'\t'
|
||
}
|
||
|
||
_issue_create() {
|
||
_fei_check_token || return 1
|
||
local title="${1:?用法: fei issue create <标题> [正文]}"
|
||
local body="${2:-}"
|
||
local repo
|
||
repo=$(_fei_repo_from_git)
|
||
|
||
local payload
|
||
payload=$(jq -n --arg t "$title" --arg b "$body" '{title: $t, body: $b}')
|
||
|
||
_fei_post "/repos/${repo}/issues" -d "$payload" | \
|
||
jq -r '"Issue #\(.number) 已创建: \(.html_url)"'
|
||
}
|
||
|
||
_issue_view() {
|
||
_fei_check_token || return 1
|
||
local num="${1:?用法: fei issue view <编号>}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/issues/${num}" | jq -r '
|
||
"Issue #\(.number): \(.title)",
|
||
"状态: \(.state) 标签: \(.labels | map(.name) | join(", ") // "无")",
|
||
"作者: \(.user.login) 创建: \(.created_at[:10])",
|
||
"",
|
||
(.body // "无描述")'
|
||
}
|
||
|
||
_issue_close() {
|
||
_fei_check_token || return 1
|
||
local num="${1:?用法: fei issue close <编号>}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
_fei_patch "/repos/${repo}/issues/${num}" -d '{"state":"closed"}' | \
|
||
jq -r '"Issue #\(.number) 已关闭"'
|
||
}
|
||
|
||
# ── Release ──
|
||
|
||
cmd_release() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
list) _release_list "$@" ;;
|
||
create) _release_create "$@" ;;
|
||
*) echo "用法: fei release {list|create}" ;;
|
||
esac
|
||
}
|
||
|
||
_release_list() {
|
||
_fei_check_token || return 1
|
||
local repo="${1:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/releases?limit=10" | \
|
||
jq -r '.[] | "\(.tag_name)\t\(.name // "-")\t\(.published_at[:10])\t\(.assets | length)个附件"' | column -t -s$'\t'
|
||
}
|
||
|
||
_release_create() {
|
||
_fei_check_token || return 1
|
||
local tag="${1:?用法: fei release create <tag> <标题> [--asset <文件>]}"
|
||
local title="${2:?用法: fei release create <tag> <标题> [--asset <文件>]}"
|
||
shift 2
|
||
local body="" asset=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--asset) asset="${2:?--asset 需要指定文件路径}"; shift 2 ;;
|
||
*) body="$1"; shift ;;
|
||
esac
|
||
done
|
||
|
||
local repo
|
||
repo=$(_fei_repo_from_git)
|
||
|
||
local payload
|
||
payload=$(jq -n --arg t "$tag" --arg n "$title" --arg b "$body" \
|
||
'{tag_name: $t, name: $n, body: $b}')
|
||
|
||
local result
|
||
result=$(_fei_post "/repos/${repo}/releases" -d "$payload")
|
||
echo "$result" | jq -r '"Release \(.tag_name) 已创建: \(.html_url)"'
|
||
|
||
if [[ -n "$asset" ]]; then
|
||
if [[ ! -f "$asset" ]]; then
|
||
echo "错误: 文件不存在: $asset" >&2
|
||
return 1
|
||
fi
|
||
local release_id
|
||
release_id=$(echo "$result" | jq -r '.id')
|
||
local filename
|
||
filename=$(basename "$asset")
|
||
echo "上传附件: $filename ..."
|
||
curl -s -o /dev/null -w '' -X POST \
|
||
-H "Authorization: token $FEI_TOKEN" \
|
||
-F "attachment=@${asset}" \
|
||
"${FEI_API}/repos/${repo}/releases/${release_id}/assets?name=$(printf '%s' "$filename" | jq -sRr @uri)" \
|
||
| jq -r '"附件已上传: \(.name) (\(.size) bytes)"' 2>/dev/null || echo "附件上传完成"
|
||
fi
|
||
}
|
||
|
||
# ── 文件操作 ──
|
||
|
||
cmd_cat() {
|
||
_fei_check_token || return 1
|
||
local filepath="${1:?用法: fei cat <文件路径> [org/repo]}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
_fei_get "/repos/${repo}/contents/${filepath}" | jq -r '.content' | base64 -d
|
||
}
|
||
|
||
cmd_ls() {
|
||
_fei_check_token || return 1
|
||
local dir="${1:-.}"
|
||
local repo="${2:-$(_fei_repo_from_git)}"
|
||
[[ "$dir" == "." ]] && dir=""
|
||
_fei_get "/repos/${repo}/contents/${dir}" | \
|
||
jq -r '.[] | "\(if .type == "dir" then "📁" else "📄" end) \(.name)\t\(.size // "")"' | column -t -s$'\t'
|
||
}
|
||
|
||
# ── 推送 (集成 wenpai 的 push-feicode 逻辑) ──
|
||
|
||
cmd_push() {
|
||
# 优先使用 push-feicode.sh(过滤规则统一)
|
||
local push_script
|
||
for push_script in "$HOME/bin/push-feicode.sh" /usr/local/bin/push-feicode.sh; do
|
||
if [[ -x "$push_script" ]]; then
|
||
exec "$push_script" "$@"
|
||
fi
|
||
done
|
||
|
||
# 内置逻辑(无 push-feicode.sh 时的 fallback)
|
||
local tag="" branch="main" remote="origin"
|
||
|
||
if [[ "${1:-}" == "--tag" ]]; then
|
||
tag="${2:?用法: fei push --tag <tag> [remote]}"
|
||
remote="${3:-origin}"
|
||
else
|
||
branch="${1:-$(git branch --show-current 2>/dev/null || echo main)}"
|
||
remote="${2:-origin}"
|
||
fi
|
||
|
||
local exclude_file=".git/push-exclude"
|
||
|
||
# 无排除文件,直接推送
|
||
if [[ ! -f "$exclude_file" ]]; then
|
||
if [[ -n "$tag" ]]; then
|
||
git push "$remote" "refs/tags/$tag"
|
||
else
|
||
git push "$remote" "$branch"
|
||
fi
|
||
return
|
||
fi
|
||
|
||
# 有排除文件,走过滤推送
|
||
if ! command -v git-filter-repo &>/dev/null; then
|
||
echo "错误: 需要 git-filter-repo (pip install git-filter-repo)" >&2
|
||
return 1
|
||
fi
|
||
|
||
local -a filter_args=()
|
||
while IFS= read -r line; do
|
||
line=$(echo "$line" | sed 's/#.*//' | xargs)
|
||
[[ -z "$line" ]] && continue
|
||
filter_args+=(--path "$line")
|
||
done < "$exclude_file"
|
||
|
||
if [[ ${#filter_args[@]} -eq 0 ]]; then
|
||
if [[ -n "$tag" ]]; then
|
||
git push "$remote" "refs/tags/$tag"
|
||
else
|
||
git push "$remote" "$branch"
|
||
fi
|
||
return
|
||
fi
|
||
|
||
local remote_url repo_name tmpdir
|
||
remote_url=$(git remote get-url "$remote" 2>/dev/null) || { echo "错误: remote '$remote' 不存在" >&2; return 1; }
|
||
repo_name=$(basename "$(git rev-parse --show-toplevel)")
|
||
tmpdir=$(mktemp -d "/tmp/fei-push-${repo_name}-XXXXXX")
|
||
trap 'rm -rf "$tmpdir"' EXIT
|
||
|
||
local clone_ref="${tag:-$branch}"
|
||
echo ">>> 克隆 ($clone_ref) 到临时目录..."
|
||
git clone --no-hardlinks --branch "$clone_ref" "$(git rev-parse --show-toplevel)" "$tmpdir/repo" --quiet
|
||
|
||
echo ">>> 过滤 ${#filter_args[@]} 个敏感路径..."
|
||
cd "$tmpdir/repo"
|
||
git filter-repo --invert-paths "${filter_args[@]}" --force --quiet
|
||
|
||
git remote add target "$remote_url"
|
||
|
||
if [[ -n "$tag" ]]; then
|
||
git tag -f "$tag"
|
||
echo ">>> 推送 tag $tag..."
|
||
git push target "refs/tags/$tag" --force
|
||
local current_branch
|
||
current_branch=$(git branch --show-current)
|
||
[[ -n "$current_branch" ]] && git push target "$current_branch" --force
|
||
else
|
||
echo ">>> 推送 $branch..."
|
||
git push target "$branch" --force
|
||
fi
|
||
echo ">>> 完成"
|
||
}
|
||
|
||
# ── Pages ──
|
||
|
||
_pages_workflow() {
|
||
# 生成标准 deploy-pages.yml 内容
|
||
cat << 'WORKFLOW'
|
||
name: Deploy Pages
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
|
||
jobs:
|
||
deploy:
|
||
runs-on: linux-arm64
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Deploy to feicode-prod
|
||
run: |
|
||
SITE_NAME="${GITHUB_REPOSITORY##*/}"
|
||
SITE_NAME="${SITE_NAME%-pages}"
|
||
PAGES_DIR="/home/www/pages/${SITE_NAME}"
|
||
|
||
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 feicode-prod "mkdir -p ${PAGES_DIR}"
|
||
|
||
rsync -av --delete \
|
||
--exclude='.git' \
|
||
--exclude='.forgejo' \
|
||
--exclude='.gitignore' \
|
||
-e "ssh -o StrictHostKeyChecking=no" \
|
||
./ feicode-prod:${PAGES_DIR}/
|
||
|
||
echo "✅ Deployed to feicode-prod:${PAGES_DIR}"
|
||
echo "🌐 https://${SITE_NAME}.feibisi.net"
|
||
WORKFLOW
|
||
}
|
||
|
||
cmd_pages() {
|
||
local sub="${1:-list}"
|
||
shift 2>/dev/null || true
|
||
case "$sub" in
|
||
create) _pages_create "$@" ;;
|
||
list) _pages_list "$@" ;;
|
||
open) _pages_open "$@" ;;
|
||
*) echo "用法: fei pages {create|list|open}" ;;
|
||
esac
|
||
}
|
||
|
||
_pages_create() {
|
||
_fei_check_token || return 1
|
||
local site="${1:?用法: fei pages create <站点名>}"
|
||
local repo_name="${site}-pages"
|
||
|
||
# 创建仓库
|
||
local result
|
||
result=$(_fei_post "/orgs/feiCode/repos" \
|
||
-d "$(jq -n --arg n "$repo_name" --arg d "Pages 站点: ${site}.feibisi.net" \
|
||
'{name: $n, description: $d, private: false, auto_init: true, default_branch: "main"}')")
|
||
|
||
local full_name
|
||
full_name=$(echo "$result" | jq -r '.full_name // empty')
|
||
if [[ -z "$full_name" ]]; then
|
||
echo "创建失败: $(echo "$result" | jq -r '.message // "未知错误"')" >&2
|
||
return 1
|
||
fi
|
||
|
||
# 添加 workflow 文件
|
||
local workflow_content
|
||
workflow_content=$(printf '%s' "$(_pages_workflow)" | base64 -w 0)
|
||
|
||
curl -sf -X POST \
|
||
-H "Authorization: token $FEI_TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$(jq -n --arg c "$workflow_content" '{message: "feat: 添加 Pages 自动部署 workflow", content: $c}')" \
|
||
"${FEI_API}/repos/${full_name}/contents/.forgejo/workflows/deploy-pages.yml" > /dev/null
|
||
|
||
echo "✅ Pages 站点已创建"
|
||
echo "仓库: https://${FEI_HOST}/${full_name}"
|
||
echo "站点: https://${site}.feibisi.net"
|
||
echo ""
|
||
echo "克隆后放入静态文件,推送 main 即自动部署:"
|
||
echo " fei clone ${full_name}"
|
||
}
|
||
|
||
_pages_list() {
|
||
_fei_check_token || return 1
|
||
_fei_get "/orgs/feiCode/repos?limit=50" | \
|
||
jq -r '.[] | select(.name | endswith("-pages")) | "\(.name | sub("-pages$";""))\thttps://\(.name | sub("-pages$";"")).feibisi.net\t\(.html_url)"' | column -t -s$'\t'
|
||
# 也检查 feibisi 用户下的
|
||
_fei_get "/user/repos?limit=50" | \
|
||
jq -r '.[] | select(.name | endswith("-pages")) | "\(.name | sub("-pages$";""))\thttps://\(.name | sub("-pages$";"")).feibisi.net\t\(.html_url)"' | column -t -s$'\t'
|
||
}
|
||
|
||
_pages_open() {
|
||
local site="${1:?用法: fei pages open <站点名>}"
|
||
local url="https://${site}.feibisi.net"
|
||
echo "$url"
|
||
xdg-open "$url" 2>/dev/null || open "$url" 2>/dev/null || echo "(请手动打开)"
|
||
}
|
||
|
||
# ── 主入口 ──
|
||
|
||
main() {
|
||
local cmd="${1:-help}"
|
||
shift 2>/dev/null || true
|
||
|
||
case "$cmd" in
|
||
clone) cmd_clone "$@" ;;
|
||
repos) cmd_repos "$@" ;;
|
||
search) cmd_search "$@" ;;
|
||
info) cmd_info "$@" ;;
|
||
web) cmd_web "$@" ;;
|
||
fork) cmd_fork "$@" ;;
|
||
branch) cmd_branch "$@" ;;
|
||
diff) cmd_diff "$@" ;;
|
||
log) cmd_log "$@" ;;
|
||
pr) cmd_pr "$@" ;;
|
||
issue) cmd_issue "$@" ;;
|
||
release) cmd_release "$@" ;;
|
||
label) cmd_label "$@" ;;
|
||
notify) cmd_notify "$@" ;;
|
||
cat) cmd_cat "$@" ;;
|
||
ls) cmd_ls "$@" ;;
|
||
push) cmd_push "$@" ;;
|
||
pages) cmd_pages "$@" ;;
|
||
admin) cmd_admin "$@" ;;
|
||
whoami) cmd_whoami "$@" ;;
|
||
orgs) cmd_orgs "$@" ;;
|
||
help|-h|--help) cmd_help ;;
|
||
--version|-V) echo "fei $FEI_VERSION" ;;
|
||
*) echo "未知命令: $cmd (fei help 查看帮助)" >&2; return 1 ;;
|
||
esac
|
||
}
|
||
|
||
main "$@"
|