# [DEF:backend.src.api.routes.git:Module] # # @SEMANTICS: git, routes, api, fastapi, repository, deployment # @PURPOSE: Provides FastAPI endpoints for Git integration operations. # @LAYER: API # @RELATION: USES -> src.services.git_service.GitService # @RELATION: USES -> src.api.routes.git_schemas # @RELATION: USES -> src.models.git # # @INVARIANT: All Git operations must be routed through GitService. from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List, Optional import typing from src.dependencies import get_config_manager, has_permission from src.core.database import get_db from src.models.git import GitServerConfig, GitStatus, DeploymentEnvironment, GitRepository from src.api.routes.git_schemas import ( GitServerConfigSchema, GitServerConfigCreate, GitRepositorySchema, BranchSchema, BranchCreate, BranchCheckout, CommitSchema, CommitCreate, DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest ) from src.services.git_service import GitService from src.core.logger import logger, belief_scope router = APIRouter(prefix="/api/git", tags=["git"]) git_service = GitService() # [DEF:get_git_configs:Function] # @PURPOSE: List all configured Git servers. # @PRE: Database session `db` is available. # @POST: Returns a list of all GitServerConfig objects from the database. # @RETURN: List[GitServerConfigSchema] @router.get("/config", response_model=List[GitServerConfigSchema]) async def get_git_configs( db: Session = Depends(get_db), _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("get_git_configs"): return db.query(GitServerConfig).all() # [/DEF:get_git_configs:Function] # [DEF:create_git_config:Function] # @PURPOSE: Register a new Git server configuration. # @PRE: `config` contains valid GitServerConfigCreate data. # @POST: A new GitServerConfig record is created in the database. # @PARAM: config (GitServerConfigCreate) # @RETURN: GitServerConfigSchema @router.post("/config", response_model=GitServerConfigSchema) async def create_git_config( config: GitServerConfigCreate, db: Session = Depends(get_db), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("create_git_config"): db_config = GitServerConfig(**config.dict()) db.add(db_config) db.commit() db.refresh(db_config) return db_config # [/DEF:create_git_config:Function] # [DEF:delete_git_config:Function] # @PURPOSE: Remove a Git server configuration. # @PRE: `config_id` corresponds to an existing configuration. # @POST: The configuration record is removed from the database. # @PARAM: config_id (str) @router.delete("/config/{config_id}") async def delete_git_config( config_id: str, db: Session = Depends(get_db), _ = Depends(has_permission("admin:settings", "WRITE")) ): with belief_scope("delete_git_config"): db_config = db.query(GitServerConfig).filter(GitServerConfig.id == config_id).first() if not db_config: raise HTTPException(status_code=404, detail="Configuration not found") db.delete(db_config) db.commit() return {"status": "success", "message": "Configuration deleted"} # [/DEF:delete_git_config:Function] # [DEF:test_git_config:Function] # @PURPOSE: Validate connection to a Git server using provided credentials. # @PRE: `config` contains provider, url, and pat. # @POST: Returns success if the connection is validated via GitService. # @PARAM: config (GitServerConfigCreate) @router.post("/config/test") async def test_git_config( config: GitServerConfigCreate, _ = Depends(has_permission("admin:settings", "READ")) ): with belief_scope("test_git_config"): success = await git_service.test_connection(config.provider, config.url, config.pat) if success: return {"status": "success", "message": "Connection successful"} else: raise HTTPException(status_code=400, detail="Connection failed") # [/DEF:test_git_config:Function] # [DEF:init_repository:Function] # @PURPOSE: Link a dashboard to a Git repository and perform initial clone/init. # @PRE: `dashboard_id` exists and `init_data` contains valid config_id and remote_url. # @POST: Repository is initialized on disk and a GitRepository record is saved in DB. # @PARAM: dashboard_id (int) # @PARAM: init_data (RepoInitRequest) @router.post("/repositories/{dashboard_id}/init") async def init_repository( dashboard_id: int, init_data: RepoInitRequest, db: Session = Depends(get_db), _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("init_repository"): # 1. Get config config = db.query(GitServerConfig).filter(GitServerConfig.id == init_data.config_id).first() if not config: raise HTTPException(status_code=404, detail="Git configuration not found") try: # 2. Perform Git clone/init logger.info(f"[init_repository][Action] Initializing repo for dashboard {dashboard_id}") git_service.init_repo(dashboard_id, init_data.remote_url, config.pat) # 3. Save to DB repo_path = git_service._get_repo_path(dashboard_id) db_repo = db.query(GitRepository).filter(GitRepository.dashboard_id == dashboard_id).first() if not db_repo: db_repo = GitRepository( dashboard_id=dashboard_id, config_id=config.id, remote_url=init_data.remote_url, local_path=repo_path ) db.add(db_repo) else: db_repo.config_id = config.id db_repo.remote_url = init_data.remote_url db_repo.local_path = repo_path db.commit() logger.info(f"[init_repository][Coherence:OK] Repository initialized for dashboard {dashboard_id}") return {"status": "success", "message": "Repository initialized"} except Exception as e: db.rollback() logger.error(f"[init_repository][Coherence:Failed] Failed to init repository: {e}") raise HTTPException(status_code=400, detail=str(e)) # [/DEF:init_repository:Function] # [DEF:get_branches:Function] # @PURPOSE: List all branches for a dashboard's repository. # @PRE: Repository for `dashboard_id` is initialized. # @POST: Returns a list of branches from the local repository. # @PARAM: dashboard_id (int) # @RETURN: List[BranchSchema] @router.get("/repositories/{dashboard_id}/branches", response_model=List[BranchSchema]) async def get_branches( dashboard_id: int, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("get_branches"): try: return git_service.list_branches(dashboard_id) except Exception as e: raise HTTPException(status_code=404, detail=str(e)) # [/DEF:get_branches:Function] # [DEF:create_branch:Function] # @PURPOSE: Create a new branch in the dashboard's repository. # @PRE: `dashboard_id` repository exists and `branch_data` has name and from_branch. # @POST: A new branch is created in the local repository. # @PARAM: dashboard_id (int) # @PARAM: branch_data (BranchCreate) @router.post("/repositories/{dashboard_id}/branches") async def create_branch( dashboard_id: int, branch_data: BranchCreate, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("create_branch"): try: git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch) return {"status": "success"} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:create_branch:Function] # [DEF:checkout_branch:Function] # @PURPOSE: Switch the dashboard's repository to a specific branch. # @PRE: `dashboard_id` repository exists and branch `checkout_data.name` exists. # @POST: The local repository HEAD is moved to the specified branch. # @PARAM: dashboard_id (int) # @PARAM: checkout_data (BranchCheckout) @router.post("/repositories/{dashboard_id}/checkout") async def checkout_branch( dashboard_id: int, checkout_data: BranchCheckout, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("checkout_branch"): try: git_service.checkout_branch(dashboard_id, checkout_data.name) return {"status": "success"} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:checkout_branch:Function] # [DEF:commit_changes:Function] # @PURPOSE: Stage and commit changes in the dashboard's repository. # @PRE: `dashboard_id` repository exists and `commit_data` has message and files. # @POST: Specified files are staged and a new commit is created. # @PARAM: dashboard_id (int) # @PARAM: commit_data (CommitCreate) @router.post("/repositories/{dashboard_id}/commit") async def commit_changes( dashboard_id: int, commit_data: CommitCreate, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("commit_changes"): try: git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files) return {"status": "success"} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:commit_changes:Function] # [DEF:push_changes:Function] # @PURPOSE: Push local commits to the remote repository. # @PRE: `dashboard_id` repository exists and has a remote configured. # @POST: Local commits are pushed to the remote repository. # @PARAM: dashboard_id (int) @router.post("/repositories/{dashboard_id}/push") async def push_changes( dashboard_id: int, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("push_changes"): try: git_service.push_changes(dashboard_id) return {"status": "success"} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:push_changes:Function] # [DEF:pull_changes:Function] # @PURPOSE: Pull changes from the remote repository. # @PRE: `dashboard_id` repository exists and has a remote configured. # @POST: Remote changes are fetched and merged into the local branch. # @PARAM: dashboard_id (int) @router.post("/repositories/{dashboard_id}/pull") async def pull_changes( dashboard_id: int, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("pull_changes"): try: git_service.pull_changes(dashboard_id) return {"status": "success"} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:pull_changes:Function] # [DEF:sync_dashboard:Function] # @PURPOSE: Sync dashboard state from Superset to Git using the GitPlugin. # @PRE: `dashboard_id` is valid; GitPlugin is available. # @POST: Dashboard YAMLs are exported from Superset and committed to Git. # @PARAM: dashboard_id (int) # @PARAM: source_env_id (Optional[str]) @router.post("/repositories/{dashboard_id}/sync") async def sync_dashboard( dashboard_id: int, source_env_id: typing.Optional[str] = None, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("sync_dashboard"): try: from src.plugins.git_plugin import GitPlugin plugin = GitPlugin() return await plugin.execute({ "operation": "sync", "dashboard_id": dashboard_id, "source_env_id": source_env_id }) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:sync_dashboard:Function] # [DEF:get_environments:Function] # @PURPOSE: List all deployment environments. # @PRE: Config manager is accessible. # @POST: Returns a list of DeploymentEnvironmentSchema objects. # @RETURN: List[DeploymentEnvironmentSchema] @router.get("/environments", response_model=List[DeploymentEnvironmentSchema]) async def get_environments( config_manager=Depends(get_config_manager), _ = Depends(has_permission("environments", "READ")) ): with belief_scope("get_environments"): envs = config_manager.get_environments() return [ DeploymentEnvironmentSchema( id=e.id, name=e.name, superset_url=e.url, is_active=True ) for e in envs ] # [/DEF:get_environments:Function] # [DEF:deploy_dashboard:Function] # @PURPOSE: Deploy dashboard from Git to a target environment. # @PRE: `dashboard_id` and `deploy_data.environment_id` are valid. # @POST: Dashboard YAMLs are read from Git and imported into the target Superset. # @PARAM: dashboard_id (int) # @PARAM: deploy_data (DeployRequest) @router.post("/repositories/{dashboard_id}/deploy") async def deploy_dashboard( dashboard_id: int, deploy_data: DeployRequest, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("deploy_dashboard"): try: from src.plugins.git_plugin import GitPlugin plugin = GitPlugin() return await plugin.execute({ "operation": "deploy", "dashboard_id": dashboard_id, "environment_id": deploy_data.environment_id }) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:deploy_dashboard:Function] # [DEF:get_history:Function] # @PURPOSE: View commit history for a dashboard's repository. # @PRE: `dashboard_id` repository exists. # @POST: Returns a list of recent commits from the repository. # @PARAM: dashboard_id (int) # @PARAM: limit (int) # @RETURN: List[CommitSchema] @router.get("/repositories/{dashboard_id}/history", response_model=List[CommitSchema]) async def get_history( dashboard_id: int, limit: int = 50, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("get_history"): try: return git_service.get_commit_history(dashboard_id, limit) except Exception as e: raise HTTPException(status_code=404, detail=str(e)) # [/DEF:get_history:Function] # [DEF:get_repository_status:Function] # @PURPOSE: Get current Git status for a dashboard repository. # @PRE: `dashboard_id` repository exists. # @POST: Returns the status of the working directory (staged, unstaged, untracked). # @PARAM: dashboard_id (int) # @RETURN: dict @router.get("/repositories/{dashboard_id}/status") async def get_repository_status( dashboard_id: int, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("get_repository_status"): try: return git_service.get_status(dashboard_id) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:get_repository_status:Function] # [DEF:get_repository_diff:Function] # @PURPOSE: Get Git diff for a dashboard repository. # @PRE: `dashboard_id` repository exists. # @POST: Returns the diff text for the specified file or all changes. # @PARAM: dashboard_id (int) # @PARAM: file_path (Optional[str]) # @PARAM: staged (bool) # @RETURN: str @router.get("/repositories/{dashboard_id}/diff") async def get_repository_diff( dashboard_id: int, file_path: Optional[str] = None, staged: bool = False, _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("get_repository_diff"): try: diff_text = git_service.get_diff(dashboard_id, file_path, staged) return diff_text except Exception as e: raise HTTPException(status_code=400, detail=str(e)) # [/DEF:get_repository_diff:Function] # [DEF:generate_commit_message:Function] # @PURPOSE: Generate a suggested commit message using LLM. # @PRE: Repository for `dashboard_id` is initialized. # @POST: Returns a suggested commit message string. @router.post("/repositories/{dashboard_id}/generate-message") async def generate_commit_message( dashboard_id: int, db: Session = Depends(get_db), _ = Depends(has_permission("plugin:git", "EXECUTE")) ): with belief_scope("generate_commit_message"): try: # 1. Get Diff diff = git_service.get_diff(dashboard_id, staged=True) if not diff: diff = git_service.get_diff(dashboard_id, staged=False) if not diff: return {"message": "No changes detected"} # 2. Get History history_objs = git_service.get_commit_history(dashboard_id, limit=5) history = [h.message for h in history_objs if hasattr(h, 'message')] # 3. Get LLM Client from ...services.llm_provider import LLMProviderService from ...plugins.llm_analysis.service import LLMClient from ...plugins.llm_analysis.models import LLMProviderType llm_service = LLMProviderService(db) providers = llm_service.get_all_providers() provider = next((p for p in providers if p.is_active), None) if not provider: raise HTTPException(status_code=400, detail="No active LLM provider found") api_key = llm_service.get_decrypted_api_key(provider.id) client = LLMClient( provider_type=LLMProviderType(provider.provider_type), api_key=api_key, base_url=provider.base_url, default_model=provider.default_model ) # 4. Generate Message from ...plugins.git.llm_extension import GitLLMExtension extension = GitLLMExtension(client) message = await extension.suggest_commit_message(diff, history) return {"message": message} except Exception as e: logger.error(f"Failed to generate commit message: {e}") raise HTTPException(status_code=400, detail=str(e)) # [/DEF:generate_commit_message:Function] # [/DEF:backend.src.api.routes.git:Module]