diff --git a/src/auto_readme/utils/git_utils.py b/src/auto_readme/utils/git_utils.py new file mode 100644 index 0000000..ff7af75 --- /dev/null +++ b/src/auto_readme/utils/git_utils.py @@ -0,0 +1,186 @@ +"""Git utilities for the Auto README Generator.""" + +import subprocess +from pathlib import Path +from typing import Optional + +from ..models import GitInfo + + +class GitUtils: + """Utility class for git operations.""" + + @classmethod + def is_git_repo(cls, path: Path) -> bool: + """Check if the path is a git repository.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--git-dir"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + return result.returncode == 0 + except (subprocess.SubprocessError, FileNotFoundError): + return False + + @classmethod + def get_remote_url(cls, path: Path) -> Optional[str]: + """Get the remote URL for the origin remote.""" + try: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return result.stdout.strip() + except (subprocess.SubprocessError, FileNotFoundError): + pass + return None + + @classmethod + def get_current_branch(cls, path: Path) -> Optional[str]: + """Get the current branch name.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return result.stdout.strip() + except (subprocess.SubprocessError, FileNotFoundError): + pass + return None + + @classmethod + def get_commit_sha(cls, path: Path, short: bool = True) -> Optional[str]: + """Get the current commit SHA.""" + try: + flag = "--short" if short else "" + result = subprocess.run( + ["git", "rev-parse", flag, "HEAD"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return result.stdout.strip() + except (subprocess.SubprocessError, FileNotFoundError): + pass + return None + + @classmethod + def get_repo_info(cls, remote_url: Optional[str]) -> tuple[Optional[str], Optional[str]]: + """Extract owner and repo name from remote URL.""" + if not remote_url: + return None, None + + import re + + patterns = [ + r"github\.com[:/](/[^/]+)/([^/]+)\.git", + r"github\.com/([^/]+)/([^/]+)", + r"git@github\.com:([^/]+)/([^/]+)\.git", + r"git@github\.com:([^/]+)/([^/]+)", + ] + + for pattern in patterns: + match = re.search(pattern, remote_url) + if match: + owner, repo = match.groups() + repo = repo.replace(".git", "") + return owner, repo + + return None, None + + @classmethod + def get_git_info(cls, path: Path) -> GitInfo: + """Get complete git information for a repository.""" + is_repo = cls.is_git_repo(path) + if not is_repo: + return GitInfo() + + remote_url = cls.get_remote_url(path) + branch = cls.get_current_branch(path) + commit_sha = cls.get_commit_sha(path) + + owner, repo_name = cls.get_repo_info(remote_url) + + return GitInfo( + remote_url=remote_url, + branch=branch, + commit_sha=commit_sha, + is_repo=True, + repo_name=repo_name, + owner=owner, + ) + + @classmethod + def get_last_commit_message(cls, path: Path) -> Optional[str]: + """Get the last commit message.""" + try: + result = subprocess.run( + ["git", "log", "-1", "--format=%s"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return result.stdout.strip() + except (subprocess.SubprocessError, FileNotFoundError): + pass + return None + + @classmethod + def get_commit_count(cls, path: Path) -> Optional[int]: + """Get the total number of commits.""" + try: + result = subprocess.run( + ["git", "rev-list", "--count", "HEAD"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + return int(result.stdout.strip()) + except (subprocess.SubprocessError, FileNotFoundError, ValueError): + pass + return None + + @classmethod + def get_contributors(cls, path: Path) -> list[str]: + """Get list of contributors.""" + try: + result = subprocess.run( + ["git", "log", "--format=%an", "--reverse"], + cwd=path, + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode == 0: + contributors = [] + seen = set() + for line in result.stdout.strip().split("\n"): + if line and line not in seen: + seen.add(line) + contributors.append(line) + return contributors + except (subprocess.SubprocessError, FileNotFoundError): + pass + return [] + + +def get_git_info(path: Path) -> GitInfo: + """Get complete git information for a repository.""" + return GitUtils.get_git_info(path)