Files
ss-tools/backend/src/api/routes/git/_repo_operations_routes.py
busya 83b8f4d999 logs: migrate to molecular CoT JSON protocol, fix regressions
- 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)
2026-05-13 09:44:50 +03:00

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