diff --git a/src/byteb4rb1e/utils/vcs/git.py b/src/byteb4rb1e/utils/vcs/git.py index ab7e87f..cd4cd87 100644 --- a/src/byteb4rb1e/utils/vcs/git.py +++ b/src/byteb4rb1e/utils/vcs/git.py @@ -146,6 +146,18 @@ def show_ref(repo: Path) -> str: return "" +def ls_remote(repo: Path, remote: str) -> str: + """Return the raw output of ``git ls-remote ``. + + Returns an empty string if the remote has no refs or on error. + """ + try: + result = _run(["ls-remote", remote], cwd=repo) + return result.stdout + except GitError: + return "" + + def mirror_push(repo: Path, remote: str) -> None: """Push the full mirror to a remote. @@ -260,22 +272,39 @@ def has_submodule(repo: Path, path: str) -> bool: """Check whether a submodule is registered at *path*. Reads ``.gitmodules`` to determine whether the submodule exists. + *path* is resolved relative to *repo*, then compared against + the repository root so the check works when *repo* is a + subdirectory of the actual git working tree. Returns False if ``.gitmodules`` does not exist. """ - gitmodules = repo / ".gitmodules" + try: + toplevel = Path( + _run( + ["rev-parse", "--show-toplevel"], cwd=repo, + ).stdout.strip() + ) + except GitError: + return False + gitmodules = toplevel / ".gitmodules" if not gitmodules.is_file(): return False + # Resolve the full path relative to the repo root + full_path = (repo / path).resolve() + try: + rel_path = str(full_path.relative_to(toplevel.resolve())) + except ValueError: + return False try: result = _run( - ["config", "--file", ".gitmodules", + ["config", "--file", str(gitmodules), "--get-regexp", r"submodule\..*\.path"], - cwd=repo, + cwd=toplevel, ) except GitError: return False for line in result.stdout.splitlines(): parts = line.split(None, 1) - if len(parts) == 2 and parts[1] == path: + if len(parts) == 2 and parts[1] == rel_path: return True return False @@ -306,3 +335,11 @@ def submodule_update(repo: Path, path: str) -> None: default_branch = result.stdout.strip() _run(["checkout", default_branch], cwd=sub_path) logger.info("Updated submodule %s to %s", path, default_branch) + + +def submodule_checkout(repo: Path, path: str, ref: str) -> None: + """Fetch and checkout a specific ref in a submodule.""" + sub_path = repo / path + _run(["fetch", "origin"], cwd=sub_path) + _run(["checkout", ref], cwd=sub_path) + logger.info("Checked out submodule %s at %s", path, ref)