|
@@ -1,9 +1,13 @@
|
|
|
import httpx
|
|
import httpx
|
|
|
from app.config import settings
|
|
from app.config import settings
|
|
|
import logging
|
|
import logging
|
|
|
|
|
+from typing import Optional
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
+# Default timeout for API requests (seconds)
|
|
|
|
|
+_DEFAULT_TIMEOUT = 30.0
|
|
|
|
|
+
|
|
|
|
|
|
|
|
class GogsClient:
|
|
class GogsClient:
|
|
|
def __init__(self):
|
|
def __init__(self):
|
|
@@ -11,11 +15,81 @@ class GogsClient:
|
|
|
self.token = settings.GOGS_TOKEN
|
|
self.token = settings.GOGS_TOKEN
|
|
|
self.headers = {"Authorization": f"token {self.token}"}
|
|
self.headers = {"Authorization": f"token {self.token}"}
|
|
|
|
|
|
|
|
- async def get_manifest(self, owner: str, repo: str, commit_id: str) -> str:
|
|
|
|
|
- """Fetch manifest.yaml raw content from a specific commit."""
|
|
|
|
|
- # Gogs raw file URL format: /{owner}/{repo}/raw/{ref}/{path}
|
|
|
|
|
- url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/raw/{commit_id}/manifest.yaml"
|
|
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+ # Repository discovery
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ async def list_user_repos(self) -> list[dict]:
|
|
|
|
|
+ """Fetch *all* repositories visible to the authenticated user.
|
|
|
|
|
+
|
|
|
|
|
+ Gogs paginates with `?page=N` (default 20 per page).
|
|
|
|
|
+ We iterate until an empty page is returned.
|
|
|
|
|
+ """
|
|
|
|
|
+ repos: list[dict] = []
|
|
|
|
|
+ page = 1
|
|
|
|
|
+
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
|
|
+ while True:
|
|
|
|
|
+ url = f"{self.base_url}/api/v1/user/repos?page={page}&limit=50"
|
|
|
|
|
+ resp = await client.get(url, headers=self.headers)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ batch = resp.json()
|
|
|
|
|
+ if not batch:
|
|
|
|
|
+ break
|
|
|
|
|
+ repos.extend(batch)
|
|
|
|
|
+ page += 1
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"Fetched {len(repos)} repositories in total")
|
|
|
|
|
+ return repos
|
|
|
|
|
+
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+ # Webhook management
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ async def list_repo_webhooks(self, owner: str, repo: str) -> list[dict]:
|
|
|
|
|
+ """List all webhooks configured on a repository."""
|
|
|
|
|
+ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/hooks"
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
|
|
+ resp = await client.get(url, headers=self.headers)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ return resp.json()
|
|
|
|
|
+
|
|
|
|
|
+ async def create_repo_webhook(
|
|
|
|
|
+ self,
|
|
|
|
|
+ owner: str,
|
|
|
|
|
+ repo: str,
|
|
|
|
|
+ webhook_url: str,
|
|
|
|
|
+ secret: str = "",
|
|
|
|
|
+ events: Optional[list[str]] = None,
|
|
|
|
|
+ ) -> dict:
|
|
|
|
|
+ """Create a push webhook on a repository.
|
|
|
|
|
+
|
|
|
|
|
+ Returns the created webhook payload from Gogs.
|
|
|
|
|
+ """
|
|
|
|
|
+ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/hooks"
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "type": "gogs",
|
|
|
|
|
+ "config": {
|
|
|
|
|
+ "url": webhook_url,
|
|
|
|
|
+ "content_type": "json",
|
|
|
|
|
+ "secret": secret,
|
|
|
|
|
+ },
|
|
|
|
|
+ "events": events or ["push"],
|
|
|
|
|
+ "active": True,
|
|
|
|
|
+ }
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
|
|
+ resp = await client.post(url, headers=self.headers, json=payload)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ return resp.json()
|
|
|
|
|
+
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+ # Manifest / file operations (existing)
|
|
|
|
|
+ # ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ async def get_manifest(self, owner: str, repo: str, ref: str) -> str | None:
|
|
|
|
|
+ """Fetch manifest.yaml raw content from a given ref (commit / branch)."""
|
|
|
|
|
+ url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/raw/{ref}/manifest.yaml"
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
resp = await client.get(url, headers=self.headers)
|
|
resp = await client.get(url, headers=self.headers)
|
|
|
if resp.status_code == 404:
|
|
if resp.status_code == 404:
|
|
|
return None
|
|
return None
|
|
@@ -25,7 +99,7 @@ class GogsClient:
|
|
|
async def get_tree(self, owner: str, repo: str, commit_id: str, path: str = "") -> list:
|
|
async def get_tree(self, owner: str, repo: str, commit_id: str, path: str = "") -> list:
|
|
|
"""Get the file tree of a repository."""
|
|
"""Get the file tree of a repository."""
|
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={commit_id}"
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={commit_id}"
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
resp = await client.get(url, headers=self.headers)
|
|
resp = await client.get(url, headers=self.headers)
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
return resp.json()
|
|
return resp.json()
|
|
@@ -37,7 +111,7 @@ class GogsClient:
|
|
|
"""
|
|
"""
|
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{file_path}?ref={commit_id}"
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{file_path}?ref={commit_id}"
|
|
|
try:
|
|
try:
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
resp = await client.get(url, headers=self.headers)
|
|
resp = await client.get(url, headers=self.headers)
|
|
|
if resp.status_code == 404:
|
|
if resp.status_code == 404:
|
|
|
return None
|
|
return None
|
|
@@ -71,7 +145,7 @@ class GogsClient:
|
|
|
"""Recursively fetch directory contents using contents API."""
|
|
"""Recursively fetch directory contents using contents API."""
|
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={commit_id}"
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}?ref={commit_id}"
|
|
|
try:
|
|
try:
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
resp = await client.get(url, headers=self.headers)
|
|
resp = await client.get(url, headers=self.headers)
|
|
|
if resp.status_code == 404:
|
|
if resp.status_code == 404:
|
|
|
logger.warning(f"Directory not found: {path}")
|
|
logger.warning(f"Directory not found: {path}")
|
|
@@ -103,7 +177,7 @@ class GogsClient:
|
|
|
"""Download raw file content."""
|
|
"""Download raw file content."""
|
|
|
# Gogs raw file URL format: /{owner}/{repo}/raw/{ref}/{path}
|
|
# Gogs raw file URL format: /{owner}/{repo}/raw/{ref}/{path}
|
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/raw/{commit_id}/{file_path}"
|
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/raw/{commit_id}/{file_path}"
|
|
|
- async with httpx.AsyncClient() as client:
|
|
|
|
|
|
|
+ async with httpx.AsyncClient(timeout=_DEFAULT_TIMEOUT) as client:
|
|
|
resp = await client.get(url, headers=self.headers)
|
|
resp = await client.get(url, headers=self.headers)
|
|
|
resp.raise_for_status()
|
|
resp.raise_for_status()
|
|
|
return resp.content
|
|
return resp.content
|