bug : git error

This commit is contained in:
null 2026-05-20 01:22:16 -05:00
parent f3746f806c
commit 0d5d6573ed
7 changed files with 224 additions and 5 deletions

View File

@ -21,6 +21,7 @@ from app.schemas.forgejo_connections import (
ForgejoConnectionUpdate,
)
from app.schemas.forgejo_validation import ForgejoConnectionValidationResponse
from app.services.forgejo_cleanup import delete_connection_with_repositories
from app.services.organizations import OrganizationContext
if TYPE_CHECKING:
@ -169,7 +170,7 @@ async def delete_connection(
if connection.organization_id != ctx.organization.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
await session.delete(connection)
await delete_connection_with_repositories(session, connection)
await session.commit()
return OkResponse()

View File

@ -21,6 +21,7 @@ from app.schemas.forgejo_repositories import (
)
from app.schemas.forgejo_validation import ForgejoRepositoryValidationResponse, ValidationStatus
from app.services.forgejo_client import get_forgejo_client
from app.services.forgejo_cleanup import delete_repository_with_dependents
from app.services.organizations import OrganizationContext
if TYPE_CHECKING:
@ -222,7 +223,7 @@ async def delete_repository(
if repository.organization_id != ctx.organization.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
await session.delete(repository)
await delete_repository_with_dependents(session, repository)
await session.commit()
return OkResponse()

View File

@ -0,0 +1,55 @@
"""Explicit cleanup helpers for Forgejo connection/repository deletes."""
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlmodel import select
from app.models.board_repository_links import BoardRepositoryLink
from app.models.forgejo_connections import ForgejoConnection
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
async def delete_repository_with_dependents(
session: AsyncSession,
repository: ForgejoRepository,
) -> None:
"""Delete a tracked repository and rows that reference it."""
link_statement = select(BoardRepositoryLink).where(
BoardRepositoryLink.repository_id == repository.id,
BoardRepositoryLink.organization_id == repository.organization_id,
)
links = (await session.exec(link_statement)).all()
for link in links:
await session.delete(link)
issue_statement = select(ForgejoIssue).where(
ForgejoIssue.repository_id == repository.id,
ForgejoIssue.organization_id == repository.organization_id,
)
issues = (await session.exec(issue_statement)).all()
for issue in issues:
await session.delete(issue)
await session.delete(repository)
async def delete_connection_with_repositories(
session: AsyncSession,
connection: ForgejoConnection,
) -> None:
"""Delete a Forgejo connection and all tracked repositories under it."""
repository_statement = select(ForgejoRepository).where(
ForgejoRepository.connection_id == connection.id,
ForgejoRepository.organization_id == connection.organization_id,
)
repositories = (await session.exec(repository_statement)).all()
for repository in repositories:
await delete_repository_with_dependents(session, repository)
await session.delete(connection)

View File

@ -0,0 +1,159 @@
# ruff: noqa: INP001
"""Integration tests for Forgejo connection CRUD behavior."""
from __future__ import annotations
from datetime import datetime
from uuid import uuid4
import pytest
from fastapi import APIRouter, FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from sqlmodel import SQLModel, col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app import models as _models
from app.api.deps import require_org_member
from app.api.forgejo_connections import router as forgejo_connections_router
from app.db.session import get_session
from app.models.board_repository_links import BoardRepositoryLink
from app.models.boards import Board
from app.models.forgejo_connections import ForgejoConnection
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.models.organization_members import OrganizationMember
from app.models.organizations import Organization
from app.services.organizations import OrganizationContext
async def _make_engine() -> AsyncEngine:
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
return engine
def _build_test_app(
session_maker: async_sessionmaker[AsyncSession],
ctx: OrganizationContext,
) -> FastAPI:
app = FastAPI()
api_v1 = APIRouter(prefix="/api/v1")
api_v1.include_router(forgejo_connections_router)
app.include_router(api_v1)
async def _override_get_session() -> AsyncSession:
async with session_maker() as session:
yield session
async def _override_require_org_member() -> OrganizationContext:
return ctx
app.dependency_overrides[get_session] = _override_get_session
app.dependency_overrides[require_org_member] = _override_require_org_member
return app
@pytest.mark.asyncio
async def test_delete_connection_removes_repositories_and_dependents() -> None:
engine = await _make_engine()
session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
organization = Organization(id=uuid4(), name="Pipeline")
member = OrganizationMember(
id=uuid4(),
organization_id=organization.id,
user_id=uuid4(),
role="owner",
)
app = _build_test_app(
session_maker,
OrganizationContext(organization=organization, member=member),
)
connection = ForgejoConnection(
id=uuid4(),
organization_id=organization.id,
name="Dream Forgejo",
base_url="https://forgejo.example.local",
)
repository = ForgejoRepository(
id=uuid4(),
organization_id=organization.id,
connection_id=connection.id,
owner="openclaw",
repo="pipeline",
display_name="Pipeline",
)
board = Board(
id=uuid4(),
organization_id=organization.id,
name="Pipeline Board",
slug="pipeline-board",
)
issue = ForgejoIssue(
id=uuid4(),
organization_id=organization.id,
repository_id=repository.id,
forgejo_issue_number=42,
title="Sync issue",
body_preview="Cached issue body",
state="open",
is_pull_request=False,
labels=[],
assignees=[],
author="kaspa",
html_url="https://forgejo.example.local/openclaw/pipeline/issues/42",
forgejo_created_at=datetime(2026, 5, 19, 12, 0, 0),
forgejo_updated_at=datetime(2026, 5, 19, 12, 5, 0),
)
link = BoardRepositoryLink(
id=uuid4(),
organization_id=organization.id,
board_id=board.id,
repository_id=repository.id,
)
try:
async with session_maker() as session:
session.add(organization)
session.add(connection)
session.add(repository)
session.add(board)
session.add(issue)
session.add(link)
await session.commit()
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
) as client:
response = await client.delete(f"/api/v1/forgejo/connections/{connection.id}")
assert response.status_code == 200
assert response.json() == {"ok": True}
async with session_maker() as session:
assert (
await session.exec(
select(ForgejoConnection).where(col(ForgejoConnection.id) == connection.id)
)
).first() is None
assert (
await session.exec(
select(ForgejoRepository).where(col(ForgejoRepository.id) == repository.id)
)
).first() is None
assert (
await session.exec(select(ForgejoIssue).where(col(ForgejoIssue.id) == issue.id))
).first() is None
assert (
await session.exec(
select(BoardRepositoryLink).where(col(BoardRepositoryLink.id) == link.id)
)
).first() is None
assert (
await session.exec(select(Board).where(col(Board.id) == board.id))
).first() is not None
finally:
await engine.dispose()

View File

@ -164,7 +164,7 @@ export default function ForgejoConnectionsEditPage({
open={deleteOpen}
onOpenChange={setDeleteOpen}
title="Delete Git Project connection"
description={`Delete "${connection.name}" from Pipeline? Repositories that use this connection will stop syncing.`}
description={`Delete "${connection.name}" from Pipeline? Tracked repositories and cached issues that use this connection will be removed.`}
onConfirm={handleDelete}
isConfirming={isDeleting}
errorMessage={deleteError}

View File

@ -167,7 +167,7 @@ export default function ForgejoConnectionsPage() {
title="Delete Git Project connection"
description={
deleteTarget
? `Delete "${deleteTarget.name}" from Pipeline? Repositories that use this connection will stop syncing.`
? `Delete "${deleteTarget.name}" from Pipeline? Tracked repositories and cached issues that use this connection will be removed.`
: ""
}
onConfirm={confirmDelete}

View File

@ -332,6 +332,9 @@ export default function GitProjectSettingsPage() {
setConnections((prev) =>
prev.filter((c) => c.id !== deleteTarget.item.id),
);
setRepositories((prev) =>
prev.filter((r) => r.connection_id !== deleteTarget.item.id),
);
setNotice({
tone: "success",
message: `Deleted "${deleteTarget.item.name}".`,
@ -670,7 +673,7 @@ export default function GitProjectSettingsPage() {
}
description={
deleteTarget?.type === "connection"
? `Delete "${deleteTarget.item.name}" from Pipeline? Repositories that use this connection will stop syncing.`
? `Delete "${deleteTarget.item.name}" from Pipeline? Tracked repositories and cached issues that use this connection will be removed.`
: deleteTarget?.type === "repository"
? `Delete "${repositoryName(deleteTarget.item)}" from Pipeline? Synced issue records for this repository will be removed.`
: ""