Commit 9403955f authored by Administrator's avatar Administrator

Update backend/routes/chat_routes.py via Son of Anton

parent 356bf7b4
""" """
Chat CRUD + message streaming — v4.2.0 with permission enforcement. Chat CRUD + message streaming — v4.3.0 with GitLab for users, branch auto-create, commit fixes.
""" """
import json import json
...@@ -55,8 +55,46 @@ class CommitFromChatBody(BaseModel): ...@@ -55,8 +55,46 @@ class CommitFromChatBody(BaseModel):
branch: str branch: str
commit_message: str commit_message: str
files: list[dict] files: list[dict]
create_branch_if_missing: bool = True
# ═══════════════════════════════════════════════════
# User-facing: Available Repos & Branches
# ═══════════════════════════════════════════════════
@router.get("/available-repos")
def list_available_repos(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""List linked repos that any user with gitlab permission can select for chats."""
perms = get_user_permissions(user.id, db)
if not perms.get("can_use_gitlab"):
return []
repos = db.query(LinkedRepo).order_by(LinkedRepo.created_at.desc()).all()
return [_repo_brief(r) for r in repos]
@router.get("/repos/{repo_id}/branches")
async def get_repo_branches(repo_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
"""Get branches for a linked repo — available to users with gitlab permission."""
check_feature(user.id, "use_gitlab", db)
repo = db.query(LinkedRepo).filter(LinkedRepo.id == repo_id).first()
if not repo:
raise HTTPException(404, "Repository not found")
settings = db.query(GitLabSettings).first()
if not settings or not settings.is_active:
raise HTTPException(400, "GitLab not configured")
try:
branches = await gitlab_service.list_branches(
settings.gitlab_url, settings.gitlab_token, repo.gitlab_project_id
)
return branches
except gitlab_service.GitLabError as e:
raise HTTPException(e.status_code, e.detail)
# ═══════════════════════════════════════════════════
# Chat CRUD
# ═══════════════════════════════════════════════════
@router.get("") @router.get("")
def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)): def list_chats(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chats = db.query(Chat).filter(Chat.user_id == user.id).order_by(Chat.updated_at.desc()).all() chats = db.query(Chat).filter(Chat.user_id == user.id).order_by(Chat.updated_at.desc()).all()
...@@ -142,6 +180,10 @@ def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Sessi ...@@ -142,6 +180,10 @@ def get_messages(chat_id: str, user: User = Depends(get_current_user), db: Sessi
return msgs return msgs
# ═══════════════════════════════════════════════════
# Streaming & Generation
# ═══════════════════════════════════════════════════
@router.get("/{chat_id}/generating") @router.get("/{chat_id}/generating")
def check_generating(chat_id: str, user: User = Depends(get_current_user)): def check_generating(chat_id: str, user: User = Depends(get_current_user)):
return {"active": gen_manager.is_active(chat_id)} return {"active": gen_manager.is_active(chat_id)}
...@@ -198,6 +240,10 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends ...@@ -198,6 +240,10 @@ async def send_message(chat_id: str, body: SendMessageBody, user: User = Depends
return StreamingResponse(generate(), media_type="text/event-stream") return StreamingResponse(generate(), media_type="text/event-stream")
# ═══════════════════════════════════════════════════
# GitLab Commit from Chat (FIXED)
# ═══════════════════════════════════════════════════
@router.post("/{chat_id}/commit") @router.post("/{chat_id}/commit")
async def commit_from_chat( async def commit_from_chat(
chat_id: str, chat_id: str,
...@@ -207,8 +253,9 @@ async def commit_from_chat( ...@@ -207,8 +253,9 @@ async def commit_from_chat(
): ):
""" """
Commit files from chat to linked GitLab repo. Commit files from chat to linked GitLab repo.
Auto-detects whether each file should be 'create' or 'update' - Auto-detects create vs update per file.
by checking the repo tree, so it never fails on wrong action type. - Auto-creates branch if it doesn't exist and create_branch_if_missing=True.
- Returns clear success/failure with details.
""" """
check_feature(user.id, "use_gitlab", db) check_feature(user.id, "use_gitlab", db)
...@@ -216,7 +263,7 @@ async def commit_from_chat( ...@@ -216,7 +263,7 @@ async def commit_from_chat(
if not chat: if not chat:
raise HTTPException(404, "Chat not found") raise HTTPException(404, "Chat not found")
if not chat.linked_repo_id: if not chat.linked_repo_id:
raise HTTPException(400, "No repository linked") raise HTTPException(400, "No repository linked to this chat")
repo = db.query(LinkedRepo).filter(LinkedRepo.id == chat.linked_repo_id).first() repo = db.query(LinkedRepo).filter(LinkedRepo.id == chat.linked_repo_id).first()
if not repo: if not repo:
...@@ -224,87 +271,124 @@ async def commit_from_chat( ...@@ -224,87 +271,124 @@ async def commit_from_chat(
settings = db.query(GitLabSettings).first() settings = db.query(GitLabSettings).first()
if not settings or not settings.is_active: if not settings or not settings.is_active:
raise HTTPException(400, "GitLab not configured") raise HTTPException(400, "GitLab not configured. Ask your admin to set it up.")
branch = body.branch.strip()
if not branch:
branch = repo.default_branch or "main"
# ── Step 1: Check if branch exists, create if needed ──
branch_exists = True
try:
branches = await gitlab_service.list_branches(
settings.gitlab_url, settings.gitlab_token, repo.gitlab_project_id
)
branch_names = {b["name"] for b in branches}
if branch not in branch_names:
branch_exists = False
except Exception:
# If we can't list branches, we'll try anyway
pass
# ── Fetch repo tree to know which files already exist ── if not branch_exists and body.create_branch_if_missing:
try:
await gitlab_service.create_branch(
settings.gitlab_url, settings.gitlab_token,
repo.gitlab_project_id, branch, repo.default_branch or "main",
)
except gitlab_service.GitLabError as e:
if "already exists" not in str(e.detail).lower():
raise HTTPException(e.status_code, f"Could not create branch '{branch}': {e.detail}")
# ── Step 2: Fetch repo tree to detect create vs update ──
existing_paths = set() existing_paths = set()
try: try:
tree = await gitlab_service.get_tree( tree = await gitlab_service.get_tree(
settings.gitlab_url, settings.gitlab_url, settings.gitlab_token,
settings.gitlab_token, repo.gitlab_project_id, ref=branch, recursive=True,
repo.gitlab_project_id,
ref=body.branch,
recursive=True,
) )
existing_paths = { existing_paths = {item["path"] for item in tree if item["type"] == "blob"}
item["path"] for item in tree if item["type"] == "blob"
}
except Exception: except Exception:
# If tree fetch fails (empty repo, network issue, etc.), # Empty repo or branch just created — all files are "create"
# we'll try all as "create" since we can't know what exists
pass pass
# ── Build actions with auto-detected create/update ── # ── Step 3: Build commit actions ──
actions = [] actions = []
committed_files = []
for f in body.files: for f in body.files:
file_path = f.get("file_path", "") file_path = f.get("file_path", "").strip()
content = f.get("content", "") content = f.get("content", "")
requested_action = f.get("action", "auto")
if not file_path or not content: if not file_path or not content:
continue continue
file_exists = file_path in existing_paths file_exists = file_path in existing_paths
actual_action = "update" if file_exists else "create"
# Smart action resolution
if requested_action in ("auto", "upsert"):
# Auto-detect: update if exists, create if not
actual_action = "update" if file_exists else "create"
elif requested_action == "update" and not file_exists:
# User said update but file doesn't exist → create instead
actual_action = "create"
elif requested_action == "create" and file_exists:
# User said create but file already exists → update instead
actual_action = "update"
else:
actual_action = requested_action
actions.append({ actions.append({
"action": actual_action, "action": actual_action,
"file_path": file_path, "file_path": file_path,
"content": content, "content": content,
}) })
committed_files.append({"file_path": file_path, "action": actual_action})
if not actions: if not actions:
raise HTTPException(400, "No valid files to commit") raise HTTPException(400, "No valid files to commit")
# ── Step 4: Execute commit ──
try: try:
result = await gitlab_service.commit_files( result = await gitlab_service.commit_files(
settings.gitlab_url, settings.gitlab_url, settings.gitlab_token,
settings.gitlab_token, repo.gitlab_project_id, branch, body.commit_message, actions,
repo.gitlab_project_id,
body.branch,
body.commit_message,
actions,
) )
gen_manager.invalidate_repo_cache(repo.id) gen_manager.invalidate_repo_cache(repo.id)
return { return {
"ok": True, "ok": True,
"commit": result, "commit": result,
"files_committed": len(actions), "files_committed": len(actions),
"files": committed_files,
"branch": branch,
"branch_created": not branch_exists and body.create_branch_if_missing,
} }
except gitlab_service.GitLabError as e: except gitlab_service.GitLabError as e:
# Check if it's a "already exists" type error (file was committed despite error)
error_detail = str(e.detail).lower()
if "a]ready exists" in error_detail or "already been taken" in error_detail:
# Files might have been committed — retry with update action
for a in actions:
if a["action"] == "create":
a["action"] = "update"
try:
result = await gitlab_service.commit_files(
settings.gitlab_url, settings.gitlab_token,
repo.gitlab_project_id, branch, body.commit_message, actions,
)
gen_manager.invalidate_repo_cache(repo.id)
return {
"ok": True,
"commit": result,
"files_committed": len(actions),
"files": committed_files,
"branch": branch,
"retried": True,
}
except gitlab_service.GitLabError as e2:
raise HTTPException(e2.status_code, f"Commit failed on retry: {e2.detail}")
raise HTTPException(e.status_code, f"Commit failed: {e.detail}") raise HTTPException(e.status_code, f"Commit failed: {e.detail}")
@router.post("/{chat_id}/refresh-repo") @router.post("/{chat_id}/refresh-repo")
async def refresh_repo_context(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)): async def refresh_repo_context(chat_id: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first() chat = db.query(Chat).filter(Chat.id == chat_id, Chat.user_id == user.id).first()
if not chat or not chat.linked_repo_id: raise HTTPException(400, "No repo linked") if not chat or not chat.linked_repo_id:
raise HTTPException(400, "No repo linked")
gen_manager.invalidate_repo_cache(chat.linked_repo_id) gen_manager.invalidate_repo_cache(chat.linked_repo_id)
return {"ok": True} return {"ok": True}
# ═══════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════
def _sse(data): def _sse(data):
return f"data: {json.dumps(data)}\n\n" return f"data: {json.dumps(data)}\n\n"
...@@ -347,4 +431,17 @@ def _att_brief(a): ...@@ -347,4 +431,17 @@ def _att_brief(a):
"id": a.id, "original_filename": a.original_filename, "id": a.id, "original_filename": a.original_filename,
"mime_type": a.mime_type, "file_type": a.file_type, "mime_type": a.mime_type, "file_type": a.file_type,
"file_size": a.file_size, "file_size": a.file_size,
}
def _repo_brief(r):
return {
"id": r.id,
"gitlab_project_id": r.gitlab_project_id,
"name": r.name,
"path_with_namespace": r.path_with_namespace,
"default_branch": r.default_branch,
"web_url": r.web_url,
"description": r.description or "",
"map_status": r.map_status or "none",
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment