- Replace BeliefFormatter with CotJsonFormatter (single-line JSON with ts, level, trace_id, src, marker, intent, payload, error, span_id) - Migrate belief_scope to use cot_log() for structured JSON output - Update monkey-patched reason/reflect/explore to pass extra= dict - Strip 200+ inline [REASON]/[REFLECT]/[EXPLORE] markers from 50+ files - Remove belief_scope from hot-path utilities (_parse_datetime, _json_load_if_needed) to eliminate startup log noise - Register TraceContextMiddleware in app.py (was implemented but unused) - Seed trace_id in startup_event/shutdown_event for background tasks - Fix broken relative imports in translate routes (3 dots → 4 dots) - Migrate 43 translate_routes log calls to logger.reason/explore - Fix SyntaxError (positional arg after extra=) in _repo_operations_routes - Fix IndentationError in rbac_permission_catalog except-block - Add smoke test tests/test_smoke_app.py (catches import errors before run)
349 lines
13 KiB
Python
349 lines
13 KiB
Python
# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS git, repository, operations, commit, push, pull, status]
|
|
# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).
|
|
# @LAYER: API
|
|
|
|
from typing import List, Optional
|
|
|
|
from fastapi import Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.core.database import get_db
|
|
from src.core.logger import belief_scope, logger
|
|
from src.dependencies import get_config_manager, get_current_user, has_permission
|
|
from src.models.auth import User
|
|
from src.models.git import GitRepository, GitServerConfig
|
|
|
|
from src.api.routes.git_schemas import (
|
|
CommitCreate,
|
|
CommitSchema,
|
|
RepoStatusBatchRequest,
|
|
RepoStatusBatchResponse,
|
|
)
|
|
|
|
from ._router import router
|
|
from ._helpers import (
|
|
_apply_git_identity_from_profile,
|
|
_build_no_repo_status_payload,
|
|
_handle_unexpected_git_route_error,
|
|
_resolve_repository_status,
|
|
)
|
|
from ._deps import get_git_service, MAX_REPOSITORY_STATUS_BATCH
|
|
|
|
|
|
# #region commit_changes [C:2] [TYPE Function]
|
|
# @BRIEF Stage and commit changes in the dashboard's repository.
|
|
@router.post("/repositories/{dashboard_ref}/commit")
|
|
async def commit_changes(
|
|
dashboard_ref: str,
|
|
commit_data: CommitCreate,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("commit_changes"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
|
_gs.commit_changes(
|
|
dashboard_id, commit_data.message, commit_data.files
|
|
)
|
|
return {"status": "success"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("commit_changes", e)
|
|
# #endregion commit_changes
|
|
|
|
|
|
# #region push_changes [C:2] [TYPE Function]
|
|
# @BRIEF Push local commits to the remote repository.
|
|
@router.post("/repositories/{dashboard_ref}/push")
|
|
async def push_changes(
|
|
dashboard_ref: str,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("push_changes"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
_gs.push_changes(dashboard_id)
|
|
return {"status": "success"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("push_changes", e)
|
|
# #endregion push_changes
|
|
|
|
|
|
# #region pull_changes [C:3] [TYPE Function]
|
|
# @BRIEF Pull changes from the remote repository.
|
|
# @RELATION CALLS -> [GitService]
|
|
@router.post("/repositories/{dashboard_ref}/pull")
|
|
async def pull_changes(
|
|
dashboard_ref: str,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("pull_changes"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
db_repo = None
|
|
config_url = None
|
|
config_provider = None
|
|
try:
|
|
db_repo_candidate = (
|
|
db.query(GitRepository)
|
|
.filter(GitRepository.dashboard_id == dashboard_id)
|
|
.first()
|
|
)
|
|
if getattr(db_repo_candidate, "config_id", None):
|
|
db_repo = db_repo_candidate
|
|
config_row = (
|
|
db.query(GitServerConfig)
|
|
.filter(GitServerConfig.id == db_repo.config_id)
|
|
.first()
|
|
)
|
|
if config_row:
|
|
config_url = config_row.url
|
|
config_provider = config_row.provider
|
|
except Exception as diagnostics_error:
|
|
logger.explore(
|
|
"Failed to load repository binding diagnostics",
|
|
extra={"src": "pull_changes", "payload": {"dashboard_id": dashboard_id}, "error": str(diagnostics_error)},
|
|
)
|
|
logger.reason(
|
|
f"Route diagnostics dashboard_ref={dashboard_ref} env_id={env_id} resolved_dashboard_id={dashboard_id} "
|
|
f"binding_exists={bool(db_repo)} binding_local_path={(db_repo.local_path if db_repo else None)} "
|
|
f"binding_remote_url={(db_repo.remote_url if db_repo else None)} "
|
|
f"binding_config_id={(db_repo.config_id if db_repo else None)} "
|
|
f"config_provider={config_provider} config_url={config_url}",
|
|
extra={"src": "pull_changes"},
|
|
)
|
|
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
|
_gs.pull_changes(dashboard_id)
|
|
return {"status": "success"}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("pull_changes", e)
|
|
# #endregion pull_changes
|
|
|
|
|
|
# #region get_repository_status [C:2] [TYPE Function]
|
|
# @BRIEF Get current Git status for a dashboard repository.
|
|
@router.get("/repositories/{dashboard_ref}/status")
|
|
async def get_repository_status(
|
|
dashboard_ref: str,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
with belief_scope("get_repository_status"):
|
|
from . import _resolve_dashboard_id_from_ref_async
|
|
|
|
try:
|
|
dashboard_id = await _resolve_dashboard_id_from_ref_async(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
return _resolve_repository_status(dashboard_id)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("get_repository_status", e)
|
|
# #endregion get_repository_status
|
|
|
|
|
|
# #region get_repository_status_batch [C:2] [TYPE Function]
|
|
# @BRIEF Get Git statuses for multiple dashboard repositories in one request.
|
|
@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.reason(
|
|
f"Batch size {len(dashboard_ids)} exceeds limit {MAX_REPOSITORY_STATUS_BATCH}. Truncating request.",
|
|
extra={"src": "get_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)
|
|
# #endregion get_repository_status_batch
|
|
|
|
|
|
# #region get_repository_diff [C:2] [TYPE Function]
|
|
# @BRIEF Get Git diff for a dashboard repository.
|
|
@router.get("/repositories/{dashboard_ref}/diff")
|
|
async def get_repository_diff(
|
|
dashboard_ref: str,
|
|
file_path: Optional[str] = None,
|
|
staged: bool = False,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("get_repository_diff"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
return _gs.get_diff(dashboard_id, file_path, staged)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("get_repository_diff", e)
|
|
# #endregion get_repository_diff
|
|
|
|
|
|
# #region get_history [C:2] [TYPE Function]
|
|
# @BRIEF View commit history for a dashboard's repository.
|
|
@router.get("/repositories/{dashboard_ref}/history", response_model=List[CommitSchema])
|
|
async def get_history(
|
|
dashboard_ref: str,
|
|
limit: int = 50,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("get_history"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
return _gs.get_commit_history(dashboard_id, limit)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("get_history", e)
|
|
# #endregion get_history
|
|
|
|
|
|
# #region generate_commit_message [C:3] [TYPE Function]
|
|
# @BRIEF Generate a suggested commit message using LLM.
|
|
# @RELATION CALLS -> [GitService]
|
|
@router.post("/repositories/{dashboard_ref}/generate-message")
|
|
async def generate_commit_message(
|
|
dashboard_ref: str,
|
|
env_id: Optional[str] = None,
|
|
config_manager=Depends(get_config_manager),
|
|
db: Session = Depends(get_db),
|
|
_=Depends(has_permission("plugin:git", "EXECUTE")),
|
|
):
|
|
_gs = get_git_service()
|
|
with belief_scope("generate_commit_message"):
|
|
from . import _resolve_dashboard_id_from_ref
|
|
|
|
try:
|
|
dashboard_id = _resolve_dashboard_id_from_ref(
|
|
dashboard_ref, config_manager, env_id
|
|
)
|
|
diff = _gs.get_diff(dashboard_id, staged=True)
|
|
if not diff:
|
|
diff = _gs.get_diff(dashboard_id, staged=False)
|
|
|
|
if not diff:
|
|
return {"message": "No changes detected"}
|
|
|
|
history_objs = _gs.get_commit_history(dashboard_id, limit=5)
|
|
history = [h.message for h in history_objs if hasattr(h, "message")]
|
|
|
|
from ...services.llm_provider import LLMProviderService
|
|
from ...plugins.llm_analysis.service import LLMClient
|
|
from ...plugins.llm_analysis.models import LLMProviderType
|
|
from ...services.llm_prompt_templates import (
|
|
DEFAULT_LLM_PROMPTS,
|
|
normalize_llm_settings,
|
|
resolve_bound_provider_id,
|
|
)
|
|
|
|
llm_service = LLMProviderService(db)
|
|
providers = llm_service.get_all_providers()
|
|
llm_settings = normalize_llm_settings(
|
|
config_manager.get_config().settings.llm
|
|
)
|
|
bound_provider_id = resolve_bound_provider_id(llm_settings, "git_commit")
|
|
provider = next((p for p in providers if p.id == bound_provider_id), None)
|
|
if not provider:
|
|
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,
|
|
)
|
|
|
|
from ...plugins.git.llm_extension import GitLLMExtension
|
|
|
|
extension = GitLLMExtension(client)
|
|
git_prompt = llm_settings["prompts"].get(
|
|
"git_commit_prompt",
|
|
DEFAULT_LLM_PROMPTS["git_commit_prompt"],
|
|
)
|
|
message = await extension.suggest_commit_message(
|
|
diff,
|
|
history,
|
|
prompt_template=git_prompt,
|
|
)
|
|
return {"message": message}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
_handle_unexpected_git_route_error("generate_commit_message", e)
|
|
# #endregion generate_commit_message
|
|
# #endregion GitRepoOperationsRoutes
|