Skip to main content
Glama
jolfr

Commit Helper MCP

by jolfr
repository_manager.py11 kB
""" Manages repository targeting and multi-repository operations. This service handles repository path resolution, validation, and manages repository-specific service instances. """ import logging import os from pathlib import Path from typing import Dict, Optional, Tuple, Any from .commitizen_core import CommitzenCore from .gitpython_core import GitPythonCore from ..config import get_settings, RepositoryConfig from ..errors import ( handle_errors, RepositoryError, ConfigurationError, ServiceError, create_success_response, ) logger = logging.getLogger(__name__) class RepositoryManager: """Manages repository targeting and multi-repository operations.""" def __init__(self): """Initialize repository manager with empty cache.""" self.settings = get_settings() self._repository_cache: Dict[ str, Tuple[CommitzenCore, Optional[GitPythonCore]] ] = {} self._repository_configs: Dict[str, RepositoryConfig] = {} logger.info("Initialized RepositoryManager") @handle_errors(log_errors=True) def resolve_repository_path(self, repo_path: Optional[str] = None) -> str: """ Resolve repository path with priority order. Priority: 1. Explicit repo_path parameter 2. Settings default_repo_path (from environment or config) 3. Current working directory (default behavior) Args: repo_path: Optional explicit repository path Returns: Resolved repository path as string """ # Priority 1: Explicit parameter if repo_path: resolved_path = Path(repo_path).resolve() logger.debug(f"Using explicit repo_path: {resolved_path}") return str(resolved_path) # Priority 2: Settings (which includes environment variable) if self.settings.default_repo_path: resolved_path = Path(self.settings.default_repo_path).resolve() logger.debug(f"Using default_repo_path from settings: {resolved_path}") return str(resolved_path) # Priority 3: Current working directory resolved_path = Path.cwd() logger.debug(f"Using current working directory: {resolved_path}") return str(resolved_path) @handle_errors(log_errors=True) def validate_repository_access(self, repo_path: str) -> bool: """ Validate repository exists and is accessible. Args: repo_path: Repository path to validate Returns: True if repository is valid and accessible """ try: path = Path(repo_path) # Check if path exists if not path.exists(): logger.warning(f"Repository path does not exist: {repo_path}") return False # Check if it's a directory if not path.is_dir(): logger.warning(f"Repository path is not a directory: {repo_path}") return False # Check if we have read access if not os.access(repo_path, os.R_OK): logger.warning(f"No read access to repository: {repo_path}") return False # Check if it's a git repository git_dir = path / ".git" if not git_dir.exists(): logger.info(f"Not a git repository (no .git directory): {repo_path}") # This is okay - we can still use Commitizen without git return True return True except Exception as e: logger.error(f"Error validating repository access: {e}") return False @handle_errors(log_errors=True) def get_repository_services( self, repo_path: Optional[str] = None ) -> Tuple[CommitzenCore, Optional[GitPythonCore]]: """ Get or create services for specific repository. Uses caching to avoid recreating services for the same repository. Args: repo_path: Optional repository path Returns: Tuple of (CommitzenCore, GitPythonCore or None) """ # Resolve repository path resolved_path = self.resolve_repository_path(repo_path) # Check cache if resolved_path in self._repository_cache: logger.debug(f"Using cached services for: {resolved_path}") return self._repository_cache[resolved_path] # Validate repository access if not self.validate_repository_access(resolved_path): raise RepositoryError( f"Invalid or inaccessible repository: {resolved_path}", repo_path=resolved_path, ) # Create new services logger.info(f"Creating new services for repository: {resolved_path}") # Create CommitzenCore (always available) try: commitizen_core = CommitzenCore(repo_path=resolved_path) except Exception as e: raise ConfigurationError( f"Failed to initialize CommitzenCore: {e}", config_file=resolved_path, cause=e, ) # Try to create GitPythonCore gitpython_core = None try: gitpython_core = GitPythonCore(repo_path=resolved_path) logger.info(f"GitPython service created for: {resolved_path}") except RepositoryError as e: # This is expected for non-git repositories logger.info( f"Not a git repository, GitPython service disabled: {resolved_path}" ) except Exception as e: logger.warning(f"Failed to create GitPython service: {e}") # Cache the services self._repository_cache[resolved_path] = (commitizen_core, gitpython_core) return commitizen_core, gitpython_core @handle_errors(log_errors=True) def manage_environment_variables(self) -> Dict[str, str]: """ Handle COMMITIZEN_REPO_PATH and other environment variables. Returns: Dict of relevant environment variables and their values """ env_vars = {} # Check for COMMITIZEN_REPO_PATH if "COMMITIZEN_REPO_PATH" in os.environ: env_vars["COMMITIZEN_REPO_PATH"] = os.environ["COMMITIZEN_REPO_PATH"] logger.info( f"COMMITIZEN_REPO_PATH set to: {env_vars['COMMITIZEN_REPO_PATH']}" ) # Check for other relevant environment variables git_vars = ["GIT_DIR", "GIT_WORK_TREE", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"] for var in git_vars: if var in os.environ: env_vars[var] = os.environ[var] return env_vars @handle_errors(log_errors=True) def clear_repository_cache(self, repo_path: Optional[str] = None) -> None: """ Clear cached repository services. Args: repo_path: Optional specific repository to clear, or None to clear all """ if repo_path: resolved_path = self.resolve_repository_path(repo_path) if resolved_path in self._repository_cache: del self._repository_cache[resolved_path] logger.info(f"Cleared cache for repository: {resolved_path}") else: self._repository_cache.clear() logger.info("Cleared all repository caches") @handle_errors(log_errors=True) def get_repository_config( self, repo_path: Optional[str] = None ) -> RepositoryConfig: """ Get repository configuration, using cache if available. Args: repo_path: Optional repository path Returns: RepositoryConfig instance """ resolved_path = self.resolve_repository_path(repo_path) if resolved_path not in self._repository_configs: try: self._repository_configs[resolved_path] = RepositoryConfig( resolved_path ) except Exception as e: raise ConfigurationError( f"Failed to load repository configuration: {e}", config_file=resolved_path, cause=e, ) return self._repository_configs[resolved_path] @handle_errors(log_errors=True) def get_cached_repositories(self) -> Dict[str, Dict[str, Any]]: """ Get information about all cached repositories. Returns: Dict with repository paths and their service status """ repositories = {} for repo_path, ( commitizen_core, gitpython_core, ) in self._repository_cache.items(): repositories[repo_path] = { "has_commitizen": commitizen_core is not None, "has_git": gitpython_core is not None, "commitizen_plugin": commitizen_core.committer.__class__.__name__ if commitizen_core else None, "git_enabled": gitpython_core is not None, } return repositories @handle_errors(log_errors=True) def switch_repository(self, new_repo_path: str) -> Dict[str, Any]: """ Switch to a different repository and return its status. Args: new_repo_path: Path to the new repository Returns: Dict with repository switch status and information """ # Validate the new repository if not self.validate_repository_access(new_repo_path): raise RepositoryError( f"Invalid or inaccessible repository: {new_repo_path}", repo_path=new_repo_path, ) # Get services for the new repository commitizen_core, gitpython_core = self.get_repository_services(new_repo_path) # Get repository information repo_info = { "repository_path": new_repo_path, "has_commitizen": True, "has_git": gitpython_core is not None, "commitizen_plugin": commitizen_core.committer.__class__.__name__, "git_enabled": gitpython_core is not None, } # Add git status if available if gitpython_core: try: status = gitpython_core.get_repository_status() repo_info["git_status"] = { "current_branch": status.get("current_branch"), "staged_files_count": status.get("staged_files_count", 0), "unstaged_files_count": status.get("unstaged_files_count", 0), "untracked_files_count": status.get("untracked_files_count", 0), } except Exception as e: logger.warning(f"Could not get git status: {e}") repo_info["git_status_error"] = str(e) return create_success_response(repo_info)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jolfr/commit-helper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server