Three-month batch sync from internal repo (~80 commits) covering Tracks F.5a, F.7e, F.8, F.17, F.18, F.X. WordPress media pipeline - Pillow-based optimization, AI image generation (OpenAI / Stability / Replicate / Google Nano Banana / OpenRouter), chunked + resumable uploads, bulk delete/reassign, idempotent retries. Capability discovery (F.7e) - Per-site credential probe + adapters for WordPress / WooCommerce / Gitea, tier-fit unions granted ∪ roles, capability badge UI with HTMX partial re-check, install hint in every companion-unreachable error. Companion plugin overhaul - Renamed wordpress-plugin/airano-mcp-seo-bridge → wordpress-plugin/airano-mcp-bridge. - Eight new endpoints: /capabilities, /bulk-meta, /export, /cache-purge, /transient-flush, /site-health, /audit-hook, /upload-and-attach. - wp.org Plugin Check pass: i18n, WP_Filesystem, scheme allowlist on audit-hook URL. Other - Gitea ergonomics (F.17): batch files, tree, search, compare, releases, fork. - Opportunistic bcrypt upgrade for legacy SHA-256 admin keys (F.8). - n8n refactor: structured errors, capability probe, missing tools backfilled. - Idempotency-Key dedup for AI media upload retries; WP client fast-fails on unreachable sites. Docs - README + CLAUDE.md drop the fixed "633 tools" claim. The total grows with each release; per-plugin approximations + dashboard-surfaced counts replace it. - Tools/Tests badges removed in favour of "Plugins: 10". Deployment - PyPI mirror chain, optional BUILD_HTTP_PROXY, Alpine→Yandex apk mirror, Debian-slim Plan-B Dockerfile, mirror.gcr.io variant. CI - Black + Ruff clean on Python 3.12; pytest tests/ green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1100 lines
42 KiB
Python
1100 lines
42 KiB
Python
"""Repository Handler - manages Gitea repositories, branches, tags, and files"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from plugins.gitea.client import GiteaClient
|
|
|
|
|
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
"""Return tool specifications for ToolGenerator"""
|
|
return [
|
|
# === REPOSITORIES ===
|
|
{
|
|
"name": "list_repositories",
|
|
"method_name": "list_repositories",
|
|
"description": "List Gitea repositories for a user/organization or current user. Returns repository list with metadata.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Repository owner username/org (null for current user repos)",
|
|
},
|
|
"type": {
|
|
"type": "string",
|
|
"description": "Filter by repository type",
|
|
"enum": ["all", "owner", "collaborative", "member"],
|
|
"default": "all",
|
|
},
|
|
"page": {
|
|
"type": "integer",
|
|
"description": "Page number",
|
|
"default": 1,
|
|
"minimum": 1,
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Items per page (1-100)",
|
|
"default": 30,
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
},
|
|
},
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "get_repository",
|
|
"method_name": "get_repository",
|
|
"description": "Get details of a specific Gitea repository. Returns complete repository information.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {
|
|
"type": "string",
|
|
"description": "Repository owner username/org",
|
|
"minLength": 1,
|
|
},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_repository",
|
|
"method_name": "create_repository",
|
|
"description": "Create a new Gitea repository. Can create user or organization repository with optional auto-initialization.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Repository name",
|
|
"minLength": 1,
|
|
"maxLength": 100,
|
|
},
|
|
"description": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Repository description",
|
|
"maxLength": 500,
|
|
},
|
|
"private": {
|
|
"type": "boolean",
|
|
"description": "Make repository private",
|
|
"default": False,
|
|
},
|
|
"auto_init": {
|
|
"type": "boolean",
|
|
"description": "Initialize with README",
|
|
"default": False,
|
|
},
|
|
"gitignores": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Gitignore template name",
|
|
},
|
|
"license": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "License template name",
|
|
},
|
|
"readme": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Readme template",
|
|
},
|
|
"default_branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Default branch name",
|
|
},
|
|
"org": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Create under organization (null for user repo)",
|
|
},
|
|
},
|
|
"required": ["name"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "update_repository",
|
|
"method_name": "update_repository",
|
|
"description": "Update Gitea repository settings like name, description, visibility, and features.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"name": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "New repository name",
|
|
},
|
|
"description": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Repository description",
|
|
},
|
|
"website": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Repository website",
|
|
},
|
|
"private": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Make repository private",
|
|
},
|
|
"archived": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Archive repository",
|
|
},
|
|
"has_issues": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Enable issues",
|
|
},
|
|
"has_wiki": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Enable wiki",
|
|
},
|
|
"default_branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Default branch",
|
|
},
|
|
"allow_merge_commits": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Allow merge commits",
|
|
},
|
|
"allow_rebase": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Allow rebase",
|
|
},
|
|
"allow_squash_merge": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Allow squash merge",
|
|
},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_repository",
|
|
"method_name": "delete_repository",
|
|
"description": "Delete a Gitea repository permanently. This action cannot be undone!",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
# === BRANCHES ===
|
|
{
|
|
"name": "list_branches",
|
|
"method_name": "list_branches",
|
|
"description": "List all branches in a Gitea repository with commit information and protection status.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"page": {
|
|
"type": "integer",
|
|
"description": "Page number",
|
|
"default": 1,
|
|
"minimum": 1,
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Items per page (1-100)",
|
|
"default": 30,
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "get_branch",
|
|
"method_name": "get_branch",
|
|
"description": "Get details of a specific branch including latest commit and protection settings.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"branch": {"type": "string", "description": "Branch name", "minLength": 1},
|
|
},
|
|
"required": ["owner", "repo", "branch"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_branch",
|
|
"method_name": "create_branch",
|
|
"description": "Create a new branch in a Gitea repository from existing branch or commit.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"new_branch_name": {
|
|
"type": "string",
|
|
"description": "New branch name",
|
|
"minLength": 1,
|
|
},
|
|
"old_branch_name": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Source branch (default: default branch)",
|
|
},
|
|
"old_ref_name": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Source commit SHA or ref",
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "new_branch_name"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_branch",
|
|
"method_name": "delete_branch",
|
|
"description": "Delete a branch from a Gitea repository. Cannot delete default branch.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"branch": {
|
|
"type": "string",
|
|
"description": "Branch name to delete",
|
|
"minLength": 1,
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "branch"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
# === TAGS ===
|
|
{
|
|
"name": "list_tags",
|
|
"method_name": "list_tags",
|
|
"description": "List all tags in a Gitea repository with commit information.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"page": {
|
|
"type": "integer",
|
|
"description": "Page number",
|
|
"default": 1,
|
|
"minimum": 1,
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "Items per page (1-100)",
|
|
"default": 30,
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_tag",
|
|
"method_name": "create_tag",
|
|
"description": "Create a new tag in a Gitea repository at specific commit.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"tag_name": {"type": "string", "description": "Tag name", "minLength": 1},
|
|
"message": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Tag message (annotated tag)",
|
|
},
|
|
"target": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Target commit SHA (default: latest)",
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "tag_name"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_tag",
|
|
"method_name": "delete_tag",
|
|
"description": "Delete a tag from a Gitea repository.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"tag": {"type": "string", "description": "Tag name to delete", "minLength": 1},
|
|
},
|
|
"required": ["owner", "repo", "tag"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
# === FILES ===
|
|
{
|
|
"name": "get_file",
|
|
"method_name": "get_file",
|
|
"description": "Get file contents from a Gitea repository. Returns file content (Base64 encoded) and metadata.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "File path in repository",
|
|
"minLength": 1,
|
|
},
|
|
"ref": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Branch/tag/commit (default: default branch)",
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "path"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_file",
|
|
"method_name": "create_file",
|
|
"description": "Create a new file in a Gitea repository with commit message.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "File path to create",
|
|
"minLength": 1,
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "File content (will be Base64 encoded automatically unless content_is_base64=true)",
|
|
},
|
|
"message": {"type": "string", "description": "Commit message", "minLength": 1},
|
|
"branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Target branch (default: default branch)",
|
|
},
|
|
"new_branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Create new branch for this commit",
|
|
},
|
|
"author_name": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Author name",
|
|
},
|
|
"author_email": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Author email",
|
|
},
|
|
"content_is_base64": {
|
|
"type": "boolean",
|
|
"description": "Set to true if content is already Base64 encoded (skip automatic encoding)",
|
|
"default": False,
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "path", "content", "message"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "update_file",
|
|
"method_name": "update_file",
|
|
"description": "Update an existing file in a Gitea repository. Requires current file SHA for conflict detection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "File path to update",
|
|
"minLength": 1,
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "New file content (will be Base64 encoded automatically)",
|
|
},
|
|
"sha": {
|
|
"type": "string",
|
|
"description": "Current file SHA (for conflict detection)",
|
|
"minLength": 1,
|
|
},
|
|
"message": {"type": "string", "description": "Commit message", "minLength": 1},
|
|
"branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Target branch (default: default branch)",
|
|
},
|
|
"new_branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Create new branch for this commit",
|
|
},
|
|
"content_is_base64": {
|
|
"type": "boolean",
|
|
"description": "Set to true if content is already Base64 encoded (skip automatic encoding)",
|
|
"default": False,
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "path", "content", "sha", "message"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "delete_file",
|
|
"method_name": "delete_file",
|
|
"description": "Delete a file from a Gitea repository. Requires current file SHA for conflict detection.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "description": "Repository owner", "minLength": 1},
|
|
"repo": {"type": "string", "description": "Repository name", "minLength": 1},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "File path to delete",
|
|
"minLength": 1,
|
|
},
|
|
"sha": {
|
|
"type": "string",
|
|
"description": "Current file SHA (for conflict detection)",
|
|
"minLength": 1,
|
|
},
|
|
"message": {"type": "string", "description": "Commit message", "minLength": 1},
|
|
"branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Target branch (default: default branch)",
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "path", "sha", "message"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
# === F.17 ergonomics: batch files, tree, search, compare, releases, fork ===
|
|
{
|
|
"name": "create_files",
|
|
"method_name": "create_files",
|
|
"description": (
|
|
"Create or update multiple files in a single commit. Uses Gitea's "
|
|
"``/repos/{owner}/{repo}/contents`` batch endpoint. Per-file operations: "
|
|
"``create`` | ``update`` | ``delete``. For create/update, pass raw UTF-8 "
|
|
"``content`` (default) or a base64 string with ``content_is_base64=true`` "
|
|
"per file. Delete requires each file's current ``sha``."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"message": {
|
|
"type": "string",
|
|
"description": "Single commit message for the whole batch.",
|
|
"minLength": 1,
|
|
},
|
|
"branch": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"new_branch": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "If set, commit on a new branch created from ``branch``.",
|
|
},
|
|
"files": {
|
|
"type": "array",
|
|
"minItems": 1,
|
|
"maxItems": 100,
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operation": {
|
|
"type": "string",
|
|
"enum": ["create", "update", "delete"],
|
|
},
|
|
"path": {"type": "string", "minLength": 1},
|
|
"content": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Required for create/update.",
|
|
},
|
|
"sha": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Required for update/delete.",
|
|
},
|
|
"content_is_base64": {"type": "boolean", "default": False},
|
|
},
|
|
"required": ["operation", "path"],
|
|
},
|
|
},
|
|
},
|
|
"required": ["owner", "repo", "files", "message"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "get_tree",
|
|
"method_name": "get_tree",
|
|
"description": (
|
|
"List the file tree of a Gitea repository. ``sha`` may be a branch "
|
|
"name, tag, or commit (default: HEAD). ``recursive=true`` returns "
|
|
"the entire tree in one payload. Use this instead of issuing many "
|
|
"``get_file`` calls when you don't know the paths ahead of time."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"sha": {"type": "string", "default": "HEAD"},
|
|
"recursive": {"type": "boolean", "default": False},
|
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
|
"per_page": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 100,
|
|
"default": 100,
|
|
},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "search_code",
|
|
"method_name": "search_code",
|
|
"description": (
|
|
"Search code inside Gitea. When ``owner`` and ``repo`` are set, the "
|
|
"search scopes to that repository; otherwise it's an instance-wide "
|
|
"code search. Returns Gitea's raw ``{ok, data: [...]}`` shape."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"keyword": {"type": "string", "minLength": 1},
|
|
"owner": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"repo": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
|
"per_page": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 50,
|
|
"default": 30,
|
|
},
|
|
},
|
|
"required": ["keyword"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "compare",
|
|
"method_name": "compare",
|
|
"description": (
|
|
"Compare two commits / branches / tags in a Gitea repository. Returns "
|
|
"commits + file diffs between ``base`` and ``head`` (the Gitea "
|
|
"``compare`` endpoint)."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"base": {"type": "string", "minLength": 1},
|
|
"head": {"type": "string", "minLength": 1},
|
|
},
|
|
"required": ["owner", "repo", "base", "head"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "list_releases",
|
|
"method_name": "list_releases",
|
|
"description": "List releases of a Gitea repository (paginated).",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"page": {"type": "integer", "minimum": 1, "default": 1},
|
|
"per_page": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 50,
|
|
"default": 30,
|
|
},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_release",
|
|
"method_name": "create_release",
|
|
"description": (
|
|
"Create a release (tag + release metadata) on a Gitea repository. "
|
|
"Set ``draft`` to hide it from users until published."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"tag_name": {"type": "string", "minLength": 1},
|
|
"name": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"body": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"target_commitish": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Branch or commit to tag (default: default branch).",
|
|
},
|
|
"draft": {"type": "boolean", "default": False},
|
|
"prerelease": {"type": "boolean", "default": False},
|
|
},
|
|
"required": ["owner", "repo", "tag_name"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
{
|
|
"name": "fork_repository",
|
|
"method_name": "fork_repository",
|
|
"description": (
|
|
"Fork a Gitea repository. Without ``organization``, forks under the "
|
|
"calling user. ``name`` optionally renames the fork."
|
|
),
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"owner": {"type": "string", "minLength": 1},
|
|
"repo": {"type": "string", "minLength": 1},
|
|
"organization": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
"name": {"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
},
|
|
"required": ["owner", "repo"],
|
|
},
|
|
"scope": "write",
|
|
},
|
|
]
|
|
|
|
|
|
async def list_repositories(
|
|
client: GiteaClient, owner: str | None = None, type: str = "all", page: int = 1, limit: int = 30
|
|
) -> str:
|
|
"""List Gitea repositories"""
|
|
repos = await client.list_repositories(owner=owner, page=page, limit=limit)
|
|
result = {"success": True, "count": len(repos), "repositories": repos}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def get_repository(client: GiteaClient, owner: str, repo: str) -> str:
|
|
"""Get repository details"""
|
|
repository = await client.get_repository(owner, repo)
|
|
result = {"success": True, "repository": repository}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def create_repository(
|
|
client: GiteaClient,
|
|
name: str,
|
|
description: str | None = None,
|
|
private: bool = False,
|
|
auto_init: bool = False,
|
|
gitignores: str | None = None,
|
|
license: str | None = None,
|
|
readme: str | None = None,
|
|
default_branch: str | None = None,
|
|
org: str | None = None,
|
|
) -> str:
|
|
"""Create a new repository"""
|
|
data = {
|
|
"name": name,
|
|
"description": description,
|
|
"private": private,
|
|
"auto_init": auto_init,
|
|
"gitignores": gitignores,
|
|
"license": license,
|
|
"readme": readme,
|
|
"default_branch": default_branch,
|
|
}
|
|
repository = await client.create_repository(data, org=org)
|
|
result = {
|
|
"success": True,
|
|
"message": f"Repository '{name}' created successfully",
|
|
"repository": repository,
|
|
}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def update_repository(client: GiteaClient, owner: str, repo: str, **kwargs) -> str:
|
|
"""Update repository settings"""
|
|
# Build update data from kwargs
|
|
data = {k: v for k, v in kwargs.items() if v is not None and k not in ["owner", "repo"]}
|
|
|
|
repository = await client.update_repository(owner, repo, data)
|
|
result = {
|
|
"success": True,
|
|
"message": f"Repository '{owner}/{repo}' updated successfully",
|
|
"repository": repository,
|
|
}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def delete_repository(client: GiteaClient, owner: str, repo: str) -> str:
|
|
"""Delete a repository"""
|
|
await client.delete_repository(owner, repo)
|
|
result = {"success": True, "message": f"Repository '{owner}/{repo}' deleted successfully"}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
# Branch operations
|
|
async def list_branches(
|
|
client: GiteaClient, owner: str, repo: str, page: int = 1, limit: int = 30
|
|
) -> str:
|
|
"""List repository branches"""
|
|
branches = await client.list_branches(owner, repo, page=page, limit=limit)
|
|
result = {"success": True, "count": len(branches), "branches": branches}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def get_branch(client: GiteaClient, owner: str, repo: str, branch: str) -> str:
|
|
"""Get branch details"""
|
|
branch_info = await client.get_branch(owner, repo, branch)
|
|
result = {"success": True, "branch": branch_info}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def create_branch(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
new_branch_name: str,
|
|
old_branch_name: str | None = None,
|
|
old_ref_name: str | None = None,
|
|
) -> str:
|
|
"""Create a new branch"""
|
|
data = {
|
|
"new_branch_name": new_branch_name,
|
|
"old_branch_name": old_branch_name,
|
|
"old_ref_name": old_ref_name,
|
|
}
|
|
branch = await client.create_branch(owner, repo, data)
|
|
result = {
|
|
"success": True,
|
|
"message": f"Branch '{new_branch_name}' created successfully",
|
|
"branch": branch,
|
|
}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def delete_branch(client: GiteaClient, owner: str, repo: str, branch: str) -> str:
|
|
"""Delete a branch"""
|
|
await client.delete_branch(owner, repo, branch)
|
|
result = {"success": True, "message": f"Branch '{branch}' deleted successfully"}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
# Tag operations
|
|
async def list_tags(
|
|
client: GiteaClient, owner: str, repo: str, page: int = 1, limit: int = 30
|
|
) -> str:
|
|
"""List repository tags"""
|
|
tags = await client.list_tags(owner, repo, page=page, limit=limit)
|
|
result = {"success": True, "count": len(tags), "tags": tags}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def create_tag(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
tag_name: str,
|
|
message: str | None = None,
|
|
target: str | None = None,
|
|
) -> str:
|
|
"""Create a new tag"""
|
|
data = {"tag_name": tag_name, "message": message, "target": target}
|
|
tag = await client.create_tag(owner, repo, data)
|
|
result = {"success": True, "message": f"Tag '{tag_name}' created successfully", "tag": tag}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def delete_tag(client: GiteaClient, owner: str, repo: str, tag: str) -> str:
|
|
"""Delete a tag"""
|
|
await client.delete_tag(owner, repo, tag)
|
|
result = {"success": True, "message": f"Tag '{tag}' deleted successfully"}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
# File operations
|
|
async def get_file(
|
|
client: GiteaClient, owner: str, repo: str, path: str, ref: str | None = None
|
|
) -> str:
|
|
"""Get file contents"""
|
|
file_data = await client.get_file(owner, repo, path, ref=ref)
|
|
result = {"success": True, "file": file_data}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def create_file(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
path: str,
|
|
content: str,
|
|
message: str,
|
|
branch: str | None = None,
|
|
new_branch: str | None = None,
|
|
author_name: str | None = None,
|
|
author_email: str | None = None,
|
|
content_is_base64: bool = False,
|
|
) -> str:
|
|
"""Create a new file"""
|
|
data = {
|
|
"content": content,
|
|
"message": message,
|
|
"branch": branch,
|
|
"new_branch": new_branch,
|
|
"content_is_base64": content_is_base64,
|
|
"author": {},
|
|
}
|
|
|
|
if author_name:
|
|
data["author"]["name"] = author_name
|
|
if author_email:
|
|
data["author"]["email"] = author_email
|
|
|
|
file_result = await client.create_file(owner, repo, path, data)
|
|
result = {
|
|
"success": True,
|
|
"message": f"File '{path}' created successfully",
|
|
"file": file_result,
|
|
}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def update_file(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
path: str,
|
|
content: str,
|
|
sha: str,
|
|
message: str,
|
|
branch: str | None = None,
|
|
new_branch: str | None = None,
|
|
content_is_base64: bool = False,
|
|
) -> str:
|
|
"""Update an existing file"""
|
|
data = {
|
|
"content": content,
|
|
"sha": sha,
|
|
"message": message,
|
|
"branch": branch,
|
|
"new_branch": new_branch,
|
|
"content_is_base64": content_is_base64,
|
|
}
|
|
|
|
file_result = await client.update_file(owner, repo, path, data)
|
|
result = {
|
|
"success": True,
|
|
"message": f"File '{path}' updated successfully",
|
|
"file": file_result,
|
|
}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
async def delete_file(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
path: str,
|
|
sha: str,
|
|
message: str,
|
|
branch: str | None = None,
|
|
) -> str:
|
|
"""Delete a file from repository"""
|
|
await client.delete_file(owner, repo, path, sha, message, branch)
|
|
result = {"success": True, "message": f"File '{path}' deleted successfully"}
|
|
return json.dumps(result, indent=2)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# F.17 ergonomics: batch files, tree, search, compare, releases, fork
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def create_files(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
files: list[dict[str, Any]],
|
|
message: str,
|
|
branch: str | None = None,
|
|
new_branch: str | None = None,
|
|
) -> str:
|
|
"""Apply a batch of create / update / delete file operations in a single commit.
|
|
|
|
Each entry in ``files`` is normalised so Gitea's batch endpoint
|
|
receives base64-encoded content with the ``content_is_base64`` flag
|
|
stripped (the server expects base64). Validation:
|
|
|
|
* ``operation`` is ``create`` / ``update`` / ``delete``.
|
|
* ``path`` required on every entry.
|
|
* ``content`` required on create / update.
|
|
* ``sha`` required on update / delete (current file SHA).
|
|
"""
|
|
prepared: list[dict[str, Any]] = []
|
|
errors: list[dict[str, Any]] = []
|
|
|
|
for idx, f in enumerate(files or []):
|
|
op = (f.get("operation") or "").strip().lower()
|
|
path = f.get("path")
|
|
if op not in {"create", "update", "delete"}:
|
|
errors.append({"index": idx, "path": path, "error": f"invalid_operation:{op!r}"})
|
|
continue
|
|
if not path:
|
|
errors.append({"index": idx, "error": "missing_path"})
|
|
continue
|
|
if op in {"update", "delete"} and not f.get("sha"):
|
|
errors.append({"index": idx, "path": path, "error": f"missing_sha_for_{op}"})
|
|
continue
|
|
|
|
entry: dict[str, Any] = {"operation": op, "path": path}
|
|
if op in {"create", "update"}:
|
|
content = f.get("content")
|
|
if content is None:
|
|
errors.append({"index": idx, "path": path, "error": "missing_content"})
|
|
continue
|
|
try:
|
|
entry["content"] = client._normalise_file_content(
|
|
content, bool(f.get("content_is_base64", False))
|
|
)
|
|
except ValueError as exc:
|
|
errors.append({"index": idx, "path": path, "error": str(exc)})
|
|
continue
|
|
if f.get("sha"):
|
|
entry["sha"] = f["sha"]
|
|
if f.get("from_path"): # Gitea also supports rename via from_path
|
|
entry["from_path"] = f["from_path"]
|
|
prepared.append(entry)
|
|
|
|
if errors:
|
|
return json.dumps(
|
|
{
|
|
"success": False,
|
|
"error": "validation_failed",
|
|
"errors": errors,
|
|
"total": len(files or []),
|
|
"prepared": len(prepared),
|
|
},
|
|
indent=2,
|
|
)
|
|
|
|
payload: dict[str, Any] = {"message": message, "files": prepared}
|
|
if branch:
|
|
payload["branch"] = branch
|
|
if new_branch:
|
|
payload["new_branch"] = new_branch
|
|
|
|
result = await client.change_files(owner, repo, payload)
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"message": f"Batched {len(prepared)} file operation(s) into one commit",
|
|
"result": result,
|
|
},
|
|
indent=2,
|
|
)
|
|
|
|
|
|
async def get_tree(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
sha: str = "HEAD",
|
|
recursive: bool = False,
|
|
page: int = 1,
|
|
per_page: int = 100,
|
|
) -> str:
|
|
tree = await client.get_tree(
|
|
owner, repo, sha, recursive=recursive, page=page, per_page=per_page
|
|
)
|
|
return json.dumps({"success": True, "tree": tree}, indent=2)
|
|
|
|
|
|
async def search_code(
|
|
client: GiteaClient,
|
|
keyword: str,
|
|
owner: str | None = None,
|
|
repo: str | None = None,
|
|
page: int = 1,
|
|
per_page: int = 30,
|
|
) -> str:
|
|
result = await client.search_code(
|
|
keyword=keyword, owner=owner, repo=repo, page=page, per_page=per_page
|
|
)
|
|
return json.dumps({"success": True, "result": result}, indent=2)
|
|
|
|
|
|
async def compare(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
base: str,
|
|
head: str,
|
|
) -> str:
|
|
diff = await client.compare(owner, repo, base, head)
|
|
return json.dumps({"success": True, "compare": diff}, indent=2)
|
|
|
|
|
|
async def list_releases(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
page: int = 1,
|
|
per_page: int = 30,
|
|
) -> str:
|
|
releases = await client.list_releases(owner, repo, page=page, per_page=per_page)
|
|
return json.dumps({"success": True, "releases": releases}, indent=2)
|
|
|
|
|
|
async def create_release(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
tag_name: str,
|
|
name: str | None = None,
|
|
body: str | None = None,
|
|
target_commitish: str | None = None,
|
|
draft: bool = False,
|
|
prerelease: bool = False,
|
|
) -> str:
|
|
data: dict[str, Any] = {
|
|
"tag_name": tag_name,
|
|
"draft": draft,
|
|
"prerelease": prerelease,
|
|
}
|
|
if name is not None:
|
|
data["name"] = name
|
|
if body is not None:
|
|
data["body"] = body
|
|
if target_commitish is not None:
|
|
data["target_commitish"] = target_commitish
|
|
release = await client.create_release(owner, repo, data)
|
|
return json.dumps({"success": True, "release": release}, indent=2)
|
|
|
|
|
|
async def fork_repository(
|
|
client: GiteaClient,
|
|
owner: str,
|
|
repo: str,
|
|
organization: str | None = None,
|
|
name: str | None = None,
|
|
) -> str:
|
|
fork = await client.fork_repository(owner, repo, organization=organization, name=name)
|
|
return json.dumps({"success": True, "fork": fork}, indent=2)
|