"""Git utilities for parsing remote URLs and detecting current branch/commit."""
import re
import subprocess
from pathlib import Path
from typing import Tuple, Optional
class GitUtilsError(Exception):
"""Base exception for git utilities."""
pass
def parse_git_remote_url(remote_url: str) -> str:
"""
Parse git remote URL (SSH or HTTPS) and extract project path.
Examples:
git@gitlab.example.com:group/project.git -> group/project
https://gitlab.example.com/group/project.git -> group/project
Args:
remote_url: The git remote URL
Returns:
Project path in format 'group/project'
Raises:
GitUtilsError: If URL format is not recognized
"""
# SSH format: git@host:group/project.git
ssh_match = re.match(r'git@[^:]+:(.+?)(?:\.git)?$', remote_url)
if ssh_match:
return ssh_match.group(1)
# HTTPS format: https://host/group/project.git
https_match = re.match(r'https?://[^/]+/(.+?)(?:\.git)?/?$', remote_url)
if https_match:
return https_match.group(1)
raise GitUtilsError(f"Unable to parse git remote URL: {remote_url}")
def get_git_remote_url(working_dir: str) -> str:
"""
Get the git remote origin URL.
Args:
working_dir: Working directory path
Returns:
Remote origin URL
Raises:
GitUtilsError: If git command fails or no remote is configured
"""
try:
result = subprocess.run(
['git', 'remote', 'get-url', 'origin'],
cwd=working_dir,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise GitUtilsError(f"Failed to get git remote URL: {e.stderr}")
except FileNotFoundError:
raise GitUtilsError("git command not found")
def get_current_branch(working_dir: str) -> str:
"""
Get the current git branch name.
Args:
working_dir: Working directory path
Returns:
Current branch name
Raises:
GitUtilsError: If git command fails
"""
try:
result = subprocess.run(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
cwd=working_dir,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise GitUtilsError(f"Failed to get current branch: {e.stderr}")
except FileNotFoundError:
raise GitUtilsError("git command not found")
def get_current_commit(working_dir: str) -> str:
"""
Get the current git commit SHA.
Args:
working_dir: Working directory path
Returns:
Current commit SHA (short or full)
Raises:
GitUtilsError: If git command fails
"""
try:
result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
cwd=working_dir,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
raise GitUtilsError(f"Failed to get current commit: {e.stderr}")
except FileNotFoundError:
raise GitUtilsError("git command not found")
def get_project_path_from_working_dir(working_dir: str) -> str:
"""
Extract GitLab project path from working directory's git remote origin.
Args:
working_dir: Working directory path
Returns:
Project path in format 'group/project'
Raises:
GitUtilsError: If git operations or parsing fail
"""
remote_url = get_git_remote_url(working_dir)
return parse_git_remote_url(remote_url)
def validate_working_dir(working_dir: str) -> None:
"""
Validate that working directory is a git repository.
Args:
working_dir: Working directory path
Raises:
GitUtilsError: If directory is not a git repository
"""
git_dir = Path(working_dir) / '.git'
if not git_dir.exists():
raise GitUtilsError(f"Not a git repository: {working_dir}")