# Gitea API Client — Library Package # # Shared Gitea client providing connection-backed API helpers. # Consumers call lib.require("gitea-client") and pass a conn dict # obtained from connections.get("gitea"). # # Entry points: # on_request(req) — REST API routes for browser-side consumers # Exported functions — Starlark consumers via lib.require() # # Modules available (via library permissions): # http — outbound HTTP (api.http) # connections — credential resolution (connections.read) # db — ext_gitea_client_* tables (db.read, db.write) # json — encode/decode (universal) load("star/http_helpers.star", "gitea_get", "gitea_post", "gitea_patch", "auth_headers", "resp") load("star/repos.star", _repos_get = "get_repos") load("star/issues.star", _issues_get = "get_issues", _issue_get = "get_issue", _issue_create = "create_issue", _issue_update = "update_issue", _comment_add = "add_comment") load("star/ci.star", _ci_get = "get_ci_status") # Re-export loaded functions so lib.require() can find them in globals. # Starlark load() names are file-local; explicit assignment makes them global. get_repos = _repos_get get_issues = _issues_get get_issue = _issue_get create_issue = _issue_create update_issue = _issue_update add_comment = _comment_add get_ci_status = _ci_get # ═══════════════════════════════════════════════ # REST API routes (/s/gitea-client/api/*) # ═══════════════════════════════════════════════ def on_request(req): path = req["path"] method = req["method"] conn = _resolve_conn() if conn == None: return resp(400, {"error": "no gitea connection configured"}) if method == "GET" and path == "/repos": repos = get_repos(conn) if repos == None: return resp(502, {"error": "gitea request failed"}) return resp(200, {"data": repos}) elif method == "GET" and path == "/issues": q = req.get("query", {}) owner = _str(q.get("owner", "")) repo = _str(q.get("repo", "")) state = _str(q.get("state", "")) or "open" if not owner or not repo: return resp(400, {"error": "owner and repo query params required"}) issues = get_issues(conn, owner, repo, state) if issues == None: return resp(502, {"error": "gitea request failed"}) return resp(200, {"data": issues, "count": len(issues)}) elif method == "GET" and path.startswith("/issues/"): parts = path[len("/issues/"):].split("/", 2) if len(parts) < 3: return resp(400, {"error": "path must be /issues/:owner/:repo/:number"}) owner, repo, num = parts[0], parts[1], int(parts[2]) issue = get_issue(conn, owner, repo, num) if issue == None: return resp(404, {"error": "issue not found"}) return resp(200, issue) elif method == "POST" and path == "/issues": body = json.decode(req.get("body", "{}")) result = create_issue(conn, body.get("owner", ""), body.get("repo", ""), body.get("title", ""), body.get("body", ""), body.get("labels", "")) if result == None: return resp(502, {"error": "failed to create issue"}) return resp(201, result) elif method == "GET" and path == "/prs": q = req.get("query", {}) owner = _str(q.get("owner", "")) repo = _str(q.get("repo", "")) state = _str(q.get("state", "")) or "open" if not owner or not repo: return resp(400, {"error": "owner and repo query params required"}) prs = get_prs(conn, owner, repo, state) if prs == None: return resp(502, {"error": "gitea request failed"}) return resp(200, {"data": prs, "count": len(prs)}) elif method == "GET" and path.startswith("/ci/"): parts = path[len("/ci/"):].split("/", 2) if len(parts) < 3: return resp(400, {"error": "path must be /ci/:owner/:repo/:ref"}) owner, repo, ref = parts[0], parts[1], parts[2] result = get_ci_status(conn, owner, repo, ref) if result == None: return resp(502, {"error": "CI status request failed"}) return resp(200, result) elif method == "POST" and path == "/sync": return _handle_sync(conn, req) return resp(404, {"error": "not found"}) # ═══════════════════════════════════════════════ # PR helper (not large enough for its own file) # ═══════════════════════════════════════════════ def get_prs(conn, owner, repo, state): """List pull requests for a repository.""" state = state or "open" path = "/repos/" + owner + "/" + repo + "/pulls?state=" + state + "&limit=50" data = gitea_get(conn, path) if data == None: return None items = [] for p in data: items.append({ "number": p.get("number", 0), "title": p.get("title", ""), "state": p.get("state", ""), "head": p.get("head", {}).get("ref", ""), "base": p.get("base", {}).get("ref", ""), "user": (p.get("user") or {}).get("login", ""), "created_at": p.get("created_at", ""), "html_url": p.get("html_url", ""), "mergeable": p.get("mergeable", None), }) return items # ═══════════════════════════════════════════════ # Sync handler — cache repos/issues to db tables # ═══════════════════════════════════════════════ def _handle_sync(conn, req): """Sync repos and issues into local cache tables.""" repos = get_repos(conn) if repos == None: return resp(502, {"error": "failed to fetch repos"}) conn_id = conn.get("id", "default") # Clear and re-insert repos db.exec("DELETE FROM repos WHERE connection_id = ?", [conn_id]) for r in repos: db.exec( "INSERT INTO repos (connection_id, full_name, owner, name, description, html_url, open_issues, synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))", [conn_id, r["full_name"], r["owner"], r["name"], r.get("description", ""), r.get("html_url", ""), r.get("open_issues", 0)] ) # Sync open issues for each repo issue_count = 0 db.exec("DELETE FROM issues WHERE connection_id = ?", [conn_id]) for r in repos: issues = get_issues(conn, r["owner"], r["name"], "open") if issues == None: continue for i in issues: labels_str = ",".join(i.get("labels", [])) db.exec( "INSERT INTO issues (connection_id, repo_full_name, number, title, state, labels, assignee, body, created_at, synced_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))", [conn_id, r["full_name"], i["number"], i["title"], i["state"], labels_str, i.get("assignee", ""), "", i.get("created_at", "")] ) issue_count += 1 return resp(200, {"repos": len(repos), "issues": issue_count}) # ═══════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════ def _resolve_conn(): """Resolve the first available gitea connection.""" return connections.get("gitea") def _str(v): if v == None: return "" return str(v)