Files
Loki/backend/app/routes/git.py
T
Claude 552548e4b4 Niveau supérieur (inspiré de Jean) : modes Plan/Build/Yolo + panneau Git & Diff
Modes d'exécution (composer) :
- routes/chat : champ mode ; _apply_mode adapte la config —
  plan = outils lecture seule + plan forcé ; yolo = confirm_shell off ;
  build = comportement normal.
- Frontend : ModeSelector dans le composer, mode dans le store + requête chat.

Panneau Git & Diff (le workspace est un dépôt git, Aider commite) :
- routes/git : /api/git/log (commits), /api/git/diff (commit ou working),
  /api/git/revert (git revert --no-edit, confiné au workspace).
- Frontend : onglet Git dans le PreviewPanel — liste des commits, diff
  colorisé, bouton Annuler (revert) avec rafraîchissement de l'arbo.

Bonus : le panneau Matériel explique que « GPU déclaré 12 Go » vient de
GPU_VRAM_MB (valeur manuelle) — à corriger en 16000 pour une 16 Go.

Tests : git log/diff/revert, modes plan/yolo/build via la route chat, builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
2026-07-15 23:48:06 +00:00

93 lines
3.1 KiB
Python

"""Routes Git du workspace : historique, diff, retour arrière.
Le workspace est un dépôt git (Aider commite chaque modification). Ces routes
donnent à l'UI un panneau Git : voir les commits, leur diff, et annuler une
modification. Toutes les commandes sont exécutées DANS le workspace.
"""
from __future__ import annotations
import subprocess
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from ..coder import ensure_git
from ..config import settings
router = APIRouter(prefix="/api/git", tags=["git"])
def _git(*args: str, timeout: int = 15) -> subprocess.CompletedProcess:
"""Exécute une commande git dans le workspace."""
ensure_git(settings.workspace_dir)
return subprocess.run(
["git", *args],
cwd=settings.workspace_dir,
capture_output=True,
text=True,
timeout=timeout,
)
@router.get("/log")
async def git_log(limit: int = 40) -> dict:
"""Historique des commits (hash court, message, auteur, date, nb fichiers)."""
fmt = "%h%x1f%s%x1f%an%x1f%ar%x1f%H"
proc = _git("log", f"-{max(1, min(limit, 200))}", f"--pretty=format:{fmt}")
if proc.returncode != 0:
# Dépôt sans commit encore.
return {"commits": []}
commits = []
for line in proc.stdout.splitlines():
parts = line.split("\x1f")
if len(parts) != 5:
continue
short, subject, author, when, full = parts
# Nombre de fichiers touchés par ce commit.
stat = _git("show", "--stat", "--oneline", "--format=", full)
files = [l for l in stat.stdout.splitlines() if "|" in l]
commits.append({
"hash": short,
"full_hash": full,
"subject": subject,
"author": author,
"when": when,
"files_changed": len(files),
})
return {"commits": commits}
@router.get("/diff")
async def git_diff(hash: str | None = None) -> dict:
"""Diff d'un commit (hash) ou des modifications non commitées si absent."""
if hash:
if not hash.replace("-", "").isalnum():
raise HTTPException(400, "hash invalide")
proc = _git("show", "--no-color", hash)
else:
proc = _git("diff", "--no-color", "HEAD")
if proc.returncode != 0:
raise HTTPException(404, "diff introuvable")
# Borne la taille pour ne pas noyer l'UI.
return {"diff": proc.stdout[:200_000]}
class RevertRequest(BaseModel):
hash: str
@router.post("/revert")
async def git_revert(req: RevertRequest) -> dict:
"""Annule un commit en créant un commit inverse (git revert)."""
h = req.hash.strip()
if not h.replace("-", "").isalnum():
raise HTTPException(400, "hash invalide")
proc = _git("revert", "--no-edit", h, timeout=30)
if proc.returncode != 0:
# Conflit ou commit introuvable : on nettoie un éventuel revert en cours.
_git("revert", "--abort")
detail = (proc.stderr or proc.stdout).strip()[:300]
raise HTTPException(409, f"revert impossible : {detail}")
return {"ok": True, "message": f"commit {h[:7]} annulé"}