from pathlib import Path from urllib.parse import urlsplit, urlunsplit from .common import WorkspaceError, run def display_origin(value): parsed = urlsplit(value) netloc = parsed.netloc if parsed.username and (parsed.scheme != "ssh" or parsed.username != "git" or parsed.password): netloc = "@" + netloc.rsplit("@", 1)[-1] return urlunsplit((parsed.scheme, netloc, parsed.path, "" if parsed.query else "", "" if parsed.fragment else "")) class Git: def __init__(self, env): self.env = env def call(self, path, *args, check=True): return run(["git", "-c", "gc.auto=0", "-c", "core.hooksPath=/dev/null", "-c", "core.fsmonitor=false", *args], path, self.env, check) def text(self, path, *args, check=True): return self.call(path, *args, check=check).stdout.strip() def inspect(self, path): if not (path / ".git").exists(): raise WorkspaceError(f"existing path is not an independent Git checkout: {path}") top = Path(self.text(path, "rev-parse", "--show-toplevel")) if top.resolve() != path.resolve(): raise WorkspaceError("component path is not a Git root") gitdir = Path(self.text(path, "rev-parse", "--absolute-git-dir")) common = Path(self.text(path, "rev-parse", "--path-format=absolute", "--git-common-dir")) locks = sorted(str(file) for directory in {gitdir, common} for file in directory.rglob("*.lock")) operations = [name for name in ("MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "BISECT_LOG") if (gitdir / name).exists()] changes = self.text(path, "status", "--porcelain=v1", "--untracked-files=all") ignored = self.text(path, "ls-files", "--others", "--ignored", "--exclude-standard") head = self.text(path, "rev-parse", "--verify", "HEAD", check=False) branch = self.text(path, "symbolic-ref", "--quiet", "--short", "HEAD", check=False) worktrees = self.text(path, "worktree", "list", "--porcelain") return {"head": head or None, "branch": branch or None, "origin": display_origin(self.text(path, "remote", "get-url", "origin", check=False)), "dirty": bool(changes or ignored), "changes": changes.splitlines(), "ignored": ignored.splitlines(), "locks": locks, "operations": operations, "worktrees": [line[9:] for line in worktrees.splitlines() if line.startswith("worktree ")], "git_dir": str(gitdir), "common_dir": str(common)} def safe(self, path, origin=None, clean=True, independent=True): actual = self.inspect(path) if origin is not None and actual["origin"] != origin: raise WorkspaceError(f"origin mismatch at {path}") if actual["locks"] or actual["operations"]: raise WorkspaceError(f"Git operation/lock in progress at {path}") if independent and (actual["git_dir"] != actual["common_dir"] or len(actual["worktrees"]) != 1): raise WorkspaceError(f"shared Git worktree ownership at {path}; owner must resolve it") if independent and ((path / ".git").is_symlink() or not (path / ".git").is_dir() or Path(actual["git_dir"]).resolve() != (path / ".git").resolve()): raise WorkspaceError(f"component Git metadata is owned outside its checkout: {path}") if clean and actual["dirty"]: raise WorkspaceError(f"dirty or ignored unique files at {path}; owner must save them") return actual def gitlink(self, root, legacy): if not legacy: return None result = self.text(root, "ls-tree", "HEAD", "--", legacy, check=False) if result.startswith("160000 commit "): return result.split()[2] return None