- Migrated from HIVE branding to WHOOSH across all components - Enhanced backend API with new services: AI models, BZZZ integration, templates, members - Added comprehensive testing suite with security, performance, and integration tests - Improved frontend with new components for project setup, AI models, and team management - Updated MCP server implementation with WHOOSH-specific tools and resources - Enhanced deployment configurations with production-ready Docker setups - Added comprehensive documentation and setup guides - Implemented age encryption service and UCXL integration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
319 lines
11 KiB
Python
319 lines
11 KiB
Python
#!/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)
|
|
} |