Coverage for core/src/version_finder/git_executer.py: 75%
59 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-03-18 10:30 +0000
1"""
2git_executor.py
3====================================
4Module for handling git command execution logic.
5"""
6from dataclasses import dataclass
7from pathlib import Path
8import subprocess
9import time
10import os
11from typing import Optional, Union
12from version_finder.logger import get_logger
13from version_finder.common import (
14 DEFAULT_GIT_TIMEOUT,
15 DEFAULT_GIT_MAX_RETRIES,
16 DEFAULT_GIT_RETRY_DELAY,
17 ENV_GIT_TIMEOUT,
18 ENV_GIT_MAX_RETRIES,
19 ENV_GIT_RETRY_DELAY
20)
23logger = get_logger()
26@dataclass
27class GitConfig:
28 """Configuration settings for git operations"""
29 timeout: int = int(os.environ.get(ENV_GIT_TIMEOUT, str(DEFAULT_GIT_TIMEOUT)))
30 max_retries: int = int(os.environ.get(ENV_GIT_MAX_RETRIES, str(DEFAULT_GIT_MAX_RETRIES)))
31 retry_delay: int = int(os.environ.get(ENV_GIT_RETRY_DELAY, str(DEFAULT_GIT_RETRY_DELAY)))
33 def __post_init__(self):
34 if self.timeout <= 0:
35 raise ValueError("timeout must be positive")
36 if self.max_retries < 0:
37 raise ValueError("max_retries cannot be negative")
38 if self.retry_delay <= 0:
39 raise ValueError("retry_delay must be positive")
42class GitCommandError(Exception):
43 """Base exception for git command failures"""
46class GitNetworkError(GitCommandError):
47 """Raised when git operations fail due to network issues"""
50class GitTimeoutError(GitCommandError):
51 """Raised when git operations timeout"""
54class GitPermissionError(GitCommandError):
55 """Raised when git operations fail due to permission issues"""
58class GitCommandExecutor:
59 def __init__(self,
60 repository_path: Path,
61 config: Optional[GitConfig] = None):
62 self.repository_path = repository_path
63 self.config = config or GitConfig()
65 # Check Git is installed
66 try:
67 subprocess.check_output(["git", "--version"])
68 except FileNotFoundError:
69 raise GitCommandError("Git is not installed")
71 def execute(self, command: list[str], retries: int = 0,
72 check: bool = True) -> Union[bytes, subprocess.CompletedProcess]:
73 """
74 Execute a git command with retry logic and timeout.
76 Args:
77 command: Git command and arguments as list
78 retries: Number of retries attempted so far
79 check: Whether to check return code and raise on error
81 Returns:
82 Command output as bytes or CompletedProcess if check=False
84 Raises:
85 GitCommandError: Base exception for command failures
86 GitNetworkError: When network-related errors occur
87 GitTimeoutError: When command execution times out
88 GitPermissionError: When permission issues occur
89 """
90 try:
91 logger.debug(f"Executing git command: {' '.join(command)}")
92 output = subprocess.check_output(
93 ["git"] + command,
94 cwd=self.repository_path,
95 stderr=subprocess.PIPE,
96 timeout=self.config.timeout
97 )
98 return output
99 except subprocess.TimeoutExpired as e:
100 if not check:
101 return subprocess.CompletedProcess(args=["git"] +
102 command, returncode=1, stdout=b"", stderr=str(e).encode())
104 if retries < self.config.max_retries:
105 logger.warning(f"Git command timed out, retrying in {self.config.retry_delay}s: {e}")
106 time.sleep(self.config.retry_delay)
107 return self.execute(command, retries + 1)
109 raise GitTimeoutError(f"Git command timed out after {self.config.timeout}s: {' '.join(command)}") from e
111 except subprocess.CalledProcessError as e:
112 if not check:
113 return e
115 error_msg = e.stderr.decode('utf-8', errors='replace')
117 # Handle specific error types
118 if any(
119 net_err in error_msg for net_err in [
120 'could not resolve host',
121 'Connection refused',
122 'Connection timed out']):
123 raise GitNetworkError(f"Network error during git operation: {error_msg}") from e
125 if any(perm_err in error_msg for perm_err in ['Permission denied', 'authentication failed']):
126 raise GitPermissionError(f"Permission error during git operation: {error_msg}") from e
128 if retries < self.config.max_retries:
129 logger.warning(f"Git command failed, retrying in {self.config.retry_delay}s: {error_msg}")
130 time.sleep(self.config.retry_delay)
131 return self.execute(command, retries + 1)
133 raise GitCommandError(f"Git command failed: {error_msg}") from e