# --- START OF FILE git_commands.py --- # git_commands.py import os import subprocess import logging # Rimosso import logging import re # Importa il nuovo gestore della coda log import log_handler # --- Custom Exception Definition (invariata) --- class GitCommandError(Exception): """ Custom exception for handling Git command execution errors. Includes the original command and stderr details if available. """ def __init__(self, message, command=None, stderr=None): """Initialize the GitCommandError.""" super().__init__(str(message)) self.command = command self.stderr = stderr if stderr else "" def __str__(self): """Return a formatted string representation including command details.""" base_message = super().__str__() details = [] if self.command: safe_command_parts = [str(part) for part in self.command] command_str = " ".join(safe_command_parts) details.append(f"Command: '{command_str}'") stderr_str = self.stderr.strip() if stderr_str: details.append(f"Stderr: {stderr_str}") if details: details_str = "; ".join(details) return f"{base_message} ({details_str})" else: return base_message # --- Main Git Commands Class --- class GitCommands: """ Manages Git command execution, logging (via log_handler), and error handling. """ # Rimosso logger da __init__ def __init__(self, logger_ignored=None): # Accetta argomento ma lo ignora """Initializes the GitCommands class.""" # Non c'è più self.logger log_handler.log_debug("GitCommands initialized.", func_name="__init__") def log_and_execute( self, command: list, working_directory: str, check: bool = True, # Usa i livelli di logging standard per il parametro log_output_level: int = logging.INFO, # Default a INFO ): """ Executes a shell command, logs details via log_handler, handles errors. Args: command (list): Command and arguments as a list of strings. working_directory (str): The directory to execute the command in. check (bool, optional): If True, raises GitCommandError on non-zero exit. log_output_level (int, optional): Logging level threshold for logging stdout/stderr on SUCCESS. Uses standard logging levels (e.g., logging.INFO, logging.DEBUG). Returns: subprocess.CompletedProcess: Result object. Raises: GitCommandError, ValueError, FileNotFoundError (wrapped), PermissionError (wrapped) """ func_name = "log_and_execute" # Per i log interni a questa funzione safe_command_parts = [str(part) for part in command] command_str = " ".join(safe_command_parts) # Log l'intento di eseguire il comando log_handler.log_debug( f"Executing in '{working_directory}': {command_str}", func_name=func_name ) # --- Validate Working Directory --- if not working_directory: msg = "Working directory cannot be None or empty." log_handler.log_error(msg, func_name=func_name) raise ValueError(msg) if working_directory == ".": effective_cwd = os.getcwd() else: effective_cwd = os.path.abspath(working_directory) if not os.path.isdir(effective_cwd): msg = ( f"Working directory does not exist or is not a dir: {effective_cwd}" ) log_handler.log_error(msg, func_name=func_name) raise GitCommandError(msg, command=safe_command_parts) log_handler.log_debug(f"Effective CWD: {effective_cwd}", func_name=func_name) # --- Execute Command --- try: # Setup per nascondere finestra console su Windows startupinfo = None creationflags = 0 if os.name == "nt": startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE # creationflags = subprocess.CREATE_NO_WINDOW # Timeout diagnostico (mantenuto per ora) timeout_seconds = 30 log_handler.log_debug( f"Setting timeout to {timeout_seconds} seconds.", func_name=func_name ) log_handler.log_debug( f"Attempting subprocess.run for: {command_str}", func_name=func_name ) # Esecuzione comando result = subprocess.run( safe_command_parts, cwd=effective_cwd, capture_output=True, text=True, check=check, encoding="utf-8", errors="replace", timeout=timeout_seconds, startupinfo=startupinfo, creationflags=creationflags, ) log_handler.log_debug( f"subprocess.run finished. RC={result.returncode}", func_name=func_name ) # --- Log Output di Successo --- # Controlla se il comando è considerato "riuscito" (RC=0 o check=False) # e se il livello di log desiderato per l'output è abilitato # (Nota: il livello del logger non è più qui, il filtro avviene nel processore della coda) # Per semplicità, logghiamo l'output qui se RC=0 o check=False # Il processore della coda deciderà se mostrarlo nella GUI. if result.returncode == 0 or not check: # Logga sempre l'output a livello DEBUG per tracciamento stdout_log_debug = ( result.stdout.strip() if result.stdout else "" ) stderr_log_debug = ( result.stderr.strip() if result.stderr else "" ) log_handler.log_debug( f"Command successful (RC={result.returncode}). Output:\n" f"--- stdout ---\n{stdout_log_debug}\n" f"--- stderr ---\n{stderr_log_debug}\n" f"--- End Output ---", func_name=func_name, ) # Logga anche al livello richiesto (es. INFO) se diverso da DEBUG if log_output_level > logging.DEBUG: # Usa lo stesso output già preparato log_handler.log_message( log_output_level, # Usa il livello passato f"Command successful. Output:\n" f"--- stdout ---\n{stdout_log_debug}\n" f"--- stderr ---\n{stderr_log_debug}\n" f"--- End Output ---", func_name=func_name, ) return result # --- Gestione Errori --- except subprocess.TimeoutExpired as e: log_handler.log_error( f"Command timed out after {timeout_seconds}s: {command_str}", func_name=func_name, ) stderr_out = e.stderr.strip() if e.stderr else "" stdout_out = e.stdout.strip() if e.stdout else "" log_handler.log_error(f"Timeout stderr: {stderr_out}", func_name=func_name) log_handler.log_error(f"Timeout stdout: {stdout_out}", func_name=func_name) raise GitCommandError( f"Timeout after {timeout_seconds}s.", command=safe_command_parts, stderr=e.stderr, ) from e except subprocess.CalledProcessError as e: log_handler.log_error( f"CalledProcessError caught. RC={e.returncode}", func_name=func_name ) stderr_err = e.stderr.strip() if e.stderr else "" stdout_err = e.stdout.strip() if e.stdout else "" err_msg = ( f"Command failed (RC {e.returncode}) in '{effective_cwd}'.\n" f"CMD: {command_str}\nSTDERR: {stderr_err}\nSTDOUT: {stdout_err}" ) log_handler.log_error(err_msg, func_name=func_name) raise GitCommandError( f"Git command failed in '{effective_cwd}'.", command=safe_command_parts, stderr=e.stderr, ) from e except FileNotFoundError as e: log_handler.log_error( f"FileNotFoundError for command: {safe_command_parts[0]}", func_name=func_name, ) error_msg = f"Command not found: '{safe_command_parts[0]}'. Is Git installed/in PATH?" log_handler.log_error(error_msg, func_name=func_name) raise GitCommandError(error_msg, command=safe_command_parts) from e except PermissionError as e: log_handler.log_error("PermissionError caught.", func_name=func_name) error_msg = f"Permission denied executing command in '{effective_cwd}'." log_handler.log_error(error_msg, func_name=func_name) raise GitCommandError( error_msg, command=safe_command_parts, stderr=str(e) ) from e except Exception as e: # Usa log_exception per includere traceback (su console) log_handler.log_exception( f"Unexpected Exception executing command: {e}", func_name=func_name ) raise GitCommandError( f"Unexpected execution error: {e}", command=safe_command_parts ) from e # --- Core Repo Operations (usano log_handler) --- def prepare_svn_for_git(self, working_directory: str): func_name = "prepare_svn_for_git" log_handler.log_info( f"Preparing directory for Git: '{working_directory}'", func_name=func_name ) if not working_directory: raise ValueError("Working directory cannot be None or empty.") if not os.path.isdir(working_directory): raise GitCommandError( f"Target directory does not exist: {working_directory}" ) gitignore_path = os.path.join(working_directory, ".gitignore") git_dir_path = os.path.join(working_directory, ".git") svn_ignore = ".svn" if not os.path.exists(git_dir_path): log_handler.log_info( "No existing Git repo found. Initializing...", func_name=func_name ) try: init_cmd = ["git", "init"] self.log_and_execute(init_cmd, working_directory, check=True) log_handler.log_info("Git repo initialized.", func_name=func_name) except (GitCommandError, ValueError) as e: log_handler.log_error(f"Failed init Git repo: {e}", func_name=func_name) raise else: log_handler.log_info( "Existing Git repo found. Skipping init.", func_name=func_name ) log_handler.log_debug( f"Checking/updating .gitignore: {gitignore_path}", func_name=func_name ) try: needs_write = False content = "" write_content = "" if not os.path.exists(gitignore_path): log_handler.log_info( "'.gitignore' not found. Creating with .svn entry.", func_name=func_name, ) write_content = f"{svn_ignore}\n" needs_write = True else: try: with open(gitignore_path, "r", encoding="utf-8") as f: lines = f.readlines() ignored = any( l.strip() == svn_ignore or l.strip().startswith(svn_ignore + "/") for l in lines ) if not ignored: log_handler.log_info( "'.svn' not found in .gitignore. Appending...", func_name=func_name, ) content = "".join(lines) write_content += ( "\n" if not content.endswith("\n") else "" ) + f"{svn_ignore}\n" needs_write = True else: log_handler.log_info( "'.svn' already in .gitignore.", func_name=func_name ) except IOError as r_err: log_handler.log_error( f"Could not read .gitignore: {r_err}", func_name=func_name ) raise IOError(f"Failed read: {r_err}") from r_err if needs_write: mode = ( "a" if os.path.exists(gitignore_path) and write_content.startswith("\n") else "w" ) log_handler.log_debug( f"Writing to {gitignore_path} (mode: {mode})", func_name=func_name ) try: with open( gitignore_path, mode, encoding="utf-8", newline="\n" ) as f: f.write(write_content) log_handler.log_info( f"Updated '{gitignore_path}'.", func_name=func_name ) except IOError as w_err: log_handler.log_error( f"Error writing .gitignore: {w_err}", func_name=func_name ) raise IOError(f"Failed write: {w_err}") from w_err except IOError as io_err: log_handler.log_error( f"IO error for .gitignore: {io_err}", func_name=func_name ) raise except Exception as e: log_handler.log_exception( f"Unexpected error managing .gitignore: {e}", func_name=func_name ) raise GitCommandError(f"Unexpected .gitignore error: {e}") from e log_handler.log_info( f"Directory preparation complete for '{working_directory}'.", func_name=func_name, ) def create_git_bundle(self, working_directory: str, bundle_path: str): func_name = "create_git_bundle" norm_path = os.path.normpath(bundle_path) git_path = norm_path.replace("\\", "/") log_handler.log_info( f"Attempting to create Git bundle: {git_path}", func_name=func_name ) log_handler.log_debug(f"Source repo: {working_directory}", func_name=func_name) command = ["git", "bundle", "create", git_path, "--all"] try: result = self.log_and_execute(command, working_directory, check=False) if result.returncode != 0: stderr_low = result.stderr.lower() if result.stderr else "" if ( "refusing to create empty bundle" in stderr_low or "does not contain any references" in stderr_low ): log_handler.log_warning( f"Bundle creation skipped: Repo '{working_directory}' empty.", func_name=func_name, ) else: err_msg = f"Git bundle command failed (RC {result.returncode})." log_handler.log_error( f"{err_msg} Stderr: {result.stderr.strip()}", func_name=func_name, ) raise GitCommandError( err_msg, command=command, stderr=result.stderr ) else: if not os.path.exists(norm_path) or os.path.getsize(norm_path) == 0: log_handler.log_warning( f"Bundle cmd OK, but file '{norm_path}' missing/empty.", func_name=func_name, ) else: log_handler.log_info( f"Bundle created successfully: '{norm_path}'.", func_name=func_name, ) except Exception as e: log_handler.log_exception( f"Unexpected bundle creation error: {e}", func_name=func_name ) raise GitCommandError( f"Unexpected bundle error: {e}", command=command ) from e def fetch_from_git_bundle(self, working_directory: str, bundle_path: str): func_name = "fetch_from_git_bundle" if not os.path.isfile(bundle_path): raise FileNotFoundError(f"Bundle file not found: {bundle_path}") norm_path = os.path.normpath(bundle_path).replace("\\", "/") log_handler.log_info( f"Fetching from '{norm_path}' into '{working_directory}'", func_name=func_name, ) fetch_cmd = ["git", "fetch", norm_path, "--verbose"] merge_cmd = ["git", "merge", "FETCH_HEAD", "--no-ff", "--no-edit"] try: log_handler.log_debug("Executing fetch...", func_name=func_name) self.log_and_execute(fetch_cmd, working_directory, check=True) log_handler.log_info("Fetch successful.", func_name=func_name) log_handler.log_debug("Executing merge...", func_name=func_name) merge_res = self.log_and_execute(merge_cmd, working_directory, check=False) out_log = merge_res.stdout.strip() if merge_res.stdout else "" err_log = merge_res.stderr.strip() if merge_res.stderr else "" combined_low = (out_log + err_log).lower() if merge_res.returncode == 0: if "already up to date" in combined_low: log_handler.log_info( "Repo already up-to-date.", func_name=func_name ) else: log_handler.log_info("Merge successful.", func_name=func_name) log_handler.log_debug( f"Merge stdout:\n{out_log}", func_name=func_name ) else: if ( "conflict" in combined_low or "automatic merge failed" in combined_low ): msg = f"Merge conflict occurred. Resolve manually in '{working_directory}' and commit." log_handler.log_error(msg, func_name=func_name) raise GitCommandError( msg, command=merge_cmd, stderr=merge_res.stderr ) else: msg = f"Merge command failed (RC {merge_res.returncode})." log_handler.log_error( f"{msg} Stderr: {err_log}", func_name=func_name ) raise GitCommandError( msg, command=merge_cmd, stderr=merge_res.stderr ) except (GitCommandError, ValueError, FileNotFoundError) as e: log_handler.log_error( f"Fetch/merge failed for '{working_directory}': {e}", func_name=func_name, ) raise except Exception as e: log_handler.log_exception( f"Unexpected fetch/merge error for '{working_directory}': {e}", func_name=func_name, ) raise GitCommandError(f"Unexpected fetch/merge error: {e}") from e # --- Commit and Status --- def git_commit(self, working_directory: str, message: str): func_name = "git_commit" log_handler.log_info( f"Attempting commit in '{working_directory}' msg: '{message[:50]}...'", func_name=func_name, ) if not message or message.isspace(): raise ValueError("Commit message empty.") try: add_cmd = ["git", "add", "."] log_handler.log_debug( "Staging all changes ('git add .')...", func_name=func_name ) self.log_and_execute(add_cmd, working_directory, check=True) log_handler.log_debug("Staging successful.", func_name=func_name) commit_cmd = ["git", "commit", "-m", message] log_handler.log_debug("Attempting commit...", func_name=func_name) result = self.log_and_execute(commit_cmd, working_directory, check=False) out_low = result.stdout.lower() if result.stdout else "" err_low = result.stderr.lower() if result.stderr else "" combined_low = out_low + err_low if result.returncode == 0: log_handler.log_info("Commit successful.", func_name=func_name) return True elif ( "nothing to commit" in combined_low or "no changes added to commit" in combined_low or "nothing added to commit" in combined_low ): log_handler.log_info( "No changes available to commit.", func_name=func_name ) return False else: msg = f"Commit command failed unexpectedly (RC {result.returncode})." log_handler.log_error( f"{msg} Stderr: {result.stderr.strip()}", func_name=func_name ) raise GitCommandError(msg, command=commit_cmd, stderr=result.stderr) except (GitCommandError, ValueError) as e: log_handler.log_error( f"Error during staging/commit: {e}", func_name=func_name ) raise def git_status_has_changes(self, working_directory: str): func_name = "git_status_has_changes" log_handler.log_debug( f"Checking status for changes in '{working_directory}'...", func_name=func_name, ) try: cmd = ["git", "status", "--porcelain=v1", "-unormal"] result = self.log_and_execute( cmd, working_directory, check=True, log_output_level=logging.DEBUG ) has_changes = bool(result.stdout.strip()) log_handler.log_debug( f"Status check complete. Has changes: {has_changes}", func_name=func_name, ) return has_changes except GitCommandError as e: log_handler.log_error( f"Git status check command failed: {e}", func_name=func_name ) raise # --- Tag Management --- def list_tags(self, working_directory: str): func_name = "list_tags" log_handler.log_info( f"Listing tags in '{working_directory}'...", func_name=func_name ) fmt = "%(refname:short)%09%(contents:subject)" cmd = ["git", "tag", "--list", f"--format={fmt}", "--sort=-creatordate"] tags = [] try: result = self.log_and_execute(cmd, working_directory, check=True) for line in result.stdout.splitlines(): line_strip = line.strip() if line_strip: parts = line_strip.split("\t", 1) name = parts[0].strip() subject = ( parts[1].strip() if len(parts) > 1 and parts[1].strip() else "(No subject/Lightweight)" ) tags.append((name, subject)) log_handler.log_info(f"Found {len(tags)} tags.", func_name=func_name) log_handler.log_debug(f"Tags found: {tags}", func_name=func_name) return tags except GitCommandError as e: log_handler.log_error(f"Error listing tags: {e}", func_name=func_name) return [] except Exception as e: log_handler.log_exception( f"Unexpected error listing tags: {e}", func_name=func_name ) return [] def create_tag(self, working_directory: str, tag_name: str, message: str): func_name = "create_tag" log_handler.log_info( f"Creating tag '{tag_name}' in '{working_directory}'", func_name=func_name ) if not tag_name or tag_name.isspace(): raise ValueError("Tag name empty.") if not message or message.isspace(): raise ValueError("Tag message empty.") pattern = r"^(?![./]|.*([./]{2,}|[.]$|\.lock$|@\{|\\\\))[^ \t\n\r\f\v~^:?*[\\]+(?