From 95fdd9a43054e365f4c90804b08817cb9de25fba Mon Sep 17 00:00:00 2001 From: monkeycode-ai Date: Fri, 13 Mar 2026 05:12:51 +0000 Subject: [PATCH] Add gitea-manager skill for Gitea repository operations Co-authored-by: monkeycode-ai --- gitea-manager/SKILL.md | 339 ++++++++++++++++++++++++ gitea-manager/gitea-manager.skill | Bin 0 -> 5701 bytes gitea-manager/gitea_client.py | 423 ++++++++++++++++++++++++++++++ 3 files changed, 762 insertions(+) create mode 100644 gitea-manager/SKILL.md create mode 100644 gitea-manager/gitea-manager.skill create mode 100644 gitea-manager/gitea_client.py diff --git a/gitea-manager/SKILL.md b/gitea-manager/SKILL.md new file mode 100644 index 0000000..ee62f36 --- /dev/null +++ b/gitea-manager/SKILL.md @@ -0,0 +1,339 @@ +--- +name: gitea-manager +description: 管理 Gitea 仓库的全能工具,支持仓库创建删除、文件操作、分支管理、Issue/PR 管理、组织和用户操作、仓库设置配置等。使用场景:(1) 创建或删除 Gitea 仓库 (2) 管理仓库文件和目录 (3) 操作分支和保护规则 (4) 创建和管理 Issue 和 Pull Request (5) 管理组织和团队成员 (6) 配置 Webhook 和 Deploy Key (7) 搜索仓库和 Issue。必需环境变量:GITEA_URL(实例地址)和 GITEA_TOKEN(API 令牌)。 +--- + +# Gitea Manager + +## 快速开始 + +### 环境配置 + +使用此 skill 前需要配置 Gitea 连接信息: + +```bash +export GITEA_URL="https://gitea.example.com" +export GITEA_TOKEN="your-api-token-here" +``` + +可以通过以下方式获取 API Token: +1. 登录 Gitea +2. 进入「设置」→「应用」 +3. 创建新的 API Token + +### 初始化客户端 + +在 Python 代码中使用: + +```python +import sys +sys.path.append("/root/.claude/skills/gitea-manager/scripts") +from gitea_client import GiteaClient + +client = GiteaClient() +``` + +或指定自定义配置: + +```python +client = GiteaClient(url="https://gitea.example.com", token="your-token") +``` + +## 仓库操作 + +### 列出仓库 + +```python +repos = client.list_my_repos() +for r in repos: + print(f"{r['full_name']}: {r.get('description', '')}") +``` + +### 创建仓库 + +```python +repo = client.create_repo( + name="my-new-repo", + description="这是一个新仓库", + private=False, + auto_init=True +) +``` + +### 在组织下创建仓库 + +```python +repo = client.create_org_repo( + org="my-org", + name="org-repo", + description="组织仓库", + private=True +) +``` + +### 获取仓库信息 + +```python +repo = client.get_repo("owner", "repo-name") +``` + +### 删除仓库 + +```python +client.delete_repo("owner", "repo-name") +``` + +## 文件操作 + +### 读取文件内容 + +```python +content = client.get_file_content("owner", "repo", "path/to/file.txt", ref="main") +``` + +### 获取文件信息 + +```python +info = client.get_file_info("owner", "repo", "path/to/file.txt", ref="main") +``` + +### 创建或更新文件 + +```python +client.create_or_update_file( + owner="owner", + repo="repo", + path="new-file.txt", + content="文件内容", + message="添加新文件", + branch="main", + email="user@example.com", + name="Username" +) +``` + +### 删除文件 + +```python +info = client.get_file_info("owner", "repo", "file.txt") +client.delete_file( + owner="owner", + repo="repo", + path="file.txt", + message="删除文件", + branch="main", + sha=info["sha"], + email="user@example.com", + name="Username" +) +``` + +### 列出目录 + +```python +files = client.list_directory("owner", "repo", "path/to/dir", ref="main") +``` + +## 分支操作 + +### 列出分支 + +```python +branches = client.list_branches("owner", "repo") +``` + +### 获取分支信息 + +```python +branch = client.get_branch("owner", "repo", "main") +``` + +### 创建分支 + +```python +client.create_branch("owner", "repo", "new-branch", "main") +``` + +### 删除分支 + +```python +client.delete_branch("owner", "repo", "old-branch") +``` + +### 设置分支保护 + +```python +client.set_protected_branch( + owner="owner", + repo="repo", + branch="main", + enable_push=True, + enable_push_whitelist=["developers"], + require_pull_request=True, + require_approvals=2 +) +``` + +## Issue 操作 + +### 列出 Issue + +```python +issues = client.list_issues("owner", "repo", state="open") +``` + +### 创建 Issue + +```python +issue = client.create_issue( + owner="owner", + repo="repo", + title="Bug: 登录失败", + body="详细描述问题..." +) +``` + +### 更新 Issue + +```python +client.update_issue("owner", "repo", issue_index, title="新标题", state="closed") +``` + +### 添加评论 + +```python +comment = client.create_issue_comment("owner", "repo", issue_index, "这是我的评论") +``` + +## Pull Request 操作 + +### 列出 PR + +```python +pulls = client.list_pulls("owner", "repo", state="open") +``` + +### 创建 PR + +```python +pr = client.create_pull( + owner="owner", + repo="repo", + title="Add new feature", + body="PR 描述...", + head="feature-branch", + base="main" +) +``` + +### 合并 PR + +```python +client.merge_pull("owner", "repo", pr_index, merge_style="merge") +``` + +## 组织操作 + +### 列出组织 + +```python +orgs = client.list_orgs() +``` + +### 列出组织仓库 + +```python +repos = client.list_org_repos("my-org") +``` + +### 创建组织 + +```python +org = client.create_org( + username="new-org", + email="org@example.com" +) +``` + +### 列出组织团队 + +```python +teams = client.list_org_teams("my-org") +``` + +## Webhook 操作 + +### 列出 Webhook + +```python +hooks = client.list_hooks("owner", "repo") +``` + +### 创建 Webhook + +```python +hook = client.create_hook( + owner="owner", + repo="repo", + config={"url": "https://example.com/webhook", "secret": "secret"}, + events=["push", "issue", "pull_request"], + active=True +) +``` + +### 删除 Webhook + +```python +client.delete_hook("owner", "repo", hook_id) +``` + +## Deploy Key 操作 + +### 列出 Deploy Key + +```python +keys = client.list_deploy_keys("owner", "repo") +``` + +### 创建 Deploy Key + +```python +key = client.create_deploy_key( + owner="owner", + repo="repo", + title="my-deploy-key", + key="ssh-rsa AAAA...", + read_only=True +) +``` + +### 删除 Deploy Key + +```python +client.delete_deploy_key("owner", "repo", key_id) +``` + +## 搜索操作 + +### 搜索仓库 + +```python +repos = client.search_repos("keyword", limit=20) +``` + +### 搜索 Issue + +```python +issues = client.search_issues("owner", "repo", "bug", limit=20) +``` + +## 命令行使用 + +脚本也支持命令行调用: + +```bash +python gitea_client.py list-repos +python gitea_client.py get-repo owner/repo +python gitea_client.py create-repo my-repo "描述" +python gitea_client.py delete-repo owner/repo +``` diff --git a/gitea-manager/gitea-manager.skill b/gitea-manager/gitea-manager.skill new file mode 100644 index 0000000000000000000000000000000000000000..16c762995f2230b5a87c24586612f91e69a8c73e GIT binary patch literal 5701 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 S82_}P{`HT4b2jNe)qepWBl@=h literal 0 HcmV?d00001 diff --git a/gitea-manager/gitea_client.py b/gitea-manager/gitea_client.py new file mode 100644 index 0000000..90258cb --- /dev/null +++ b/gitea-manager/gitea_client.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +"""Gitea API Client - 封装 Gitea REST API 的所有操作""" + +import os +import json +import requests +from typing import Optional, Dict, List, Any + + +class GiteaClient: + """Gitea API 客户端""" + + def __init__(self, url: Optional[str] = None, token: Optional[str] = None): + self.url = url or os.environ.get("GITEA_URL", "http://localhost:3000") + self.token = token or os.environ.get("GITEA_TOKEN", "") + self.session = requests.Session() + self.session.headers.update({ + "Authorization": f"token {self.token}", + "Content-Type": "application/json", + "Accept": "application/json" + }) + if not self.token: + raise ValueError("GITEA_TOKEN 环境变量未设置") + + def _request(self, method: str, path: str, **kwargs) -> requests.Response: + """发送 API 请求""" + url = f"{self.url.rstrip('/')}{path}" + response = self.session.request(method, url, **kwargs) + response.raise_for_status() + return response + + # ==================== 用户操作 ==================== + + def get_current_user(self) -> Dict: + """获取当前认证用户信息""" + return self._request("GET", "/user").json() + + def get_user(self, username: str) -> Dict: + """获取指定用户信息""" + return self._request("GET", f"/users/{username}").json() + + def list_user_repos(self, username: str, page: int = 1, limit: int = 50) -> List[Dict]: + """列出用户的所有仓库""" + return self._request("GET", f"/users/{username}/repos", params={"page": page, "limit": limit}).json() + + def create_user(self, email: str, username: str, password: str, **kwargs) -> Dict: + """创建用户(需要管理员权限)""" + data = {"email": email, "username": username, "password": password} + data.update(kwargs) + return self._request("POST", "/admin/users", json=data).json() + + def delete_user(self, username: str) -> None: + """删除用户(需要管理员权限)""" + self._request("DELETE", f"/admin/users/{username}") + + # ==================== 组织操作 ==================== + + def list_orgs(self, page: int = 1, limit: int = 50) -> List[Dict]: + """列出当前用户所属组织""" + return self._request("GET", "/user/orgs", params={"page": page, "limit": limit}).json() + + def get_org(self, username: str) -> Dict: + """获取组织信息""" + return self._request("GET", f"/orgs/{username}").json() + + def create_org(self, username: str, email: str, **kwargs) -> Dict: + """创建组织""" + data = {"username": username, "email": email} + data.update(kwargs) + return self._request("POST", "/admin/orgs", json=data).json() + + def list_org_repos(self, org: str, page: int = 1, limit: int = 50) -> List[Dict]: + """列出组织的所有仓库""" + return self._request("GET", f"/orgs/{org}/repos", params={"page": page, "limit": limit}).json() + + # ==================== 仓库操作 ==================== + + def list_my_repos(self, page: int = 1, limit: int = 50) -> List[Dict]: + """列出当前用户的仓库""" + return self._request("GET", "/user/repos", params={"page": page, "limit": limit}).json() + + def get_repo(self, owner: str, repo: str) -> Dict: + """获取仓库信息""" + return self._request("GET", f"/repos/{owner}/{repo}").json() + + def create_repo(self, name: str, description: str = "", private: bool = False, + auto_init: bool = True, **kwargs) -> Dict: + """创建仓库""" + data = { + "name": name, + "description": description, + "private": private, + "auto_init": auto_init + } + data.update(kwargs) + return self._request("POST", "/user/repos", json=data).json() + + def create_org_repo(self, org: str, name: str, description: str = "", + private: bool = False, auto_init: bool = True, **kwargs) -> Dict: + """在组织下创建仓库""" + data = { + "name": name, + "description": description, + "private": private, + "auto_init": auto_init + } + data.update(kwargs) + return self._request("POST", f"/orgs/{org}/repos", json=data).json() + + def delete_repo(self, owner: str, repo: str) -> None: + """删除仓库""" + self._request("DELETE", f"/repos/{owner}/{repo}") + + def get_repo_info(self, owner: str, repo: str) -> Dict: + """获取仓库详细信息""" + return self._request("GET", f"/repos/{owner}/{repo}").json() + + def update_repo(self, owner: str, repo: str, **kwargs) -> Dict: + """更新仓库设置""" + return self._request("PATCH", f"/repos/{owner}/{repo}", json=kwargs).json() + + # ==================== 分支操作 ==================== + + def list_branches(self, owner: str, repo: str, page: int = 1, limit: int = 50) -> List[Dict]: + """列出仓库分支""" + return self._request("GET", f"/repos/{owner}/{repo}/branches", + params={"page": page, "limit": limit}).json() + + def get_branch(self, owner: str, repo: str, branch: str) -> Dict: + """获取分支信息""" + return self._request("GET", f"/repos/{owner}/{repo}/branches/{branch}").json() + + def create_branch(self, owner: str, repo: str, branch_name: str, from_branch: str) -> Dict: + """创建分支""" + data = { + "new_branch_name": branch_name, + "from_branch": from_branch + } + return self._request("POST", f"/repos/{owner}/{repo}/branches", json=data).json() + + def delete_branch(self, owner: str, repo: str, branch: str) -> None: + """删除分支""" + self._request("DELETE", f"/repos/{owner}/{repo}/branches/{branch}") + + def list_protected_branches(self, owner: str, repo: str) -> List[Dict]: + """列出受保护分支""" + return self._request("GET", f"/repos/{owner}/{repo}/protected_branches").json() + + def get_protected_branch(self, owner: str, repo: str, branch: str) -> Dict: + """获取受保护分支规则""" + return self._request("GET", f"/repos/{owner}/{repo}/protected_branches/{branch}").json() + + def set_protected_branch(self, owner: str, repo: str, branch: str, **kwargs) -> Dict: + """设置分支保护规则""" + return self._request("PUT", f"/repos/{owner}/{repo}/protected_branches/{branch}", json=kwargs).json() + + # ==================== 文件操作 ==================== + + def get_file_content(self, owner: str, repo: str, path: str, ref: str = "main") -> str: + """获取文件内容(Base64 编码)""" + response = self._request("GET", f"/repos/{owner}/{repo}/raw/{path}", params={"ref": ref}) + import base64 + return base64.b64encode(response.content).decode('utf-8') + + def get_file_info(self, owner: str, repo: str, path: str, ref: str = "main") -> Dict: + """获取文件信息""" + return self._request("GET", f"/repos/{owner}/{repo}/contents/{path}", params={"ref": ref}).json() + + def create_or_update_file(self, owner: str, repo: str, path: str, content: str, + message: str, branch: str, email: str, name: str) -> Dict: + """创建或更新文件""" + import base64 + data = { + "content": base64.b64encode(content.encode()).decode('utf-8'), + "message": message, + "branch": branch, + "author": {"email": email, "name": name}, + "committer": {"email": email, "name": name} + } + return self._request("POST", f"/repos/{owner}/{repo}/contents/{path}", json=data).json() + + def delete_file(self, owner: str, repo: str, path: str, message: str, + branch: str, sha: str, email: str, name: str) -> Dict: + """删除文件""" + data = { + "message": message, + "branch": branch, + "sha": sha, + "author": {"email": email, "name": name}, + "committer": {"email": email, "name": name} + } + return self._request("DELETE", f"/repos/{owner}/{repo}/contents/{path}", json=data).json() + + def list_directory(self, owner: str, repo: str, path: str = "", + ref: str = "main", page: int = 1, limit: int = 100) -> List[Dict]: + """列出目录内容""" + return self._request("GET", f"/repos/{owner}/{repo}/contents/{path}", + params={"ref": ref, "page": page, "limit": limit}).json() + + # ==================== Issue 操作 ==================== + + def list_issues(self, owner: str, repo: str, state: str = "open", + page: int = 1, limit: int = 50, **kwargs) -> List[Dict]: + """列出仓库的 Issue""" + params = {"state": state, "page": page, "limit": limit} + params.update(kwargs) + return self._request("GET", f"/repos/{owner}/{repo}/issues", params=params).json() + + def get_issue(self, owner: str, repo: str, index: int) -> Dict: + """获取 Issue 详情""" + return self._request("GET", f"/repos/{owner}/{repo}/issues/{index}").json() + + def create_issue(self, owner: str, repo: str, title: str, body: str = "", + **kwargs) -> Dict: + """创建 Issue""" + data = {"title": title, "body": body} + data.update(kwargs) + return self._request("POST", f"/repos/{owner}/{repo}/issues", json=data).json() + + def update_issue(self, owner: str, repo: str, index: int, **kwargs) -> Dict: + """更新 Issue""" + return self._request("PATCH", f"/repos/{owner}/{repo}/issues/{index}", json=kwargs).json() + + def close_issue(self, owner: str, repo: str, index: int) -> Dict: + """关闭 Issue""" + return self.update_issue(owner, repo, index, state="closed") + + def list_issue_comments(self, owner: str, repo: str, issue_index: int, + page: int = 1, limit: int = 50) -> List[Dict]: + """列出 Issue 的评论""" + return self._request("GET", f"/repos/{owner}/{repo}/issues/{issue_index}/comments", + params={"page": page, "limit": limit}).json() + + def create_issue_comment(self, owner: str, repo: str, issue_index: int, body: str) -> Dict: + """创建 Issue 评论""" + return self._request("POST", f"/repos/{owner}/{repo}/issues/{issue_index}/comments", + json={"body": body}).json() + + # ==================== Pull Request 操作 ==================== + + def list_pulls(self, owner: str, repo: str, state: str = "open", + page: int = 1, limit: int = 50) -> List[Dict]: + """列出 Pull Request""" + return self._request("GET", f"/repos/{owner}/{repo}/pulls", + params={"state": state, "page": page, "limit": limit}).json() + + def get_pull(self, owner: str, repo: str, index: int) -> Dict: + """获取 Pull Request 详情""" + return self._request("GET", f"/repos/{owner}/{repo}/pulls/{index}").json() + + def create_pull(self, owner: str, repo: str, title: str, body: str, + head: str, base: str, **kwargs) -> Dict: + """创建 Pull Request""" + data = { + "title": title, + "body": body, + "head": head, + "base": base + } + data.update(kwargs) + return self._request("POST", f"/repos/{owner}/{repo}/pulls", json=data).json() + + def update_pull(self, owner: str, repo: str, index: int, **kwargs) -> Dict: + """更新 Pull Request""" + return self._request("PATCH", f"/repos/{owner}/{repo}/pulls/{index}", json=kwargs).json() + + def merge_pull(self, owner: str, repo: str, index: int, + merge_message: str = "", merge_style: str = "merge") -> Dict: + """合并 Pull Request""" + data = { + "merge_commit_message": merge_message, + "merge_style": merge_style + } + return self._request("POST", f"/repos/{owner}/{repo}/pulls/{index}/merge", json=data).json() + + def list_pull_comments(self, owner: str, repo: str, index: int) -> List[Dict]: + """列出 Pull Request 的评论""" + return self._request("GET", f"/repos/{owner}/{repo}/issues/{index}/comments").json() + + # ==================== Webhook 操作 ==================== + + def list_hooks(self, owner: str, repo: str) -> List[Dict]: + """列出仓库的 Webhook""" + return self._request("GET", f"/repos/{owner}/{repo}/hooks").json() + + def create_hook(self, owner: str, repo: str, config: Dict, events: List[str], + active: bool = True) -> Dict: + """创建 Webhook""" + data = { + "type": "gitea", + "config": config, + "events": events, + "active": active + } + return self._request("POST", f"/repos/{owner}/{repo}/hooks", json=data).json() + + def delete_hook(self, owner: str, repo: str, hook_id: int) -> None: + """删除 Webhook""" + self._request("DELETE", f"/repos/{owner}/{repo}/hooks/{hook_id}") + + # ==================== Deploy Key 操作 ==================== + + def list_deploy_keys(self, owner: str, repo: str) -> List[Dict]: + """列出仓库的 Deploy Key""" + return self._request("GET", f"/repos/{owner}/{repo}/keys").json() + + def create_deploy_key(self, owner: str, repo: str, title: str, key: str, + read_only: bool = True) -> Dict: + """创建 Deploy Key""" + data = { + "title": title, + "key": key, + "read_only": read_only + } + return self._request("POST", f"/repos/{owner}/{repo}/keys", json=data).json() + + def delete_deploy_key(self, owner: str, repo: str, key_id: int) -> None: + """删除 Deploy Key""" + self._request("DELETE", f"/repos/{owner}/{repo}/keys/{key_id}") + + # ==================== 标签操作 ==================== + + def list_tags(self, owner: str, repo: str, page: int = 1, limit: int = 50) -> List[Dict]: + """列出仓库的标签""" + return self._request("GET", f"/repos/{owner}/{repo}/tags", + params={"page": page, "limit": limit}).json() + + def create_tag(self, owner: str, repo: str, tag_name: str, target: str, + message: str = "", **kwargs) -> Dict: + """创建标签""" + data = { + "tag_name": tag_name, + "target": target, + "message": message + } + data.update(kwargs) + return self._request("POST", f"/repos/{owner}/{repo}/tags", json=data).json() + + # ==================== 搜索操作 ==================== + + def search_repos(self, query: str, page: int = 1, limit: int = 50, **kwargs) -> List[Dict]: + """搜索仓库""" + params = {"q": query, "page": page, "limit": limit} + params.update(kwargs) + return self._request("GET", "/repos/search", params=params).json().get("data", []) + + def search_issues(self, owner: str, repo: str, query: str, + page: int = 1, limit: int = 50) -> List[Dict]: + """搜索 Issue""" + params = {"q": query, "page": page, "limit": limit} + return self._request("GET", f"/repos/{owner}/{repo}/issues/search", params=params).json() + + # ==================== 团队操作 ==================== + + def list_org_teams(self, org: str) -> List[Dict]: + """列出组织的团队""" + return self._request("GET", f"/orgs/{org}/teams").json() + + def create_team(self, org: str, name: str, permission: str = "read", **kwargs) -> Dict: + """创建团队""" + data = {"name": name, "permission": permission} + data.update(kwargs) + return self._request("POST", f"/orgs/{org}/teams", json=data).json() + + def add_team_member(self, org: str, team: str, username: str) -> None: + """添加团队成员""" + self._request("PUT", f"/orgs/{org}/teams/{team}/members/{username}") + + def remove_team_member(self, org: str, team: str, username: str) -> None: + """移除团队成员""" + self._request("DELETE", f"/orgs/{org}/teams/{team}/members/{username}") + + def add_team_repo(self, org: str, team: str, owner: str, repo: str) -> None: + """添加团队仓库""" + self._request("PUT", f"/orgs/{org}/teams/{team}/repos/{owner}/{repo}") + + def remove_team_repo(self, org: str, team: str, owner: str, repo: str) -> None: + """移除团队仓库""" + self._request("DELETE", f"/orgs/{org}/teams/{team}/repos/{owner}/{repo}") + + +def main(): + """命令行入口""" + import argparse + + parser = argparse.ArgumentParser(description="Gitea API Client") + parser.add_argument("--url", default=None, help="Gitea URL") + parser.add_argument("--token", default=None, help="API Token") + parser.add_argument("command", help="要执行的命令") + parser.add_argument("args", nargs="*", help="命令参数") + + args = parser.parse_args() + + client = GiteaClient(url=args.url, token=args.token) + + if args.command == "list-repos": + repos = client.list_my_repos() + for r in repos: + print(f"{r['full_name']}: {r.get('description', '')}") + + elif args.command == "get-repo": + owner, repo = args.args[0].split("/") + r = client.get_repo(owner, repo) + print(json.dumps(r, indent=2)) + + elif args.command == "create-repo": + name = args.args[0] + description = args.args[1] if len(args.args) > 1 else "" + r = client.create_repo(name, description) + print(json.dumps(r, indent=2)) + + elif args.command == "delete-repo": + owner, repo = args.args[0].split("/") + client.delete_repo(owner, repo) + print(f"Repository {owner}/{repo} deleted") + + else: + print(f"Unknown command: {args.command}") + parser.print_help() + + +if __name__ == "__main__": + main()