"""Service for editing Forgejo issues and updating local cache.""" from __future__ import annotations from typing import TYPE_CHECKING from uuid import UUID from sqlmodel import select from app.core.logging import get_logger from app.core.time import utcnow from app.db import crud from app.models.forgejo_connections import ForgejoConnection from app.models.forgejo_issues import ForgejoIssue from app.models.forgejo_repositories import ForgejoRepository from app.services.forgejo_client import get_forgejo_client if TYPE_CHECKING: from sqlalchemy.ext.asyncio.session import AsyncSession logger = get_logger(__name__) class EditIssueError(Exception): """Base exception for edit issue errors.""" class EditIssueNotFoundError(EditIssueError): """Raised when issue, repository, or connection is not found.""" class EditIssueRemoteError(EditIssueError): """Raised when the Forgejo API call fails.""" async def edit_cached_issue( session: AsyncSession, issue: ForgejoIssue, *, title: str | None = None, body: str | None = None, state: str | None = None, actor_user_id: UUID | None = None, actor_agent_id: UUID | None = None, ) -> dict[str, object]: """Edit a Forgejo issue remotely and sync changes to the local cache. Raises: EditIssueNotFoundError: If repository or connection not found. EditIssueRemoteError: If the Forgejo API call fails. """ repository = await crud.get_by_id(session, ForgejoRepository, issue.repository_id) if repository is None: raise EditIssueNotFoundError("Repository not found") connection = await crud.get_by_id(session, ForgejoConnection, repository.connection_id) if connection is None: raise EditIssueNotFoundError("Connection not found") try: async with get_forgejo_client(connection) as client: updated = await client.edit_issue( owner=repository.owner, repo=repository.repo, issue_number=issue.forgejo_issue_number, title=title, body=body, state=state, ) except Exception as exc: raise EditIssueRemoteError(f"Failed to edit issue on Forgejo: {exc}") from exc now = utcnow() if title is not None: issue.title = title if body is not None: issue.body = body issue.body_preview = body[:1000] if body else None if state is not None: issue.state = state if state == "closed" and issue.forgejo_closed_at is None: issue.forgejo_closed_at = now issue.forgejo_updated_at = now issue.last_synced_at = now if isinstance(issue.forgejo_payload, dict): payload = dict(issue.forgejo_payload) if title is not None: payload["title"] = title if body is not None: payload["body"] = body if state is not None: payload["state"] = state payload["updated_at"] = now.isoformat() issue.forgejo_payload = payload session.add(issue) await session.flush() logger.info( "forgejo.issue.edited", extra={ "issue_id": str(issue.id), "forgejo_issue_number": issue.forgejo_issue_number, "repository_id": str(repository.id), "fields_changed": [ f for f, v in [("title", title), ("body", body), ("state", state)] if v is not None ], "actor_user_id": str(actor_user_id) if actor_user_id else None, "actor_agent_id": str(actor_agent_id) if actor_agent_id else None, }, ) _ = updated return { "success": True, "issue_id": str(issue.id), "forgejo_issue_number": issue.forgejo_issue_number, "title": issue.title, "body": issue.body, "state": issue.state, "forgejo_updated_at": issue.forgejo_updated_at.isoformat(), } async def edit_issue_by_id( session: AsyncSession, issue_id: UUID, *, title: str | None = None, body: str | None = None, state: str | None = None, actor_user_id: UUID | None = None, actor_agent_id: UUID | None = None, ) -> dict[str, object]: """Edit a Forgejo issue by its local ID.""" issue = (await session.exec(select(ForgejoIssue).where(ForgejoIssue.id == issue_id))).first() if issue is None: raise EditIssueNotFoundError("Issue not found") return await edit_cached_issue( session=session, issue=issue, title=title, body=body, state=state, actor_user_id=actor_user_id, actor_agent_id=actor_agent_id, )