SXXXXXXX_GitUtility/action_handler.py
2025-04-07 14:35:48 +02:00

345 lines
15 KiB
Python

# action_handler.py
import logging
import os
# Import dependencies
from git_commands import GitCommands, GitCommandError
from backup_handler import BackupHandler # To perform backups
class ActionHandler:
"""Handles the execution logic for core application actions."""
def __init__(self, logger, git_commands: GitCommands, backup_handler: BackupHandler):
"""
Initializes the ActionHandler.
Args:
logger (logging.Logger): Logger instance.
git_commands (GitCommands): Instance for executing Git commands.
backup_handler (BackupHandler): Instance for handling backups.
"""
self.logger = logger
self.git_commands = git_commands
self.backup_handler = backup_handler # Store backup handler instance
def _perform_backup_if_enabled(self, svn_path, profile_name,
autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs):
"""
Performs backup if enabled.
Args:
svn_path (str): Path to the repository.
profile_name (str): Name of the profile (for backup naming).
autobackup_enabled (bool): Flag from settings.
backup_base_dir (str): Destination directory for backups.
excluded_extensions (set): Set of file extensions to exclude.
excluded_dirs (set): Set of directory names to exclude.
Returns:
bool: True if backup succeeded or was not needed, False on failure.
"""
if not autobackup_enabled:
self.logger.debug("Autobackup disabled, skipping backup.")
return True # Not an error, just skipped
self.logger.info("Autobackup enabled. Starting backup...")
try:
self.backup_handler.create_zip_backup(
svn_path, backup_base_dir, profile_name,
excluded_extensions, excluded_dirs
)
self.logger.info("Backup completed successfully.")
return True # Indicate backup success
except Exception as backup_e:
# Log error and indicate backup failure
self.logger.error(f"Backup failed: {backup_e}", exc_info=True)
# Let the caller decide how to handle backup failure (e.g., show error)
# For now, just return False
return False
def execute_prepare_repo(self, svn_path):
"""
Executes the 'prepare repository' action.
Args:
svn_path (str): Validated path to the repository.
Returns:
bool: True on success.
Raises:
ValueError: If repository is already prepared.
GitCommandError/IOError/Exception: If preparation fails.
"""
self.logger.info(f"Executing preparation for: {svn_path}")
# Check if already prepared first
git_dir = os.path.join(svn_path, ".git")
if os.path.exists(git_dir):
self.logger.warning("Repository is already prepared.")
# Raise specific error that UI can interpret
raise ValueError("Repository is already prepared.")
# Attempt preparation using GitCommands
try:
self.git_commands.prepare_svn_for_git(svn_path)
self.logger.info("Repository prepared successfully.")
return True
except (GitCommandError, ValueError, IOError) as e:
# Log and re-raise known errors
self.logger.error(f"Failed to prepare repository: {e}")
raise
except Exception as e:
# Log and re-raise unexpected errors
self.logger.exception(f"Unexpected error during preparation: {e}")
raise
def execute_create_bundle(self, svn_path, bundle_full_path, profile_name,
autobackup_enabled, backup_base_dir,
autocommit_enabled, commit_message,
excluded_extensions, excluded_dirs):
"""
Executes 'create bundle', including backup and commit logic.
Args:
svn_path (str): Validated path to repository.
bundle_full_path (str): Validated path for bundle file.
profile_name (str): Current profile name.
autobackup_enabled (bool): Whether to perform backup.
backup_base_dir (str): Validated base directory for backups.
autocommit_enabled (bool): Whether to perform autocommit.
commit_message (str): Commit message from GUI (for autocommit).
excluded_extensions (set): File extensions to exclude from backup.
excluded_dirs (set): Directory names to exclude from backup.
Returns:
str or None: Path to created bundle on success, None if empty/not created.
Raises:
Exception: Relays exceptions from backup, commit, or bundle creation.
"""
# --- Backup Step ---
if autobackup_enabled:
backup_success = self._perform_backup_if_enabled(
svn_path, profile_name, True, backup_base_dir,
excluded_extensions, excluded_dirs
)
if not backup_success:
# Raise specific error if backup fails? Or just log and continue?
# For now, let's raise an error to stop the process.
raise IOError("Autobackup failed. Bundle creation aborted.")
# --- Autocommit Step ---
if autocommit_enabled:
self.logger.info("Autocommit before bundle is enabled.")
try:
has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes:
self.logger.info("Changes detected, performing autocommit...")
commit_msg_to_use = commit_message if commit_message else \
f"Autocommit '{profile_name}' before bundle"
self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'")
# Perform commit (logs success/nothing internally)
self.git_commands.git_commit(svn_path, commit_msg_to_use)
else:
self.logger.info("No changes detected for autocommit.")
except Exception as commit_e:
# Let caller handle commit errors
self.logger.error(f"Autocommit failed: {commit_e}", exc_info=True)
raise commit_e # Re-raise to abort bundle creation
# --- Create Bundle Step ---
self.logger.info(f"Creating bundle file: {bundle_full_path}")
try:
# Execute command via GitCommands
self.git_commands.create_git_bundle(svn_path, bundle_full_path)
# Check result after command execution
bundle_exists = os.path.exists(bundle_full_path)
bundle_not_empty = bundle_exists and os.path.getsize(bundle_full_path) > 0
if bundle_exists and bundle_not_empty:
self.logger.info("Git bundle created successfully.")
return bundle_full_path # Return path on success
else:
# Bundle empty or not created (logged by GitCommands)
self.logger.warning("Bundle file not created or is empty.")
if bundle_exists and not bundle_not_empty:
try: os.remove(bundle_full_path) # Clean up
except OSError: pass
return None # Indicate non-fatal issue (empty bundle)
except Exception as bundle_e:
# Let caller handle bundle creation errors
self.logger.error(f"Bundle creation failed: {bundle_e}", exc_info=True)
raise bundle_e
def execute_fetch_bundle(self, svn_path, bundle_full_path, profile_name,
autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs):
"""
Executes the 'fetch bundle' action, including backup logic.
Args: See execute_create_bundle args, excluding commit related ones.
Raises:
Exception: Relays exceptions from backup or fetch/merge operations.
"""
# --- Backup Step ---
if autobackup_enabled:
backup_success = self._perform_backup_if_enabled(
svn_path, profile_name, True, backup_base_dir,
excluded_extensions, excluded_dirs
)
if not backup_success:
raise IOError("Autobackup failed. Fetch operation aborted.")
# --- Fetch and Merge Step ---
self.logger.info(f"Fetching into '{svn_path}' from: {bundle_full_path}")
try:
# Execute fetch/merge via GitCommands
# This method might raise GitCommandError for conflicts
self.git_commands.fetch_from_git_bundle(svn_path, bundle_full_path)
self.logger.info("Fetch/merge process completed.")
# Return True or some status? For now, rely on exceptions for errors.
except Exception as fetch_e:
# Let caller handle fetch/merge errors (incl. conflicts)
self.logger.error(f"Fetch/merge failed: {fetch_e}", exc_info=True)
raise fetch_e
def execute_manual_commit(self, svn_path, commit_message):
"""
Executes a manual commit with the provided message.
Args:
svn_path (str): Validated path to the repository.
commit_message (str): The commit message (checked not empty by caller).
Returns:
bool: True if a commit was made, False if no changes were committed.
Raises:
GitCommandError: If the commit command fails.
Exception: For other unexpected errors.
"""
if not commit_message:
# This validation should ideally happen before calling this method
self.logger.error("Manual commit attempted with empty message.")
raise ValueError("Commit message cannot be empty.")
self.logger.info(f"Executing manual commit for: {svn_path}")
try:
# git_commit handles staging and committing
commit_made = self.git_commands.git_commit(svn_path, commit_message)
if commit_made:
self.logger.info("Manual commit successful.")
else:
self.logger.info("Manual commit: Nothing to commit.")
return commit_made # Return status
except Exception as e:
self.logger.error(f"Manual commit failed: {e}", exc_info=True)
raise # Re-raise for caller to handle
def execute_create_tag(self, svn_path, commit_message, tag_name, tag_message):
"""
Executes tag creation, including pre-commit using commit_message if needed.
Args: See execute_manual_commit and add tag_name, tag_message.
Returns:
bool: True on successful tag creation.
Raises:
ValueError: If validation fails (e.g., changes exist but no commit message).
GitCommandError: If commit or tag command fails.
Exception: For unexpected errors.
"""
self.logger.info(f"Executing create tag '{tag_name}' for: {svn_path}")
# --- Pre-commit Step (only if changes exist AND message provided) ---
try:
has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes:
self.logger.info("Uncommitted changes detected before tagging.")
if not commit_message:
# Block if changes exist but no message provided
msg = "Changes exist. Commit message required before tagging."
self.logger.error(f"Tag creation blocked: {msg}")
raise ValueError(msg)
# Perform the pre-tag commit with the provided message
self.logger.debug(f"Performing pre-tag commit: '{commit_message}'")
self.git_commands.git_commit(svn_path, commit_message)
# Log success/nothing based on return value if needed
self.logger.info("Pre-tag commit attempt finished.")
else:
self.logger.info("No uncommitted changes detected.")
except Exception as e:
# Catch errors during status check or commit
self.logger.error(f"Error during pre-tag commit step: {e}", exc_info=True)
raise # Re-raise commit-related errors
# --- Create Tag Step ---
self.logger.info(f"Proceeding to create tag '{tag_name}'...")
try:
# git_commands.create_tag handles validation and execution
self.git_commands.create_tag(svn_path, tag_name, tag_message)
self.logger.info(f"Tag '{tag_name}' created successfully.")
return True # Indicate success
except Exception as e:
# Catch errors during tag creation (e.g., exists, invalid name)
self.logger.error(f"Failed to create tag '{tag_name}': {e}", exc_info=True)
raise # Re-raise tag creation errors
def execute_checkout_tag(self, svn_path, tag_name):
"""
Executes checkout for the specified tag after checking for changes.
Args:
svn_path (str): Validated path to repository.
tag_name (str): The tag name to check out (already validated).
Returns:
bool: True on successful checkout.
Raises:
ValueError: If uncommitted changes exist.
GitCommandError: If status check or checkout command fails.
Exception: For unexpected errors.
"""
if not tag_name:
# Should be validated by caller, but double-check
raise ValueError("Tag name required for checkout.")
self.logger.info(f"Executing checkout for tag '{tag_name}' in: {svn_path}")
# --- Check for Uncommitted Changes ---
try:
has_changes = self.git_commands.git_status_has_changes(svn_path)
if has_changes:
self.logger.error("Checkout blocked: Uncommitted changes exist.")
# Raise specific error for UI to handle clearly
raise ValueError("Uncommitted changes exist. Commit or stash first.")
self.logger.debug("No uncommitted changes found.")
except Exception as e:
# Catch errors during status check
self.logger.error(f"Error checking status before checkout: {e}",
exc_info=True)
raise # Re-raise status check errors
# --- Execute Checkout ---
try:
checkout_success = self.git_commands.checkout_tag(svn_path, tag_name)
# git_commands.checkout_tag raises error on failure
if checkout_success:
self.logger.info(f"Tag '{tag_name}' checked out successfully.")
return True
else:
# This path should theoretically not be reached if check=True used
self.logger.error("Checkout command reported failure unexpectedly.")
# Raise generic error if this happens?
raise GitCommandError(f"Checkout failed for '{tag_name}' for unknown reasons.")
except Exception as e:
self.logger.error(f"Failed to checkout tag '{tag_name}': {e}", exc_info=True)
raise # Re-raise checkout errors