Why: Preserve a single Dev entry and independent repository ownership while allowing active feature branches to migrate without losing source or Git state. Evidence: SELF 29 real Git/Go workspace tests and ordinary component evidence/lock tests passed; detailed RED/GREEN runs are recorded by make evidence in the task card. Impact: Add fixed cloning, selected-version baselines, development overlays, npm source resolution, and independently preserved legacy snapshots. No other active worktree is modified. Refs: Versioned dependencies U2/U5; R15; feature/dependency-workspace
71 lines
3.8 KiB
Python
71 lines
3.8 KiB
Python
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 = "<redacted>@" + netloc.rsplit("@", 1)[-1]
|
|
return urlunsplit((parsed.scheme, netloc, parsed.path,
|
|
"<redacted>" if parsed.query else "", "<redacted>" 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
|