import shutil
import subprocess
from pathlib import Path
from typing import Any
type RunKwargs = dict[str, Any]
class ExecutableNotFoundError(Exception):
def get_safe_executable(command: str) -> str:
full_path = shutil.which(command)
if full_path is None:
raise ExecutableNotFoundError(f"Required executable '{command}' not found in PATH")
return full_path
def _build_run_kwargs(function_name: str, **kwargs: Any) -> RunKwargs:
if kwargs.get("shell"):
msg = f"shell=True is not allowed in {function_name}"
raise ValueError(msg)
if "executable" in kwargs:
msg = f"Overriding 'executable' is not allowed in {function_name}"
raise ValueError(msg)
kwargs.pop("text", None)
run_kwargs = {
"capture_output": True,
"text": True,
"check": True, **kwargs, }
run_kwargs.setdefault("encoding", "utf-8")
return run_kwargs
def run_git_command(
args: list[str],
cwd: Path | None = None,
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
git_path = get_safe_executable("git")
run_kwargs = _build_run_kwargs("run_git_command", **kwargs)
return subprocess.run( [git_path, *args],
cwd=cwd,
**run_kwargs,
)
def run_cargo_command(
args: list[str],
cwd: Path | None = None,
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
cargo_path = get_safe_executable("cargo")
run_kwargs = _build_run_kwargs("run_cargo_command", **kwargs)
return subprocess.run( [cargo_path, *args],
cwd=cwd,
**run_kwargs,
)
def run_safe_command(
command: str,
args: list[str],
cwd: Path | None = None,
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
command_path = get_safe_executable(command)
run_kwargs = _build_run_kwargs(f"run_safe_command for {command}", **kwargs)
return subprocess.run( [command_path, *args],
cwd=cwd,
**run_kwargs,
)
def get_git_commit_hash(cwd: Path | None = None) -> str:
result = run_git_command(["rev-parse", "HEAD"], cwd=cwd)
return result.stdout.strip()
def get_git_remote_url(remote: str = "origin", cwd: Path | None = None) -> str:
result = run_git_command(["remote", "get-url", remote], cwd=cwd)
return result.stdout.strip()
def check_git_repo() -> bool:
try:
run_git_command(["rev-parse", "--git-dir"])
except (ExecutableNotFoundError, subprocess.CalledProcessError):
return False
else:
return True
def check_git_history() -> bool:
try:
run_git_command(["log", "--oneline", "-n", "1"])
except (ExecutableNotFoundError, subprocess.CalledProcessError):
return False
else:
return True
def run_git_command_with_input(
args: list[str],
input_data: str,
cwd: Path | None = None,
**kwargs: Any,
) -> subprocess.CompletedProcess[str]:
git_path = get_safe_executable("git")
run_kwargs = _build_run_kwargs("run_git_command_with_input", **kwargs)
return subprocess.run( [git_path, *args],
cwd=cwd,
input=input_data,
**run_kwargs,
)
class ProjectRootNotFoundError(Exception):
def find_project_root() -> Path:
current_dir = Path.cwd()
project_root = current_dir
while project_root != project_root.parent:
if (project_root / "Cargo.toml").exists():
return project_root
project_root = project_root.parent
msg = "Could not locate Cargo.toml to determine project root"
raise ProjectRootNotFoundError(msg)