import abc import subprocess from pathlib import Path from typing import Any class GitOperations(abc.ABC): @abc.abstractmethod def clone(self, git_url: str, dest: Path, credential_id: str) -> None: pass @abc.abstractmethod def fetch(self, repo_path: Path, credential_id: str) -> None: pass @abc.abstractmethod def push(self, repo_path: Path, credential_id: str) -> None: pass @abc.abstractmethod def get_status(self, repo_path: Path) -> dict[str, Any]: pass class LocalGitOperations(GitOperations): def clone(self, git_url: str, dest: Path, credential_id: str) -> None: cmd = ["git", "clone", git_url, str(dest)] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Git clone failed: {result.stderr}") def fetch(self, repo_path: Path, credential_id: str) -> None: cmd = ["git", "-C", str(repo_path), "fetch", "--all"] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Git fetch failed: {result.stderr}") def push(self, repo_path: Path, credential_id: str) -> None: cmd = ["git", "-C", str(repo_path), "push"] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Git push failed: {result.stderr}") def get_status(self, repo_path: Path) -> dict[str, Any]: if not repo_path.exists() or not (repo_path / ".git").is_dir(): raise RuntimeError("Not a git repository") try: branch_result = subprocess.run( ["git", "-C", str(repo_path), "branch", "--show-current"], capture_output=True, text=True, encoding="utf-8", check=True, ) branch = branch_result.stdout.strip() status_result = subprocess.run( ["git", "-C", str(repo_path), "status", "--porcelain"], capture_output=True, text=True, encoding="utf-8", check=True, ) except subprocess.CalledProcessError as exc: raise RuntimeError("Git command failed") from exc untracked: list[str] = [] modified: list[str] = [] staged: list[str] = [] deleted: list[str] = [] for line in status_result.stdout.splitlines(): if len(line) < 3: continue index_status = line[0] worktree_status = line[1] filename = line[3:] if index_status == "?" and worktree_status == "?": untracked.append(filename) elif index_status in ("M", "A"): staged.append(filename) if index_status == "D" or worktree_status == "D": deleted.append(filename) if worktree_status == "M": modified.append(filename) clean = not (untracked or modified or staged or deleted) return { "branch": branch, "clean": clean, "untracked": untracked, "modified": modified, "staged": staged, "deleted": deleted, }