bifroest/app/changes/routes.py
2026-10-27 08:34:17 +01:00

124 lines
6.8 KiB
Python

"""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