Source code for tools.gitea_tools

"""Gitea API tools using per-user access tokens.

Provides repository, issue, PR, commit, file, notification, and star
operations via the Gitea REST API. Supports custom base URLs for
self-hosted instances. Requires set_user_api_key service=gitea.
"""

from __future__ import annotations

import base64
import json
import logging
from typing import Any, TYPE_CHECKING

import aiohttp

if TYPE_CHECKING:
    from tool_context import ToolContext

logger = logging.getLogger(__name__)


async def _gitea_request(
    method: str,
    path: str,
    token: str,
    base_url: str,
    *,
    params: dict[str, Any] | None = None,
    json_body: dict[str, Any] | None = None,
) -> dict[str, Any] | list[Any] | str:
    """Make a Gitea API request. base_url should not include /api/v1."""
    base = base_url.rstrip("/")
    url = f"{base}/api/v1{path}"
    headers = {
        "Authorization": f"token {token}",
        "Accept": "application/json",
    }
    async with aiohttp.ClientSession() as session:
        async with session.request(
            method, url, headers=headers, params=params, json=json_body
        ) as resp:
            if resp.status == 204:
                return {"status": "success", "code": 204}
            body = await resp.text()
            if resp.status >= 400:
                return {"error": f"Gitea API error ({resp.status})", "detail": body[:2000]}
            if not body:
                return {}
            try:
                return json.loads(body)
            except json.JSONDecodeError:
                return body[:4000]


async def _get_credentials(ctx: ToolContext | None) -> tuple[str, str]:
    """Return (token, base_url). Raises if not configured."""
    if ctx is None or ctx.redis is None:
        raise RuntimeError("Context or Redis not available")
    from tools.manage_api_keys import get_gitea_credentials, missing_api_key_error

    creds = await get_gitea_credentials(
        ctx.user_id,
        redis_client=ctx.redis,
        channel_id=getattr(ctx, "channel_id", None),
        fallback_to_pool=True,
        config=getattr(ctx, "config", None),
    )
    if not creds:
        raise RuntimeError(missing_api_key_error("gitea"))
    return creds


def _fmt_repo(r: dict) -> dict:
    return {
        "full_name": r.get("full_name"),
        "description": r.get("description"),
        "private": r.get("private"),
        "fork": r.get("fork"),
        "stars_count": r.get("stars_count"),
        "forks_count": r.get("forks_count"),
        "default_branch": r.get("default_branch"),
        "html_url": r.get("html_url"),
        "updated_at": r.get("updated_at"),
    }


def _fmt_issue(i: dict) -> dict:
    user = i.get("user") or {}
    return {
        "number": i.get("number"),
        "title": i.get("title"),
        "state": i.get("state"),
        "user": user.get("login") if isinstance(user, dict) else None,
        "labels": [l.get("name") for l in i.get("labels", []) if isinstance(l, dict)],
        "created_at": i.get("created_at"),
        "updated_at": i.get("updated_at"),
        "html_url": i.get("html_url"),
        "body": (i.get("body") or "")[:1000],
    }


def _fmt_pr(p: dict) -> dict:
    user = p.get("user") or {}
    return {
        "number": p.get("number"),
        "title": p.get("title"),
        "state": p.get("state"),
        "user": user.get("login") if isinstance(user, dict) else None,
        "draft": p.get("draft"),
        "merged": p.get("merged"),
        "created_at": p.get("created_at"),
        "updated_at": p.get("updated_at"),
        "html_url": p.get("html_url"),
        "additions": p.get("additions"),
        "deletions": p.get("deletions"),
        "changed_files": p.get("changed_files"),
    }


# ---------------------------------------------------------------------------
# Tool handlers
# ---------------------------------------------------------------------------

[docs] async def gitea_list_repos( page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List the authenticated user's Gitea repositories.""" token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request("GET", "/user/repos", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) repos = [_fmt_repo(r) for r in data] if isinstance(data, list) else [] return json.dumps({"count": len(repos), "repos": repos})
[docs] async def gitea_search_repos( q: str, sort: str = "updated", order: str = "desc", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """Search Gitea repositories by keyword.""" token, base_url = await _get_credentials(ctx) params = {"q": q, "sort": sort, "order": order, "page": page, "limit": min(limit, 50)} data = await _gitea_request("GET", "/repos/search", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) items = data.get("data", []) if isinstance(data, dict) else [] repos = [_fmt_repo(r) for r in items] return json.dumps({"count": len(repos), "repos": repos})
[docs] async def gitea_get_repo( owner: str, repo: str, ctx: ToolContext | None = None, ) -> str: """Get details of a Gitea repository.""" token, base_url = await _get_credentials(ctx) data = await _gitea_request("GET", f"/repos/{owner}/{repo}", token, base_url) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
[docs] async def gitea_create_repo( name: str, description: str = "", private: bool = False, auto_init: bool = True, ctx: ToolContext | None = None, ) -> str: """Create a new Gitea repository for the authenticated user.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"name": name} if description: payload["description"] = description payload["private"] = private payload["auto_init"] = auto_init data = await _gitea_request("POST", "/user/repos", token, base_url, json_body=payload) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
[docs] async def gitea_list_issues( owner: str, repo: str, state: str = "open", labels: str = "", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List issues on a Gitea repository.""" token, base_url = await _get_credentials(ctx) params: dict[str, Any] = {"state": state, "page": page, "limit": min(limit, 50)} if labels: params["labels"] = labels data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/issues", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) issues = [_fmt_issue(i) for i in data] if isinstance(data, list) else [] return json.dumps({"count": len(issues), "issues": issues})
[docs] async def gitea_get_issue( owner: str, repo: str, index: int, ctx: ToolContext | None = None, ) -> str: """Get a single issue by index.""" token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/issues/{index}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_create_issue( owner: str, repo: str, title: str, body: str = "", labels: list[int] | None = None, assignees: list[str] | None = None, ctx: ToolContext | None = None, ) -> str: """Create a new issue on a Gitea repository.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"title": title} if body: payload["body"] = body if labels: payload["labels"] = labels if assignees: payload["assignees"] = assignees data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/issues", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_update_issue( owner: str, repo: str, index: int, title: str | None = None, body: str | None = None, state: str | None = None, ctx: ToolContext | None = None, ) -> str: """Update an existing issue.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {} if title is not None: payload["title"] = title if body is not None: payload["body"] = body if state is not None: payload["state"] = state data = await _gitea_request( "PATCH", f"/repos/{owner}/{repo}/issues/{index}", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_issue(data) if isinstance(data, dict) else data)
[docs] async def gitea_list_pull_requests( owner: str, repo: str, state: str = "open", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List pull requests on a Gitea repository.""" token, base_url = await _get_credentials(ctx) params = {"state": state, "page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/pulls", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) prs = [_fmt_pr(p) for p in data] if isinstance(data, list) else [] return json.dumps({"count": len(prs), "pull_requests": prs})
[docs] async def gitea_get_pull_request( owner: str, repo: str, index: int, ctx: ToolContext | None = None, ) -> str: """Get details of a specific pull request.""" token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/pulls/{index}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) result = _fmt_pr(data) if isinstance(data, dict) else {"raw": data} if isinstance(data, dict) and data.get("body"): result["body"] = (data.get("body") or "")[:2000] return json.dumps(result)
[docs] async def gitea_create_pull_request( owner: str, repo: str, title: str, head: str, base: str = "main", body: str = "", ctx: ToolContext | None = None, ) -> str: """Create a new pull request.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"title": title, "head": head, "base": base} if body: payload["body"] = body data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/pulls", token, base_url, json_body=payload ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_pr(data) if isinstance(data, dict) else data)
[docs] async def gitea_merge_pull_request( owner: str, repo: str, index: int, merge_style: str = "merge", delete_branch_after_merge: bool = False, ctx: ToolContext | None = None, ) -> str: """Merge a pull request. merge_style: merge, rebase, rebase-merge, squash.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = {"Do": merge_style} if delete_branch_after_merge: payload["delete_branch_after_merge"] = True data = await _gitea_request( "POST", f"/repos/{owner}/{repo}/pulls/{index}/merge", token, base_url, json_body=payload, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps({"status": "success", "message": "Pull request merged"})
[docs] async def gitea_get_file( owner: str, repo: str, path: str, ref: str = "", ctx: ToolContext | None = None, ) -> str: """Get the contents of a file or directory listing from a Gitea repository.""" token, base_url = await _get_credentials(ctx) params = {} if ref: params["ref"] = ref data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/contents/{path}", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) if isinstance(data, dict) and data.get("type") == "file": content_b64 = data.get("content", "") try: content = base64.b64decode(content_b64).decode("utf-8", errors="replace") except Exception: content = "(binary file)" return json.dumps({ "name": data.get("name"), "path": data.get("path"), "size": data.get("size"), "sha": data.get("sha"), "content": content[:16000], "html_url": data.get("html_url"), }) if isinstance(data, list): entries = [ {"name": e.get("name"), "type": e.get("type"), "path": e.get("path")} for e in data ] return json.dumps({"type": "directory", "entries": entries}) return json.dumps(data if isinstance(data, dict) else {"raw": str(data)[:4000]})
[docs] async def gitea_list_commits( owner: str, repo: str, sha: str = "", page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List commits in a Gitea repository.""" token, base_url = await _get_credentials(ctx) params: dict[str, Any] = {"page": page, "limit": min(limit, 50)} if sha: params["sha"] = sha data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/commits", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) commits = [] for c in (data if isinstance(data, list) else []): commit = c.get("commit") or {} author = commit.get("author") or {} commits.append({ "sha": c.get("sha"), "message": commit.get("message", "")[:500], "author": author.get("name"), "date": author.get("date"), "html_url": c.get("html_url"), }) return json.dumps({"count": len(commits), "commits": commits})
[docs] async def gitea_get_commit( owner: str, repo: str, sha: str, ctx: ToolContext | None = None, ) -> str: """Get a single commit by SHA.""" token, base_url = await _get_credentials(ctx) data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/git/commits/{sha}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) commit = data.get("commit") or {} if isinstance(data, dict) else {} author = commit.get("author") or {} return json.dumps({ "sha": data.get("sha") if isinstance(data, dict) else None, "message": commit.get("message", "")[:2000], "author": author.get("name"), "date": author.get("date"), "html_url": data.get("html_url") if isinstance(data, dict) else None, "stats": data.get("stats") if isinstance(data, dict) else None, })
[docs] async def gitea_list_notifications( all_notifications: bool = False, page: int = 1, limit: int = 20, ctx: ToolContext | None = None, ) -> str: """List the user's Gitea notifications.""" token, base_url = await _get_credentials(ctx) params = {"all": str(all_notifications).lower(), "page": page, "limit": min(limit, 50)} data = await _gitea_request("GET", "/notifications", token, base_url, params=params) if isinstance(data, dict) and "error" in data: return json.dumps(data) notifs = [] for n in (data if isinstance(data, list) else []): subject = n.get("subject") or {} notifs.append({ "id": n.get("id"), "unread": n.get("unread"), "subject_title": subject.get("title"), "subject_type": subject.get("type"), "repository": n.get("repository", {}).get("full_name") if isinstance(n.get("repository"), dict) else None, "updated_at": n.get("updated_at"), }) return json.dumps({"count": len(notifs), "notifications": notifs})
[docs] async def gitea_star_repo( owner: str, repo: str, star: bool = True, ctx: ToolContext | None = None, ) -> str: """Star or unstar a Gitea repository.""" token, base_url = await _get_credentials(ctx) method = "PUT" if star else "DELETE" data = await _gitea_request( method, f"/user/starred/{owner}/{repo}", token, base_url ) if isinstance(data, dict) and "error" in data: return json.dumps(data) action = "starred" if star else "unstarred" return json.dumps({"status": "success", "action": action, "repo": f"{owner}/{repo}"})
[docs] async def gitea_list_branches( owner: str, repo: str, page: int = 1, limit: int = 30, ctx: ToolContext | None = None, ) -> str: """List branches in a Gitea repository.""" token, base_url = await _get_credentials(ctx) params = {"page": page, "limit": min(limit, 50)} data = await _gitea_request( "GET", f"/repos/{owner}/{repo}/branches", token, base_url, params=params ) if isinstance(data, dict) and "error" in data: return json.dumps(data) branches = [ {"name": b.get("name"), "protected": b.get("protected"), "commit_sha": (b.get("commit") or {}).get("sha")} for b in (data if isinstance(data, list) else []) ] return json.dumps({"count": len(branches), "branches": branches})
[docs] async def gitea_create_repo_from_template( template_owner: str, template_repo: str, name: str, description: str = "", private: bool = False, git_content: bool = True, ctx: ToolContext | None = None, ) -> str: """Create a new repository from a template repository.""" token, base_url = await _get_credentials(ctx) payload: dict[str, Any] = { "name": name, "private": private, "git_content": git_content, } if description: payload["description"] = description data = await _gitea_request( "POST", f"/repos/{template_owner}/{template_repo}/generate", token, base_url, json_body=payload, ) if isinstance(data, dict) and "error" in data: return json.dumps(data) return json.dumps(_fmt_repo(data) if isinstance(data, dict) else data)
# --------------------------------------------------------------------------- # Tool registration (multi-tool format) # --------------------------------------------------------------------------- TOOLS = [ { "name": "gitea_list_repos", "description": "List the authenticated user's Gitea repositories. Supports custom base URLs for self-hosted instances.", "parameters": { "type": "object", "properties": { "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page (max 50)"}, }, }, "handler": gitea_list_repos, }, { "name": "gitea_search_repos", "description": "Search Gitea repositories by keyword.", "parameters": { "type": "object", "properties": { "q": {"type": "string", "description": "Search keyword"}, "sort": {"type": "string", "enum": ["alpha", "created", "updated", "size", "stars", "forks", "id"], "description": "Sort order"}, "order": {"type": "string", "enum": ["asc", "desc"], "description": "Sort direction"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["q"], }, "handler": gitea_search_repos, }, { "name": "gitea_get_repo", "description": "Get details of a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, }, "required": ["owner", "repo"], }, "handler": gitea_get_repo, }, { "name": "gitea_create_repo", "description": "Create a new Gitea repository for the authenticated user.", "parameters": { "type": "object", "properties": { "name": {"type": "string", "description": "Repository name"}, "description": {"type": "string", "description": "Repository description"}, "private": {"type": "boolean", "description": "Whether the repo is private"}, "auto_init": {"type": "boolean", "description": "Initialize with README"}, }, "required": ["name"], }, "handler": gitea_create_repo, }, { "name": "gitea_list_issues", "description": "List issues on a Gitea repository with optional filters.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "state": {"type": "string", "enum": ["open", "closed", "all"], "description": "Filter by state"}, "labels": {"type": "string", "description": "Comma-separated label names"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_issues, }, { "name": "gitea_get_issue", "description": "Get a single issue by index.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Issue number"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_get_issue, }, { "name": "gitea_create_issue", "description": "Create a new issue on a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "title": {"type": "string", "description": "Issue title"}, "body": {"type": "string", "description": "Issue body (Markdown)"}, "labels": {"type": "array", "items": {"type": "integer"}, "description": "Label IDs"}, "assignees": {"type": "array", "items": {"type": "string"}, "description": "Assignee usernames"}, }, "required": ["owner", "repo", "title"], }, "handler": gitea_create_issue, }, { "name": "gitea_update_issue", "description": "Update an existing issue (title, body, state).", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Issue number"}, "title": {"type": "string", "description": "New title"}, "body": {"type": "string", "description": "New body"}, "state": {"type": "string", "enum": ["open", "closed"], "description": "New state"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_update_issue, }, { "name": "gitea_list_pull_requests", "description": "List pull requests on a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "state": {"type": "string", "enum": ["open", "closed", "all"], "description": "Filter by state"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_pull_requests, }, { "name": "gitea_get_pull_request", "description": "Get details of a specific pull request.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Pull request number"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_get_pull_request, }, { "name": "gitea_create_pull_request", "description": "Create a new pull request.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "title": {"type": "string", "description": "PR title"}, "head": {"type": "string", "description": "Head branch (or owner:branch for forks)"}, "base": {"type": "string", "description": "Base branch (default: main)"}, "body": {"type": "string", "description": "PR body (Markdown)"}, }, "required": ["owner", "repo", "title", "head"], }, "handler": gitea_create_pull_request, }, { "name": "gitea_merge_pull_request", "description": "Merge a pull request. merge_style: merge, rebase, rebase-merge, squash.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "index": {"type": "integer", "description": "Pull request number"}, "merge_style": {"type": "string", "enum": ["merge", "rebase", "rebase-merge", "squash"], "description": "Merge method"}, "delete_branch_after_merge": {"type": "boolean", "description": "Delete head branch after merge"}, }, "required": ["owner", "repo", "index"], }, "handler": gitea_merge_pull_request, }, { "name": "gitea_get_file", "description": "Get the contents of a file or directory listing from a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "path": {"type": "string", "description": "File or directory path"}, "ref": {"type": "string", "description": "Branch, tag, or commit SHA"}, }, "required": ["owner", "repo", "path"], }, "handler": gitea_get_file, }, { "name": "gitea_list_commits", "description": "List commits in a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "sha": {"type": "string", "description": "Branch or SHA to list from"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_commits, }, { "name": "gitea_get_commit", "description": "Get a single commit by SHA.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "sha": {"type": "string", "description": "Commit SHA or ref"}, }, "required": ["owner", "repo", "sha"], }, "handler": gitea_get_commit, }, { "name": "gitea_list_notifications", "description": "List the user's Gitea notifications (issues, PRs, etc.).", "parameters": { "type": "object", "properties": { "all_notifications": {"type": "boolean", "description": "Include read notifications"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, }, "handler": gitea_list_notifications, }, { "name": "gitea_star_repo", "description": "Star or unstar a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "star": {"type": "boolean", "description": "True to star, false to unstar"}, }, "required": ["owner", "repo"], }, "handler": gitea_star_repo, }, { "name": "gitea_list_branches", "description": "List branches in a Gitea repository.", "parameters": { "type": "object", "properties": { "owner": {"type": "string", "description": "Repository owner"}, "repo": {"type": "string", "description": "Repository name"}, "page": {"type": "integer", "description": "Page number"}, "limit": {"type": "integer", "description": "Results per page"}, }, "required": ["owner", "repo"], }, "handler": gitea_list_branches, }, { "name": "gitea_create_repo_from_template", "description": "Create a new repository from a template repository.", "parameters": { "type": "object", "properties": { "template_owner": {"type": "string", "description": "Template repo owner"}, "template_repo": {"type": "string", "description": "Template repo name"}, "name": {"type": "string", "description": "Name for the new repo"}, "description": {"type": "string", "description": "Description"}, "private": {"type": "boolean", "description": "Whether private"}, "git_content": {"type": "boolean", "description": "Include git content from template"}, }, "required": ["template_owner", "template_repo", "name"], }, "handler": gitea_create_repo_from_template, }, ]