feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
"""Thin API wrappers for Forgejo repository CRUD."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
|
2026-05-24 19:42:07 -05:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
from sqlmodel import select
|
|
|
|
|
|
2026-05-19 20:14:16 -05:00
|
|
|
from app.api.deps import require_org_member
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
from app.db import crud
|
|
|
|
|
from app.db.session import get_session
|
2026-05-22 03:57:13 -05:00
|
|
|
from app.models.boards import Board
|
|
|
|
|
from app.models.board_repository_links import BoardRepositoryLink
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
from app.models.forgejo_connections import ForgejoConnection
|
|
|
|
|
from app.models.forgejo_repositories import ForgejoRepository
|
2026-05-22 03:57:13 -05:00
|
|
|
from app.schemas.boards import BoardRead
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
from app.schemas.common import OkResponse
|
|
|
|
|
from app.schemas.forgejo_repositories import (
|
|
|
|
|
ForgejoRepositoryCreate,
|
|
|
|
|
ForgejoRepositoryRead,
|
|
|
|
|
ForgejoRepositoryUpdate,
|
|
|
|
|
)
|
feat(forgejo): add validation, cached issues, sync service, and human issue APIs (Issues 5-8); add connection and repository admin UI (Issues 3-4); fix migration graph and client bugs
Backend Issues 5-8:
- POST /forgejo/connections/{id}/validate and /repositories/{id}/validate
- ForgejoIssue model with unique constraint (repo_id, issue_number)
- IssueSyncService with pagination and upsert
- GET /forgejo/issues with filtering, search, pagination
- GET /forgejo/issues/{id} with org-scoped access
- Fixed ForgejoAPIClient /api/v1 path prefix
- Fixed get_forgejo_client async context (was async def, now regular def)
- Fixed Forgejo API response parsing (list, not dict with items)
- Fixed None-safe handling for labels/assignees
Frontend Issues 3-4:
- Connections list, new, edit pages with token masking
- Repositories list, new, edit pages with connection selector
- ForgejoConnectionForm, ForgejoConnectionsTable components
- ForgejoRepositoryForm, ForgejoRepositoriesTable components
- api-forgejo.ts client library
Migration cleanup:
- Consolidated forgejo_issues table into f5a2b3c4d5e6 migration
- Removed orphan branch migrations for already-existing tables
- Fixed migration graph to single head (f5a2b3c4d5e6)
- Stamped DB to correct revision
2026-05-19 03:10:32 -05:00
|
|
|
from app.schemas.forgejo_validation import ForgejoRepositoryValidationResponse, ValidationStatus
|
|
|
|
|
from app.services.forgejo_client import get_forgejo_client
|
2026-05-20 01:22:16 -05:00
|
|
|
from app.services.forgejo_cleanup import delete_repository_with_dependents
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
from app.services.organizations import OrganizationContext
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/forgejo/repositories", tags=["forgejo-repositories"])
|
|
|
|
|
SESSION_DEP = Depends(get_session)
|
2026-05-19 20:14:16 -05:00
|
|
|
ORG_MEMBER_DEP = Depends(require_org_member)
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_connection_info(connection: ForgejoConnection) -> dict[str, object]:
|
|
|
|
|
"""Create safe connection metadata for repository responses."""
|
|
|
|
|
return {
|
|
|
|
|
"id": connection.id,
|
|
|
|
|
"organization_id": connection.organization_id,
|
|
|
|
|
"name": connection.name,
|
|
|
|
|
"base_url": connection.base_url,
|
|
|
|
|
"has_token": connection.token is not None,
|
|
|
|
|
"token_last_eight": connection.token[-8:] if connection.token and len(connection.token) >= 8 else connection.token,
|
|
|
|
|
"active": connection.active,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=list[ForgejoRepositoryRead])
|
|
|
|
|
async def list_repositories(
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
) -> list[ForgejoRepositoryRead]:
|
|
|
|
|
"""List Forgejo repositories for the caller's organization."""
|
|
|
|
|
statement = (
|
|
|
|
|
select(ForgejoRepository)
|
|
|
|
|
.where(ForgejoRepository.organization_id == ctx.organization.id)
|
|
|
|
|
.order_by(ForgejoRepository.created_at.desc())
|
|
|
|
|
)
|
|
|
|
|
repositories = (await session.exec(statement)).all()
|
|
|
|
|
# Fetch connections in batch for response building
|
|
|
|
|
conn_ids = {r.connection_id for r in repositories}
|
|
|
|
|
conn_map: dict[UUID, ForgejoConnection] = {}
|
|
|
|
|
for cid in conn_ids:
|
|
|
|
|
c = await crud.get_by_id(session, ForgejoConnection, cid)
|
|
|
|
|
if c is not None:
|
|
|
|
|
conn_map[cid] = c
|
|
|
|
|
result = []
|
|
|
|
|
for r in repositories:
|
|
|
|
|
result.append(_mask_repository(r, conn_map.get(r.connection_id)))
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=ForgejoRepositoryRead)
|
|
|
|
|
async def create_repository(
|
|
|
|
|
payload: ForgejoRepositoryCreate,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
) -> ForgejoRepositoryRead:
|
|
|
|
|
"""Create a Forgejo repository tracked for the caller's organization."""
|
|
|
|
|
# Validate connection belongs to caller's org
|
|
|
|
|
connection = await crud.get_by_id(session, ForgejoConnection, payload.connection_id)
|
|
|
|
|
if connection is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
|
|
|
detail="connection_id is invalid",
|
|
|
|
|
)
|
|
|
|
|
if connection.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
|
|
|
detail="connection_id is invalid",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Check for duplicate active repo
|
|
|
|
|
existing = await crud.get_one_by(
|
|
|
|
|
session,
|
|
|
|
|
ForgejoRepository,
|
|
|
|
|
organization_id=ctx.organization.id,
|
|
|
|
|
connection_id=payload.connection_id,
|
|
|
|
|
owner=payload.owner,
|
|
|
|
|
repo=payload.repo,
|
|
|
|
|
)
|
|
|
|
|
if existing is not None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
|
|
|
detail=f"Repository {payload.owner}/{payload.repo} is already tracked",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
data = payload.model_dump()
|
|
|
|
|
data["organization_id"] = ctx.organization.id
|
|
|
|
|
# Ensure connection_id is included for foreign key relationship
|
|
|
|
|
data["connection_id"] = payload.connection_id
|
|
|
|
|
repository = await crud.create(session, ForgejoRepository, **data)
|
|
|
|
|
# Load connection for response
|
|
|
|
|
conn = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
|
|
|
|
|
return _mask_repository(repository, conn)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{repository_id}", response_model=ForgejoRepositoryRead)
|
|
|
|
|
async def get_repository(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
) -> ForgejoRepositoryRead:
|
|
|
|
|
"""Return one Forgejo repository by id for the caller's organization."""
|
|
|
|
|
statement = (
|
|
|
|
|
select(ForgejoRepository)
|
|
|
|
|
.where(ForgejoRepository.id == repository_id, ForgejoRepository.organization_id == ctx.organization.id)
|
|
|
|
|
)
|
|
|
|
|
repository = (await session.exec(statement)).first()
|
|
|
|
|
if repository is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
# Load connection for response
|
|
|
|
|
conn = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
|
|
|
|
|
return _mask_repository(repository, conn)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{repository_id}", response_model=ForgejoRepositoryRead)
|
|
|
|
|
async def update_repository(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
payload: ForgejoRepositoryUpdate,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
) -> ForgejoRepositoryRead:
|
|
|
|
|
"""Patch a Forgejo repository for the caller's organization."""
|
|
|
|
|
# Get repository
|
|
|
|
|
statement = (
|
|
|
|
|
select(ForgejoRepository)
|
|
|
|
|
.where(ForgejoRepository.id == repository_id, ForgejoRepository.organization_id == ctx.organization.id)
|
|
|
|
|
)
|
|
|
|
|
repository = (await session.exec(statement)).first()
|
|
|
|
|
if repository is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
# Load connection for updates validation
|
|
|
|
|
conn = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
|
|
|
|
|
if conn is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Referenced connection not found")
|
|
|
|
|
|
|
|
|
|
updates = payload.model_dump(exclude_unset=True)
|
|
|
|
|
|
|
|
|
|
# Handle connection_id validation
|
|
|
|
|
if "connection_id" in updates:
|
|
|
|
|
new_conn_id = updates["connection_id"]
|
|
|
|
|
connection = await crud.get_by_id(session, ForgejoConnection, new_conn_id)
|
|
|
|
|
if connection is None:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
|
|
|
detail="connection_id is invalid",
|
|
|
|
|
)
|
|
|
|
|
if connection.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
|
|
|
detail="connection_id is invalid",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Check for duplicate active repo (same connection/owner/repo)
|
|
|
|
|
if "owner" in updates or "repo" in updates or "connection_id" in updates:
|
|
|
|
|
current_conn = repository.connection_id
|
|
|
|
|
if "connection_id" in updates:
|
|
|
|
|
current_conn = updates["connection_id"]
|
|
|
|
|
current_owner = updates.get("owner", repository.owner)
|
|
|
|
|
current_repo = updates.get("repo", repository.repo)
|
|
|
|
|
|
|
|
|
|
existing = await crud.get_one_by(
|
|
|
|
|
session,
|
|
|
|
|
ForgejoRepository,
|
|
|
|
|
organization_id=ctx.organization.id,
|
|
|
|
|
connection_id=current_conn,
|
|
|
|
|
owner=current_owner,
|
|
|
|
|
repo=current_repo,
|
|
|
|
|
)
|
|
|
|
|
if existing is not None and existing.id != repository.id:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
|
|
|
detail=f"Repository {current_owner}/{current_repo} is already tracked",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Apply updates
|
|
|
|
|
for key, value in updates.items():
|
|
|
|
|
setattr(repository, key, value)
|
|
|
|
|
|
|
|
|
|
from app.core.time import utcnow
|
|
|
|
|
repository.updated_at = utcnow()
|
|
|
|
|
# Reload connection to get latest state
|
|
|
|
|
await crud.save(session, repository)
|
|
|
|
|
# Load connection for response
|
|
|
|
|
conn = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
|
|
|
|
|
return _mask_repository(repository, conn)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{repository_id}", response_model=OkResponse)
|
|
|
|
|
async def delete_repository(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
) -> OkResponse:
|
|
|
|
|
"""Delete a Forgejo repository for the caller's organization."""
|
|
|
|
|
repository = await crud.get_by_id(session, ForgejoRepository, repository_id)
|
|
|
|
|
if repository is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
if repository.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
2026-05-20 01:22:16 -05:00
|
|
|
await delete_repository_with_dependents(session, repository)
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
await session.commit()
|
|
|
|
|
return OkResponse()
|
|
|
|
|
|
|
|
|
|
|
feat(forgejo): add validation, cached issues, sync service, and human issue APIs (Issues 5-8); add connection and repository admin UI (Issues 3-4); fix migration graph and client bugs
Backend Issues 5-8:
- POST /forgejo/connections/{id}/validate and /repositories/{id}/validate
- ForgejoIssue model with unique constraint (repo_id, issue_number)
- IssueSyncService with pagination and upsert
- GET /forgejo/issues with filtering, search, pagination
- GET /forgejo/issues/{id} with org-scoped access
- Fixed ForgejoAPIClient /api/v1 path prefix
- Fixed get_forgejo_client async context (was async def, now regular def)
- Fixed Forgejo API response parsing (list, not dict with items)
- Fixed None-safe handling for labels/assignees
Frontend Issues 3-4:
- Connections list, new, edit pages with token masking
- Repositories list, new, edit pages with connection selector
- ForgejoConnectionForm, ForgejoConnectionsTable components
- ForgejoRepositoryForm, ForgejoRepositoriesTable components
- api-forgejo.ts client library
Migration cleanup:
- Consolidated forgejo_issues table into f5a2b3c4d5e6 migration
- Removed orphan branch migrations for already-existing tables
- Fixed migration graph to single head (f5a2b3c4d5e6)
- Stamped DB to correct revision
2026-05-19 03:10:32 -05:00
|
|
|
@router.post(
|
|
|
|
|
"/{repository_id}/validate",
|
|
|
|
|
response_model=ForgejoRepositoryValidationResponse,
|
|
|
|
|
summary="Validate Forgejo Repository",
|
|
|
|
|
description="Test if a Forgejo repository exists and can be accessed with the connection token.",
|
|
|
|
|
)
|
|
|
|
|
async def validate_repository(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add validation, cached issues, sync service, and human issue APIs (Issues 5-8); add connection and repository admin UI (Issues 3-4); fix migration graph and client bugs
Backend Issues 5-8:
- POST /forgejo/connections/{id}/validate and /repositories/{id}/validate
- ForgejoIssue model with unique constraint (repo_id, issue_number)
- IssueSyncService with pagination and upsert
- GET /forgejo/issues with filtering, search, pagination
- GET /forgejo/issues/{id} with org-scoped access
- Fixed ForgejoAPIClient /api/v1 path prefix
- Fixed get_forgejo_client async context (was async def, now regular def)
- Fixed Forgejo API response parsing (list, not dict with items)
- Fixed None-safe handling for labels/assignees
Frontend Issues 3-4:
- Connections list, new, edit pages with token masking
- Repositories list, new, edit pages with connection selector
- ForgejoConnectionForm, ForgejoConnectionsTable components
- ForgejoRepositoryForm, ForgejoRepositoriesTable components
- api-forgejo.ts client library
Migration cleanup:
- Consolidated forgejo_issues table into f5a2b3c4d5e6 migration
- Removed orphan branch migrations for already-existing tables
- Fixed migration graph to single head (f5a2b3c4d5e6)
- Stamped DB to correct revision
2026-05-19 03:10:32 -05:00
|
|
|
) -> ForgejoRepositoryValidationResponse:
|
|
|
|
|
"""Validate a Forgejo repository by testing API access."""
|
|
|
|
|
repository = await crud.get_by_id(session, ForgejoRepository, repository_id)
|
|
|
|
|
if repository is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
if repository.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
# Load connection
|
|
|
|
|
connection = await crud.get_by_id(session, ForgejoConnection, repository.connection_id)
|
|
|
|
|
if connection is None or connection.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
from app.core.time import utcnow
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
repo_exists = None
|
|
|
|
|
try:
|
|
|
|
|
async with get_forgejo_client(connection) as client:
|
|
|
|
|
# Test if repository exists and is accessible
|
|
|
|
|
await client.get_repository(owner=repository.owner, repo=repository.repo)
|
|
|
|
|
repo_exists = True
|
|
|
|
|
response_time_ms = (time.time() - start_time) * 1000
|
|
|
|
|
return ForgejoRepositoryValidationResponse(
|
|
|
|
|
repository_id=str(repository.id),
|
|
|
|
|
status=ValidationStatus(ok=True, status="ok"),
|
|
|
|
|
response_time_ms=response_time_ms,
|
|
|
|
|
validated_at=utcnow(),
|
|
|
|
|
repo_exists=repo_exists,
|
|
|
|
|
)
|
|
|
|
|
except HTTPException as e:
|
|
|
|
|
response_time_ms = (time.time() - start_time) * 1000
|
|
|
|
|
return ForgejoRepositoryValidationResponse(
|
|
|
|
|
repository_id=str(repository.id),
|
|
|
|
|
status=ValidationStatus(ok=False, status="error", error_message=str(e.detail)),
|
|
|
|
|
response_time_ms=response_time_ms,
|
|
|
|
|
validated_at=utcnow(),
|
|
|
|
|
repo_exists=False,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
response_time_ms = (time.time() - start_time) * 1000
|
|
|
|
|
return ForgejoRepositoryValidationResponse(
|
|
|
|
|
repository_id=str(repository.id),
|
|
|
|
|
status=ValidationStatus(ok=False, status="error", error_message=str(e)),
|
|
|
|
|
response_time_ms=response_time_ms,
|
|
|
|
|
validated_at=utcnow(),
|
|
|
|
|
repo_exists=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/{repository_id}/sync",
|
|
|
|
|
summary="Sync Issues from Repository",
|
2026-05-19 20:14:16 -05:00
|
|
|
description="Sync issues from a Forgejo repository.",
|
feat(forgejo): add validation, cached issues, sync service, and human issue APIs (Issues 5-8); add connection and repository admin UI (Issues 3-4); fix migration graph and client bugs
Backend Issues 5-8:
- POST /forgejo/connections/{id}/validate and /repositories/{id}/validate
- ForgejoIssue model with unique constraint (repo_id, issue_number)
- IssueSyncService with pagination and upsert
- GET /forgejo/issues with filtering, search, pagination
- GET /forgejo/issues/{id} with org-scoped access
- Fixed ForgejoAPIClient /api/v1 path prefix
- Fixed get_forgejo_client async context (was async def, now regular def)
- Fixed Forgejo API response parsing (list, not dict with items)
- Fixed None-safe handling for labels/assignees
Frontend Issues 3-4:
- Connections list, new, edit pages with token masking
- Repositories list, new, edit pages with connection selector
- ForgejoConnectionForm, ForgejoConnectionsTable components
- ForgejoRepositoryForm, ForgejoRepositoriesTable components
- api-forgejo.ts client library
Migration cleanup:
- Consolidated forgejo_issues table into f5a2b3c4d5e6 migration
- Removed orphan branch migrations for already-existing tables
- Fixed migration graph to single head (f5a2b3c4d5e6)
- Stamped DB to correct revision
2026-05-19 03:10:32 -05:00
|
|
|
)
|
|
|
|
|
async def sync_repository_issues(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-05-19 20:14:16 -05:00
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
feat(forgejo): add validation, cached issues, sync service, and human issue APIs (Issues 5-8); add connection and repository admin UI (Issues 3-4); fix migration graph and client bugs
Backend Issues 5-8:
- POST /forgejo/connections/{id}/validate and /repositories/{id}/validate
- ForgejoIssue model with unique constraint (repo_id, issue_number)
- IssueSyncService with pagination and upsert
- GET /forgejo/issues with filtering, search, pagination
- GET /forgejo/issues/{id} with org-scoped access
- Fixed ForgejoAPIClient /api/v1 path prefix
- Fixed get_forgejo_client async context (was async def, now regular def)
- Fixed Forgejo API response parsing (list, not dict with items)
- Fixed None-safe handling for labels/assignees
Frontend Issues 3-4:
- Connections list, new, edit pages with token masking
- Repositories list, new, edit pages with connection selector
- ForgejoConnectionForm, ForgejoConnectionsTable components
- ForgejoRepositoryForm, ForgejoRepositoriesTable components
- api-forgejo.ts client library
Migration cleanup:
- Consolidated forgejo_issues table into f5a2b3c4d5e6 migration
- Removed orphan branch migrations for already-existing tables
- Fixed migration graph to single head (f5a2b3c4d5e6)
- Stamped DB to correct revision
2026-05-19 03:10:32 -05:00
|
|
|
) -> dict[str, int]:
|
|
|
|
|
"""Sync issues from a Forgejo repository."""
|
|
|
|
|
repository = await crud.get_by_id(session, ForgejoRepository, repository_id)
|
|
|
|
|
if repository is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
if repository.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
from app.services.forgejo_issue_sync import IssueSyncService
|
|
|
|
|
from app.core.time import utcnow
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sync_service = IssueSyncService(session=session, organization_id=ctx.organization.id)
|
|
|
|
|
result = await sync_service.sync_repository_issues(repository_id=repository_id)
|
|
|
|
|
return result
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
# Update error on repository
|
|
|
|
|
repository.last_sync_error = str(e)
|
|
|
|
|
repository.updated_at = utcnow()
|
|
|
|
|
await crud.save(session, repository)
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# Update error on repository
|
|
|
|
|
repository.last_sync_error = str(e)
|
|
|
|
|
repository.updated_at = utcnow()
|
|
|
|
|
await crud.save(session, repository)
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-05-24 19:42:07 -05:00
|
|
|
@router.post(
|
|
|
|
|
"/{repository_id}/sync/recent",
|
|
|
|
|
summary="Light Sync Issues from Repository",
|
|
|
|
|
description="Fetch only issues updated in the last `days` days. Faster than full sync — uses batch DB lookups and skips per-issue enrichment calls.",
|
|
|
|
|
)
|
|
|
|
|
async def sync_repository_issues_recent(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
days: int = Query(default=7, ge=1, le=90),
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
|
|
|
|
) -> dict[str, int]:
|
|
|
|
|
"""Light sync: fetch only issues updated in the last `days` days."""
|
|
|
|
|
repository = await crud.get_by_id(session, ForgejoRepository, repository_id)
|
|
|
|
|
if repository is None or repository.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
from app.services.forgejo_issue_sync import IssueSyncService
|
|
|
|
|
from app.core.time import utcnow
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sync_service = IssueSyncService(session=session, organization_id=ctx.organization.id)
|
|
|
|
|
result = await sync_service.sync_recent(repository_id=repository_id, days=days)
|
|
|
|
|
return result
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
repository.last_sync_error = str(e)
|
|
|
|
|
repository.updated_at = utcnow()
|
|
|
|
|
await crud.save(session, repository)
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
repository.last_sync_error = str(e)
|
|
|
|
|
repository.updated_at = utcnow()
|
|
|
|
|
await crud.save(session, repository)
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
def _mask_repository(repository: ForgejoRepository, connection: ForgejoConnection | None = None) -> dict[str, object]:
|
|
|
|
|
"""Return repository dict with safe connection metadata."""
|
|
|
|
|
return {
|
|
|
|
|
"id": repository.id,
|
|
|
|
|
"organization_id": repository.organization_id,
|
|
|
|
|
"connection_id": repository.connection_id,
|
|
|
|
|
"owner": repository.owner,
|
|
|
|
|
"repo": repository.repo,
|
|
|
|
|
"display_name": repository.display_name,
|
|
|
|
|
"default_branch": repository.default_branch,
|
|
|
|
|
"active": repository.active,
|
|
|
|
|
"connection": _create_connection_info(connection) if connection is not None else None,
|
2026-05-19 04:16:32 -05:00
|
|
|
"has_webhook_secret": bool(repository.webhook_secret),
|
2026-05-19 21:49:45 -05:00
|
|
|
"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 [],
|
2026-05-19 21:34:11 -05:00
|
|
|
"labels": repository.labels if repository.labels is not None else [],
|
feat(forgejo): add DB models, CRUD APIs, client service, and Git Projects nav (Issues 1-4, FI2)
Backend:
- ForgejoConnection + ForgejoRepository SQLModel models with migration
- Admin CRUD API for connections (GET/POST/PATCH/DELETE)
- Admin CRUD API for repositories (GET/POST/PATCH/DELETE)
- Token redaction, URL normalization, duplicate prevention
- ForgejoAPIClient service (httpx async, list_issues, close_issue, get_repository)
- Removed stale feast import that crashed startup
Frontend:
- Git Projects sidebar nav item (FolderGit icon)
- /git-projects shell page with empty/loading/error states
Verified: all endpoints live, CRUD tested, migration applied.
2026-05-19 02:46:27 -05:00
|
|
|
"last_sync_at": repository.last_sync_at,
|
|
|
|
|
"last_sync_error": repository.last_sync_error,
|
|
|
|
|
"created_at": repository.created_at,
|
|
|
|
|
"updated_at": repository.updated_at,
|
|
|
|
|
}
|
2026-05-22 03:57:13 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{repository_id}/boards", response_model=list[BoardRead])
|
|
|
|
|
async def list_boards_for_repository(
|
|
|
|
|
repository_id: UUID,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
ctx: OrganizationContext = ORG_MEMBER_DEP,
|
|
|
|
|
) -> list[BoardRead]:
|
|
|
|
|
"""Return all boards that have this repository linked to them."""
|
|
|
|
|
repository = await crud.get_by_id(session, ForgejoRepository, repository_id)
|
|
|
|
|
if repository is None or repository.organization_id != ctx.organization.id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found")
|
|
|
|
|
|
|
|
|
|
links = (
|
|
|
|
|
await session.exec(
|
|
|
|
|
select(BoardRepositoryLink).where(
|
|
|
|
|
BoardRepositoryLink.organization_id == ctx.organization.id,
|
|
|
|
|
BoardRepositoryLink.repository_id == repository_id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
|
|
if not links:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
board_ids = [link.board_id for link in links]
|
|
|
|
|
boards = (
|
|
|
|
|
await session.exec(
|
|
|
|
|
select(Board).where(
|
|
|
|
|
Board.id.in_(board_ids),
|
|
|
|
|
Board.organization_id == ctx.organization.id,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
).all()
|
|
|
|
|
return [BoardRead.model_validate(b) for b in boards]
|