From 87802db0f486f4a385323076959eb39d9e6648d0 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 19 May 2026 21:49:45 -0500 Subject: [PATCH] feat: new git imports (desc, open, is_archived) --- backend/app/api/forgejo_repositories.py | 4 ++ backend/app/models/forgejo_issues.py | 5 +- backend/app/models/forgejo_repositories.py | 7 ++ backend/app/schemas/forgejo_issues.py | 2 + backend/app/schemas/forgejo_repositories.py | 4 ++ backend/app/services/forgejo_issue_sync.py | 46 +++++++++++- ...e5f6a7_add_body_milestone_repo_metadata.py | 71 +++++++++++++++++++ frontend/src/lib/api-forgejo.ts | 15 ++++ 8 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 backend/migrations/versions/b2c3d4e5f6a7_add_body_milestone_repo_metadata.py diff --git a/backend/app/api/forgejo_repositories.py b/backend/app/api/forgejo_repositories.py index 9685872..e321ff7 100644 --- a/backend/app/api/forgejo_repositories.py +++ b/backend/app/api/forgejo_repositories.py @@ -339,6 +339,10 @@ 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), + "description": repository.description, + "open_issues_count": repository.open_issues_count if repository.open_issues_count is not None else 0, + "is_archived": bool(repository.is_archived), + "topics": repository.topics if repository.topics is not None else [], "labels": repository.labels if repository.labels is not None else [], "last_sync_at": repository.last_sync_at, "last_sync_error": repository.last_sync_error, diff --git a/backend/app/models/forgejo_issues.py b/backend/app/models/forgejo_issues.py index f4325de..4c479c0 100644 --- a/backend/app/models/forgejo_issues.py +++ b/backend/app/models/forgejo_issues.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime from uuid import UUID, uuid4 +import sqlalchemy as sa from sqlalchemy import JSON, Column from sqlmodel import Field, Index, SQLModel @@ -24,13 +25,15 @@ class ForgejoIssue(SQLModel, table=True): forgejo_issue_number: int = Field(index=True) title: str + body: str | None = Field(default=None, sa_column=Column(sa.Text, nullable=True)) body_preview: str | None = Field(default=None, max_length=1000) - state: str = Field(default="open") # open, closed, open + state: str = Field(default="open") is_pull_request: bool = Field(default=False) # JSON fields for complex data labels: list[dict[str, object]] = Field(default_factory=list, sa_column=Column(JSON)) assignees: list[dict[str, object]] = Field(default_factory=list, sa_column=Column(JSON)) + milestone: dict[str, object] | None = Field(default=None, sa_column=Column(JSON, nullable=True)) author: str html_url: str diff --git a/backend/app/models/forgejo_repositories.py b/backend/app/models/forgejo_repositories.py index 5eb1f7e..773e881 100644 --- a/backend/app/models/forgejo_repositories.py +++ b/backend/app/models/forgejo_repositories.py @@ -29,6 +29,13 @@ class ForgejoRepository(QueryModel, table=True): default_branch: str = Field(default="main") active: bool = Field(default=True) webhook_secret: str | None = Field(default=None) + description: str | None = Field(default=None) + open_issues_count: int = Field(default=0) + is_archived: bool = Field(default=False) + topics: list[str] = Field( + default_factory=list, + sa_column=Column(JSON, nullable=False, server_default="[]"), + ) labels: list[dict[str, object]] = Field( default_factory=list, sa_column=Column(JSON, nullable=False, server_default="[]"), diff --git a/backend/app/schemas/forgejo_issues.py b/backend/app/schemas/forgejo_issues.py index eb7bfc3..d295ff4 100644 --- a/backend/app/schemas/forgejo_issues.py +++ b/backend/app/schemas/forgejo_issues.py @@ -14,11 +14,13 @@ class ForgejoIssueBase(SQLModel): forgejo_issue_number: int title: str + body: str | None = None body_preview: str | None = None state: str is_pull_request: bool labels: list[dict[str, Any]] = [] assignees: list[dict[str, Any]] = [] + milestone: dict[str, Any] | None = None author: str html_url: str forgejo_created_at: datetime diff --git a/backend/app/schemas/forgejo_repositories.py b/backend/app/schemas/forgejo_repositories.py index df0df7c..5720717 100644 --- a/backend/app/schemas/forgejo_repositories.py +++ b/backend/app/schemas/forgejo_repositories.py @@ -108,6 +108,10 @@ class ForgejoRepositoryRead(ForgejoRepositoryBase): connection_id: UUID connection: ForgejoRepositoryConnectionInfo has_webhook_secret: bool = False + description: str | None = None + open_issues_count: int = 0 + is_archived: bool = False + topics: list[str] = Field(default_factory=list) labels: list[dict[str, object]] = Field(default_factory=list) last_sync_at: datetime | None last_sync_error: str | None diff --git a/backend/app/services/forgejo_issue_sync.py b/backend/app/services/forgejo_issue_sync.py index cc23c63..77d3311 100644 --- a/backend/app/services/forgejo_issue_sync.py +++ b/backend/app/services/forgejo_issue_sync.py @@ -79,6 +79,7 @@ class IssueSyncService: labels_data = [] for label in (issue_data.get("labels") or []): labels_data.append({ + "id": label.get("id"), "name": label.get("name", ""), "color": label.get("color", ""), "description": label.get("description", ""), @@ -93,6 +94,24 @@ class IssueSyncService: "avatar_url": assignee.get("avatar_url", ""), }) + # Parse milestone + milestone_data = None + raw_milestone = issue_data.get("milestone") + if raw_milestone and isinstance(raw_milestone, dict): + milestone_data = { + "id": raw_milestone.get("id"), + "title": raw_milestone.get("title", ""), + "state": raw_milestone.get("state", "open"), + "description": raw_milestone.get("description") or None, + "due_on": raw_milestone.get("due_on") or None, + "closed_at": raw_milestone.get("closed_at") or None, + } + + # Full body and preview + raw_body = issue_data.get("body") or "" + body_full = raw_body if raw_body else None + body_preview = raw_body[:1000] if raw_body else None + # Parse dates created_at = self._parse_iso_date(issue_data.get("created_at")) updated_at = self._parse_iso_date(issue_data.get("updated_at")) @@ -107,11 +126,13 @@ class IssueSyncService: repository_id=repository_id, forgejo_issue_number=forgejo_number, title=issue_data.get("title", ""), - body_preview=(issue_data.get("body") or "")[:1000], + body=body_full, + body_preview=body_preview, state=state, is_pull_request=False, labels=labels_data, assignees=assignees_data, + milestone=milestone_data, author=issue_data.get("user", {}).get("login", ""), html_url=issue_data.get("html_url", ""), forgejo_created_at=created_at, @@ -123,10 +144,12 @@ class IssueSyncService: created += 1 else: existing.title = issue_data.get("title", "") - existing.body_preview = (issue_data.get("body") or "")[:1000] + existing.body = body_full + existing.body_preview = body_preview existing.state = state existing.labels = labels_data existing.assignees = assignees_data + existing.milestone = milestone_data existing.author = issue_data.get("user", {}).get("login", "") existing.html_url = issue_data.get("html_url", "") existing.forgejo_created_at = created_at @@ -170,6 +193,25 @@ class IssueSyncService: error=str(exc), ) + # Sync repository remote metadata (description, archived, topics, issue count) + try: + async with get_forgejo_client(connection) as client: + repo_meta = await client.get_repository( + owner=repository.owner, + repo=repository.repo, + ) + repository.description = repo_meta.get("description") or None + repository.open_issues_count = int(repo_meta.get("open_issues_count") or 0) + repository.is_archived = bool(repo_meta.get("archived", False)) + raw_topics = repo_meta.get("topics") + repository.topics = list(raw_topics) if isinstance(raw_topics, list) else [] + except Exception as exc: + logger.warning( + "repo_metadata_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/b2c3d4e5f6a7_add_body_milestone_repo_metadata.py b/backend/migrations/versions/b2c3d4e5f6a7_add_body_milestone_repo_metadata.py new file mode 100644 index 0000000..02039eb --- /dev/null +++ b/backend/migrations/versions/b2c3d4e5f6a7_add_body_milestone_repo_metadata.py @@ -0,0 +1,71 @@ +"""add body and milestone to issues; description, open_issues_count, is_archived, topics to repos + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f7 +Create Date: 2026-05-19 00:00:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "b2c3d4e5f6a7" +down_revision = "a1b2c3d4e5f7" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # forgejo_issues — full body text and milestone JSON + op.add_column( + "forgejo_issues", + sa.Column("body", sa.Text(), nullable=True), + ) + op.add_column( + "forgejo_issues", + sa.Column("milestone", sa.JSON(), nullable=True), + ) + + # forgejo_repositories — remote metadata fields + op.add_column( + "forgejo_repositories", + sa.Column("description", sa.String(), nullable=True), + ) + op.add_column( + "forgejo_repositories", + sa.Column( + "open_issues_count", + sa.Integer(), + nullable=False, + server_default="0", + ), + ) + op.add_column( + "forgejo_repositories", + sa.Column( + "is_archived", + sa.Boolean(), + nullable=False, + server_default="false", + ), + ) + op.add_column( + "forgejo_repositories", + sa.Column( + "topics", + sa.JSON(), + nullable=False, + server_default="[]", + ), + ) + + +def downgrade() -> None: + op.drop_column("forgejo_repositories", "topics") + op.drop_column("forgejo_repositories", "is_archived") + op.drop_column("forgejo_repositories", "open_issues_count") + op.drop_column("forgejo_repositories", "description") + op.drop_column("forgejo_issues", "milestone") + op.drop_column("forgejo_issues", "body") diff --git a/frontend/src/lib/api-forgejo.ts b/frontend/src/lib/api-forgejo.ts index 2a5833f..04d3792 100644 --- a/frontend/src/lib/api-forgejo.ts +++ b/frontend/src/lib/api-forgejo.ts @@ -44,6 +44,10 @@ export interface ForgejoRepository { default_branch: string; active: boolean; has_webhook_secret: boolean; + description: string | null; + open_issues_count: number; + is_archived: boolean; + topics: string[]; labels: ForgejoRepoLabel[]; connection: ForgejoConnection; last_sync_at: string | null; @@ -258,17 +262,28 @@ export interface ForgejoIssueLabel { description?: string | null; } +export interface ForgejoIssueMilestone { + id: number | null; + title: string; + state: string; + description: string | null; + due_on: string | null; + closed_at: string | null; +} + export interface ForgejoIssue { id: string; organization_id: string; repository_id: string; forgejo_issue_number: number; title: string; + body: string | null; body_preview: string | null; state: string; is_pull_request: boolean; labels: ForgejoIssueLabel[]; assignees: Record[]; + milestone: ForgejoIssueMilestone | null; author: string; html_url: string; forgejo_created_at: string;