From 8f130b2cbde510a9aeecac84340866a8e4ea6e9e Mon Sep 17 00:00:00 2001 From: Ricel Leite Date: Thu, 19 Feb 2026 00:40:38 -0300 Subject: [PATCH] feat: add Gitea integration service and API endpoints --- app/api/__init__.py | 2 + app/api/gitea.py | 109 ++++++++++++++++++++++++++++++++++++++ app/services/gitea.py | 119 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 4 files changed, 231 insertions(+) create mode 100644 app/api/gitea.py create mode 100644 app/services/gitea.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 4297a9f..e06081a 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -6,6 +6,7 @@ from .integrations import router as integrations_router from .issues import router as issues_router from .webhooks import router as webhooks_router from .reports import router as reports_router +from .gitea import router as gitea_router api_router = APIRouter() api_router.include_router(auth_router, prefix="/auth", tags=["Authentication"]) @@ -15,3 +16,4 @@ api_router.include_router(integrations_router, prefix="/integrations", tags=["In api_router.include_router(issues_router, prefix="/issues", tags=["Issues"]) api_router.include_router(webhooks_router, prefix="/webhooks", tags=["Webhooks"]) api_router.include_router(reports_router, prefix="/reports", tags=["Reports"]) +api_router.include_router(gitea_router, prefix="/gitea", tags=["Gitea"]) diff --git a/app/api/gitea.py b/app/api/gitea.py new file mode 100644 index 0000000..f2f08a0 --- /dev/null +++ b/app/api/gitea.py @@ -0,0 +1,109 @@ +"""Gitea integration endpoints.""" +from typing import List, Dict, Any +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from app.core.database import get_db +from app.models.integration import Integration, IntegrationType +from app.models.organization import OrganizationMember +from app.api.deps import require_role +from app.services.gitea import GiteaService + +router = APIRouter() + +@router.get("/repos", response_model=List[Dict[str, Any]]) +async def list_repositories( + org_id: int, + member: OrganizationMember = Depends(require_role("viewer")), + db: AsyncSession = Depends(get_db) +): + """List Gitea repositories for organization.""" + # Get Gitea integration + result = await db.execute( + select(Integration) + .where(Integration.organization_id == org_id) + .where(Integration.type == IntegrationType.GITLAB) # Using GITLAB as Gitea + .where(Integration.status == "ACTIVE") + ) + integration = result.scalar_one_or_none() + + if not integration or not integration.base_url or not integration.api_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Gitea integration not configured" + ) + + gitea = GiteaService(integration.base_url, integration.api_key) + repos = await gitea.list_repositories("startdata") # Fixed owner for now + + return repos + +@router.get("/repos/{owner}/{repo}") +async def get_repository( + org_id: int, + owner: str, + repo: str, + member: OrganizationMember = Depends(require_role("viewer")), + db: AsyncSession = Depends(get_db) +): + """Get repository details.""" + result = await db.execute( + select(Integration) + .where(Integration.organization_id == org_id) + .where(Integration.type == IntegrationType.GITLAB) + .where(Integration.status == "ACTIVE") + ) + integration = result.scalar_one_or_none() + + if not integration or not integration.base_url or not integration.api_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Gitea integration not configured" + ) + + gitea = GiteaService(integration.base_url, integration.api_key) + repo_data = await gitea.get_repo(owner, repo) + + if not repo_data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Repository not found" + ) + + return repo_data + +@router.get("/repos/{owner}/{repo}/file") +async def get_file( + org_id: int, + owner: str, + repo: str, + path: str, + ref: str = "main", + member: OrganizationMember = Depends(require_role("viewer")), + db: AsyncSession = Depends(get_db) +): + """Get file content from repository.""" + result = await db.execute( + select(Integration) + .where(Integration.organization_id == org_id) + .where(Integration.type == IntegrationType.GITLAB) + .where(Integration.status == "ACTIVE") + ) + integration = result.scalar_one_or_none() + + if not integration or not integration.base_url or not integration.api_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Gitea integration not configured" + ) + + gitea = GiteaService(integration.base_url, integration.api_key) + content = await gitea.get_file(owner, repo, path, ref) + + if content is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="File not found" + ) + + return {"path": path, "content": content, "ref": ref} diff --git a/app/services/gitea.py b/app/services/gitea.py new file mode 100644 index 0000000..e41ab07 --- /dev/null +++ b/app/services/gitea.py @@ -0,0 +1,119 @@ +"""Gitea integration service.""" +import httpx +from typing import Optional, Dict, Any, List + +class GiteaService: + def __init__(self, base_url: str, token: str): + self.base_url = base_url.rstrip('/') + self.token = token + self.headers = { + "Authorization": f"token {token}", + "Content-Type": "application/json" + } + + async def get_repo(self, owner: str, repo: str) -> Optional[Dict[str, Any]]: + """Get repository details.""" + async with httpx.AsyncClient() as client: + try: + response = await client.get( + f"{self.base_url}/api/v1/repos/{owner}/{repo}", + headers=self.headers, + timeout=10.0 + ) + response.raise_for_status() + return response.json() + except Exception: + return None + + async def get_file(self, owner: str, repo: str, path: str, ref: str = "main") -> Optional[str]: + """Get file content from repository.""" + async with httpx.AsyncClient() as client: + try: + response = await client.get( + f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={ref}", + headers=self.headers, + timeout=10.0 + ) + response.raise_for_status() + data = response.json() + # Gitea returns base64 encoded content + import base64 + return base64.b64decode(data.get("content", "")).decode("utf-8") + except Exception: + return None + + async def create_branch(self, owner: str, repo: str, branch: str, from_branch: str = "main") -> bool: + """Create a new branch.""" + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"{self.base_url}/api/v1/repos/{owner}/{repo}/branches", + headers=self.headers, + json={"new_branch_name": branch, "old_branch_name": from_branch}, + timeout=10.0 + ) + response.raise_for_status() + return True + except Exception: + return False + + async def update_file(self, owner: str, repo: str, path: str, content: str, + message: str, branch: str, sha: Optional[str] = None) -> bool: + """Update file in repository.""" + import base64 + async with httpx.AsyncClient() as client: + try: + payload = { + "content": base64.b64encode(content.encode()).decode(), + "message": message, + "branch": branch + } + if sha: + payload["sha"] = sha + + response = await client.put( + f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}", + headers=self.headers, + json=payload, + timeout=10.0 + ) + response.raise_for_status() + return True + except Exception: + return False + + async def create_pull_request(self, owner: str, repo: str, title: str, + body: str, head: str, base: str = "main") -> Optional[str]: + """Create a pull request.""" + async with httpx.AsyncClient() as client: + try: + response = await client.post( + f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls", + headers=self.headers, + json={ + "title": title, + "body": body, + "head": head, + "base": base + }, + timeout=10.0 + ) + response.raise_for_status() + pr_data = response.json() + return pr_data.get("html_url") + except Exception: + return None + + async def list_repositories(self, owner: str) -> List[Dict[str, Any]]: + """List repositories for owner.""" + async with httpx.AsyncClient() as client: + try: + response = await client.get( + f"{self.base_url}/api/v1/users/{owner}/repos", + headers=self.headers, + timeout=10.0 + ) + response.raise_for_status() + return response.json() + except Exception: + return [] diff --git a/requirements.txt b/requirements.txt index 5b232e1..21285c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ bcrypt==3.2.2 httpx==0.26.0 python-multipart==0.0.6 email-validator>=2.0.0 +httpx==0.25.2