"""Config profile resolver service functions.""" import logging import os import subprocess import uuid from typing import Any from fastapi import HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.models import ConfigProfile, SSHKey, UserConfig from src.services.shared.ssh_keys import _get_fernet from src.utils.git_url_parser import parse_git_url from src.schemas.config import ValidateGitUrlResponse logger = logging.getLogger(__name__) async def resolve_default_profile( session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, tool_type_id: uuid.UUID, ) -> dict: """Resolve the default config profile for a project/tool combination.""" query = ( select(ConfigProfile) .where(ConfigProfile.user_id == user_id) .where( (ConfigProfile.project_id.is_(None) & ConfigProfile.tool_type_id.is_(None)) | (ConfigProfile.project_id == project_id) | (ConfigProfile.tool_type_id == tool_type_id) | ( (ConfigProfile.project_id == project_id) & (ConfigProfile.tool_type_id == tool_type_id) ) ) .order_by(ConfigProfile.created_at) ) result = await session.execute(query) profiles = result.scalars().all() if not profiles: return {"profile_id": None, "profile_name": None} explicit_defaults = [p for p in profiles if p.is_default] for p in explicit_defaults: if p.project_id == project_id and p.tool_type_id == tool_type_id: return {"profile_id": str(p.id), "profile_name": p.name} for p in explicit_defaults: if p.project_id == project_id and p.tool_type_id is None: return {"profile_id": str(p.id), "profile_name": p.name} for p in explicit_defaults: if p.project_id is None and p.tool_type_id == tool_type_id: return {"profile_id": str(p.id), "profile_name": p.name} for p in explicit_defaults: if p.project_id is None and p.tool_type_id is None: return {"profile_id": str(p.id), "profile_name": p.name} first = profiles[0] return {"profile_id": str(first.id), "profile_name": first.name} async def validate_git_url( session: AsyncSession, current_user_id: uuid.UUID, url: str, ssh_key_id: str | None, ) -> ValidateGitUrlResponse: """Validate a git remote URL and list available branches.""" parse_result = parse_git_url(url) original_url = url.strip() url_to_check = parse_result.get("base_url") or original_url if not url_to_check: return ValidateGitUrlResponse( valid=False, error=parse_result.get("message", "Invalid URL"), error_code=parse_result.get("error_code", "INVALID_URL"), ) if parse_result.get("needs_parsing") and url_to_check != original_url: return ValidateGitUrlResponse( valid=False, suggested_url=url_to_check, error=parse_result.get("message"), error_code=parse_result.get("error_code", "URL_NEEDS_PARSING"), ) env = None key_path = None if ssh_key_id: try: ssh_key_uuid = uuid.UUID(ssh_key_id) except ValueError: return ValidateGitUrlResponse( valid=False, error="Invalid SSH key ID format", error_code="INVALID_SSH_KEY", ) ssh_key = await session.get(SSHKey, ssh_key_uuid) if ssh_key is None or ssh_key.user_id != current_user_id: return ValidateGitUrlResponse( valid=False, error="SSH key not found or not authorized", error_code="SSH_KEY_NOT_FOUND", ) import tempfile fernet = _get_fernet() private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode() fd, key_path = tempfile.mkstemp(prefix="ssh_key_") try: os.write(fd, private_key.encode()) finally: os.close(fd) os.chmod(key_path, 0o600) env = { "GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" } try: result = subprocess.run( ["git", "ls-remote", "--heads", url_to_check], capture_output=True, text=True, timeout=30, env={**os.environ, **env} if env else None, ) except subprocess.TimeoutExpired: if key_path and os.path.exists(key_path): os.unlink(key_path) return ValidateGitUrlResponse( valid=False, error="Remote repository check timed out", error_code="TIMEOUT", ) except FileNotFoundError: if key_path and os.path.exists(key_path): os.unlink(key_path) return ValidateGitUrlResponse( valid=False, error="git command not found on server", error_code="GIT_NOT_FOUND", ) finally: if key_path and os.path.exists(key_path): os.unlink(key_path) if result.returncode != 0: stderr = result.stderr.strip() if ( "could not resolve" in stderr.lower() or "unable to access" in stderr.lower() ): error_msg = "Could not reach repository. Check the URL and network access." error_code = "UNREACHABLE" elif ( "authentication" in stderr.lower() or "permission denied" in stderr.lower() ): error_msg = ( "Authentication failed. Provide an SSH key for private repositories." ) error_code = "AUTH_FAILED" else: error_msg = f"Repository not accessible: {stderr[:200]}" error_code = "REMOTE_ERROR" return ValidateGitUrlResponse( valid=False, error=error_msg, error_code=error_code, ) branches: list[str] = [] default_branch = "main" for line in result.stdout.strip().split("\n"): if not line.strip(): continue parts = line.split() if len(parts) == 2: ref = parts[1] if ref.startswith("refs/heads/"): branch_name = ref[len("refs/heads/") :] branches.append(branch_name) if branch_name in ("main", "master"): default_branch = branch_name if not branches: return ValidateGitUrlResponse( valid=False, error="No branches found in remote repository", error_code="NO_BRANCHES", ) return ValidateGitUrlResponse( valid=True, suggested_url=url_to_check if url_to_check != original_url else None, branches=branches, default_branch=default_branch, )