refactor: decompose 6 monolithic modules (>1000 LOC) into domain packages
Decompose 6 large modules into packages with <400-line modules to satisfy INV_7: - superset_client.py (2145->0) -> superset_client/ package (12 modules) - assistant.py (2683->0) -> assistant/ package (12 modules) - git_service.py (2101->11 shim) -> services/git/ package (9 modules) - git.py (1757->0) -> routes/git/ package (11 modules) - dashboards.py (1619->0) -> routes/dashboards/ package (8 modules) - translate.py (1587->0) -> routes/translate/ package (11 modules) - superset_context_extractor.py (1397->0) -> utils/superset_context_extractor/ (7 modules) All modules <400 lines (INV_7). All 80 tests pass. All external imports preserved via __init__.py re-exports or shim. Semantic [DEF] contracts with @LAYER/@SEMANTICS added to all Module types.
This commit is contained in:
177
backend/src/services/git/_sync.py
Normal file
177
backend/src/services/git/_sync.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# [DEF:GitServiceSyncMixin:Module]
|
||||
# @COMPLEXITY: 3
|
||||
# @LAYER: Infra
|
||||
# @SEMANTICS: git, push, pull, sync, remote
|
||||
# @PURPOSE: Push and pull operations for GitService with origin host auto-alignment.
|
||||
# @RELATION: USED_BY -> [GitService]
|
||||
# @RELATION: DEPENDS_ON -> [GitServiceUrlMixin]
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
from git import Repo
|
||||
from git.exc import GitCommandError
|
||||
from fastapi import HTTPException
|
||||
from src.core.database import SessionLocal
|
||||
from src.core.logger import logger, belief_scope
|
||||
from src.models.git import GitRepository, GitServerConfig
|
||||
|
||||
|
||||
# [DEF:GitServiceSyncMixin:Class]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Mixin providing push and pull operations with origin host alignment.
|
||||
class GitServiceSyncMixin:
|
||||
# [DEF:push_changes:Function]
|
||||
# @PURPOSE: Push local commits to remote.
|
||||
# @PRE: Repository exists and has an 'origin' remote.
|
||||
# @POST: Local branch commits are pushed to origin.
|
||||
def push_changes(self, dashboard_id: int):
|
||||
with belief_scope("GitService.push_changes"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
if not repo.heads:
|
||||
logger.warning(f"[push_changes][Coherence:Failed] No local branches to push for dashboard {dashboard_id}")
|
||||
return
|
||||
try:
|
||||
origin = repo.remote(name='origin')
|
||||
except ValueError:
|
||||
logger.error(f"[push_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}")
|
||||
raise HTTPException(status_code=400, detail="Remote 'origin' not configured")
|
||||
try:
|
||||
origin_urls = list(origin.urls)
|
||||
except Exception:
|
||||
origin_urls = []
|
||||
binding_remote_url = None
|
||||
binding_config_id = None
|
||||
binding_config_url = None
|
||||
try:
|
||||
session = SessionLocal()
|
||||
try:
|
||||
db_repo = (
|
||||
session.query(GitRepository)
|
||||
.filter(GitRepository.dashboard_id == int(dashboard_id))
|
||||
.first()
|
||||
)
|
||||
if db_repo:
|
||||
binding_remote_url = db_repo.remote_url
|
||||
binding_config_id = db_repo.config_id
|
||||
db_config = (
|
||||
session.query(GitServerConfig)
|
||||
.filter(GitServerConfig.id == db_repo.config_id)
|
||||
.first()
|
||||
)
|
||||
if db_config:
|
||||
binding_config_url = db_config.url
|
||||
finally:
|
||||
session.close()
|
||||
except Exception as diag_error:
|
||||
logger.warning(
|
||||
"[push_changes][Action] Failed to load repository binding diagnostics for dashboard %s: %s",
|
||||
dashboard_id, diag_error,
|
||||
)
|
||||
realigned_origin_url = self._align_origin_host_with_config(
|
||||
dashboard_id=dashboard_id,
|
||||
origin=origin,
|
||||
config_url=binding_config_url,
|
||||
current_origin_url=(origin_urls[0] if origin_urls else None),
|
||||
binding_remote_url=binding_remote_url,
|
||||
)
|
||||
try:
|
||||
origin_urls = list(origin.urls)
|
||||
except Exception:
|
||||
origin_urls = []
|
||||
logger.info(
|
||||
"[push_changes][Action] Push diagnostics dashboard=%s config_id=%s config_url=%s binding_remote_url=%s origin_urls=%s origin_realigned=%s",
|
||||
dashboard_id, binding_config_id, binding_config_url, binding_remote_url, origin_urls, bool(realigned_origin_url),
|
||||
)
|
||||
try:
|
||||
current_branch = repo.active_branch
|
||||
logger.info(f"[push_changes][Action] Pushing branch {current_branch.name} to origin")
|
||||
tracking_branch = None
|
||||
try:
|
||||
tracking_branch = current_branch.tracking_branch()
|
||||
except Exception:
|
||||
tracking_branch = None
|
||||
if tracking_branch is None:
|
||||
repo.git.push("--set-upstream", "origin", f"{current_branch.name}:{current_branch.name}")
|
||||
else:
|
||||
push_info = origin.push(refspec=f'{current_branch.name}:{current_branch.name}')
|
||||
for info in push_info:
|
||||
if info.flags & info.ERROR:
|
||||
logger.error(f"[push_changes][Coherence:Failed] Error pushing ref {info.remote_ref_string}: {info.summary}")
|
||||
raise Exception(f"Git push error for {info.remote_ref_string}: {info.summary}")
|
||||
except GitCommandError as e:
|
||||
details = str(e)
|
||||
lowered = details.lower()
|
||||
if "non-fast-forward" in lowered or "rejected" in lowered:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Push rejected: remote branch contains newer commits. Run Pull first, resolve conflicts if any, then push again.",
|
||||
)
|
||||
logger.error(f"[push_changes][Coherence:Failed] Failed to push changes: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Git push failed: {details}")
|
||||
except Exception as e:
|
||||
logger.error(f"[push_changes][Coherence:Failed] Failed to push changes: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Git push failed: {str(e)}")
|
||||
# [/DEF:push_changes:Function]
|
||||
|
||||
# [DEF:pull_changes:Function]
|
||||
# @PURPOSE: Pull changes from remote.
|
||||
# @PRE: Repository exists and has an 'origin' remote.
|
||||
# @POST: Changes from origin are pulled and merged into the active branch.
|
||||
def pull_changes(self, dashboard_id: int):
|
||||
with belief_scope("GitService.pull_changes"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
merge_head_path = os.path.join(repo.git_dir, "MERGE_HEAD")
|
||||
if os.path.exists(merge_head_path):
|
||||
payload = self._build_unfinished_merge_payload(repo)
|
||||
logger.warning(
|
||||
"[pull_changes][Action] Unfinished merge detected for dashboard %s (repo_path=%s git_dir=%s branch=%s merge_head=%s merge_msg=%s)",
|
||||
dashboard_id, payload["repository_path"], payload["git_dir"],
|
||||
payload["current_branch"], payload["merge_head"], payload["merge_message_preview"],
|
||||
)
|
||||
raise HTTPException(status_code=409, detail=payload)
|
||||
try:
|
||||
origin = repo.remote(name='origin')
|
||||
current_branch = repo.active_branch.name
|
||||
try:
|
||||
origin_urls = list(origin.urls)
|
||||
except Exception:
|
||||
origin_urls = []
|
||||
logger.info(
|
||||
"[pull_changes][Action] Pull diagnostics dashboard=%s repo_path=%s branch=%s origin_urls=%s",
|
||||
dashboard_id, repo.working_tree_dir, current_branch, origin_urls,
|
||||
)
|
||||
origin.fetch(prune=True)
|
||||
remote_ref = f"origin/{current_branch}"
|
||||
has_remote_branch = any(ref.name == remote_ref for ref in repo.refs)
|
||||
logger.info(
|
||||
"[pull_changes][Action] Pull remote branch check dashboard=%s branch=%s remote_ref=%s exists=%s",
|
||||
dashboard_id, current_branch, remote_ref, has_remote_branch,
|
||||
)
|
||||
if not has_remote_branch:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"Remote branch '{current_branch}' does not exist yet. Push this branch first.",
|
||||
)
|
||||
logger.info(f"[pull_changes][Action] Pulling changes from origin/{current_branch}")
|
||||
repo.git.pull("--no-rebase", "origin", current_branch)
|
||||
except ValueError:
|
||||
logger.error(f"[pull_changes][Coherence:Failed] Remote 'origin' not found for dashboard {dashboard_id}")
|
||||
raise HTTPException(status_code=400, detail="Remote 'origin' not configured")
|
||||
except GitCommandError as e:
|
||||
details = str(e)
|
||||
lowered = details.lower()
|
||||
if "conflict" in lowered or "not possible to fast-forward" in lowered:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Pull requires conflict resolution. Resolve conflicts in repository and repeat operation.",
|
||||
)
|
||||
logger.error(f"[pull_changes][Coherence:Failed] Failed to pull changes: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Git pull failed: {details}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[pull_changes][Coherence:Failed] Failed to pull changes: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Git pull failed: {str(e)}")
|
||||
# [/DEF:pull_changes:Function]
|
||||
# [/DEF:GitServiceSyncMixin:Class]
|
||||
# [/DEF:GitServiceSyncMixin:Module]
|
||||
Reference in New Issue
Block a user