git list refactor

This commit is contained in:
2026-03-01 12:13:19 +03:00
parent e15eb115c2
commit 4769fbd258
26 changed files with 10313 additions and 2179 deletions

View File

@@ -14,6 +14,7 @@ from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List, Optional
import typing
import os
from src.dependencies import get_config_manager, has_permission
from src.core.database import get_db
from src.models.git import GitServerConfig, GitRepository
@@ -21,7 +22,8 @@ from src.api.routes.git_schemas import (
GitServerConfigSchema, GitServerConfigCreate,
BranchSchema, BranchCreate,
BranchCheckout, CommitSchema, CommitCreate,
DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest
DeploymentEnvironmentSchema, DeployRequest, RepoInitRequest,
RepoStatusBatchRequest, RepoStatusBatchResponse,
)
from src.services.git_service import GitService
from src.core.logger import logger, belief_scope
@@ -33,6 +35,69 @@ from ...services.llm_prompt_templates import (
router = APIRouter(tags=["git"])
git_service = GitService()
MAX_REPOSITORY_STATUS_BATCH = 50
# [DEF:_build_no_repo_status_payload:Function]
# @PURPOSE: Build a consistent status payload for dashboards without initialized repositories.
# @PRE: None.
# @POST: Returns a stable payload compatible with frontend repository status parsing.
# @RETURN: dict
def _build_no_repo_status_payload() -> dict:
return {
"is_dirty": False,
"untracked_files": [],
"modified_files": [],
"staged_files": [],
"current_branch": None,
"upstream_branch": None,
"has_upstream": False,
"ahead_count": 0,
"behind_count": 0,
"is_diverged": False,
"sync_state": "NO_REPO",
"sync_status": "NO_REPO",
"has_repo": False,
}
# [/DEF:_build_no_repo_status_payload:Function]
# [DEF:_handle_unexpected_git_route_error:Function]
# @PURPOSE: Convert unexpected route-level exceptions to stable 500 API responses.
# @PRE: `error` is a non-HTTPException instance.
# @POST: Raises HTTPException(500) with route-specific context.
# @PARAM: route_name (str)
# @PARAM: error (Exception)
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
logger.error(f"[{route_name}][Coherence:Failed] {error}")
raise HTTPException(status_code=500, detail=f"{route_name} failed: {str(error)}")
# [/DEF:_handle_unexpected_git_route_error:Function]
# [DEF:_resolve_repository_status:Function]
# @PURPOSE: Resolve repository status for one dashboard with graceful NO_REPO semantics.
# @PRE: `dashboard_id` is a valid integer.
# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.
# @PARAM: dashboard_id (int)
# @RETURN: dict
def _resolve_repository_status(dashboard_id: int) -> dict:
repo_path = git_service._get_repo_path(dashboard_id)
if not os.path.exists(repo_path):
logger.debug(
f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}"
)
return _build_no_repo_status_payload()
try:
return git_service.get_status(dashboard_id)
except HTTPException as e:
if e.status_code == 404:
logger.debug(
f"[get_repository_status][Action] Repository is not initialized for dashboard {dashboard_id}"
)
return _build_no_repo_status_payload()
raise
# [/DEF:_resolve_repository_status:Function]
# [DEF:get_git_configs:Function]
# @PURPOSE: List all configured Git servers.
@@ -153,7 +218,9 @@ async def init_repository(
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))
if isinstance(e, HTTPException):
raise
_handle_unexpected_git_route_error("init_repository", e)
# [/DEF:init_repository:Function]
# [DEF:get_branches:Function]
@@ -170,8 +237,10 @@ async def get_branches(
with belief_scope("get_branches"):
try:
return git_service.list_branches(dashboard_id)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=404, detail=str(e))
_handle_unexpected_git_route_error("get_branches", e)
# [/DEF:get_branches:Function]
# [DEF:create_branch:Function]
@@ -190,8 +259,10 @@ async def create_branch(
try:
git_service.create_branch(dashboard_id, branch_data.name, branch_data.from_branch)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("create_branch", e)
# [/DEF:create_branch:Function]
# [DEF:checkout_branch:Function]
@@ -210,8 +281,10 @@ async def checkout_branch(
try:
git_service.checkout_branch(dashboard_id, checkout_data.name)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("checkout_branch", e)
# [/DEF:checkout_branch:Function]
# [DEF:commit_changes:Function]
@@ -230,8 +303,10 @@ async def commit_changes(
try:
git_service.commit_changes(dashboard_id, commit_data.message, commit_data.files)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("commit_changes", e)
# [/DEF:commit_changes:Function]
# [DEF:push_changes:Function]
@@ -248,8 +323,10 @@ async def push_changes(
try:
git_service.push_changes(dashboard_id)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("push_changes", e)
# [/DEF:push_changes:Function]
# [DEF:pull_changes:Function]
@@ -266,8 +343,10 @@ async def pull_changes(
try:
git_service.pull_changes(dashboard_id)
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("pull_changes", e)
# [/DEF:pull_changes:Function]
# [DEF:sync_dashboard:Function]
@@ -291,8 +370,10 @@ async def sync_dashboard(
"dashboard_id": dashboard_id,
"source_env_id": source_env_id
})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("sync_dashboard", e)
# [/DEF:sync_dashboard:Function]
# [DEF:get_environments:Function]
@@ -338,8 +419,10 @@ async def deploy_dashboard(
"dashboard_id": dashboard_id,
"environment_id": deploy_data.environment_id
})
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("deploy_dashboard", e)
# [/DEF:deploy_dashboard:Function]
# [DEF:get_history:Function]
@@ -358,14 +441,16 @@ async def get_history(
with belief_scope("get_history"):
try:
return git_service.get_commit_history(dashboard_id, limit)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=404, detail=str(e))
_handle_unexpected_git_route_error("get_history", 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).
# @PRE: `dashboard_id` is a valid integer.
# @POST: Returns repository status; if repo is not initialized, returns `NO_REPO` payload.
# @PARAM: dashboard_id (int)
# @RETURN: dict
@router.get("/repositories/{dashboard_id}/status")
@@ -375,11 +460,57 @@ async def get_repository_status(
):
with belief_scope("get_repository_status"):
try:
return git_service.get_status(dashboard_id)
return _resolve_repository_status(dashboard_id)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("get_repository_status", e)
# [/DEF:get_repository_status:Function]
# [DEF:get_repository_status_batch:Function]
# @PURPOSE: Get Git statuses for multiple dashboard repositories in one request.
# @PRE: `request.dashboard_ids` is provided.
# @POST: Returns `statuses` map where each key is dashboard ID and value is repository status payload.
# @PARAM: request (RepoStatusBatchRequest)
# @RETURN: RepoStatusBatchResponse
@router.post("/repositories/status/batch", response_model=RepoStatusBatchResponse)
async def get_repository_status_batch(
request: RepoStatusBatchRequest,
_ = Depends(has_permission("plugin:git", "EXECUTE"))
):
with belief_scope("get_repository_status_batch"):
dashboard_ids = list(dict.fromkeys(request.dashboard_ids))
if len(dashboard_ids) > MAX_REPOSITORY_STATUS_BATCH:
logger.warning(
"[get_repository_status_batch][Action] Batch size %s exceeds limit %s. Truncating request.",
len(dashboard_ids),
MAX_REPOSITORY_STATUS_BATCH,
)
dashboard_ids = dashboard_ids[:MAX_REPOSITORY_STATUS_BATCH]
statuses = {}
for dashboard_id in dashboard_ids:
try:
statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)
except HTTPException:
statuses[str(dashboard_id)] = {
**_build_no_repo_status_payload(),
"sync_state": "ERROR",
"sync_status": "ERROR",
}
except Exception as e:
logger.error(
f"[get_repository_status_batch][Coherence:Failed] Failed for dashboard {dashboard_id}: {e}"
)
statuses[str(dashboard_id)] = {
**_build_no_repo_status_payload(),
"sync_state": "ERROR",
"sync_status": "ERROR",
}
return RepoStatusBatchResponse(statuses=statuses)
# [/DEF:get_repository_status_batch:Function]
# [DEF:get_repository_diff:Function]
# @PURPOSE: Get Git diff for a dashboard repository.
# @PRE: `dashboard_id` repository exists.
@@ -399,8 +530,10 @@ async def get_repository_diff(
try:
diff_text = git_service.get_diff(dashboard_id, file_path, staged)
return diff_text
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("get_repository_diff", e)
# [/DEF:get_repository_diff:Function]
# [DEF:generate_commit_message:Function]
@@ -466,9 +599,10 @@ async def generate_commit_message(
)
return {"message": message}
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to generate commit message: {e}")
raise HTTPException(status_code=400, detail=str(e))
_handle_unexpected_git_route_error("generate_commit_message", e)
# [/DEF:generate_commit_message:Function]
# [/DEF:backend.src.api.routes.git:Module]