"""Ansicht „Änderungen“ im Chat und die Aktionen dazu (→ Git-Workflow). Jede Aktion löst ein Mitglied des Workspace aus; per HTMX kommt die aktualisierte Ansicht zurück, Fehler erscheinen darin. """ from collections.abc import Callable from typing import Annotated from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse, Response from app import web from app.changes import service as changes from app.changes.models import ChatBranch from app.chat import service as chats from app.core.errors import Invalid, NotFound from app.data_sources.connectors.git_repo import GitError Text = Annotated[str, Form()] def router() -> APIRouter: r = APIRouter() def panel(request: Request, principal, chat_id: int, error: str = "", notice: str = "", status: int = 200) -> HTMLResponse: with web.db(request).session() as session: chat = chats.get(session, principal, chat_id) views = changes.views(session, web.settings(request), web.vault(request), chat.id) return web.render(request, "_changes.html", {"chat": chat, "views": views, "error": error, "notice": notice}, status) def act(request: Request, principal, branch_id: int, action: Callable, notice: str) -> Response: with web.db(request).session() as session: branch = session.get(ChatBranch, branch_id) if branch is None: raise NotFound chat_id = branch.chat_session_id try: with web.db(request).session() as session: action(session) except (Invalid, GitError) as e: return panel(request, principal, chat_id, error=str(e), status=400) if request.headers.get("HX-Request"): return panel(request, principal, chat_id, notice=notice) return web.redirect(f"/chats/{chat_id}#changes") @r.get("/chats/{chat_id}/changes") def show(request: Request, principal: web.User, chat_id: int) -> HTMLResponse: return panel(request, principal, chat_id) @r.post("/branches/{branch_id}/push") def push(request: Request, principal: web.Checked, branch_id: int) -> Response: return act(request, principal, branch_id, lambda s: changes.push( s, principal, web.settings(request), web.vault(request), branch_id), "Übertragen.") @r.post("/branches/{branch_id}/pull-request") def pull_request(request: Request, principal: web.Checked, branch_id: int, title: Text, description: Text = "", draft: Annotated[bool, Form()] = False) -> Response: def create(session) -> None: branch = changes.create_pull_request(session, principal, web.settings(request), web.vault(request), branch_id, title, description, draft) if not branch.pr_id: raise Invalid(f"Branch {branch.name} ist gepusht. Für diese Quelle ist kein Pull-Request-Anbieter " "eingestellt; den Pull Request bitte im Git-Server anlegen.") return act(request, principal, branch_id, create, "Pull Request erstellt.") @r.post("/branches/{branch_id}/ready") def ready(request: Request, principal: web.Checked, branch_id: int) -> Response: return act(request, principal, branch_id, lambda s: changes.publish( s, principal, web.settings(request), web.vault(request), branch_id), "Als bereit zum Review markiert.") @r.post("/branches/{branch_id}/undo") def undo(request: Request, principal: web.Checked, branch_id: int) -> Response: return act(request, principal, branch_id, lambda s: changes.undo( s, principal, web.settings(request), web.vault(request), branch_id), "Letzten Commit zurückgenommen.") @r.post("/branches/{branch_id}/refresh") def refresh(request: Request, principal: web.Checked, branch_id: int) -> Response: def update(session) -> None: t = changes.load(session, principal, web.settings(request), web.vault(request), branch_id) if not t.branch.pr_id: raise Invalid("Es gibt noch keinen Pull Request.") for event in changes.refresh(session, web.settings(request), web.vault(request), t.branch): if event != "comments": changes.notify_event(session, t.branch, event) return act(request, principal, branch_id, update, "Stand aus Bitbucket aktualisiert.") @r.post("/branches/{branch_id}/sync") def sync(request: Request, principal: web.Checked, branch_id: int) -> Response: """Mit dem Ziel-Branch abgleichen (Merge-Commit); nur konfliktfrei, Konflikte löst die KI.""" return act(request, principal, branch_id, lambda s: changes.sync_target( web.db(request), principal, web.settings(request), web.vault(request), branch_id), "Mit dem Ziel-Branch abgeglichen (Merge-Commit, noch nicht übertragen).") @r.post("/branches/{branch_id}/resolve") def resolve(request: Request, principal: web.Checked, branch_id: int) -> Response: """Konflikte von der KI lösen lassen: eine Nachricht im Chat, die KI nutzt merge_target.""" with web.db(request).session() as session: t = changes.load(session, principal, web.settings(request), web.vault(request), branch_id, lock=False) changes.check_open(t.branch) chats.send(session, principal, t.chat.id, f"Bitte gleiche den Branch {t.branch.name} mit {t.branch.target} ab: rufe merge_target auf " f"und löse die Konflikte so, dass die Änderungen aus {t.branch.target} erhalten bleiben und " "unsere Änderungen weiter wirken. Fasse danach zusammen, wie du die Konflikte gelöst hast.") chat_id = t.chat.id return web.redirect(f"/chats/{chat_id}#messages-end") @r.post("/branches/{branch_id}/review-comments") def review_comments(request: Request, principal: web.Checked, branch_id: int) -> Response: """Kommentare einarbeiten: eine Nachricht im Chat, die KI liest die Kommentare mit git_pr_comments.""" with web.db(request).session() as session: t = changes.load(session, principal, web.settings(request), web.vault(request), branch_id, lock=False) if not t.branch.pr_id or t.branch.closed: raise Invalid("Es gibt keinen offenen Pull Request.") chats.send(session, principal, t.chat.id, f"Bitte arbeite die Review-Kommentare aus Pull Request #{t.branch.pr_id} ein: lies sie mit " "git_pr_comments, setze berechtigte Hinweise mit commit_change um und fasse zusammen, was du " "geändert hast und was offen bleibt.") chat_id = t.chat.id return web.redirect(f"/chats/{chat_id}#messages-end") return r