"""
Repository Configuration
Handles repository-specific configuration including Commitizen config files,
git configuration, and repository validation.
"""
import os
import json
import toml
from pathlib import Path
from typing import Dict, Any, Optional, List
from dataclasses import dataclass, field
from ..errors import (
handle_errors,
ConfigurationError,
RepositoryError,
create_success_response,
)
@dataclass
class RepositoryConfig:
"""Repository-specific configuration."""
repo_path: Path
commitizen_config: Dict[str, Any] = field(default_factory=dict)
git_config: Dict[str, Any] = field(default_factory=dict)
is_valid_repo: bool = field(default=False)
config_file_path: Optional[Path] = field(default=None)
def __post_init__(self):
"""Initialize repository configuration."""
self.repo_path = Path(self.repo_path).resolve()
self._detect_repository()
self._load_commitizen_config()
self._load_git_config()
@handle_errors(log_errors=True)
def _detect_repository(self):
"""Detect if path is a valid git repository."""
git_dir = self.repo_path / ".git"
self.is_valid_repo = git_dir.exists() and (
git_dir.is_dir() or git_dir.is_file()
)
@handle_errors(log_errors=True)
def _load_commitizen_config(self):
"""Load Commitizen configuration from various sources."""
config_files = ["pyproject.toml", ".cz.toml", ".cz.json", "setup.cfg"]
for config_file in config_files:
config_path = self.repo_path / config_file
if config_path.exists():
try:
config = self._parse_config_file(config_path)
if config:
self.commitizen_config = config
self.config_file_path = config_path
break
except Exception as e:
# Continue to next file
pass
@handle_errors(log_errors=True)
def _parse_config_file(self, config_path: Path) -> Optional[Dict[str, Any]]:
"""Parse configuration file based on extension."""
if config_path.suffix == ".toml":
return self._parse_toml_config(config_path)
elif config_path.suffix == ".json":
return self._parse_json_config(config_path)
elif config_path.name == "setup.cfg":
return self._parse_setup_cfg(config_path)
return None
@handle_errors(log_errors=True)
def _parse_toml_config(self, config_path: Path) -> Optional[Dict[str, Any]]:
"""Parse TOML configuration file."""
try:
data = toml.load(config_path)
# Check for commitizen config in pyproject.toml
if "tool" in data and "commitizen" in data["tool"]:
return data["tool"]["commitizen"]
# Check for direct commitizen config in .cz.toml
if "commitizen" in data:
return data["commitizen"]
# Return root level for .cz.toml files
return data
except Exception as e:
raise ConfigurationError(
f"Failed to parse TOML config file: {config_path}",
config_file=str(config_path),
cause=e,
)
@handle_errors(log_errors=True)
def _parse_json_config(self, config_path: Path) -> Optional[Dict[str, Any]]:
"""Parse JSON configuration file."""
try:
with open(config_path, "r") as f:
return json.load(f)
except Exception as e:
raise ConfigurationError(
f"Failed to parse JSON config file: {config_path}",
config_file=str(config_path),
cause=e,
)
@handle_errors(log_errors=True)
def _parse_setup_cfg(self, config_path: Path) -> Optional[Dict[str, Any]]:
"""Parse setup.cfg configuration file."""
try:
import configparser
config = configparser.ConfigParser()
config.read(config_path)
if "tool:commitizen" in config:
return dict(config["tool:commitizen"])
except Exception as e:
raise ConfigurationError(
f"Failed to parse setup.cfg file: {config_path}",
config_file=str(config_path),
cause=e,
)
return None
@handle_errors(log_errors=True)
def _load_git_config(self):
"""Load git configuration if available."""
if not self.is_valid_repo:
return
try:
# Try to load git config using GitPython if available
from git import Repo
repo = Repo(self.repo_path)
config_reader = repo.config_reader()
self.git_config = {
"user_name": config_reader.get_value("user", "name", fallback=""),
"user_email": config_reader.get_value("user", "email", fallback=""),
"core_editor": config_reader.get_value("core", "editor", fallback=""),
}
except Exception as e:
# Fallback to basic git config detection
self.git_config = {}
raise RepositoryError(
f"Failed to load git configuration for repository: {self.repo_path}",
repo_path=str(self.repo_path),
cause=e,
)
@handle_errors(log_errors=True)
def get_commitizen_setting(self, key: str, default: Any = None) -> Any:
"""Get specific Commitizen setting."""
return self.commitizen_config.get(key, default)
@handle_errors(log_errors=True)
def get_plugin_name(self) -> str:
"""Get configured plugin name."""
return self.get_commitizen_setting("name", "cz_conventional_commits")
@handle_errors(log_errors=True)
def get_version_scheme(self) -> str:
"""Get version scheme."""
return self.get_commitizen_setting("version_scheme", "semver")
@handle_errors(log_errors=True)
def get_tag_format(self) -> str:
"""Get tag format."""
return self.get_commitizen_setting("tag_format", "v$version")
@handle_errors(log_errors=True)
def validate_configuration(self) -> List[str]:
"""Validate repository configuration and return issues."""
issues = []
if not self.is_valid_repo:
issues.append("Not a valid git repository")
if not self.commitizen_config:
issues.append("No Commitizen configuration found")
if not self.git_config.get("user_name"):
issues.append("Git user.name not configured")
if not self.git_config.get("user_email"):
issues.append("Git user.email not configured")
return issues
@handle_errors(log_errors=True)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary representation."""
return create_success_response(
{
"repo_path": str(self.repo_path),
"commitizen_config": self.commitizen_config,
"git_config": self.git_config,
"is_valid_repo": self.is_valid_repo,
"config_file_path": str(self.config_file_path)
if self.config_file_path
else None,
}
)
@handle_errors(log_errors=True)
def load_repository_config(repo_path: str) -> RepositoryConfig:
"""Load configuration for a specific repository."""
if not repo_path:
raise RepositoryError("Repository path cannot be empty", repo_path=repo_path)
return RepositoryConfig(repo_path=repo_path)