semantics
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# #region GitServiceModule [C:3] [TYPE Module] [SEMANTICS git, package, export, mixin, decomposition]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Composed GitService via multiple inheritance from domain-specific mixins.
|
||||
# @RELATION DEPENDS_ON -> [GitServiceBase]
|
||||
# @RELATION DEPENDS_ON -> [GitServiceBranchMixin]
|
||||
@@ -11,11 +11,11 @@
|
||||
# @RELATION DEPENDS_ON -> [GitServiceGithubMixin]
|
||||
# @RELATION DEPENDS_ON -> [GitServiceGitlabMixin]
|
||||
#
|
||||
# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into
|
||||
# @RATIONALE Decomposed from monolithic git_service.py (2101 lines) into
|
||||
# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class
|
||||
# preserves the original public API surface — all consumers continue to import
|
||||
# `from src.services.git_service import GitService` without changes.
|
||||
# @REJECTED: Keeping a single 2101-line file — violates fractal limit INV_7.
|
||||
# @REJECTED Keeping a single 2101-line file — violates fractal limit INV_7.
|
||||
|
||||
from ._base import GitServiceBase
|
||||
from ._branch import GitServiceBranchMixin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]
|
||||
# @LAYER Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.
|
||||
# @RELATION DEPENDS_ON -> [GitRepository]
|
||||
# @RELATION DEPENDS_ON -> [GitRepository]
|
||||
@@ -348,12 +348,12 @@ class GitServiceBase:
|
||||
with belief_scope("GitService.get_repo"):
|
||||
repo_path = self._get_repo_path(dashboard_id)
|
||||
if not os.path.exists(repo_path):
|
||||
logger.error(f"[get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist")
|
||||
logger.error(f"[EXT:method:get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist")
|
||||
raise HTTPException(status_code=404, detail=f"Repository for dashboard {dashboard_id} not found")
|
||||
try:
|
||||
return Repo(repo_path)
|
||||
except Exception as e:
|
||||
logger.error(f"[get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}")
|
||||
logger.error(f"[EXT:method:get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to open local Git repository")
|
||||
# endregion get_repo
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceBranchMixin [C:4] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace, lock]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks).
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
|
||||
from datetime import datetime
|
||||
import os
|
||||
@@ -17,8 +17,8 @@ from src.core.logger import belief_scope, logger
|
||||
class GitServiceBranchMixin:
|
||||
# region _ensure_gitflow_branches [C:4] [TYPE Function] [SEMANTICS git,gitflow,branch,lock]
|
||||
# @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.
|
||||
# @PRE: repo is a valid GitPython Repo instance.
|
||||
# @POST: main, dev, preprod are available in local repository and pushed to origin when available.
|
||||
# @PRE repo is a valid GitPython Repo instance.
|
||||
# @POST main, dev, preprod are available in local repository and pushed to origin when available.
|
||||
# Active branch unchanged (no spurious checkout).
|
||||
def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:
|
||||
with belief_scope("GitService._ensure_gitflow_branches"):
|
||||
@@ -95,9 +95,9 @@ class GitServiceBranchMixin:
|
||||
|
||||
# region list_branches [C:4] [TYPE Function] [SEMANTICS git,branch,list,lock]
|
||||
# @PURPOSE: List all branches (excluding tags) for a dashboard's repository, concurrent-safe.
|
||||
# @PRE: Repository for dashboard_id exists.
|
||||
# @POST: Returns a list of branch metadata dictionaries (no tag refs).
|
||||
# @RETURN: List[dict]
|
||||
# @PRE Repository for dashboard_id exists.
|
||||
# @POST Returns a list of branch metadata dictionaries (no tag refs).
|
||||
# @RETURN List[dict]
|
||||
def list_branches(self, dashboard_id: int) -> list[dict]:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.list_branches"):
|
||||
@@ -140,10 +140,10 @@ class GitServiceBranchMixin:
|
||||
|
||||
# region create_branch [C:4] [TYPE Function] [SEMANTICS git,branch,create,lock]
|
||||
# @PURPOSE: Create a new branch from an existing one (concurrent-safe).
|
||||
# @PARAM: name (str) - New branch name.
|
||||
# @PARAM: from_branch (str) - Source branch.
|
||||
# @PRE: Repository exists; name is valid; from_branch exists or repo is empty.
|
||||
# @POST: A new branch is created in the repository.
|
||||
# @PARAM name (str) - New branch name.
|
||||
# @PARAM from_branch (str) - Source branch.
|
||||
# @PRE Repository exists; name is valid; from_branch exists or repo is empty.
|
||||
# @POST A new branch is created in the repository.
|
||||
def create_branch(self, dashboard_id: int, name: str, from_branch: str = "main"):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.create_branch"):
|
||||
@@ -172,8 +172,8 @@ class GitServiceBranchMixin:
|
||||
|
||||
# region checkout_branch [C:4] [TYPE Function] [SEMANTICS git,branch,checkout,lock]
|
||||
# @PURPOSE: Switch to a specific branch (concurrent-safe).
|
||||
# @PRE: Repository exists and the specified branch name exists.
|
||||
# @POST: The repository working directory is updated to the specified branch.
|
||||
# @PRE Repository exists and the specified branch name exists.
|
||||
# @POST The repository working directory is updated to the specified branch.
|
||||
def checkout_branch(self, dashboard_id: int, name: str):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.checkout_branch"):
|
||||
@@ -184,10 +184,10 @@ class GitServiceBranchMixin:
|
||||
|
||||
# region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock]
|
||||
# @PURPOSE: Stage and commit changes (concurrent-safe).
|
||||
# @PARAM: message (str) - Commit message.
|
||||
# @PARAM: files (List[str]) - Optional list of specific files to stage.
|
||||
# @PRE: Repository exists and has changes (dirty) or files are specified.
|
||||
# @POST: Changes are staged and a new commit is created.
|
||||
# @PARAM message (str) - Commit message.
|
||||
# @PARAM files (List[str]) - Optional list of specific files to stage.
|
||||
# @PRE Repository exists and has changes (dirty) or files are specified.
|
||||
# @POST Changes are staged and a new commit is created.
|
||||
def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.commit_changes"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceGiteaMixin [C:4] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection, http_pool]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling.
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
@@ -18,10 +18,10 @@ from src.models.git import GitProvider
|
||||
class GitServiceGiteaMixin:
|
||||
# region test_connection [TYPE Function]
|
||||
# @PURPOSE: Test connection to Git provider using PAT.
|
||||
# @PARAM: provider (GitProvider), url (str), pat (str)
|
||||
# @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided.
|
||||
# @POST: Returns True if connection to the provider's API succeeds.
|
||||
# @RETURN: bool
|
||||
# @PARAM provider (GitProvider), url (str), pat (str)
|
||||
# @PRE provider is valid; url is a valid HTTP(S) URL; pat is provided.
|
||||
# @POST Returns True if connection to the provider's API succeeds.
|
||||
# @RETURN bool
|
||||
async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:
|
||||
with belief_scope("GitService.test_connection"):
|
||||
if ".local" in url or "localhost" in url:
|
||||
@@ -59,9 +59,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region _gitea_headers [TYPE Function]
|
||||
# @PURPOSE: Build Gitea API authorization headers.
|
||||
# @PRE: pat is provided.
|
||||
# @POST: Returns headers with token auth.
|
||||
# @RETURN: Dict[str, str]
|
||||
# @PRE pat is provided.
|
||||
# @POST Returns headers with token auth.
|
||||
# @RETURN Dict[str, str]
|
||||
def _gitea_headers(self, pat: str) -> dict[str, str]:
|
||||
token = (pat or "").strip()
|
||||
if not token:
|
||||
@@ -75,9 +75,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region _gitea_request [TYPE Function]
|
||||
# @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.
|
||||
# @PRE: method and endpoint are valid.
|
||||
# @POST: Returns decoded JSON payload.
|
||||
# @RETURN: Any
|
||||
# @PRE method and endpoint are valid.
|
||||
# @POST Returns decoded JSON payload.
|
||||
# @RETURN Any
|
||||
async def _gitea_request(
|
||||
self, method: str, server_url: str, pat: str, endpoint: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
@@ -106,9 +106,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region get_gitea_current_user [TYPE Function]
|
||||
# @PURPOSE: Resolve current Gitea user for PAT.
|
||||
# @PRE: server_url and pat are valid.
|
||||
# @POST: Returns current username.
|
||||
# @RETURN: str
|
||||
# @PRE server_url and pat are valid.
|
||||
# @POST Returns current username.
|
||||
# @RETURN str
|
||||
async def get_gitea_current_user(self, server_url: str, pat: str) -> str:
|
||||
payload = await self._gitea_request("GET", server_url, pat, "/user")
|
||||
username = payload.get("login") or payload.get("username")
|
||||
@@ -119,9 +119,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region list_gitea_repositories [TYPE Function]
|
||||
# @PURPOSE: List repositories visible to authenticated Gitea user.
|
||||
# @PRE: server_url and pat are valid.
|
||||
# @POST: Returns repository list from Gitea.
|
||||
# @RETURN: List[dict]
|
||||
# @PRE server_url and pat are valid.
|
||||
# @POST Returns repository list from Gitea.
|
||||
# @RETURN List[dict]
|
||||
async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:
|
||||
payload = await self._gitea_request("GET", server_url, pat, "/user/repos?limit=100&page=1")
|
||||
if not isinstance(payload, list):
|
||||
@@ -131,9 +131,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region create_gitea_repository [TYPE Function]
|
||||
# @PURPOSE: Create repository in Gitea for authenticated user.
|
||||
# @PRE: name is non-empty and PAT has repo creation permission.
|
||||
# @POST: Returns created repository payload.
|
||||
# @RETURN: dict
|
||||
# @PRE name is non-empty and PAT has repo creation permission.
|
||||
# @POST Returns created repository payload.
|
||||
# @RETURN dict
|
||||
async def create_gitea_repository(
|
||||
self, server_url: str, pat: str, name: str, private: bool = True,
|
||||
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
|
||||
@@ -151,8 +151,8 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region delete_gitea_repository [TYPE Function]
|
||||
# @PURPOSE: Delete repository in Gitea.
|
||||
# @PRE: owner and repo_name are non-empty.
|
||||
# @POST: Repository deleted on Gitea server.
|
||||
# @PRE owner and repo_name are non-empty.
|
||||
# @POST Repository deleted on Gitea server.
|
||||
async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:
|
||||
if not owner or not repo_name:
|
||||
raise HTTPException(status_code=400, detail="owner and repo_name are required")
|
||||
@@ -161,9 +161,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region _gitea_branch_exists [TYPE Function]
|
||||
# @PURPOSE: Check whether a branch exists in Gitea repository.
|
||||
# @PRE: owner/repo/branch are non-empty.
|
||||
# @POST: Returns True when branch exists, False when 404.
|
||||
# @RETURN: bool
|
||||
# @PRE owner/repo/branch are non-empty.
|
||||
# @POST Returns True when branch exists, False when 404.
|
||||
# @RETURN bool
|
||||
async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:
|
||||
if not owner or not repo or not branch:
|
||||
return False
|
||||
@@ -179,9 +179,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region _build_gitea_pr_404_detail [TYPE Function]
|
||||
# @PURPOSE: Build actionable error detail for Gitea PR 404 responses.
|
||||
# @PRE: owner/repo/from_branch/to_branch are provided.
|
||||
# @POST: Returns specific branch-missing message when detected.
|
||||
# @RETURN: Optional[str]
|
||||
# @PRE owner/repo/from_branch/to_branch are provided.
|
||||
# @POST Returns specific branch-missing message when detected.
|
||||
# @RETURN Optional[str]
|
||||
async def _build_gitea_pr_404_detail(
|
||||
self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,
|
||||
) -> str | None:
|
||||
@@ -200,9 +200,9 @@ class GitServiceGiteaMixin:
|
||||
|
||||
# region create_gitea_pull_request [TYPE Function]
|
||||
# @PURPOSE: Create pull request in Gitea.
|
||||
# @PRE: Config and remote URL are valid.
|
||||
# @POST: Returns normalized PR metadata.
|
||||
# @RETURN: Dict[str, Any]
|
||||
# @PRE Config and remote URL are valid.
|
||||
# @POST Returns normalized PR metadata.
|
||||
# @RETURN Dict[str, Any]
|
||||
async def create_gitea_pull_request(
|
||||
self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,
|
||||
title: str, description: str | None = None,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -235,11 +235,11 @@ class GitServiceMergeMixin:
|
||||
|
||||
# region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]
|
||||
# @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.
|
||||
# @PRE: Repository exists and both branches are valid.
|
||||
# @POST: Target branch contains merged changes from source branch. Active branch restored to original.
|
||||
# @PRE Repository exists and both branches are valid.
|
||||
# @POST Target branch contains merged changes from source branch. Active branch restored to original.
|
||||
# Merge survives locally even if push fails (partial success).
|
||||
# @SIDE_EFFECT: Changes local branch state during merge; restores original branch in finally block.
|
||||
# @RETURN: Dict[str, Any]
|
||||
# @SIDE_EFFECT Changes local branch state during merge; restores original branch in finally block.
|
||||
# @RETURN Dict[str, Any]
|
||||
def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.promote_direct_merge"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceRemoteMixin [C:4] [TYPE Module] [SEMANTICS git, provider, github, remote, url, http_pool]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling.
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
@@ -17,9 +17,9 @@ from src.core.logger import logger
|
||||
class GitServiceGithubMixin:
|
||||
# region create_github_repository [TYPE Function]
|
||||
# @PURPOSE: Create repository in GitHub or GitHub Enterprise.
|
||||
# @PRE: PAT has repository create permission.
|
||||
# @POST: Returns created repository payload.
|
||||
# @RETURN: dict
|
||||
# @PRE PAT has repository create permission.
|
||||
# @POST Returns created repository payload.
|
||||
# @RETURN dict
|
||||
async def create_github_repository(
|
||||
self, server_url: str, pat: str, name: str, private: bool = True,
|
||||
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
|
||||
@@ -60,9 +60,9 @@ class GitServiceGithubMixin:
|
||||
class GitServiceGitlabMixin:
|
||||
# region create_gitlab_repository [TYPE Function]
|
||||
# @PURPOSE: Create repository(project) in GitLab.
|
||||
# @PRE: PAT has api scope.
|
||||
# @POST: Returns created repository payload.
|
||||
# @RETURN: dict
|
||||
# @PRE PAT has api scope.
|
||||
# @POST Returns created repository payload.
|
||||
# @RETURN dict
|
||||
async def create_gitlab_repository(
|
||||
self, server_url: str, pat: str, name: str, private: bool = True,
|
||||
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
|
||||
@@ -105,9 +105,9 @@ class GitServiceGitlabMixin:
|
||||
|
||||
# region create_gitlab_merge_request [TYPE Function]
|
||||
# @PURPOSE: Create merge request in GitLab.
|
||||
# @PRE: Config and remote URL are valid.
|
||||
# @POST: Returns normalized MR metadata.
|
||||
# @RETURN: Dict[str, Any]
|
||||
# @PRE Config and remote URL are valid.
|
||||
# @POST Returns normalized MR metadata.
|
||||
# @RETURN Dict[str, Any]
|
||||
async def create_gitlab_merge_request(
|
||||
self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,
|
||||
title: str, description: str | None = None, remove_source_branch: bool = False,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceStatusMixin [C:4] [TYPE Module] [SEMANTICS git, status, diff, log, history, lock]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Status, diff, and commit history operations for GitService (all concurrent-safe via per-dashboard locks).
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@@ -13,9 +13,9 @@ from src.core.logger import belief_scope, logger
|
||||
class GitServiceStatusMixin:
|
||||
# region _parse_status_porcelain [TYPE Function]
|
||||
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
|
||||
# @PRE: `repo` is an open GitPython Repo instance.
|
||||
# @POST: Returns (staged, modified, untracked) tuple of file path lists.
|
||||
# @RATIONALE: Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally
|
||||
# @PRE `repo` is an open GitPython Repo instance.
|
||||
# @POST Returns (staged, modified, untracked) tuple of file path lists.
|
||||
# @RATIONALE Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally
|
||||
# call git diff --cached, a flag unsupported in some Git environments (exit 129).
|
||||
# Using git status --porcelain is self-contained and avoids the --cached flag entirely.
|
||||
def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]:
|
||||
@@ -52,9 +52,9 @@ class GitServiceStatusMixin:
|
||||
|
||||
# region get_status [C:4] [TYPE Function] [SEMANTICS git,status,lock]
|
||||
# @PURPOSE: Get current repository status (concurrent-safe).
|
||||
# @PRE: Repository for dashboard_id exists.
|
||||
# @POST: Returns a dictionary representing the Git status.
|
||||
# @RETURN: dict
|
||||
# @PRE Repository for dashboard_id exists.
|
||||
# @POST Returns a dictionary representing the Git status.
|
||||
# @RETURN dict
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_status"):
|
||||
@@ -113,11 +113,11 @@ class GitServiceStatusMixin:
|
||||
|
||||
# region get_diff [C:4] [TYPE Function] [SEMANTICS git,diff,lock]
|
||||
# @PURPOSE: Generate diff for a file or the whole repository (concurrent-safe).
|
||||
# @PARAM: file_path (str) - Optional specific file.
|
||||
# @PARAM: staged (bool) - Whether to show staged changes.
|
||||
# @PRE: Repository for dashboard_id exists.
|
||||
# @POST: Returns the diff text as a string.
|
||||
# @RETURN: str
|
||||
# @PARAM file_path (str) - Optional specific file.
|
||||
# @PARAM staged (bool) - Whether to show staged changes.
|
||||
# @PRE Repository for dashboard_id exists.
|
||||
# @POST Returns the diff text as a string.
|
||||
# @RETURN str
|
||||
def get_diff(self, dashboard_id: int, file_path: str = None, staged: bool = False) -> str:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_diff"):
|
||||
@@ -132,10 +132,10 @@ class GitServiceStatusMixin:
|
||||
|
||||
# region get_commit_history [C:4] [TYPE Function] [SEMANTICS git,history,lock]
|
||||
# @PURPOSE: Retrieve commit history for a repository (concurrent-safe).
|
||||
# @PARAM: limit (int) - Max number of commits to return.
|
||||
# @PRE: Repository for dashboard_id exists.
|
||||
# @POST: Returns a list of dictionaries for each commit in history.
|
||||
# @RETURN: List[dict]
|
||||
# @PARAM limit (int) - Max number of commits to return.
|
||||
# @PRE Repository for dashboard_id exists.
|
||||
# @POST Returns a list of dictionaries for each commit in history.
|
||||
# @RETURN List[dict]
|
||||
def get_commit_history(self, dashboard_id: int, limit: int = 50) -> list[dict]:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_commit_history"):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# #region GitServiceSyncMixin [C:4] [TYPE Module] [SEMANTICS git, sync, push, pull, remote, lock]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF Push and pull operations for GitService with origin host auto-alignment (concurrent-safe).
|
||||
# @RELATION USED_BY -> [GitService]
|
||||
# @RELATION CALLED_BY -> [GitService]
|
||||
# @RELATION DEPENDS_ON -> [GitServiceUrlMixin]
|
||||
|
||||
import os
|
||||
@@ -19,8 +19,8 @@ from src.models.git import GitRepository, GitServerConfig
|
||||
class GitServiceSyncMixin:
|
||||
# region push_changes [C:4] [TYPE Function] [SEMANTICS git,push,lock]
|
||||
# @PURPOSE: Push local commits to remote (concurrent-safe).
|
||||
# @PRE: Repository exists and has an 'origin' remote.
|
||||
# @POST: Local branch commits are pushed to origin.
|
||||
# @PRE Repository exists and has an 'origin' remote.
|
||||
# @POST Local branch commits are pushed to origin.
|
||||
def push_changes(self, dashboard_id: int):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.push_changes"):
|
||||
@@ -113,8 +113,8 @@ class GitServiceSyncMixin:
|
||||
|
||||
# region pull_changes [C:4] [TYPE Function] [SEMANTICS git,pull,lock]
|
||||
# @PURPOSE: Pull changes from remote (concurrent-safe).
|
||||
# @PRE: Repository exists and has an 'origin' remote.
|
||||
# @POST: Changes from origin are pulled and merged into the active branch.
|
||||
# @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 self._locked(dashboard_id):
|
||||
with belief_scope("GitService.pull_changes"):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# #region GitServiceUrlMixin [C:3] [TYPE Module] [SEMANTICS git, url, parse, remote, endpoint]
|
||||
# @LAYER: Infra
|
||||
# @LAYER Infrastructure
|
||||
# @BRIEF URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.
|
||||
# @RELATION USED_BY -> [GitServiceSyncMixin]
|
||||
# @RELATION USED_BY -> [GitServiceGiteaMixin]
|
||||
# @RELATION USED_BY -> [GitServiceRemoteMixin]
|
||||
# @RELATION CALLED_BY -> [GitServiceSyncMixin]
|
||||
# @RELATION CALLED_BY -> [GitServiceGiteaMixin]
|
||||
# @RELATION CALLED_BY -> [GitServiceRemoteMixin]
|
||||
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
@@ -19,9 +19,9 @@ from src.models.git import GitRepository
|
||||
class GitServiceUrlMixin:
|
||||
# region _extract_http_host [TYPE Function]
|
||||
# @PURPOSE: Extract normalized host[:port] from HTTP(S) URL.
|
||||
# @PRE: url_value may be empty.
|
||||
# @POST: Returns lowercase host token or None.
|
||||
# @RETURN: Optional[str]
|
||||
# @PRE url_value may be empty.
|
||||
# @POST Returns lowercase host token or None.
|
||||
# @RETURN Optional[str]
|
||||
def _extract_http_host(self, url_value: str | None) -> str | None:
|
||||
normalized = str(url_value or "").strip()
|
||||
if not normalized:
|
||||
@@ -42,9 +42,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _strip_url_credentials [TYPE Function]
|
||||
# @PURPOSE: Remove credentials from URL while preserving scheme/host/path.
|
||||
# @PRE: url_value may contain credentials.
|
||||
# @POST: Returns URL without username/password.
|
||||
# @RETURN: str
|
||||
# @PRE url_value may contain credentials.
|
||||
# @POST Returns URL without username/password.
|
||||
# @RETURN str
|
||||
def _strip_url_credentials(self, url_value: str) -> str:
|
||||
normalized = str(url_value or "").strip()
|
||||
if not normalized:
|
||||
@@ -63,9 +63,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _replace_host_in_url [TYPE Function]
|
||||
# @PURPOSE: Replace source URL host with host from configured server URL.
|
||||
# @PRE: source_url and config_url are HTTP(S) URLs.
|
||||
# @POST: Returns source URL with updated host (credentials preserved) or None.
|
||||
# @RETURN: Optional[str]
|
||||
# @PRE source_url and config_url are HTTP(S) URLs.
|
||||
# @POST Returns source URL with updated host (credentials preserved) or None.
|
||||
# @RETURN Optional[str]
|
||||
def _replace_host_in_url(self, source_url: str | None, config_url: str | None) -> str | None:
|
||||
source = str(source_url or "").strip()
|
||||
config = str(config_url or "").strip()
|
||||
@@ -95,9 +95,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _align_origin_host_with_config [TYPE Function]
|
||||
# @PURPOSE: Auto-align local origin host to configured Git server host when they drift.
|
||||
# @PRE: origin remote exists.
|
||||
# @POST: origin URL host updated and DB binding normalized when mismatch detected.
|
||||
# @RETURN: Optional[str]
|
||||
# @PRE origin remote exists.
|
||||
# @POST origin URL host updated and DB binding normalized when mismatch detected.
|
||||
# @RETURN Optional[str]
|
||||
def _align_origin_host_with_config(
|
||||
self,
|
||||
dashboard_id: int,
|
||||
@@ -151,9 +151,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _parse_remote_repo_identity [TYPE Function]
|
||||
# @PURPOSE: Parse owner/repo from remote URL for Git server API operations.
|
||||
# @PRE: remote_url is a valid git URL.
|
||||
# @POST: Returns owner/repo tokens.
|
||||
# @RETURN: Dict[str, str]
|
||||
# @PRE remote_url is a valid git URL.
|
||||
# @POST Returns owner/repo tokens.
|
||||
# @RETURN Dict[str, str]
|
||||
def _parse_remote_repo_identity(self, remote_url: str) -> dict[str, str]:
|
||||
normalized = str(remote_url or "").strip()
|
||||
if not normalized:
|
||||
@@ -177,9 +177,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _derive_server_url_from_remote [TYPE Function]
|
||||
# @PURPOSE: Build API base URL from remote repository URL without credentials.
|
||||
# @PRE: remote_url may be any git URL.
|
||||
# @POST: Returns normalized http(s) base URL or None when derivation is impossible.
|
||||
# @RETURN: Optional[str]
|
||||
# @PRE remote_url may be any git URL.
|
||||
# @POST Returns normalized http(s) base URL or None when derivation is impossible.
|
||||
# @RETURN Optional[str]
|
||||
def _derive_server_url_from_remote(self, remote_url: str) -> str | None:
|
||||
normalized = str(remote_url or "").strip()
|
||||
if not normalized or normalized.startswith("git@"):
|
||||
@@ -197,9 +197,9 @@ class GitServiceUrlMixin:
|
||||
|
||||
# region _normalize_git_server_url [TYPE Function]
|
||||
# @PURPOSE: Normalize Git server URL for provider API calls.
|
||||
# @PRE: raw_url is non-empty.
|
||||
# @POST: Returns URL without trailing slash.
|
||||
# @RETURN: str
|
||||
# @PRE raw_url is non-empty.
|
||||
# @POST Returns URL without trailing slash.
|
||||
# @RETURN str
|
||||
def _normalize_git_server_url(self, raw_url: str) -> str:
|
||||
normalized = (raw_url or "").strip()
|
||||
if not normalized:
|
||||
|
||||
Reference in New Issue
Block a user