#!/usr/bin/env python3 """ Git Repositories API Endpoints for WHOOSH Provides REST API for git repository management and integration """ import logging from typing import Dict, List, Any, Optional from fastapi import APIRouter, HTTPException, Query, Depends from pydantic import BaseModel, Field, field_validator from ..services.git_repository_service import git_repository_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/git-repositories", tags=["git-repositories"]) # Request/Response Models class GitCredentialsRequest(BaseModel): username: Optional[str] = Field(None, description="Git username") password: Optional[str] = Field(None, description="Git password or token") ssh_key_content: Optional[str] = Field(None, description="SSH private key content") ssh_key_path: Optional[str] = Field(None, description="Path to SSH private key file") auth_type: str = Field(default="https", description="Authentication type: https, ssh, token") @field_validator('auth_type') @classmethod def validate_auth_type(cls, v): if v not in ['https', 'ssh', 'token']: raise ValueError('auth_type must be one of: https, ssh, token') return v class AddRepositoryRequest(BaseModel): name: str = Field(..., description="Repository display name") url: str = Field(..., description="Git repository URL") credentials: GitCredentialsRequest = Field(..., description="Git authentication credentials") project_id: Optional[str] = Field(None, description="Associated project ID") @field_validator('url') @classmethod def validate_url(cls, v): if not v.startswith(('http://', 'https://', 'git@', 'ssh://')): raise ValueError('URL must be a valid git repository URL') return v class UpdateCredentialsRequest(BaseModel): credentials: GitCredentialsRequest = Field(..., description="Updated git credentials") # API Endpoints @router.get("/") async def list_repositories( project_id: Optional[str] = Query(None, description="Filter by project ID") ) -> Dict[str, Any]: """Get list of all git repositories, optionally filtered by project""" try: logger.info(f"📂 Listing repositories (project_id: {project_id})") repositories = await git_repository_service.get_repositories(project_id) return { "success": True, "data": { "repositories": repositories, "count": len(repositories) } } except Exception as e: logger.error(f"❌ Error listing repositories: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/") async def add_repository(request: AddRepositoryRequest) -> Dict[str, Any]: """Add a new git repository with credentials""" try: logger.info(f"📥 Adding repository: {request.name}") # Convert credentials to dict credentials_dict = request.credentials.dict() result = await git_repository_service.add_repository( name=request.name, url=request.url, credentials=credentials_dict, project_id=request.project_id ) if result["success"]: logger.info(f"✅ Repository {request.name} added successfully") else: logger.error(f"❌ Failed to add repository {request.name}: {result.get('error')}") return { "success": result["success"], "data": result } except Exception as e: logger.error(f"❌ Error adding repository: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/{repo_id}") async def get_repository(repo_id: str) -> Dict[str, Any]: """Get details of a specific repository""" try: logger.info(f"🔍 Getting repository: {repo_id}") repository = await git_repository_service.get_repository(repo_id) if not repository: raise HTTPException(status_code=404, detail="Repository not found") return { "success": True, "data": repository } except HTTPException: raise except Exception as e: logger.error(f"❌ Error getting repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put("/{repo_id}/credentials") async def update_credentials( repo_id: str, request: UpdateCredentialsRequest ) -> Dict[str, Any]: """Update git credentials for a repository""" try: logger.info(f"🔐 Updating credentials for repository: {repo_id}") # Check if repository exists repo = await git_repository_service.get_repository(repo_id) if not repo: raise HTTPException(status_code=404, detail="Repository not found") # Update credentials in the repository object if repo_id in git_repository_service.repositories: credentials_dict = request.credentials.dict() from ..services.git_repository_service import GitCredentials git_repo = git_repository_service.repositories[repo_id] git_repo.credentials = GitCredentials( repo_url=git_repo.url, **credentials_dict ) await git_repository_service._save_repositories() logger.info(f"✅ Credentials updated for repository: {repo_id}") return { "success": True, "message": "Credentials updated successfully" } else: raise HTTPException(status_code=404, detail="Repository not found") except HTTPException: raise except Exception as e: logger.error(f"❌ Error updating credentials for repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/{repo_id}/update") async def update_repository(repo_id: str) -> Dict[str, Any]: """Pull latest changes from repository""" try: logger.info(f"🔄 Updating repository: {repo_id}") result = await git_repository_service.update_repository(repo_id) if result["success"]: logger.info(f"✅ Repository {repo_id} updated successfully") else: logger.error(f"❌ Failed to update repository {repo_id}: {result.get('error')}") return { "success": result["success"], "data": result } except Exception as e: logger.error(f"❌ Error updating repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/{repo_id}") async def remove_repository(repo_id: str) -> Dict[str, Any]: """Remove a git repository""" try: logger.info(f"🗑️ Removing repository: {repo_id}") result = await git_repository_service.remove_repository(repo_id) if result["success"]: logger.info(f"✅ Repository {repo_id} removed successfully") else: logger.error(f"❌ Failed to remove repository {repo_id}: {result.get('error')}") return { "success": result["success"], "data": result } except Exception as e: logger.error(f"❌ Error removing repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/{repo_id}/files") async def get_repository_files( repo_id: str, path: str = Query("", description="Directory path within repository"), max_depth: int = Query(2, description="Maximum directory depth to scan") ) -> Dict[str, Any]: """Get file structure of a repository""" try: logger.info(f"📁 Getting files for repository: {repo_id}, path: {path}") result = await git_repository_service.get_repository_files( repo_id=repo_id, path=path, max_depth=max_depth ) if result["success"]: logger.info(f"✅ Files retrieved for repository {repo_id}") else: logger.error(f"❌ Failed to get files for repository {repo_id}: {result.get('error')}") return { "success": result["success"], "data": result } except Exception as e: logger.error(f"❌ Error getting files for repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/{repo_id}/files/content") async def get_file_content( repo_id: str, file_path: str = Query(..., description="Path to file within repository"), max_size: int = Query(1024*1024, description="Maximum file size in bytes") ) -> Dict[str, Any]: """Get content of a specific file in the repository""" try: logger.info(f"📄 Getting file content: {repo_id}/{file_path}") result = await git_repository_service.get_file_content( repo_id=repo_id, file_path=file_path, max_size=max_size ) if result["success"]: logger.info(f"✅ File content retrieved: {repo_id}/{file_path}") else: logger.error(f"❌ Failed to get file content {repo_id}/{file_path}: {result.get('error')}") return { "success": result["success"], "data": result } except Exception as e: logger.error(f"❌ Error getting file content {repo_id}/{file_path}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/{repo_id}/status") async def get_repository_status(repo_id: str) -> Dict[str, Any]: """Get current status of a repository (cloning, ready, error, etc.)""" try: logger.info(f"📊 Getting status for repository: {repo_id}") repository = await git_repository_service.get_repository(repo_id) if not repository: raise HTTPException(status_code=404, detail="Repository not found") return { "success": True, "data": { "repository_id": repo_id, "name": repository["name"], "status": repository["status"], "last_updated": repository.get("last_updated"), "commit_hash": repository.get("commit_hash"), "commit_message": repository.get("commit_message"), "error_message": repository.get("error_message") } } except HTTPException: raise except Exception as e: logger.error(f"❌ Error getting status for repository {repo_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) # Health check for the git repository service @router.get("/health/check") async def health_check() -> Dict[str, Any]: """Health check for git repository service""" try: return { "success": True, "service": "git_repositories", "status": "healthy", "repositories_count": len(git_repository_service.repositories) } except Exception as e: logger.error(f"❌ Health check failed: {e}") return { "success": False, "service": "git_repositories", "status": "unhealthy", "error": str(e) }