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

76 lines
4.1 KiB
Python

"""Branch je Chat und seine Commits (→ Git-Workflow: Chat = Branch, Mensch erstellt den PR).
Die KI committet frei auf den Branch ihres Chats. Die Commits liegen im Klon der
Plattform, bis ein Mensch „Branch pushen“ oder „Pull Request erstellen“ wählt.
Je Commit sind die Änderungen gespeichert, damit sich der Branch nach einem Verlust
des Klons exakt nachbauen lässt (fester Zeitpunkt, gleicher Inhalt → gleicher Commit).
"""
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import JSON, BigInteger, ForeignKey, Identity, Index, String, Text, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base, now
OPEN, MERGED, DECLINED = "OPEN", "MERGED", "DECLINED"
class ChatBranch(Base):
__tablename__ = "chat_branches"
# Je Chat und Quelle höchstens ein aktiver Branch; nach Merge oder Ablehnung beginnt ein neuer
__table_args__ = (Index("uq_chat_branches_active", "chat_session_id", "source_id", unique=True,
postgresql_where=text("closed IS NULL")),)
id: Mapped[int] = mapped_column(primary_key=True)
workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id", ondelete="CASCADE"), index=True)
chat_session_id: Mapped[int] = mapped_column(ForeignKey("chat_sessions.id", ondelete="CASCADE"), index=True)
source_id: Mapped[int] = mapped_column(ForeignKey("data_sources.id", ondelete="CASCADE"), index=True)
name: Mapped[str] = mapped_column(String(250)) # ai/c<chat>-<titel>
target: Mapped[str] = mapped_column(String(250)) # Ziel-Branch der Quelle beim Anlegen
base: Mapped[str] = mapped_column(String(40)) # Ausgangs-Commit
head: Mapped[str] = mapped_column(String(40)) # aktueller Stand in der Plattform
pushed: Mapped[str | None] = mapped_column(String(40)) # zuletzt übertragener Stand
pr_id: Mapped[str | None] = mapped_column(String(40))
pr_url: Mapped[str | None] = mapped_column(Text)
pr_state: Mapped[str] = mapped_column(String(20), default="") # OPEN | MERGED | DECLINED
pr_draft: Mapped[bool] = mapped_column(default=False)
# Zuletzt gesehener Stand aus Bitbucket: Version, Kommentare, Reviewer
pr_info: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
pr_checked: Mapped[datetime | None]
closed: Mapped[datetime | None] # PR gemergt oder abgelehnt: keine weiteren Commits
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
created: Mapped[datetime] = mapped_column(default=now)
updated: Mapped[datetime] = mapped_column(default=now)
@property
def ref(self) -> str:
"""Lokale Referenz im Klon; hält die Commits gegen Aufräumen durch git gc fest."""
return f"refs/aiw/branch-{self.id}"
@property
def unpushed(self) -> bool:
return self.head != self.pushed and self.head != self.base
class BranchCommit(Base):
__tablename__ = "branch_commits"
id: Mapped[int] = mapped_column(BigInteger, Identity(), primary_key=True)
branch_id: Mapped[int] = mapped_column(ForeignKey("chat_branches.id", ondelete="CASCADE"), index=True)
sha: Mapped[str] = mapped_column(String(40))
parent: Mapped[str] = mapped_column(String(40))
# Merge-Commit „Mit Ziel-Branch abgleichen“: zweiter Elternteil (Stand des Ziel-Branches); changes sind
# dann die Konfliktlösungen
merged: Mapped[str | None] = mapped_column(String(40))
message: Mapped[str] = mapped_column(Text)
# Geprüfte Änderungen und Zeitpunkt: daraus entsteht der Commit erneut
changes: Mapped[list[dict[str, Any]]] = mapped_column(JSON)
date: Mapped[str] = mapped_column(String(40))
files: Mapped[list[str]] = mapped_column(JSON, default=list)
# Hinweise der statischen Prüfung (Syntax), z. B. "a.py:3: Python-Syntaxfehler: …"; der Commit entsteht trotzdem
warnings: Mapped[list[str]] = mapped_column(JSON, default=list, server_default="[]")
run_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, ForeignKey("task_runs.id", ondelete="SET NULL"))
created: Mapped[datetime] = mapped_column(default=now)