# GitUtility.py import os # Rimosso shutil se non usato altrove import datetime import tkinter as tk from tkinter import messagebox, filedialog # Assicurati che filedialog sia importato import logging import re # Rimosso zipfile, threading, queue # Import application modules try: from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR # ActionHandler e BackupHandler sono necessari from action_handler import ActionHandler from backup_handler import BackupHandler from git_commands import GitCommands, GitCommandError from logger_config import setup_logger # Import GUI classes (SENZA WaitWindow) from gui import ( MainFrame, GitignoreEditorWindow, CreateTagDialog, CreateBranchDialog, # Rimuovi WaitWindow se importata precedentemente ) except ImportError as e: logging.basicConfig(level=logging.CRITICAL) logging.critical( f"Critical Error: Failed to import required application modules: {e}", exc_info=True, ) print(f"FATAL: Failed to import required application modules: {e}") exit(1) class GitSvnSyncApp: """ Main application class for the Git SVN Sync Tool. Coordinates the GUI, configuration, and delegates actions to ActionHandler. (Versione Sincrona - Senza Threading/WaitWindow) """ def __init__(self, master): """ Initializes the GitSvnSyncApp. Args: master (tk.Tk): The main Tkinter root window. """ self.master = master master.title("Git SVN Sync Tool") master.protocol("WM_DELETE_WINDOW", self.on_closing) # Basic logging setup first logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) self.logger = logging.getLogger(self.__class__.__name__) # Rimuovi queue - non serve più # self.result_queue = queue.Queue() # --- Initialize Core Components --- try: self.config_manager = ConfigManager(self.logger) self.git_commands = GitCommands(self.logger) self.backup_handler = BackupHandler(self.logger) # Istanza necessaria # Istanzia ActionHandler passando le dipendenze self.action_handler = ActionHandler( self.logger, self.git_commands, self.backup_handler ) except Exception as e: self.logger.critical( f"Failed to initialize core components: {e}", exc_info=True ) self.show_fatal_error( f"Initialization Error:\n{e}\n\nApplication cannot start." ) if master and master.winfo_exists(): master.destroy() return # --- Initialize GUI (MainFrame) --- try: # Assicurati che le callback puntino ai metodi corretti (ora sincroni) self.main_frame = MainFrame( master, load_profile_settings_cb=self.load_profile_settings, browse_folder_cb=self.browse_folder, update_svn_status_cb=self.update_svn_status_indicator, prepare_svn_for_git_cb=self.prepare_svn_for_git, # Chiama metodo sincrono create_git_bundle_cb=self.create_git_bundle, # Chiama metodo sincrono fetch_from_git_bundle_cb=self.fetch_from_git_bundle, # Chiama metodo sincrono open_gitignore_editor_cb=self.open_gitignore_editor, manual_backup_cb=self.manual_backup, # Chiama metodo sincrono config_manager_instance=self.config_manager, profile_sections_list=self.config_manager.get_profile_sections(), add_profile_cb=self.add_profile, remove_profile_cb=self.remove_profile, save_profile_cb=self.save_profile_settings, commit_changes_cb=self.commit_changes, # Chiama metodo sincrono refresh_tags_cb=self.refresh_tag_list, # Chiama metodo sincrono create_tag_cb=self.create_tag, # Chiama metodo sincrono checkout_tag_cb=self.checkout_tag, # Chiama metodo sincrono refresh_branches_cb=self.refresh_branch_list, # Chiama metodo sincrono checkout_branch_cb=self.checkout_branch, # Chiama metodo sincrono create_branch_cb=self.create_branch, # Chiama metodo sincrono refresh_history_cb=self.refresh_commit_history, # Chiama metodo sincrono ) # --- GESTIONE TRACE INIZIALE (Come corretto precedentemente) --- trace_info = self.main_frame.profile_var.trace_info() write_trace_callback_info = None if trace_info: for info in trace_info: if "write" in info[1]: try: # trace_vinfo might fail if var destroyed write_trace_callback_info = ( self.main_frame.profile_var.trace_vinfo()[0] ) self.logger.debug( f"Temporarily removing 'write' trace: {write_trace_callback_info}" ) self.main_frame.profile_var.trace_vdelete( *write_trace_callback_info ) except tk.TclError: write_trace_callback_info = None break #self._initialize_profile_selection() self._perform_initial_load() # Carica dati iniziali ORA if write_trace_callback_info: self.logger.debug(f"Re-adding 'write' trace.") self.main_frame.profile_var.trace_add( "write", lambda n, i, m: self.load_profile_settings( self.main_frame.profile_var.get() ), ) # --- FINE GESTIONE TRACE --- except Exception as e: self.logger.critical( f"Failed to initialize MainFrame GUI: {e}", exc_info=True ) self.show_fatal_error( f"GUI Initialization Error:\n{e}\n\nApplication cannot start." ) if master and master.winfo_exists(): master.destroy() return # --- Finalize Logger Setup --- try: if hasattr(self.main_frame, "log_text"): self.logger = setup_logger(self.main_frame.log_text) self.config_manager.logger = self.logger self.git_commands.logger = self.logger self.backup_handler.logger = self.logger self.action_handler.logger = self.logger self.logger.info("Logger setup complete with GUI handler.") else: self.logger.error("GUI log text widget not found.") except Exception as log_e: self.logger.error( f"Error setting up logger with GUI TextHandler: {log_e}", exc_info=True ) self.logger.info("Git SVN Sync Tool initialization sequence complete.") def _perform_initial_load(self): """Loads the initially selected profile and updates UI state.""" # (Come definito precedentemente) self.logger.debug("Performing initial profile load and UI update.") initial_profile = self.main_frame.profile_var.get() if initial_profile: self.load_profile_settings(initial_profile) else: self.logger.warning("No initial profile set during initial load.") self._clear_and_disable_fields() #self.logger.info("Application started and initial state set.") def _handle_gitignore_save(self): """ Callback function triggered after .gitignore is saved successfully. Initiates the process to untrack newly ignored files. """ self.logger.info("Callback triggered: .gitignore saved. Checking for files to untrack automatically...") # Need the svn_path again here svn_path = self._get_and_validate_svn_path("Automatic Untracking") if not svn_path: self.logger.error("Cannot perform automatic untracking: Invalid SVN path after save.") # Show error? This shouldn't happen if editor opened correctly. return try: # Call the ActionHandler method untracked = self.action_handler.execute_untrack_files_from_gitignore(svn_path) if untracked: self.main_frame.show_info( "Automatic Untrack", "Successfully untracked newly ignored files and created commit.\nCheck log for details." ) # UI might need refreshing after commit - handled after window closes in open_gitignore_editor else: # No files needed untracking, or commit failed (ActionHandler logs warnings) self.logger.info("Automatic untracking check complete. No files untracked or no commit needed.") except (GitCommandError, ValueError) as e: self.logger.error(f"Automatic untracking failed: {e}", exc_info=True) self.main_frame.show_error("Untrack Error", f"Failed automatic untracking:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error during automatic untracking: {e}") self.main_frame.show_error("Untrack Error", f"Unexpected error during untracking:\n{e}") def on_closing(self): """Handles the window close event.""" # (Come definito precedentemente) self.logger.info("Application closing.") if self.master and self.master.winfo_exists(): self.master.destroy() # --- Profile Management Callbacks --- # (load_profile_settings, save_profile_settings, add_profile, remove_profile) # Nessuna modifica necessaria rispetto alla versione precedente (Sezione 2) # Assicurarsi che carichino/salvino self.main_frame.backup_exclude_dirs_var.get() def load_profile_settings(self, profile_name): """Loads settings for the selected profile into the GUI.""" self.logger.info(f"Loading settings for profile: '{profile_name}'") if ( not profile_name or profile_name not in self.config_manager.get_profile_sections() ): self.logger.warning(f"Profile '{profile_name}' invalid or not found.") self._clear_and_disable_fields() if profile_name: self.main_frame.show_error( "Profile Error", f"Profile '{profile_name}' not found." ) return cm = self.config_manager expected_keys = cm._get_expected_keys_with_defaults() settings = {} for key, default in expected_keys.items(): settings[key] = cm.get_profile_option(profile_name, key, fallback=default) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return mf = self.main_frame try: # Repo Tab mf.svn_path_entry.delete(0, tk.END) mf.svn_path_entry.insert(0, settings.get("svn_working_copy_path", "")) mf.usb_path_entry.delete(0, tk.END) mf.usb_path_entry.insert(0, settings.get("usb_drive_path", "")) mf.bundle_name_entry.delete(0, tk.END) mf.bundle_name_entry.insert(0, settings.get("bundle_name", "")) mf.bundle_updated_name_entry.delete(0, tk.END) mf.bundle_updated_name_entry.insert( 0, settings.get("bundle_name_updated", "") ) # Backup Tab mf.autobackup_var.set( str(settings.get("autobackup", "False")).lower() == "true" ) mf.backup_dir_var.set(settings.get("backup_dir", DEFAULT_BACKUP_DIR)) mf.backup_exclude_extensions_var.set( settings.get("backup_exclude_extensions", "") ) mf.backup_exclude_dirs_var.set( settings.get("backup_exclude_dirs", "") ) # <<< Carica dirs mf.toggle_backup_dir() # Commit Tab mf.autocommit_var.set( str(settings.get("autocommit", "False")).lower() == "true" ) mf.clear_commit_message() if ( hasattr(mf, "commit_message_text") and mf.commit_message_text.winfo_exists() ): if mf.commit_message_text.cget("state") == tk.DISABLED: mf.commit_message_text.config(state=tk.NORMAL) mf.commit_message_text.insert("1.0", settings.get("commit_message", "")) self.logger.info(f"Successfully loaded settings for '{profile_name}'.") # Post-Load Updates svn_path_loaded = settings.get("svn_working_copy_path", "") self.update_svn_status_indicator(svn_path_loaded) repo_is_ready = self._is_repo_ready(svn_path_loaded) if repo_is_ready: self.refresh_tag_list() self.refresh_branch_list() self.refresh_commit_history() else: if hasattr(mf, "update_tag_list"): mf.update_tag_list([]) if hasattr(mf, "update_branch_list"): mf.update_branch_list([], None) if hasattr(mf, "update_history_display"): mf.update_history_display([]) if hasattr(mf, "update_history_branch_filter"): mf.update_history_branch_filter([]) except Exception as e: self.logger.error( f"Error applying loaded settings for '{profile_name}': {e}", exc_info=True, ) self.main_frame.show_error("Load Error", f"Failed to apply settings:\n{e}") def save_profile_settings(self): """Saves current GUI values to the selected profile.""" profile_name = self.main_frame.profile_var.get() if not profile_name: self.logger.warning("Save failed: No profile selected.") return False self.logger.info(f"Saving settings for profile: '{profile_name}'") if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return False mf = self.main_frame cm = self.config_manager try: settings_to_save = { "svn_working_copy_path": mf.svn_path_entry.get(), "usb_drive_path": mf.usb_path_entry.get(), "bundle_name": mf.bundle_name_entry.get(), "bundle_name_updated": mf.bundle_updated_name_entry.get(), "autocommit": str(mf.autocommit_var.get()), "commit_message": mf.get_commit_message(), "autobackup": str(mf.autobackup_var.get()), "backup_dir": mf.backup_dir_var.get(), "backup_exclude_extensions": mf.backup_exclude_extensions_var.get(), "backup_exclude_dirs": mf.backup_exclude_dirs_var.get(), # <<< Salva dirs } for key, value in settings_to_save.items(): cm.set_profile_option(profile_name, key, value) cm.save_config() self.logger.info(f"Settings saved for '{profile_name}'.") return True except Exception as e: self.logger.error(f"Error saving '{profile_name}': {e}", exc_info=True) self.main_frame.show_error("Save Error", f"Failed save:\n{e}") return False def add_profile(self): """Handles adding a new profile.""" # (Nessuna modifica necessaria rispetto alla versione precedente) self.logger.debug("'Add Profile' clicked.") name = self.main_frame.ask_new_profile_name() if not name: self.logger.info("Add cancelled.") return name = name.strip() if not name: self.logger.warning("Empty name.") self.main_frame.show_error("Error", "Name empty.") return if name in self.config_manager.get_profile_sections(): self.logger.warning(f"Exists: '{name}'") self.main_frame.show_error("Error", f"'{name}' exists.") return self.logger.info(f"Adding profile: '{name}'") try: defaults = self.config_manager._get_expected_keys_with_defaults() defaults["bundle_name"] = f"{name}_repo.bundle" defaults["bundle_name_updated"] = f"{name}_update.bundle" defaults["svn_working_copy_path"] = "" defaults["usb_drive_path"] = "" for k, v in defaults.items(): self.config_manager.set_profile_option(name, k, v) self.config_manager.save_config() sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) self.main_frame.profile_var.set(name) self.logger.info(f"Profile '{name}' added.") except Exception as e: self.logger.error(f"Error adding '{name}': {e}", exc_info=True) self.main_frame.show_error("Error", f"Failed add:\n{e}") def remove_profile(self): """Handles removing the selected profile.""" # (Nessuna modifica necessaria rispetto alla versione precedente) self.logger.debug("'Remove Profile' clicked.") profile = self.main_frame.profile_var.get() if not profile: self.logger.warning("No profile.") self.main_frame.show_error("Error", "No profile.") return if profile == DEFAULT_PROFILE: self.logger.warning("Cannot remove default.") self.main_frame.show_error("Error", f"Cannot remove '{DEFAULT_PROFILE}'.") return if self.main_frame.ask_yes_no("Remove Profile", f"Remove '{profile}'?"): self.logger.info(f"Removing: '{profile}'") try: if self.config_manager.remove_profile_section(profile): self.config_manager.save_config() self.logger.info("Removed.") sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) else: self.main_frame.show_error("Error", "Failed remove.") except Exception as e: self.logger.error(f"Error removing: {e}", exc_info=True) self.main_frame.show_error("Error", f"Error removing:\n{e}") else: self.logger.info("Removal cancelled.") # --- GUI Interaction & Helper Methods --- # (browse_folder, update_svn_status_indicator, _is_repo_ready, # _get_and_validate_svn_path, _get_and_validate_usb_path, open_gitignore_editor) # Mantenere le versioni precedenti (Sezione 3) - NON servono modifiche qui per questa richiesta def browse_folder(self, entry_widget): """Opens folder dialog to update an entry widget.""" current = entry_widget.get() initial = current if os.path.isdir(current) else os.path.expanduser("~") directory = filedialog.askdirectory( initialdir=initial, title="Select Directory", parent=self.master ) if directory: entry_widget.delete(0, tk.END) entry_widget.insert(0, directory) if ( hasattr(self.main_frame, "svn_path_entry") and entry_widget == self.main_frame.svn_path_entry ): self.update_svn_status_indicator(directory) def update_svn_status_indicator(self, svn_path): """Checks repo status and updates dependent GUI widgets. Allows 'Fetch from Bundle' if target dir is valid and bundle file exists, even if the target is not yet a Git repository.""" # (Mantenere versione precedente robusta) is_valid_dir = bool(svn_path and os.path.isdir(svn_path)) # is_repo_ready checks specifically for the presence of '.git' is_repo_ready = is_valid_dir and os.path.exists(os.path.join(svn_path, ".git")) self.logger.debug( f"Updating status for '{svn_path}'. ValidDir:{is_valid_dir}, RepoReady:{is_repo_ready}" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return mf = self.main_frame # Update indicator color based on whether it's a prepared Git repo mf.update_svn_indicator(is_repo_ready) # Indicator shows repo readiness # Determine state for most buttons: NORMAL if repo ready, DISABLED otherwise repo_ready_state = tk.NORMAL if is_repo_ready else tk.DISABLED # Determine state for buttons needing only a valid directory path valid_path_state = tk.NORMAL if is_valid_dir else tk.DISABLED # --- MODIFICA: Logica specifica per abilitare Fetch Button --- fetch_button_state = tk.DISABLED # Default to disabled try: # Check conditions needed to potentially *clone* from bundle svn_path_str = mf.svn_path_entry.get().strip() usb_path_str = mf.usb_path_entry.get().strip() bundle_fetch_name = mf.bundle_updated_name_entry.get().strip() # Condition 1: SVN Path validity (must exist or parent must exist) can_create_svn_dir = False if os.path.isdir(svn_path_str): can_create_svn_dir = True # Directory exists else: parent_dir = os.path.dirname(svn_path_str) if parent_dir and os.path.isdir(parent_dir): can_create_svn_dir = True # Parent exists, can create subdir # Condition 2: USB Path must be a valid directory is_valid_usb_dir = os.path.isdir(usb_path_str) # Condition 3: Fetch bundle name must be provided has_bundle_name = bool(bundle_fetch_name) # Condition 4: Fetch bundle file must exist bundle_file_exists = False if is_valid_usb_dir and has_bundle_name: bundle_full_path = os.path.join(usb_path_str, bundle_fetch_name) bundle_file_exists = os.path.isfile(bundle_full_path) self.logger.debug(f"Checking fetch bundle file: '{bundle_full_path}' - Exists: {bundle_file_exists}") # Enable Fetch button if: # - EITHER the repo is already prepared (standard fetch/merge) # - OR the conditions for cloning from bundle are met if is_repo_ready or (can_create_svn_dir and is_valid_usb_dir and has_bundle_name and bundle_file_exists): fetch_button_state = tk.NORMAL else: # Log why fetch is disabled if not obvious if not is_repo_ready: self.logger.debug(f"Fetch disabled: Repo not ready and clone conditions not met " f"(can_create_svn:{can_create_svn_dir}, valid_usb:{is_valid_usb_dir}, " f"has_name:{has_bundle_name}, file_exists:{bundle_file_exists})") except Exception as e_check: self.logger.error(f"Error checking conditions for Fetch button state: {e_check}", exc_info=True) fetch_button_state = tk.DISABLED # Disable on error # Set state for the Fetch button if hasattr(mf, "fetch_bundle_button"): mf.fetch_bundle_button.config(state=fetch_button_state) # --- FINE MODIFICA --- # Update state for other buttons based on original logic try: if hasattr(mf, "edit_gitignore_button"): # Edit gitignore should arguably only work if .git exists too, # or be smarter about creating it? Let's tie it to repo_ready for now. mf.edit_gitignore_button.config(state=repo_ready_state) # CHANGED from valid_path_state if hasattr(mf, "create_bundle_button"): mf.create_bundle_button.config(state=repo_ready_state) if hasattr(mf, "manual_backup_button"): mf.manual_backup_button.config( state=valid_path_state # Backup needs only a valid source dir ) if hasattr(mf, "autocommit_checkbox"): mf.autocommit_checkbox.config(state=repo_ready_state) if hasattr(mf, "commit_message_text"): # Commit message text state might depend on autocommit checkbox as well? # Keeping it simple: enable if repo is ready mf.commit_message_text.config(state=repo_ready_state) if hasattr(mf, "commit_button"): mf.commit_button.config(state=repo_ready_state) # Tag/Branch/History buttons require a ready repo if hasattr(mf, "refresh_tags_button"): mf.refresh_tags_button.config(state=repo_ready_state) if hasattr(mf, "create_tag_button"): mf.create_tag_button.config(state=repo_ready_state) if hasattr(mf, "checkout_tag_button"): mf.checkout_tag_button.config(state=repo_ready_state) if hasattr(mf, "refresh_branches_button"): mf.refresh_branches_button.config(state=repo_ready_state) if hasattr(mf, "create_branch_button"): mf.create_branch_button.config(state=repo_ready_state) if hasattr(mf, "checkout_branch_button"): mf.checkout_branch_button.config(state=repo_ready_state) if hasattr(mf, "refresh_history_button"): mf.refresh_history_button.config(state=repo_ready_state) if hasattr(mf, "history_branch_filter_combo"): mf.history_branch_filter_combo.config( state="readonly" if is_repo_ready else tk.DISABLED ) if hasattr(mf, "history_text"): # History text area state should reflect button state state_hist_text = tk.NORMAL if repo_ready_state == tk.NORMAL else tk.DISABLED # Need to check if it's currently DISABLED before changing to NORMAL temporarily to clear/update # This part is handled within update_history_display now. # Just ensure the conceptual link: history area usable == repo ready. pass # No direct state change needed here, handled by update method # Prepare button state is inverse of repo_ready if hasattr(mf, "prepare_svn_button"): prepare_state = tk.DISABLED if is_repo_ready else valid_path_state # Can prepare if path valid & not ready mf.prepare_svn_button.config(state=prepare_state) except Exception as e: self.logger.error(f"Error updating widget states: {e}", exc_info=True) def _is_repo_ready(self, repo_path): """Helper function to check if a given path points to a valid, prepared Git repo.""" return bool( repo_path and os.path.isdir(repo_path) and os.path.exists(os.path.join(repo_path, ".git")) ) def _parse_exclusions(self): """ Parses exclusion strings (extensions and directories) from the GUI variables. Returns: tuple: (excluded_extensions_set, excluded_dirs_set) Both sets contain lowercase strings. excluded_dirs_set includes default (.git, .svn) and custom dirs. """ # (Mantenere versione precedente robusta) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return set(), {".git", ".svn"} mf = self.main_frame extensions_set = set() dirs_set = {".git", ".svn"} # Start with defaults # Parse Extensions exclude_ext_str = mf.backup_exclude_extensions_var.get() if exclude_ext_str: for ext in exclude_ext_str.split(","): clean_ext = ext.strip().lower() if clean_ext: extensions_set.add( "." + clean_ext if not clean_ext.startswith(".") else clean_ext ) # Parse Directories exclude_dirs_str = mf.backup_exclude_dirs_var.get() # <<< Legge dalla nuova var if exclude_dirs_str: for dir_name in exclude_dirs_str.split(","): clean_dir = dir_name.strip().lower().strip("/\\") if clean_dir and clean_dir not in (".", ".."): dirs_set.add(clean_dir) self.logger.debug( f"Parsed Exclusions - Exts: {extensions_set}, Dirs: {dirs_set}" ) return extensions_set, dirs_set def _get_and_validate_svn_path(self, operation_name="Operation"): """Retrieves and validates the SVN Working Copy Path from the GUI.""" # (Mantenere versione precedente robusta) if not hasattr(self, "main_frame") or not hasattr( self.main_frame, "svn_path_entry" ): return None svn_path_str = self.main_frame.svn_path_entry.get().strip() if not svn_path_str: self.main_frame.show_error( "Input Error", "SVN Working Copy Path cannot be empty." ) return None abs_path = os.path.abspath(svn_path_str) if not os.path.isdir(abs_path): self.main_frame.show_error( "Input Error", f"Invalid SVN path (not a directory):\n{abs_path}" ) return None self.logger.debug(f"{operation_name}: Using validated SVN path: {abs_path}") return abs_path def _get_and_validate_usb_path(self, operation_name="Operation"): """Retrieves and validates the Bundle Target Directory path from the GUI.""" # (Mantenere versione precedente robusta) if not hasattr(self, "main_frame") or not hasattr( self.main_frame, "usb_path_entry" ): return None usb_path_str = self.main_frame.usb_path_entry.get().strip() if not usb_path_str: self.main_frame.show_error( "Input Error", "Bundle Target Directory path cannot be empty." ) return None abs_path = os.path.abspath(usb_path_str) if not os.path.isdir(abs_path): self.main_frame.show_error( "Input Error", f"Invalid Bundle Target path (not a directory):\n{abs_path}", ) return None self.logger.debug( f"{operation_name}: Using validated Bundle Target path: {abs_path}" ) return abs_path def open_gitignore_editor(self): """Opens the modal editor window for the .gitignore file and triggers automatic untracking check on successful save.""" self.logger.info("--- Action: Edit .gitignore ---") svn_path = self._get_and_validate_svn_path("Edit .gitignore") if not svn_path: return gitignore_path = os.path.join(svn_path, ".gitignore") self.logger.debug(f"Target .gitignore path: {gitignore_path}") try: self.logger.debug("Opening GitignoreEditorWindow...") # --- MODIFICA: Passa il metodo _handle_gitignore_save come callback --- GitignoreEditorWindow( self.master, gitignore_path, self.logger, on_save_success_callback=self._handle_gitignore_save # Passa il riferimento al metodo ) # --- FINE MODIFICA --- # Execution waits here until the Toplevel window is closed self.logger.debug("Gitignore editor window closed.") # Note: The untracking logic is now triggered *by* the callback *before* the window closes. # We might still want to refresh UI elements *after* it closes. # Refresh status indicator and potentially history/branches after editor closes self.update_svn_status_indicator(svn_path) self.refresh_commit_history() self.refresh_branch_list() # Commit might affect branch display except Exception as e: self.logger.exception(f"Error during .gitignore editing or post-save action: {e}") self.main_frame.show_error("Editor Error", f"An error occurred:\n{e}") # --- Threading Helpers (REMOVED) --- # Rimuovi _run_action_with_wait # Rimuovi _check_thread_status # --- Action Callbacks (Modified to be Synchronous and pass exclusions) --- def prepare_svn_for_git(self): """Handles the 'Prepare SVN Repo' action (Synchronous).""" self.logger.info("--- Action Triggered: Prepare Repo ---") # Validazione input svn_path = self._get_and_validate_svn_path("Prepare Repository") if not svn_path: return # Esecuzione sincrona try: # Chiamata diretta a ActionHandler self.action_handler.execute_prepare_repo(svn_path) self.main_frame.show_info("Success", "Repository prepared successfully.") except (ValueError, GitCommandError, IOError) as e: self.logger.error(f"Prepare Repo failed: {e}", exc_info=True) # Mostra errore specifico (ValueError è spesso per 'già preparato') if isinstance(e, ValueError): self.main_frame.show_warning("Prepare Info", f"{e}") else: self.main_frame.show_error( "Prepare Error", f"Failed to prepare repository:\n{e}" ) except Exception as e: self.logger.exception(f"Unexpected error during Prepare Repo: {e}") self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) finally: # Aggiorna stato UI in ogni caso dopo l'operazione self.update_svn_status_indicator(svn_path) def create_git_bundle(self): """Handles the 'Create Bundle' action (Synchronous).""" self.logger.info("--- Action Triggered: Create Bundle ---") # Validazione Input e Preparazione Argomenti profile = self.main_frame.profile_var.get() if not profile: self.main_frame.show_error("Error", "No profile selected.") return svn_path = self._get_and_validate_svn_path("Create Bundle") if not svn_path: return usb_path = self._get_and_validate_usb_path("Create Bundle") if not usb_path: return bundle_name = self.main_frame.bundle_name_entry.get().strip() if not bundle_name: self.main_frame.show_error("Input Error", "Bundle filename empty.") return if not bundle_name.lower().endswith(".bundle"): bundle_name += ".bundle" self.main_frame.bundle_name_entry.delete(0, tk.END) self.main_frame.bundle_name_entry.insert(0, bundle_name) bundle_full_path = os.path.join(usb_path, bundle_name) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): return # --- MODIFICA: Parse exclusions --- excluded_extensions, excluded_dirs = self._parse_exclusions() # --- FINE MODIFICA --- autobackup_enabled = self.main_frame.autobackup_var.get() backup_base_dir = self.main_frame.backup_dir_var.get() autocommit_enabled = self.main_frame.autocommit_var.get() commit_message = self.main_frame.get_commit_message() # Esecuzione sincrona try: # --- MODIFICA: Passa i set parsati a ActionHandler --- bundle_path_result = self.action_handler.execute_create_bundle( svn_path, bundle_full_path, profile, autobackup_enabled, backup_base_dir, autocommit_enabled, commit_message, excluded_extensions, excluded_dirs, # Passa i set qui ) # --- FINE MODIFICA --- if bundle_path_result: self.main_frame.show_info( "Bundle Created", f"Bundle created:\n{bundle_path_result}" ) else: self.main_frame.show_info( "Bundle Info", "Bundle creation finished, but no file generated (repo empty?).", ) except (IOError, GitCommandError, ValueError) as e: self.logger.error(f"Create Bundle failed: {e}", exc_info=True) self.main_frame.show_error("Create Bundle Error", f"Operation failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error during Create Bundle: {e}") self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) def fetch_from_git_bundle(self): """Handles the 'Fetch from Bundle' action (Synchronous).""" self.logger.info("--- Action Triggered: Fetch from Bundle ---") # Validazione Input e Preparazione Argomenti profile = self.main_frame.profile_var.get() if not profile: self.main_frame.show_error("Error", "No profile selected.") return svn_path = self._get_and_validate_svn_path("Fetch from Bundle") if not svn_path: return usb_path = self._get_and_validate_usb_path("Fetch from Bundle") if not usb_path: return bundle_name = self.main_frame.bundle_updated_name_entry.get().strip() if not bundle_name: self.main_frame.show_error("Input Error", "Fetch bundle filename empty.") return bundle_full_path = os.path.join(usb_path, bundle_name) # Controllo file spostato in ActionHandler, ma potrebbe rimanere qui per feedback UI più rapido if not os.path.isfile(bundle_full_path): self.main_frame.show_error( "File Not Found", f"Bundle file not found:\n{bundle_full_path}" ) return if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): return # --- MODIFICA: Parse exclusions --- excluded_extensions, excluded_dirs = self._parse_exclusions() # --- FINE MODIFICA --- autobackup_enabled = self.main_frame.autobackup_var.get() backup_base_dir = self.main_frame.backup_dir_var.get() # Esecuzione sincrona try: # --- MODIFICA: Passa i set parsati a ActionHandler --- success = self.action_handler.execute_fetch_bundle( svn_path, bundle_full_path, profile, autobackup_enabled, backup_base_dir, excluded_extensions, excluded_dirs, # Passa i set qui ) # --- FINE MODIFICA --- if success: # ActionHandler ora restituisce bool (o solleva eccezione) self.main_frame.show_info( "Fetch Complete", "Fetch and merge completed.\nCheck log for details.", ) # else non dovrebbe accadere se ActionHandler solleva eccezioni except FileNotFoundError as e: # Cattura se il file scompare tra check e uso self.logger.error(f"Fetch failed: {e}", exc_info=False) # Log meno verboso self.main_frame.show_error("File Not Found", f"{e}") except GitCommandError as e: # Gestione specifica conflitti self.logger.error(f"Fetch failed: {e}", exc_info=True) if "merge conflict" in str(e).lower(): self.main_frame.show_error( "Merge Conflict", f"Conflict occurred during merge.\nPlease resolve conflicts manually in:\n{svn_path}\nThen commit the changes.", ) else: self.main_frame.show_error( "Fetch Error (Git)", f"Operation failed:\n{e}" ) except (IOError, ValueError) as e: self.logger.error(f"Fetch failed: {e}", exc_info=True) self.main_frame.show_error("Fetch Error", f"Operation failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error during Fetch: {e}") self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) def manual_backup(self): """Handles the 'Backup Now' button click (Synchronous).""" self.logger.info("--- Action Triggered: Manual Backup ---") # Validazione Input profile = self.main_frame.profile_var.get() if not profile: self.main_frame.show_error("Error", "No profile selected.") return svn_path = self._get_and_validate_svn_path(f"Manual Backup ({profile})") if not svn_path: return backup_base_dir = self.main_frame.backup_dir_var.get().strip() if not backup_base_dir: self.main_frame.show_error("Input Error", "Backup directory path empty.") return if not self.save_profile_settings(): # Salva le esclusioni correnti if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): return # --- MODIFICA: Parse exclusions --- excluded_extensions, excluded_dirs = self._parse_exclusions() # --- FINE MODIFICA --- # Esecuzione sincrona self.logger.info(f"Starting manual backup for '{profile}'...") try: # --- MODIFICA: Chiamata diretta a BackupHandler con i set --- # Non c'è un metodo dedicato in ActionHandler per backup manuale, # quindi chiamiamo direttamente BackupHandler qui. backup_path_result = self.backup_handler.create_zip_backup( source_repo_path=svn_path, backup_base_dir=backup_base_dir, profile_name=profile, excluded_extensions=excluded_extensions, excluded_dirs_base=excluded_dirs, # Passa il set qui ) # --- FINE MODIFICA --- if backup_path_result: self.main_frame.show_info( "Backup Complete", f"Manual backup created:\n{backup_path_result}" ) else: self.main_frame.show_warning( "Backup Info", "Backup finished, but no file generated (source empty?).", ) except (IOError, ValueError, PermissionError) as e: # Cattura errori specifici self.logger.error(f"Manual backup failed: {e}", exc_info=True) self.main_frame.show_error("Backup Error", f"Manual backup failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error during manual backup: {e}") self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) def commit_changes(self): """Handles the 'Commit Changes' button click (Synchronous).""" self.logger.info("--- Action Triggered: Commit Changes ---") # Validazione Input svn_path = self._get_and_validate_svn_path("Commit Changes") if not svn_path: return commit_msg = self.main_frame.get_commit_message() if not commit_msg: self.main_frame.show_error( "Commit Error", "Commit message cannot be empty." ) return # Esecuzione sincrona try: commit_made = self.action_handler.execute_manual_commit( svn_path, commit_msg ) if commit_made: self.main_frame.show_info("Commit Successful", "Changes committed.") self.main_frame.clear_commit_message() self.refresh_commit_history() # Aggiorna UI dopo successo self.refresh_branch_list() else: self.main_frame.show_info( "No Changes", "No changes were available to commit." ) except (GitCommandError, ValueError) as e: self.logger.error(f"Commit failed: {e}", exc_info=True) self.main_frame.show_error("Commit Error", f"Failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected commit error: {e}") self.main_frame.show_error("Error", f"Unexpected:\n{e}") # --- Tag Management Callbacks (Synchronous) --- def refresh_tag_list(self): """Refreshes the tag list (Synchronous).""" self.logger.info("--- Action Triggered: Refresh Tags ---") svn_path = self._get_and_validate_svn_path("Refresh Tags") tags_data = [] # Default a lista vuota if svn_path and self._is_repo_ready(svn_path): try: tags_data = self.git_commands.list_tags(svn_path) self.logger.info(f"Tag list refreshed ({len(tags_data)} tags found).") except Exception as e: self.logger.error(f"Failed refresh tags: {e}", exc_info=True) self.main_frame.show_error("Error", f"Could not refresh tags:\n{e}") else: self.logger.debug("Refresh Tags skipped: Repository not ready.") # Aggiorna GUI in ogni caso (anche con lista vuota o errore) if hasattr(self.main_frame, "update_tag_list"): self.main_frame.update_tag_list(tags_data) def _generate_next_tag_suggestion(self, svn_path): """ Generates a suggested tag name based on the latest tag matching v.X.X.X.X pattern. Args: svn_path (str): Path to the repository. Returns: str: The suggested tag name (e.g., "v.0.0.0.1" or incremented version). """ self.logger.debug("Generating next tag suggestion...") default_suggestion = "v.0.0.0.1" latest_valid_tag = None tag_pattern = re.compile(r"^v\.(\d{1,2})\.(\d{1,2})\.(\d{1,2})\.(\d{1,2})$") try: # Ottieni i tag ordinati, dal più recente # list_tags restituisce [(name, subject), ...] tags_data = self.git_commands.list_tags(svn_path) if not tags_data: self.logger.debug("No existing tags found. Suggesting default.") return default_suggestion # Cerca il primo tag che corrisponde al pattern for tag_name, _ in tags_data: match = tag_pattern.match(tag_name) if match: latest_valid_tag = tag_name self.logger.debug(f"Found latest tag matching pattern: {latest_valid_tag}") break # Trovato il più recente, esci if not latest_valid_tag: self.logger.debug("No tags matched the pattern v.X.X.X.X. Suggesting default.") return default_suggestion # Estrai e incrementa i numeri match = tag_pattern.match(latest_valid_tag) # Riesegui match per sicurezza if not match: # Non dovrebbe succedere, ma controllo difensivo self.logger.error(f"Internal error: Could not re-match tag {latest_valid_tag}") return default_suggestion v1, v2, v3, v4 = map(int, match.groups()) # Incrementa gestendo i riporti da 99 a 0 v4 += 1 if v4 > 99: v4 = 0 v3 += 1 if v3 > 99: v3 = 0 v2 += 1 if v2 > 99: v2 = 0 v1 += 1 # Non mettiamo limiti a v1 per ora (può diventare > 99) next_tag = f"v.{v1}.{v2}.{v3}.{v4}" self.logger.debug(f"Generated suggestion: {next_tag}") return next_tag except Exception as e: self.logger.error(f"Error generating tag suggestion: {e}", exc_info=True) return default_suggestion # Ritorna il default in caso di errore def create_tag(self): """Handles 'Create Tag' (Synchronous after dialog), suggesting the next tag name.""" self.logger.info("--- Action Triggered: Create Tag ---") svn_path = self._get_and_validate_svn_path("Create Tag") if not svn_path: return # --- MODIFICA: Genera suggerimento PRIMA di aprire il dialogo --- suggested_tag = self._generate_next_tag_suggestion(svn_path) # --- FINE MODIFICA --- # Ottieni input utente (Dialog sincrono) # --- MODIFICA: Passa il suggerimento al Dialog --- dialog = CreateTagDialog(self.master, suggested_tag_name=suggested_tag) # --- FINE MODIFICA --- tag_info = dialog.result if tag_info: tag_name, tag_message = tag_info # (Logica commit pre-tag e chiamata ad action_handler invariata) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): return commit_message = self.main_frame.get_commit_message() try: success = self.action_handler.execute_create_tag( svn_path, commit_message, tag_name, tag_message ) if success: self.main_frame.show_info("Success", f"Tag '{tag_name}' created.") self.refresh_tag_list() self.refresh_commit_history() self.refresh_branch_list() except (GitCommandError, ValueError) as e: self.logger.error(f"Failed create tag '{tag_name}': {e}", exc_info=True) self.main_frame.show_error("Tag Error", f"Failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected tag error: {e}") self.main_frame.show_error("Error", f"Unexpected:\n{e}") else: self.logger.info("Tag creation cancelled.") def checkout_tag(self): """Handles the 'Checkout Selected Tag' action (Synchronous).""" self.logger.info("--- Action Triggered: Checkout Tag ---") svn_path = self._get_and_validate_svn_path("Checkout Tag") if not svn_path: return selected_tag = self.main_frame.get_selected_tag() if not selected_tag: self.main_frame.show_error("Error", "Select tag.") return # Conferma utente confirm_msg = f"Checkout tag '{selected_tag}'?\n\nWARNINGS:\n- Files overwritten.\n- NO backup.\n- Detached HEAD state." if not self.main_frame.ask_yes_no("Confirm Checkout", confirm_msg): self.logger.info("Checkout cancelled.") return # Esecuzione sincrona try: success = self.action_handler.execute_checkout_tag(svn_path, selected_tag) if success: self.main_frame.show_info( "Success", f"Checked out '{selected_tag}'.\n\nNOTE: Detached HEAD state.", ) self.refresh_branch_list() self.refresh_commit_history() # Aggiorna UI except (GitCommandError, ValueError) as e: self.logger.error(f"Failed checkout '{selected_tag}': {e}", exc_info=True) self.main_frame.show_error("Error", f"Checkout failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected checkout error: {e}") self.main_frame.show_error("Error", f"Unexpected:\n{e}") # --- Branch Management Callbacks (Synchronous) --- def refresh_branch_list(self): """Refreshes the branch list (Synchronous).""" self.logger.info("--- Action Triggered: Refresh Branches ---") svn_path = self._get_and_validate_svn_path("Refresh Branches") branches, current = [], None # Default if svn_path and self._is_repo_ready(svn_path): try: branches, current = self.git_commands.list_branches(svn_path) self.logger.info( f"Branch list refreshed ({len(branches)} found). Current: {current}" ) except Exception as e: self.logger.error(f"Failed refresh branches: {e}", exc_info=True) self.main_frame.show_error("Error", f"Could not refresh branches:\n{e}") else: self.logger.debug("Refresh Branches skipped: Repository not ready.") # Aggiorna GUI if hasattr(self.main_frame, "update_branch_list"): self.main_frame.update_branch_list(branches, current) if hasattr(self.main_frame, "update_history_branch_filter"): self.main_frame.update_history_branch_filter(branches or [], current) def create_branch(self): """Handles the 'Create Branch' action (Synchronous after dialog).""" self.logger.info("--- Action Triggered: Create Branch ---") svn_path = self._get_and_validate_svn_path("Create Branch") if not svn_path: return # Ottieni input utente (Dialog sincrono) dialog = CreateBranchDialog(self.master) new_branch_name = dialog.result if new_branch_name: # Esecuzione sincrona try: success = self.action_handler.execute_create_branch( svn_path, new_branch_name ) if success: self.main_frame.show_info( "Success", f"Branch '{new_branch_name}' created." ) self.refresh_branch_list() # Aggiorna lista # Chiedi checkout (sincrono) if self.main_frame.ask_yes_no( "Checkout New Branch?", f"Switch to new branch '{new_branch_name}'?", ): self.checkout_branch( branch_to_checkout=new_branch_name, repo_path_override=svn_path, ) # Chiama checkout sync else: self.refresh_commit_history() # Aggiorna storia anche se non fai checkout except (GitCommandError, ValueError) as e: self.logger.error( f"Failed create branch '{new_branch_name}': {e}", exc_info=True ) self.main_frame.show_error("Error", f"Create failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected create branch: {e}") self.main_frame.show_error("Error", f"Unexpected:\n{e}") else: self.logger.info("Branch creation cancelled.") def checkout_branch(self, branch_to_checkout=None, repo_path_override=None): """Handles 'Checkout Selected Branch' (Synchronous).""" # (Mantenere versione precedente robusta, ma con chiamate sincrone) log_target = branch_to_checkout or "Selected" self.logger.info( f"--- Action Triggered: Checkout Branch (Target: {log_target}) ---" ) svn_path = repo_path_override or self._get_and_validate_svn_path( "Checkout Branch" ) if not svn_path: return if branch_to_checkout: selected_branch = branch_to_checkout needs_confirmation = False else: selected_branch = self.main_frame.get_selected_branch() needs_confirmation = True if not selected_branch: self.main_frame.show_error("Selection Error", "Select a branch.") return if needs_confirmation: if not self.main_frame.ask_yes_no( "Confirm Checkout", f"Switch to branch '{selected_branch}'?" ): self.logger.info("Checkout cancelled.") return # Esecuzione sincrona try: success = self.action_handler.execute_switch_branch( svn_path, selected_branch ) if success: self.main_frame.show_info( "Success", f"Switched to branch '{selected_branch}'." ) self.refresh_branch_list() self.refresh_commit_history() # Aggiorna UI except (GitCommandError, ValueError) as e: self.logger.error( f"Failed checkout '{selected_branch}': {e}", exc_info=True ) self.main_frame.show_error("Error", f"Checkout failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected checkout error: {e}") self.main_frame.show_error("Error", f"Unexpected:\n{e}") # --- History Method (Synchronous) --- def refresh_commit_history(self): """Refreshes the commit history display (Synchronous).""" self.logger.info("--- Action Triggered: Refresh History ---") svn_path = self._get_and_validate_svn_path("Refresh History") log_data = [] # Default if svn_path and self._is_repo_ready(svn_path): branch_filter = None if hasattr(self.main_frame, "history_branch_filter_var"): selected_filter = self.main_frame.history_branch_filter_var.get() if selected_filter and selected_filter != "-- All History --": branch_filter = selected_filter self.logger.debug( f"Refreshing history with filter: {branch_filter or 'All'}" ) try: log_data = self.git_commands.get_commit_log( svn_path, max_count=200, branch=branch_filter ) self.logger.info(f"History refreshed ({len(log_data)} entries).") except Exception as e: self.logger.error(f"Failed refresh history: {e}", exc_info=True) self.main_frame.show_error("Error", f"Could not refresh history:\n{e}") else: self.logger.debug("Refresh History skipped: Repository not ready.") # Aggiorna GUI if hasattr(self.main_frame, "update_history_display"): self.main_frame.update_history_display(log_data) # --- GUI State Utilities --- def _clear_and_disable_fields(self): """Clears input fields and disables widgets when no valid profile/repo.""" # (Mantenere versione precedente robusta) if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): mf = self.main_frame self.logger.debug("Clearing/disabling GUI fields.") if hasattr(mf, "svn_path_entry"): mf.svn_path_entry.delete(0, tk.END) if hasattr(mf, "usb_path_entry"): mf.usb_path_entry.delete(0, tk.END) if hasattr(mf, "bundle_name_entry"): mf.bundle_name_entry.delete(0, tk.END) if hasattr(mf, "bundle_updated_name_entry"): mf.bundle_updated_name_entry.delete(0, tk.END) if hasattr(mf, "autobackup_var"): mf.autobackup_var.set(False) if hasattr(mf, "backup_dir_var"): mf.backup_dir_var.set("") mf.toggle_backup_dir() if hasattr(mf, "autocommit_var"): mf.autocommit_var.set(False) if hasattr(mf, "clear_commit_message"): mf.clear_commit_message() if hasattr(mf, "update_tag_list"): mf.update_tag_list([]) if hasattr(mf, "update_branch_list"): mf.update_branch_list([], None) if hasattr(mf, "update_history_display"): mf.update_history_display([]) if hasattr(mf, "update_history_branch_filter"): mf.update_history_branch_filter([]) self.update_svn_status_indicator("") # This disables most buttons if hasattr(mf, "remove_profile_button"): mf.remove_profile_button.config(state=tk.DISABLED) if hasattr(mf, "save_settings_button"): mf.save_settings_button.config(state=tk.DISABLED) def show_fatal_error(self, message): """Shows a fatal error message box.""" # (Mantenere versione precedente robusta) self.logger.critical(f"FATAL ERROR: {message}") try: parent = ( self.master if hasattr(self, "master") and self.master and self.master.winfo_exists() else None ) messagebox.showerror("Fatal Error", message, parent=parent) except Exception as e: print(f"FATAL ERROR (GUI message failed: {e}): {message}") # --- Application Entry Point --- def main(): """Main function: Creates Tkinter root and runs the application.""" # Imposta logging base PRIMA di tutto, utile per errori di import o init logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("main") # Logger specifico per main root = None # Inizializza a None app = None try: root = tk.Tk() # Imposta dimensioni minime per assicurare visibilità di tutti gli elementi # Regola questi valori se necessario in base al layout finale root.minsize(750, 700) # Altezza leggermente ridotta rispetto a prima logger.info("Tkinter root window created.") app = GitSvnSyncApp(root) # Crea l'applicazione principale # Controlla se l'app è stata inizializzata correttamente prima di avviare mainloop if hasattr(app, "main_frame") and app.main_frame: logger.info("Starting Tkinter main event loop.") root.mainloop() logger.info("Tkinter main event loop finished.") else: logger.critical( "Application initialization failed before mainloop could start." ) if root and root.winfo_exists(): root.destroy() # Pulisci finestra se esiste except Exception as e: logger.exception("Fatal error during application startup or main loop.") try: # Tenta di mostrare un errore finale all'utente parent = root if root and root.winfo_exists() else None messagebox.showerror( "Fatal Application Error", f"Application failed to run:\n{e}", parent=parent, ) except Exception as msg_e: print(f"FATAL ERROR (GUI error reporting failed: {msg_e}):\n{e}") finally: logging.info("Application exiting.") if __name__ == "__main__": main()