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