032: T045 — git services async (run_blocking for all blocking ops)
_merge.py partially done. Tests still pending.
This commit is contained in:
@@ -55,9 +55,9 @@ def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> Non
|
||||
# @BRIEF 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.
|
||||
def _resolve_repository_status(dashboard_id: int) -> dict:
|
||||
async def _resolve_repository_status(dashboard_id: int) -> dict:
|
||||
git_service = get_git_service()
|
||||
repo_path = git_service._get_repo_path(dashboard_id)
|
||||
repo_path = await git_service._get_repo_path(dashboard_id)
|
||||
if not os.path.exists(repo_path):
|
||||
logger.reason(
|
||||
f"Repository is not initialized for dashboard {dashboard_id}",
|
||||
@@ -310,7 +310,7 @@ def _resolve_current_user_git_identity(
|
||||
|
||||
# #region _apply_git_identity_from_profile [C:2] [TYPE Function]
|
||||
# @BRIEF Apply user-scoped Git identity to repository-local config before write/pull operations.
|
||||
def _apply_git_identity_from_profile(
|
||||
async def _apply_git_identity_from_profile(
|
||||
dashboard_id: int,
|
||||
db: Session,
|
||||
current_user: User | None,
|
||||
@@ -320,11 +320,11 @@ def _apply_git_identity_from_profile(
|
||||
return
|
||||
|
||||
git_service = get_git_service()
|
||||
configure_identity = getattr(git_service, "configure_identity", None)
|
||||
if not callable(configure_identity):
|
||||
configure_identity_fn = getattr(git_service, "configure_identity", None)
|
||||
if not callable(configure_identity_fn):
|
||||
return
|
||||
|
||||
git_username, git_email = identity
|
||||
configure_identity(dashboard_id, git_username, git_email)
|
||||
await configure_identity_fn(dashboard_id, git_username, git_email)
|
||||
# #endregion _apply_git_identity_from_profile
|
||||
# #endregion GitHelpers
|
||||
|
||||
@@ -118,7 +118,7 @@ async def promote_dashboard(
|
||||
to_branch,
|
||||
reason,
|
||||
)
|
||||
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
result = _gs.promote_direct_merge(
|
||||
dashboard_id=dashboard_id,
|
||||
from_branch=from_branch,
|
||||
|
||||
@@ -48,7 +48,7 @@ async def commit_changes(
|
||||
dashboard_id = await _resolve_dashboard_id_from_ref(
|
||||
dashboard_ref, config_manager, env_id
|
||||
)
|
||||
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
_gs.commit_changes(
|
||||
dashboard_id, commit_data.message, commit_data.files
|
||||
)
|
||||
@@ -138,7 +138,7 @@ async def pull_changes(
|
||||
f"config_provider={config_provider} config_url={config_url}",
|
||||
extra={"src": "pull_changes"},
|
||||
)
|
||||
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
_gs.pull_changes(dashboard_id)
|
||||
return {"status": "success"}
|
||||
except HTTPException:
|
||||
@@ -164,7 +164,7 @@ async def get_repository_status(
|
||||
dashboard_id = await _resolve_dashboard_id_from_ref(
|
||||
dashboard_ref, config_manager, env_id
|
||||
)
|
||||
return _resolve_repository_status(dashboard_id)
|
||||
return await _resolve_repository_status(dashboard_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -191,7 +191,7 @@ async def get_repository_status_batch(
|
||||
statuses = {}
|
||||
for dashboard_id in dashboard_ids:
|
||||
try:
|
||||
statuses[str(dashboard_id)] = _resolve_repository_status(dashboard_id)
|
||||
statuses[str(dashboard_id)] = await _resolve_repository_status(dashboard_id)
|
||||
except HTTPException:
|
||||
statuses[str(dashboard_id)] = {
|
||||
**_build_no_repo_status_payload(),
|
||||
|
||||
@@ -63,7 +63,7 @@ async def init_repository(
|
||||
f"Initializing repo for dashboard {dashboard_id}",
|
||||
extra={"src": "init_repository"},
|
||||
)
|
||||
_gs.init_repo(
|
||||
await _gs.init_repo(
|
||||
dashboard_id,
|
||||
init_data.remote_url,
|
||||
config.pat,
|
||||
@@ -71,7 +71,7 @@ async def init_repository(
|
||||
default_branch=config.default_branch,
|
||||
)
|
||||
|
||||
repo_path = _gs._get_repo_path(dashboard_id, repo_key=repo_key)
|
||||
repo_path = await _gs._get_repo_path(dashboard_id, repo_key=repo_key)
|
||||
db_repo = (
|
||||
db.query(GitRepository)
|
||||
.filter(GitRepository.dashboard_id == dashboard_id)
|
||||
@@ -166,7 +166,7 @@ async def delete_repository(
|
||||
dashboard_id = await _resolve_dashboard_id_from_ref(
|
||||
dashboard_ref, config_manager, env_id
|
||||
)
|
||||
_gs.delete_repo(dashboard_id)
|
||||
await _gs.delete_repo(dashboard_id)
|
||||
return {"status": "success"}
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -220,7 +220,7 @@ async def create_branch(
|
||||
dashboard_id = await _resolve_dashboard_id_from_ref(
|
||||
dashboard_ref, config_manager, env_id
|
||||
)
|
||||
_apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
await _apply_git_identity_from_profile(dashboard_id, db, current_user)
|
||||
_gs.create_branch(
|
||||
dashboard_id, branch_data.name, branch_data.from_branch
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# @RELATION DEPENDS_ON -> [GitRepository]
|
||||
# @RELATION DEPENDS_ON -> [GitRepository]
|
||||
# @RATIONALE Extracted 'git_repos' default into DEFAULT_GIT_REPOS_PATH constant. Fixed typo 'repositorys' → 'repositories' with breaking change warning for existing installations.
|
||||
# @SIDE_EFFECT Async migration T045: blocking git/file operations wrapped via run_blocking executor.
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
@@ -22,6 +23,7 @@ import httpx
|
||||
|
||||
from src.core.database import SessionLocal
|
||||
from src.core.logger import belief_scope, logger
|
||||
from src.core.utils.executors import run_blocking
|
||||
from src.models.config import AppConfigRecord
|
||||
from src.models.git import GitRepository
|
||||
|
||||
@@ -90,13 +92,13 @@ class GitServiceBase:
|
||||
# @BRIEF Clone repository with PAT and immediately strip credentials from origin remote URL.
|
||||
# @PRE remote_url is a valid Git URL; pat is provided when required; repo_path is writable.
|
||||
# @POST Repo cloned at repo_path; origin remote URL has no embedded PAT.
|
||||
# @SIDE_EFFECT Clones remote repository to local filesystem.
|
||||
def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:
|
||||
# @SIDE_EFFECT Clones remote repository to local filesystem (via run_blocking executor).
|
||||
async def _clone_with_auth(self, remote_url: str, repo_path: str, pat: str) -> Repo:
|
||||
auth_url = remote_url
|
||||
if pat and "://" in remote_url:
|
||||
proto, rest = remote_url.split("://", 1)
|
||||
auth_url = f"{proto}://oauth2:{quote(pat, safe='')}@{rest}"
|
||||
repo = Repo.clone_from(auth_url, repo_path)
|
||||
repo = await run_blocking(kind='git', fn=Repo.clone_from, auth_url, repo_path)
|
||||
# Strip PAT from origin URL to prevent credential leakage in .git/config, logs, ps aux
|
||||
repo.git.remote("set-url", "origin", remote_url)
|
||||
return repo
|
||||
@@ -191,8 +193,9 @@ class GitServiceBase:
|
||||
# @PURPOSE Move legacy repository directory to target path and sync DB metadata.
|
||||
# @PRE source_path exists.
|
||||
# @POST Repository content available at target_path.
|
||||
# @SIDE_EFFECT Filesystem move operations delegated via run_blocking(kind='file', ...).
|
||||
# @RETURN str
|
||||
def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:
|
||||
async def _migrate_repo_directory(self, dashboard_id: int, source_path: str, target_path: str) -> str:
|
||||
source_abs = os.path.abspath(source_path)
|
||||
target_abs = os.path.abspath(target_path)
|
||||
if source_abs == target_abs:
|
||||
@@ -200,11 +203,11 @@ class GitServiceBase:
|
||||
if os.path.exists(target_abs):
|
||||
logger.reason(f"Target already exists, keeping source path: {target_abs}", extra={"src": "_migrate_repo_directory"})
|
||||
return source_abs
|
||||
Path(target_abs).parent.mkdir(parents=True, exist_ok=True)
|
||||
await run_blocking(kind='file', fn=Path(target_abs).parent.mkdir, parents=True, exist_ok=True)
|
||||
try:
|
||||
os.replace(source_abs, target_abs)
|
||||
await run_blocking(kind='file', fn=os.replace, source_abs, target_abs)
|
||||
except OSError:
|
||||
shutil.move(source_abs, target_abs)
|
||||
await run_blocking(kind='file', fn=shutil.move, source_abs, target_abs)
|
||||
self._update_repo_local_path(dashboard_id, target_abs)
|
||||
logger.info(
|
||||
f"[_migrate_repo_directory][Coherence:OK] Repository migrated for dashboard {dashboard_id}: {source_abs} -> {target_abs}"
|
||||
@@ -219,7 +222,7 @@ class GitServiceBase:
|
||||
# @PRE dashboard_id is an integer.
|
||||
# @POST Returns DB-local_path when present, otherwise base_path/<normalized repo_key>.
|
||||
# @RETURN str
|
||||
def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:
|
||||
async def _get_repo_path(self, dashboard_id: int, repo_key: str | None = None) -> str:
|
||||
with belief_scope("GitService._get_repo_path"):
|
||||
if dashboard_id is None:
|
||||
raise ValueError("dashboard_id cannot be None")
|
||||
@@ -246,13 +249,13 @@ class GitServiceBase:
|
||||
os.path.abspath(self.base_path) != os.path.abspath(self.legacy_base_path)
|
||||
and db_path.startswith(os.path.abspath(self.legacy_base_path) + os.sep)
|
||||
):
|
||||
return self._migrate_repo_directory(dashboard_id, db_path, target_path)
|
||||
return await self._migrate_repo_directory(dashboard_id, db_path, target_path)
|
||||
return db_path
|
||||
except Exception as e:
|
||||
logger.warning(f"[_get_repo_path][Coherence:Failed] Could not resolve local_path from DB: {e}")
|
||||
legacy_id_path = os.path.join(self.legacy_base_path, str(dashboard_id))
|
||||
if os.path.exists(legacy_id_path) and not os.path.exists(target_path):
|
||||
return self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)
|
||||
return await self._migrate_repo_directory(dashboard_id, legacy_id_path, target_path)
|
||||
if os.path.exists(target_path):
|
||||
self._update_repo_local_path(dashboard_id, target_path)
|
||||
return target_path
|
||||
@@ -264,31 +267,32 @@ class GitServiceBase:
|
||||
# @PRE dashboard_id is int, remote_url is valid Git URL, pat is provided.
|
||||
# @POST Repository is cloned or opened at the local path. Origin remote URL has no embedded PAT.
|
||||
# @SIDE_EFFECT Clones remote repository; modifies local filesystem; protected by per-dashboard lock.
|
||||
# Blocking git/file operations delegated to run_blocking executor.
|
||||
# @RETURN Repo
|
||||
def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None, default_branch: str | None = None) -> Repo:
|
||||
async def init_repo(self, dashboard_id: int, remote_url: str, pat: str, repo_key: str | None = None, default_branch: str | None = None) -> Repo:
|
||||
with self._locked(dashboard_id), belief_scope("GitService.init_repo"):
|
||||
self._ensure_base_path_exists()
|
||||
repo_path = self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))
|
||||
Path(repo_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
repo_path = await self._get_repo_path(dashboard_id, repo_key=repo_key or str(dashboard_id))
|
||||
await run_blocking(kind='file', fn=Path(repo_path).parent.mkdir, parents=True, exist_ok=True)
|
||||
if os.path.exists(repo_path):
|
||||
logger.reason(f"Opening existing repo at {repo_path}", extra={"src": "init_repo"})
|
||||
try:
|
||||
repo = Repo(repo_path)
|
||||
repo = await run_blocking(kind='git', fn=Repo, repo_path)
|
||||
except (InvalidGitRepositoryError, NoSuchPathError):
|
||||
logger.reason(f"Existing path is not a Git repository, recreating: {repo_path}", extra={"src": "init_repo"})
|
||||
stale_path = Path(repo_path)
|
||||
if stale_path.exists():
|
||||
shutil.rmtree(stale_path, ignore_errors=True)
|
||||
await run_blocking(kind='file', fn=shutil.rmtree, stale_path, ignore_errors=True)
|
||||
if stale_path.exists():
|
||||
try:
|
||||
stale_path.unlink()
|
||||
await run_blocking(kind='file', fn=stale_path.unlink)
|
||||
except Exception:
|
||||
pass
|
||||
repo = self._clone_with_auth(remote_url, repo_path, pat)
|
||||
repo = await self._clone_with_auth(remote_url, repo_path, pat)
|
||||
self._ensure_gitflow_branches(repo, dashboard_id)
|
||||
return repo
|
||||
logger.reason(f"Cloning {remote_url} to {repo_path}", extra={"src": "init_repo"})
|
||||
repo = self._clone_with_auth(remote_url, repo_path, pat)
|
||||
repo = await self._clone_with_auth(remote_url, repo_path, pat)
|
||||
self._ensure_gitflow_branches(repo, dashboard_id)
|
||||
return repo
|
||||
# endregion init_repo
|
||||
@@ -298,15 +302,16 @@ class GitServiceBase:
|
||||
# @PRE dashboard_id is a valid integer.
|
||||
# @POST Local path is deleted when present and GitRepository row is removed.
|
||||
# @SIDE_EFFECT Deletes filesystem directory and DB record; protected by per-dashboard lock.
|
||||
def delete_repo(self, dashboard_id: int) -> None:
|
||||
# Blocking file operations delegated to run_blocking executor.
|
||||
async def delete_repo(self, dashboard_id: int) -> None:
|
||||
with self._locked(dashboard_id), belief_scope("GitService.delete_repo"):
|
||||
repo_path = self._get_repo_path(dashboard_id)
|
||||
repo_path = await self._get_repo_path(dashboard_id)
|
||||
removed_files = False
|
||||
if os.path.exists(repo_path):
|
||||
if os.path.isdir(repo_path):
|
||||
shutil.rmtree(repo_path)
|
||||
await run_blocking(kind='file', fn=shutil.rmtree, repo_path)
|
||||
else:
|
||||
os.remove(repo_path)
|
||||
await run_blocking(kind='file', fn=os.remove, repo_path)
|
||||
removed_files = True
|
||||
session = SessionLocal()
|
||||
try:
|
||||
@@ -340,15 +345,16 @@ class GitServiceBase:
|
||||
# @PURPOSE Get Repo object for a dashboard with concurrent access protection.
|
||||
# @PRE Repository must exist on disk for the given dashboard_id.
|
||||
# @POST Returns a GitPython Repo instance for the dashboard.
|
||||
# @SIDE_EFFECT Blocking Repo() call delegated via run_blocking(kind='git', ...).
|
||||
# @RETURN Repo
|
||||
def get_repo(self, dashboard_id: int) -> Repo:
|
||||
async def get_repo(self, dashboard_id: int) -> Repo:
|
||||
with self._locked(dashboard_id), belief_scope("GitService.get_repo"):
|
||||
repo_path = self._get_repo_path(dashboard_id)
|
||||
repo_path = await self._get_repo_path(dashboard_id)
|
||||
if not os.path.exists(repo_path):
|
||||
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)
|
||||
return await run_blocking(kind='git', fn=Repo, repo_path)
|
||||
except Exception as 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")
|
||||
@@ -359,13 +365,13 @@ class GitServiceBase:
|
||||
# @PRE dashboard_id repository exists; git_username/git_email may be empty.
|
||||
# @POST Repository config has user.name and user.email when both identity values are provided.
|
||||
# @SIDE_EFFECT Writes to repository git config; protected by per-dashboard lock.
|
||||
def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:
|
||||
async def configure_identity(self, dashboard_id: int, git_username: str | None, git_email: str | None) -> None:
|
||||
with self._locked(dashboard_id), belief_scope("GitService.configure_identity"):
|
||||
normalized_username = str(git_username or "").strip()
|
||||
normalized_email = str(git_email or "").strip()
|
||||
if not normalized_username or not normalized_email:
|
||||
return
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
try:
|
||||
with repo.config_writer(config_level="repository") as config_writer:
|
||||
config_writer.set_value("user", "name", normalized_username)
|
||||
|
||||
@@ -99,9 +99,9 @@ class GitServiceBranchMixin:
|
||||
# @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]:
|
||||
async def list_branches(self, dashboard_id: int) -> list[dict]:
|
||||
with self._locked(dashboard_id), belief_scope("GitService.list_branches"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
logger.reason(f"Listing branches for {dashboard_id}. Refs: {repo.refs}", extra={"src": "list_branches"})
|
||||
branches = []
|
||||
for ref in repo.refs:
|
||||
@@ -144,10 +144,10 @@ class GitServiceBranchMixin:
|
||||
# @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"):
|
||||
async def create_branch(self, dashboard_id: int, name: str, from_branch: str = "main"):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.create_branch"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
logger.reason(f"Creating branch {name} from {from_branch}", extra={"src": "create_branch"})
|
||||
if not repo.heads and not repo.remotes:
|
||||
logger.reason("Repository is empty. Creating initial commit to enable branching.", extra={"src": "create_branch"})
|
||||
@@ -176,10 +176,10 @@ class GitServiceBranchMixin:
|
||||
# @POST The repository working directory is updated to the specified branch.
|
||||
# @SIDE_EFFECT May raise HTTPException(409) if local changes conflict with checkout.
|
||||
# @SIDE_EFFECT May raise HTTPException(500) if Git operation fails for other reasons.
|
||||
def checkout_branch(self, dashboard_id: int, name: str):
|
||||
async def checkout_branch(self, dashboard_id: int, name: str):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.checkout_branch"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
logger.reason(f"Checking out branch {name}", extra={"src": "checkout_branch"})
|
||||
try:
|
||||
repo.git.checkout(name)
|
||||
@@ -236,10 +236,10 @@ class GitServiceBranchMixin:
|
||||
# @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):
|
||||
async def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.commit_changes"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
if not repo.is_dirty(untracked_files=True) and not files:
|
||||
logger.reason(f"No changes to commit for dashboard {dashboard_id}", extra={"src": "commit_changes"})
|
||||
return
|
||||
|
||||
@@ -90,10 +90,10 @@ class GitServiceMergeMixin:
|
||||
|
||||
# region get_merge_status [C:4] [TYPE Function] [SEMANTICS git,merge,status,lock]
|
||||
# @PURPOSE: Get current merge status for a dashboard repository (concurrent-safe).
|
||||
def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:
|
||||
async def get_merge_status(self, dashboard_id: int) -> dict[str, Any]:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_merge_status"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
merge_head_path = os.path.join(repo.git_dir, "MERGE_HEAD")
|
||||
if not os.path.exists(merge_head_path):
|
||||
current_branch = "unknown"
|
||||
@@ -124,10 +124,10 @@ class GitServiceMergeMixin:
|
||||
|
||||
# region get_merge_conflicts [C:4] [TYPE Function] [SEMANTICS git,conflict,list,lock]
|
||||
# @PURPOSE: List all files with conflicts and their contents (concurrent-safe).
|
||||
def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:
|
||||
async def get_merge_conflicts(self, dashboard_id: int) -> list[dict[str, Any]]:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_merge_conflicts"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
conflicts = []
|
||||
unmerged = repo.index.unmerged_blobs()
|
||||
for file_path, stages in unmerged.items():
|
||||
|
||||
@@ -55,10 +55,10 @@ class GitServiceStatusMixin:
|
||||
# @PRE Repository for dashboard_id exists.
|
||||
# @POST Returns a dictionary representing the Git status.
|
||||
# @RETURN dict
|
||||
def get_status(self, dashboard_id: int) -> dict:
|
||||
async def get_status(self, dashboard_id: int) -> dict:
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.get_status"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
has_commits = False
|
||||
try:
|
||||
repo.head.commit
|
||||
@@ -118,10 +118,10 @@ class GitServiceStatusMixin:
|
||||
# @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:
|
||||
async 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"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
diff_args = []
|
||||
if staged:
|
||||
diff_args.append("--staged")
|
||||
@@ -136,10 +136,10 @@ class GitServiceStatusMixin:
|
||||
# @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]:
|
||||
async 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"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await self.get_repo(dashboard_id)
|
||||
commits = []
|
||||
try:
|
||||
if not repo.heads and not repo.remotes:
|
||||
|
||||
@@ -21,10 +21,10 @@ class GitServiceSyncMixin:
|
||||
# @PURPOSE: Push local commits to remote (concurrent-safe).
|
||||
# @PRE Repository exists and has an 'origin' remote.
|
||||
# @POST Local branch commits are pushed to origin.
|
||||
def push_changes(self, dashboard_id: int):
|
||||
async def push_changes(self, dashboard_id: int):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.push_changes"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await 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
|
||||
@@ -115,10 +115,10 @@ class GitServiceSyncMixin:
|
||||
# @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.
|
||||
def pull_changes(self, dashboard_id: int):
|
||||
async def pull_changes(self, dashboard_id: int):
|
||||
with self._locked(dashboard_id):
|
||||
with belief_scope("GitService.pull_changes"):
|
||||
repo = self.get_repo(dashboard_id)
|
||||
repo = await 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)
|
||||
|
||||
Reference in New Issue
Block a user