#!/usr/bin/env python3 """ gitea_api.py — Pure stdlib Gitea API helper for https://git.shumengya.top Usage: python scripts/gitea_api.py repos list python scripts/gitea_api.py repos create [--desc DESC] [--private] python scripts/gitea_api.py repos delete python scripts/gitea_api.py repos exists python scripts/gitea_api.py issues list [--repo owner/name] python scripts/gitea_api.py issues create --repo owner/name --title TITLE [--body BODY] Zero external dependencies (stdlib only: urllib, json, argparse, sys, os). """ from __future__ import annotations import argparse import json import os import sys import urllib.error import urllib.request from typing import Any # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- DEFAULT_URL = "https://git.shumengya.top" DEFAULT_TOKEN = "dbc5dfe55b19e9a32f685a97f126cafc2ef76449" DEFAULT_OWNER = "shumengya" # Allow override via environment variables GITEA_URL = os.environ.get("GITEA_URL", DEFAULT_URL).rstrip("/") GITEA_TOKEN = os.environ.get("GITEA_TOKEN", DEFAULT_TOKEN) API_BASE = f"{GITEA_URL}/api/v1" # --------------------------------------------------------------------------- # HTTP helpers # --------------------------------------------------------------------------- def _headers() -> dict[str, str]: return { "Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json", "Accept": "application/json", } def _request( method: str, path: str, data: dict[str, Any] | None = None, ) -> tuple[int, Any]: """Send HTTP request to Gitea API. Returns (status_code, parsed_json).""" url = f"{API_BASE}{path}" body = json.dumps(data).encode("utf-8") if data else None req = urllib.request.Request( url, data=body, headers=_headers(), method=method, ) try: with urllib.request.urlopen(req) as resp: raw = resp.read() status = resp.status if raw.strip(): return status, json.loads(raw.decode("utf-8")) return status, None except urllib.error.HTTPError as e: try: detail = json.loads(e.read().decode("utf-8")) except Exception: detail = {"message": str(e)} return e.code, detail except urllib.error.URLError as e: print(f"[ERROR] Network error: {e.reason}", file=sys.stderr) sys.exit(1) # --------------------------------------------------------------------------- # Repository commands # --------------------------------------------------------------------------- def cmd_repos_list(_args: argparse.Namespace) -> None: status, data = _request("GET", f"/users/{DEFAULT_OWNER}/repos?limit=100") if status != 200: print(f"[ERROR] Failed to list repos (HTTP {status}): {data}") sys.exit(1) if not data: print("No repositories found.") return print(f"{'Name':<30} {'Private':<8} {'Description'}") print("-" * 80) for r in sorted(data, key=lambda x: x.get("name", "")): name = r.get("name", "?") private = "yes" if r.get("private") else "no" desc = (r.get("description") or "")[:45] print(f"{name:<30} {private:<8} {desc}") def cmd_repos_create(args: argparse.Namespace) -> None: payload: dict[str, Any] = { "name": args.name, "description": args.desc or "", "private": args.private, "auto_init": False, } status, data = _request("POST", "/user/repos", payload) if status in (201, 200): print(f"[OK] Repository created: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}") print(f" HTTPS: {GITEA_URL}/{DEFAULT_OWNER}/{args.name}.git") print(f" SSH: ssh://git@{GITEA_URL.split('://')[1]}:8022/{DEFAULT_OWNER}/{args.name}.git") elif status == 409: print(f"[WARN] Repository '{args.name}' already exists.", file=sys.stderr) else: print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr) sys.exit(1) def cmd_repos_delete(args: argparse.Namespace) -> None: print(f"Deleting repository {DEFAULT_OWNER}/{args.name}...", end=" ") status, data = _request("DELETE", f"/repos/{DEFAULT_OWNER}/{args.name}") if status in (204, 200): print("[OK] Deleted.") else: print(f"\n[ERROR] HTTP {status}: {data}", file=sys.stderr) sys.exit(1) def cmd_repos_exists(args: argparse.Namespace) -> None: status, _data = _request("GET", f"/repos/{DEFAULT_OWNER}/{args.name}") if status == 200: print(f"[YES] Repository '{args.name}' exists.") elif status == 404: print(f"[NO] Repository '{args.name}' does not exist.") else: print(f"[UNKNOWN] HTTP {status}", file=sys.stderr) # --------------------------------------------------------------------------- # Issue commands # --------------------------------------------------------------------------- def cmd_issues_list(args: argparse.Namespace) -> None: repo = args.repo or f"{DEFAULT_OWNER}/sproutclaw" status, data = _request("GET", f"/repos/{repo}/issues?state={args.state}&limit=50") if status != 200: print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr) sys.exit(1) if not data: print(f"No {args.state} issues in {repo}.") return print(f"{'#':<6} {'State':<8} {'Title'}") print("-" * 70) for issue in data: # PRs also appear in issues endpoint; skip them if issue.get("pull_request"): continue num = issue.get("number", "?") state = issue.get("state", "?") title = issue.get("title", "?") print(f"#{num:<5} {state:<8} {title}") def cmd_issues_create(args: argparse.Namespace) -> None: if not args.repo: print("[ERROR] --repo is required (e.g. shumengya/sproutclaw)", file=sys.stderr) sys.exit(1) payload: dict[str, Any] = {"title": args.title} if args.body: payload["body"] = args.body status, data = _request("POST", f"/repos/{args.repo}/issues", payload) if status in (201, 200): url = data.get("html_url", "?") print(f"[OK] Issue created: {url}") else: print(f"[ERROR] HTTP {status}: {data}", file=sys.stderr) sys.exit(1) # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Gitea API helper — 纯 Python 标准库,零外部依赖。" ) sub = parser.add_subparsers(dest="resource", required=True) # --- repos --- repos_parser = sub.add_parser("repos", help="仓库操作") repos_commands = repos_parser.add_subparsers(dest="action", required=True) # repos list repos_commands.add_parser("list", help="列出所有仓库") # repos create rc = repos_commands.add_parser("create", help="创建新仓库") rc.add_argument("name", help="仓库名称") rc.add_argument("--desc", "-d", default="", help="仓库描述") rc.add_argument("--private", action="store_true", help="创建私有仓库") # repos delete rd = repos_commands.add_parser("delete", help="删除仓库") rd.add_argument("name", help="仓库名称") # repos exists re = repos_commands.add_parser("exists", help="检查仓库是否存在") re.add_argument("name", help="仓库名称") # --- issues --- issues_parser = sub.add_parser("issues", help="Issue 操作") issues_commands = issues_parser.add_subparsers(dest="action", required=True) # issues list il = issues_commands.add_parser("list", help="列出 Issue") il.add_argument("--repo", "-r", help="仓库路径 (owner/name)") il.add_argument("--state", default="open", choices=["open", "closed", "all"]) # issues create ic = issues_commands.add_parser("create", help="创建 Issue") ic.add_argument("--repo", "-r", required=True, help="仓库路径 (owner/name)") ic.add_argument("--title", "-t", required=True, help="Issue 标题") ic.add_argument("--body", "-b", default="", help="Issue 描述") return parser def main() -> None: parser = build_parser() args = parser.parse_args() if not GITEA_TOKEN: print("[ERROR] GITEA_TOKEN environment variable is not set and no default available.") sys.exit(1) dispatch = { ("repos", "list"): cmd_repos_list, ("repos", "create"): cmd_repos_create, ("repos", "delete"): cmd_repos_delete, ("repos", "exists"): cmd_repos_exists, ("issues", "list"): cmd_issues_list, ("issues", "create"): cmd_issues_create, } key = (args.resource, args.action) if key in dispatch: dispatch[key](args) else: parser.print_help() sys.exit(1) if __name__ == "__main__": main()