From 017ab4951ad1d4e6aa21665fa1887cbafe5f161e Mon Sep 17 00:00:00 2001 From: null Date: Tue, 19 May 2026 21:34:11 -0500 Subject: [PATCH] feat: git label import --- backend/app/api/forgejo_repositories.py | 1 + backend/app/models/forgejo_repositories.py | 6 ++++ backend/app/schemas/forgejo_repositories.py | 1 + backend/app/services/forgejo_client.py | 26 +++++++++++++++ backend/app/services/forgejo_issue_sync.py | 24 ++++++++++++++ ...e5f7_add_labels_to_forgejo_repositories.py | 33 +++++++++++++++++++ frontend/src/lib/api-forgejo.ts | 8 +++++ 7 files changed, 99 insertions(+) create mode 100644 backend/migrations/versions/a1b2c3d4e5f7_add_labels_to_forgejo_repositories.py diff --git a/backend/app/api/forgejo_repositories.py b/backend/app/api/forgejo_repositories.py index ea9a127..9685872 100644 --- a/backend/app/api/forgejo_repositories.py +++ b/backend/app/api/forgejo_repositories.py @@ -339,6 +339,7 @@ def _mask_repository(repository: ForgejoRepository, connection: ForgejoConnectio "active": repository.active, "connection": _create_connection_info(connection) if connection is not None else None, "has_webhook_secret": bool(repository.webhook_secret), + "labels": repository.labels if repository.labels is not None else [], "last_sync_at": repository.last_sync_at, "last_sync_error": repository.last_sync_error, "created_at": repository.created_at, diff --git a/backend/app/models/forgejo_repositories.py b/backend/app/models/forgejo_repositories.py index 573aab3..5eb1f7e 100644 --- a/backend/app/models/forgejo_repositories.py +++ b/backend/app/models/forgejo_repositories.py @@ -5,6 +5,8 @@ from __future__ import annotations from datetime import datetime from uuid import UUID, uuid4 +from sqlalchemy import Column +from sqlalchemy import JSON from sqlmodel import Field from app.core.time import utcnow @@ -27,6 +29,10 @@ class ForgejoRepository(QueryModel, table=True): default_branch: str = Field(default="main") active: bool = Field(default=True) webhook_secret: str | None = Field(default=None) + labels: list[dict[str, object]] = Field( + default_factory=list, + sa_column=Column(JSON, nullable=False, server_default="[]"), + ) last_sync_at: datetime | None = Field(default=None) last_sync_error: str | None = Field(default=None) created_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/schemas/forgejo_repositories.py b/backend/app/schemas/forgejo_repositories.py index 44e89c5..df0df7c 100644 --- a/backend/app/schemas/forgejo_repositories.py +++ b/backend/app/schemas/forgejo_repositories.py @@ -108,6 +108,7 @@ class ForgejoRepositoryRead(ForgejoRepositoryBase): connection_id: UUID connection: ForgejoRepositoryConnectionInfo has_webhook_secret: bool = False + labels: list[dict[str, object]] = Field(default_factory=list) last_sync_at: datetime | None last_sync_error: str | None created_at: datetime diff --git a/backend/app/services/forgejo_client.py b/backend/app/services/forgejo_client.py index 9862ad9..b383522 100644 --- a/backend/app/services/forgejo_client.py +++ b/backend/app/services/forgejo_client.py @@ -157,6 +157,32 @@ class ForgejoAPIClient: response.raise_for_status() return response.json() + async def list_labels( + self, + owner: str, + repo: str, + limit: int = 50, + ) -> list[dict[str, object]]: + """ + List all labels defined on a repository. + + Args: + owner: Repository owner + repo: Repository name + limit: Max labels to fetch per page + + Returns: + List of label dicts with id, name, color, description. + """ + client = await self._get_client() + response = await client.get( + f"/api/v1/repos/{owner}/{repo}/labels", + params={"limit": limit, "page": 1}, + ) + response.raise_for_status() + data = response.json() + return list(data) if isinstance(data, list) else [] + async def list_user_repos( self, limit: int = 50, diff --git a/backend/app/services/forgejo_issue_sync.py b/backend/app/services/forgejo_issue_sync.py index e316fd8..cc23c63 100644 --- a/backend/app/services/forgejo_issue_sync.py +++ b/backend/app/services/forgejo_issue_sync.py @@ -146,6 +146,30 @@ class IssueSyncService: break current_page += 1 + # Sync repository label catalog + try: + async with get_forgejo_client(connection) as client: + fetched_labels = await client.list_labels( + owner=repository.owner, + repo=repository.repo, + ) + repository.labels = [ + { + "id": lbl.get("id"), + "name": lbl.get("name", ""), + "color": lbl.get("color", ""), + "description": lbl.get("description") or "", + } + for lbl in fetched_labels + if lbl.get("name") + ] + except Exception as exc: + logger.warning( + "label_sync_failed", + repository_id=str(repository_id), + error=str(exc), + ) + # Update repository sync metadata repository.last_sync_at = utcnow() repository.last_sync_error = None diff --git a/backend/migrations/versions/a1b2c3d4e5f7_add_labels_to_forgejo_repositories.py b/backend/migrations/versions/a1b2c3d4e5f7_add_labels_to_forgejo_repositories.py new file mode 100644 index 0000000..b855173 --- /dev/null +++ b/backend/migrations/versions/a1b2c3d4e5f7_add_labels_to_forgejo_repositories.py @@ -0,0 +1,33 @@ +"""add labels column to forgejo_repositories + +Revision ID: a1b2c3d4e5f7 +Revises: f5a2b3c4d5e6 +Create Date: 2026-05-19 00:00:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "a1b2c3d4e5f7" +down_revision = "f5a2b3c4d5e6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "forgejo_repositories", + sa.Column( + "labels", + sa.JSON(), + nullable=False, + server_default="[]", + ), + ) + + +def downgrade() -> None: + op.drop_column("forgejo_repositories", "labels") diff --git a/frontend/src/lib/api-forgejo.ts b/frontend/src/lib/api-forgejo.ts index c30844e..5794349 100644 --- a/frontend/src/lib/api-forgejo.ts +++ b/frontend/src/lib/api-forgejo.ts @@ -27,6 +27,13 @@ export interface ForgejoConnectionUpdate { } // Forgejo Repository types +export interface ForgejoRepoLabel { + id: number | null; + name: string; + color: string; + description: string; +} + export interface ForgejoRepository { id: string; organization_id: string; @@ -37,6 +44,7 @@ export interface ForgejoRepository { default_branch: string; active: boolean; has_webhook_secret: boolean; + labels: ForgejoRepoLabel[]; connection: ForgejoConnection; last_sync_at: string | null; last_sync_error: string | null;