Source code for ralph.git.commit_cleanup

"""Git cleanup operations for commit hardening.

This module provides deterministic git operations for the commit cleanup phase,
handling file deletion, gitignore updates, and git exclude patterns.
"""

from __future__ import annotations

from contextlib import suppress
from pathlib import Path, PurePath

from git import InvalidGitRepositoryError, Repo
from loguru import logger


[docs] def ensure_git_initialized(repo_root: Path | str) -> None: """Ensure the directory is a git repository, initializing if necessary. Args: repo_root: Path to the repository root. """ with suppress(InvalidGitRepositoryError): Repo(repo_root, search_parent_directories=False) return Repo.init(repo_root) # type: ignore[misc] # reason: external library has no type support, see docs/agents/type-ignore-policy.md#external-library logger.info("Initialized git repository at {}", repo_root)
[docs] def delete_file_from_repo(repo_root: Path | str, relative_path: str) -> None: """Remove a file from the repository, unstaging if necessary. Args: repo_root: Path to the repository root. relative_path: Path relative to repo_root of the file to delete. """ repo_root_path = Path(repo_root).resolve() path = PurePath(relative_path) if path.is_absolute() or any(part == ".." for part in path.parts): raise ValueError( f"Refusing to delete path outside repository root: {relative_path!r}" ) target = (repo_root_path / path).resolve(strict=False) try: target.relative_to(repo_root_path) except ValueError as exc: raise ValueError( f"Refusing to delete path outside repository root: {relative_path!r}" ) from exc if target.is_symlink(): raise ValueError( "Refusing to delete symlink path during commit cleanup: " f"{relative_path!r}" ) if not target.exists(): logger.debug("File {} does not exist, nothing to delete", relative_path) return with suppress(InvalidGitRepositoryError): repo = Repo(repo_root_path) with suppress(Exception): repo.git.rm("--cached", "--", relative_path) with suppress(OSError): target.unlink(missing_ok=True) logger.debug("Deleted file {}", relative_path)
[docs] def add_to_git_exclude(repo_root: Path | str, patterns: list[str]) -> None: """Append patterns to .git/info/exclude for machine-local excludes. Args: repo_root: Path to the repository root. patterns: List of patterns to add to exclude. """ repo = Repo(repo_root, search_parent_directories=False) exclude_path = Path(repo.git_dir) / "info" / "exclude" exclude_path.parent.mkdir(parents=True, exist_ok=True) existing: set[str] = set() if exclude_path.exists(): existing = set(exclude_path.read_text().splitlines()) new_patterns = [p for p in patterns if p not in existing] if new_patterns: with exclude_path.open("a", encoding="utf-8") as f: if new_patterns[0]: f.write("\n") f.write("\n".join(new_patterns)) logger.debug("Added {} patterns to .git/info/exclude", len(new_patterns))