# GitUtility.py import os import datetime import tkinter as tk from tkinter import messagebox, filedialog import logging import re # Importato per l'analisi dei tag in _generate_next_tag_suggestion # Import application modules try: from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR 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 from gui import ( MainFrame, GitignoreEditorWindow, CreateTagDialog, CreateBranchDialog, ) # Importa la nuova finestra di diff from diff_viewer import DiffViewerWindow except ImportError as e: # Configurazione di logging minimale per errori critici all'avvio logging.basicConfig(level=logging.CRITICAL) logging.critical( f"Critical Error: Failed to import required application modules: {e}", exc_info=True, ) # Stampa anche su console se il logging non funziona o non è visibile print(f"FATAL: Failed to import required application modules: {e}") # Uscita pulita se i moduli essenziali mancano exit(1) class GitSvnSyncApp: """ Main application class for the Git Sync Tool. Coordinates the GUI, configuration, and delegates actions to ActionHandler. (Versione Sincrona) """ def __init__(self, master): """ Initializes the GitSvnSyncApp. Args: master (tk.Tk): The main Tkinter root window. """ self.master = master master.title("Git Sync Tool (Bundle Manager)") # Titolo aggiornato master.protocol("WM_DELETE_WINDOW", self.on_closing) # Basic logging setup first (verrà riconfigurato dopo la GUI) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) self.logger = logging.getLogger(self.__class__.__name__) # --- Initialize Core Components --- try: self.config_manager = ConfigManager(self.logger) self.git_commands = GitCommands(self.logger) self.backup_handler = BackupHandler(self.logger) 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 ) # Usa show_fatal_error che gestisce la distruzione della finestra self.show_fatal_error( f"Initialization Error:\n{e}\n\nApplication cannot start." ) # Non ritornare subito, lascia che show_fatal_error gestisca la finestra return # --- Initialize GUI (MainFrame) --- try: self.main_frame = MainFrame( master, # Callbacks gestione profilo e percorsi load_profile_settings_cb=self.load_profile_settings, browse_folder_cb=self.browse_folder, update_svn_status_cb=self.update_svn_status_indicator, add_profile_cb=self.add_profile, remove_profile_cb=self.remove_profile, save_profile_cb=self.save_profile_settings, # Callbacks azioni principali prepare_svn_for_git_cb=self.prepare_svn_for_git, create_git_bundle_cb=self.create_git_bundle, fetch_from_git_bundle_cb=self.fetch_from_git_bundle, # Callbacks azioni specifiche open_gitignore_editor_cb=self.open_gitignore_editor, manual_backup_cb=self.manual_backup, commit_changes_cb=self.commit_changes, # Callbacks Tag refresh_tags_cb=self.refresh_tag_list, create_tag_cb=self.create_tag, checkout_tag_cb=self.checkout_tag, # Callbacks Branch refresh_branches_cb=self.refresh_branch_list, checkout_branch_cb=self.checkout_branch, create_branch_cb=self.create_branch, # Callbacks History & Changes refresh_history_cb=self.refresh_commit_history, refresh_changed_files_cb=self.refresh_changed_files_list, # Nuovo open_diff_viewer_cb=self.open_diff_viewer, # Nuovo # Passa istanza config manager e profili iniziali config_manager_instance=self.config_manager, profile_sections_list=self.config_manager.get_profile_sections(), add_selected_file_cb=self.add_selected_file, ) # --- Collega widget GUI specifici (che non passano dal costruttore di MainFrame) --- if hasattr(self.main_frame, 'refresh_changes_button'): self.main_frame.refresh_changes_button.config( command=self.refresh_changed_files_list ) # Il doppio click sulla lista file è collegato internamente in MainFrame # al callback 'open_diff_viewer_cb' passato sopra. # --- GESTIONE TRACE INIZIALE (Per caricamento profilo senza triggerare loop) --- # Salva info trace scrittura sulla variabile del profilo trace_info = self.main_frame.profile_var.trace_info() write_trace_callback_info = None if trace_info: for info in trace_info: # trace_info può essere vuoto o non contenere 'write' if info and len(info) > 1 and "write" in info[1]: try: # Ottiene info specifiche del callback ('write', nome_callback) write_trace_callback_info = ( self.main_frame.profile_var.trace_vinfo()[0] ) self.logger.debug( "Temporarily removing 'write' trace for initial load: " f"{write_trace_callback_info}" ) # Rimuove temporaneamente il trace per evitare chiamate multiple self.main_frame.profile_var.trace_vdelete( *write_trace_callback_info ) except (tk.TclError, IndexError) as trace_err: self.logger.warning( f"Could not get/remove profile var trace: {trace_err}" ) write_trace_callback_info = None # Non rimuovere/riaggiungere break # Trovato e rimosso (o fallito), esci dal loop # Esegui il caricamento iniziale del profilo ORA, senza il trace attivo self._perform_initial_load() # Riaggiungi il trace di scrittura se era stato rimosso con successo if write_trace_callback_info: self.logger.debug("Re-adding 'write' trace for profile variable.") try: self.main_frame.profile_var.trace_add( "write", # Usa una lambda per passare il valore corrente alla callback lambda n, i, m: self.load_profile_settings( self.main_frame.profile_var.get() ), ) except tk.TclError as trace_err: self.logger.error(f"Could not re-add profile var trace: {trace_err}") # --- 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." ) return # --- Finalize Logger Setup --- # Associa l'handler della GUI al logger dopo che la GUI è stata creata try: if hasattr(self.main_frame, "log_text"): # Setup logger con il widget della GUI self.logger = setup_logger(self.main_frame.log_text) # Propaga il logger aggiornato ai componenti che lo usano 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: # Questo non dovrebbe succedere se MainFrame è costruito correttamente self.logger.error("GUI log text widget not found after GUI initialization.") except Exception as log_e: self.logger.error( f"Error setting up logger with GUI TextHandler: {log_e}", exc_info=True ) # --- Imposta stato iniziale Status Bar --- self.main_frame.update_status_bar("Ready.") # --- Fine Stato Iniziale --- self.logger.info("Git Sync Tool initialization sequence complete.") def _perform_initial_load(self): """Loads the initially selected profile and updates UI state.""" self.logger.debug("Performing initial profile load and UI update.") initial_profile = self.main_frame.profile_var.get() if initial_profile: # Chiama direttamente load_profile_settings ORA self.load_profile_settings(initial_profile) else: self.logger.warning("No initial profile set during initial load.") # Pulisci e disabilita i campi se nessun profilo è selezionato self._clear_and_disable_fields() # Imposta stato barra se nessun profilo if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("No profile selected.") # Non impostare "Ready." qui, load_profile_settings lo farà. def on_closing(self): """Handles the window close event.""" # Opzionale: aggiorna status bar alla chiusura if hasattr(self, 'main_frame'): try: self.main_frame.update_status_bar("Exiting...") except: pass # Ignora errori se la finestra sta già chiudendo self.logger.info("Application closing.") if self.master and self.master.winfo_exists(): self.master.destroy() # --- Profile Management Callbacks --- 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 hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Loading profile: {profile_name}...") # Validazione profilo 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() # Pulisci e disabilita if profile_name and hasattr(self, 'main_frame'): # Mostra errore solo se si cercava un profilo specifico self.main_frame.show_error( "Profile Error", f"Profile '{profile_name}' not found." ) self.main_frame.update_status_bar(f"Error: Profile '{profile_name}' not found.") elif hasattr(self, 'main_frame'): self.main_frame.update_status_bar("No profile selected.") return # Esce se il profilo non è valido # Ottieni dati dal config manager 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) # Applica impostazioni alla GUI if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): self.logger.error("Main frame not available during profile load.") if hasattr(self, 'main_frame'): # Prova ad aggiornare status bar anche se frame parziale self.main_frame.update_status_bar("Error: GUI not ready during load.") return mf = self.main_frame status_final = f"Profile '{profile_name}' loaded." # Default successo 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", "") ) # Aggiorna stato widget backup dir mf.toggle_backup_dir() # Commit Tab mf.autocommit_var.set( str(settings.get("autocommit", "False")).lower() == "true" ) # Pulisci e riempi messaggio commit mf.clear_commit_message() if ( hasattr(mf, "commit_message_text") and mf.commit_message_text.winfo_exists() ): # Assicura che sia scrivibile prima di inserire current_state = mf.commit_message_text.cget("state") if current_state == tk.DISABLED: mf.commit_message_text.config(state=tk.NORMAL) mf.commit_message_text.insert("1.0", settings.get("commit_message", "")) # Ripristina stato originale se necessario (verrà gestito da update_svn_status) # mf.commit_message_text.config(state=current_state) self.logger.info(f"Successfully loaded settings for '{profile_name}'.") # Post-Load Updates (Stato, Liste) svn_path_loaded = settings.get("svn_working_copy_path", "") # Aggiorna stato indicatori e pulsanti self.update_svn_status_indicator(svn_path_loaded) # Questo aggiorna anche stato commit message repo_is_ready = self._is_repo_ready(svn_path_loaded) if repo_is_ready: # Aggiorna liste solo se il repository è pronto self.refresh_tag_list() self.refresh_branch_list() self.refresh_commit_history() self.refresh_changed_files_list() # Aggiorna anche nuova lista else: # Pulisci le liste se il repository non è pronto 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([]) if hasattr(mf, "update_changed_files_list"): mf.update_changed_files_list(["(Repository not ready)"]) # Pulisci nuova lista status_final = f"Profile '{profile_name}' loaded (Repo not ready)." except Exception as e: self.logger.error( f"Error applying loaded settings for '{profile_name}': {e}", exc_info=True, ) status_final = f"Error loading profile '{profile_name}'." mf.show_error("Load Error", f"Failed to apply settings:\n{e}") finally: mf.update_status_bar(status_final) 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.") # Aggiorna status bar se main_frame esiste if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Save failed: No profile selected.") return False # Indica fallimento self.logger.info(f"Saving settings for profile: '{profile_name}'") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Saving profile: {profile_name}...") # Verifica esistenza main_frame if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): self.logger.error("Cannot save profile: Main frame not available.") # Non possiamo aggiornare status bar qui return False mf = self.main_frame cm = self.config_manager status_final = "Ready." # Default per finally success = False try: # Raccogli valori dalla GUI 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 ogni opzione tramite ConfigManager for key, value in settings_to_save.items(): cm.set_profile_option(profile_name, key, value) # Scrivi sul file .ini cm.save_config() self.logger.info(f"Settings saved successfully for '{profile_name}'.") status_final = f"Profile '{profile_name}' saved." success = True except Exception as e: self.logger.error(f"Error saving profile '{profile_name}': {e}", exc_info=True) status_final = f"Error saving profile '{profile_name}'." mf.show_error("Save Error", f"Failed to save settings:\n{e}") success = False finally: mf.update_status_bar(status_final) return success def add_profile(self): """Handles adding a new profile.""" self.logger.debug("'Add Profile' button clicked.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Adding new profile...") # Chiedi nome all'utente name = self.main_frame.ask_new_profile_name() if not name: self.logger.info("Add profile cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Add profile cancelled.") return # Esce se l'utente annulla # Validazione nome name = name.strip() if not name: self.logger.warning("Add profile failed: Name cannot be empty.") self.main_frame.show_error("Input Error", "Profile name cannot be empty.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Add profile failed: Empty name.") return if name in self.config_manager.get_profile_sections(): self.logger.warning(f"Add profile failed: Profile '{name}' already exists.") self.main_frame.show_error("Error", f"Profile '{name}' already exists.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Add profile failed: '{name}' exists.") return self.logger.info(f"Attempting to add new profile: '{name}'") status_final = "Ready." try: # Chiama ProfileHandler o logica ConfigManager per aggiungere # (Assumiamo che la logica sia robusta come discusso precedentemente) success = self.action_handler.add_new_profile(name) # Assumendo che ActionHandler abbia questo metodo # O chiama direttamente ConfigManager se preferito: # success = self._add_profile_logic(name) # Esempio se la logica fosse qui if success: self.logger.info(f"Profile '{name}' added successfully.") # Aggiorna il dropdown nella GUI sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) # Seleziona automaticamente il nuovo profilo self.main_frame.profile_var.set(name) # Questo triggererà load_profile_settings status_final = f"Profile '{name}' added and loaded." # Non aggiornare status bar qui, load_profile_settings lo farà else: # ActionHandler/ConfigManager dovrebbe aver loggato l'errore self.logger.error(f"Failed to add profile '{name}' (reported by backend).") status_final = f"Error adding profile '{name}'." self.main_frame.show_error("Error", f"Failed to add profile '{name}'.\nCheck logs for details.") except Exception as e: self.logger.error(f"Unexpected error adding profile '{name}': {e}", exc_info=True) status_final = f"Error adding profile '{name}'." self.main_frame.show_error("Error", f"Failed to add profile:\n{e}") finally: # Non aggiornare status qui se il set() ha triggerato load_profile_settings # self.main_frame.update_status_bar(status_final) pass # Lo stato verrà aggiornato dal caricamento def remove_profile(self): """Handles removing the selected profile.""" self.logger.debug("'Remove Profile' button clicked.") profile = self.main_frame.profile_var.get() # Validazioni iniziali if not profile: self.logger.warning("Remove profile failed: No profile selected.") self.main_frame.show_error("Error", "No profile selected to remove.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Remove failed: No profile selected.") return if profile == DEFAULT_PROFILE: self.logger.warning("Attempt to remove default profile denied.") self.main_frame.show_error("Action Denied", f"Cannot remove the default profile ('{DEFAULT_PROFILE}').") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Cannot remove default profile.") return # Conferma utente if self.main_frame.ask_yes_no("Confirm Remove", f"Are you sure you want to remove the profile '{profile}'?"): self.logger.info(f"Attempting to remove profile: '{profile}'") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Removing profile: {profile}...") status_final = "Ready." try: # Esegui rimozione tramite ConfigManager removed = self.config_manager.remove_profile_section(profile) if removed: self.config_manager.save_config() # Salva solo se la rimozione è riuscita self.logger.info(f"Profile '{profile}' removed successfully.") status_final = f"Profile '{profile}' removed." # Aggiorna il dropdown e seleziona il default o il primo disponibile sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) # Il set() del dropdown triggererà load_profile_settings del nuovo profilo else: # ConfigManager.remove_profile_section dovrebbe loggare il motivo self.logger.error(f"Failed to remove profile '{profile}' (reported by ConfigManager).") status_final = f"Error removing profile '{profile}'." self.main_frame.show_error("Error", f"Could not remove profile '{profile}'.") except Exception as e: self.logger.error(f"Error removing profile '{profile}': {e}", exc_info=True) status_final = f"Error removing profile '{profile}'." self.main_frame.show_error("Error", f"Failed to remove profile:\n{e}") finally: # Non aggiornare qui se load_profile_settings verrà chiamato # self.main_frame.update_status_bar(status_final) pass else: self.logger.info("Profile removal cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Profile removal cancelled.") # --- GUI Interaction & Helper Methods --- def browse_folder(self, entry_widget): """Opens folder dialog to update an entry widget.""" current_path = entry_widget.get() # Imposta directory iniziale basandosi sul percorso corrente o home utente initial_dir = current_path if os.path.isdir(current_path) else os.path.expanduser("~") self.logger.debug(f"Opening folder browser, initial dir: {initial_dir}") # Mostra dialogo selezione cartella directory = filedialog.askdirectory( initialdir=initial_dir, title="Select Directory", parent=self.master # Assicura che sia modale rispetto alla finestra principale ) # Se l'utente seleziona una cartella, aggiorna l'entry widget if directory: self.logger.debug(f"Directory selected: {directory}") entry_widget.delete(0, tk.END) entry_widget.insert(0, directory) # Se l'entry aggiornata è quella del percorso SVN, aggiorna lo stato if ( hasattr(self.main_frame, "svn_path_entry") and entry_widget == self.main_frame.svn_path_entry ): self.update_svn_status_indicator(directory) else: self.logger.debug("Folder browse cancelled.") def update_svn_status_indicator(self, svn_path): """Checks repo status and updates dependent GUI widgets and status bar.""" # Validazione preliminare del percorso fornito is_valid_dir_input = bool(svn_path and os.path.isdir(svn_path)) # Verifica specifica se è un repository Git pronto is_repo_ready = is_valid_dir_input and os.path.exists(os.path.join(svn_path, ".git")) self.logger.debug( f"Updating status for path '{svn_path}'. ValidDir:{is_valid_dir_input}, RepoReady:{is_repo_ready}" ) # Verifica esistenza main_frame if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): self.logger.error("Cannot update status indicator: Main frame not available.") return mf = self.main_frame # Aggiorna indicatore colore e tooltip mf.update_svn_indicator(is_repo_ready) # Questo gestisce colore e tooltip base # Determina lo stato abilitato/disabilitato per i vari widget repo_ready_state = tk.NORMAL if is_repo_ready else tk.DISABLED # Stato per widget che richiedono solo un percorso di directory valido (potrebbe non essere repo) valid_path_state = tk.NORMAL if is_valid_dir_input else tk.DISABLED # Stato per Prepare: abilitato se path valido MA non è repo pronto prepare_state = tk.DISABLED if is_repo_ready else valid_path_state # --- Logica specifica per abilitare Fetch Button (come discusso prima) --- fetch_button_state = tk.DISABLED # Default disabilitato try: 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() can_create_svn_dir = False if os.path.isdir(svn_path_str): can_create_svn_dir = True else: parent_dir = os.path.dirname(svn_path_str) if parent_dir and os.path.isdir(parent_dir) and svn_path_str: # Assicura che il nome non sia vuoto can_create_svn_dir = True is_valid_usb_dir = os.path.isdir(usb_path_str) has_bundle_name = bool(bundle_fetch_name) 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) 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 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 # Sicurezza: disabilita su errore # --- Aggiornamento stato widget --- try: # Pulsanti Tab Repository if hasattr(mf, "prepare_svn_button"): mf.prepare_svn_button.config(state=prepare_state) if hasattr(mf, "create_bundle_button"): mf.create_bundle_button.config(state=repo_ready_state) if hasattr(mf, "fetch_bundle_button"): mf.fetch_bundle_button.config(state=fetch_button_state) # Usa stato calcolato if hasattr(mf, "edit_gitignore_button"): # Edit gitignore richiede repo pronto (file .git deve esistere) mf.edit_gitignore_button.config(state=repo_ready_state) # Pulsanti Tab Backup if hasattr(mf, "manual_backup_button"): mf.manual_backup_button.config(state=valid_path_state) # Richiede solo sorgente valida # Widget Tab Commit if hasattr(mf, "autocommit_checkbox"): mf.autocommit_checkbox.config(state=repo_ready_state) if hasattr(mf, "commit_message_text"): # Stato normale se repo pronto, altrimenti disabilitato mf.commit_message_text.config(state=repo_ready_state) if hasattr(mf, "commit_button"): mf.commit_button.config(state=repo_ready_state) if hasattr(mf, "refresh_changes_button"): mf.refresh_changes_button.config(state=repo_ready_state) if hasattr(mf, "changed_files_listbox"): # La listbox è sempre selezionabile, ma il suo stato logico dipende da repo_ready if repo_ready_state == tk.DISABLED: # Pulisci se il repo non è pronto mf.update_changed_files_list(["(Repository not ready)"]) # Widget Tab Tags, Branches, History (richiedono repo pronto) widgets_require_ready = [ mf.refresh_tags_button, mf.create_tag_button, mf.checkout_tag_button, mf.refresh_branches_button, mf.create_branch_button, mf.checkout_branch_button, mf.refresh_history_button, mf.history_branch_filter_combo, mf.history_text, ] for widget in widgets_require_ready: if hasattr(mf, widget.winfo_name()): # Controlla se l'attributo esiste prima di usarlo target_widget = getattr(mf, widget.winfo_name()) if target_widget and target_widget.winfo_exists(): current_state = repo_ready_state if isinstance(target_widget, ttk.Combobox): target_widget.config(state="readonly" if current_state == tk.NORMAL else tk.DISABLED) elif isinstance(target_widget, (tk.Text, scrolledtext.ScrolledText)): # Lo stato di history_text viene gestito da update_history_display if target_widget != mf.history_text: target_widget.config(state=current_state) else: # Buttons, etc. target_widget.config(state=current_state) # Pulisci le liste se il repo non è pronto (spostato in load_profile_settings) except Exception as e: self.logger.error(f"Error updating widget states based on repo status: {e}", exc_info=True) def _is_repo_ready(self, repo_path): """Helper function: Checks if path is a valid dir containing .git.""" # Verifica che il percorso sia non vuoto, sia una directory e contenga .git 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 dirs) from GUI vars.""" # Verifica esistenza main_frame if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): # Ritorna valori predefiniti sicuri se la GUI non è pronta return set(), {".git", ".svn"} mf = self.main_frame extensions_set = set() # Inizia con le directory sempre escluse dirs_set = {".git", ".svn"} # 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: # Aggiungi punto iniziale se manca if not clean_ext.startswith('.'): clean_ext = '.' + clean_ext extensions_set.add(clean_ext) # Parse Directories exclude_dirs_str = mf.backup_exclude_dirs_var.get() if exclude_dirs_str: for dir_name in exclude_dirs_str.split(','): # Rimuovi spazi, converti in minuscolo, rimuovi eventuali slash iniziali/finali clean_dir = dir_name.strip().lower().strip(os.path.sep + '/') # Ignora nomi non validi o relativi alla directory corrente/parent if clean_dir and clean_dir not in {".", "..", ""}: dirs_set.add(clean_dir) self.logger.debug( f"Parsed Exclusions - Extensions: {extensions_set}, Directories: {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 exists and is a directory.""" # Verifica esistenza main_frame if not hasattr(self, "main_frame") or not hasattr(self.main_frame, "svn_path_entry"): self.logger.error(f"{operation_name} failed: SVN path entry widget not found.") return None svn_path_str = self.main_frame.svn_path_entry.get().strip() if not svn_path_str: self.logger.warning(f"{operation_name} failed: SVN Working Copy Path is empty.") self.main_frame.show_error( "Input Error", "SVN Working Copy Path cannot be empty." ) return None # Ottieni percorso assoluto abs_path = os.path.abspath(svn_path_str) # Controlla se è una directory valida if not os.path.isdir(abs_path): self.logger.warning(f"{operation_name} failed: Invalid SVN path (not a directory): {abs_path}") self.main_frame.show_error( "Input Error", f"Invalid SVN path (not a directory):\n{abs_path}" ) return None # Se tutto ok, logga e ritorna il percorso assoluto 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.""" # Verifica esistenza main_frame if not hasattr(self, "main_frame") or not hasattr(self.main_frame, "usb_path_entry"): self.logger.error(f"{operation_name} failed: Bundle target entry widget not found.") return None usb_path_str = self.main_frame.usb_path_entry.get().strip() if not usb_path_str: self.logger.warning(f"{operation_name} failed: Bundle Target Directory path is empty.") self.main_frame.show_error( "Input Error", "Bundle Target Directory path cannot be empty." ) return None # Ottieni percorso assoluto abs_path = os.path.abspath(usb_path_str) # Controlla se è una directory valida if not os.path.isdir(abs_path): self.logger.warning(f"{operation_name} failed: Invalid Bundle Target path (not a directory): {abs_path}") self.main_frame.show_error( "Input Error", f"Invalid Bundle Target path (not a directory):\n{abs_path}", ) return None # Se tutto ok, logga e ritorna il percorso assoluto 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 ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Opening .gitignore editor...") # Ottieni percorso validato (deve essere un repo pronto per editare .gitignore) svn_path = self._get_and_validate_svn_path("Edit .gitignore") if not svn_path or not self._is_repo_ready(svn_path): # Errore se il percorso non è valido o non è un repo self.logger.warning("Cannot edit .gitignore: Repository path is invalid or not ready.") self.main_frame.show_error("Error", "Select a valid and prepared repository path first.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Edit .gitignore failed: Repository not ready.") return # Costruisci percorso .gitignore gitignore_path = os.path.join(svn_path, ".gitignore") self.logger.debug(f"Target .gitignore path: {gitignore_path}") status_final = "Ready." # Default per il finally try: self.logger.debug("Opening GitignoreEditorWindow...") # Apri la finestra, passando il callback GitignoreEditorWindow( self.master, gitignore_path, self.logger, on_save_success_callback=self._handle_gitignore_save ) # Esecuzione attende qui finché la finestra non è chiusa self.logger.debug("Gitignore editor window closed.") # La status bar verrà aggiornata dal callback o impostata a Ready qui sotto # Aggiorna liste dopo la chiusura, poiché untrack potrebbe aver creato un commit self.update_svn_status_indicator(svn_path) # Aggiorna stato generale pulsanti self.refresh_commit_history() self.refresh_branch_list() self.refresh_changed_files_list() # Ricarica lista cambiamenti status_final = "Ready." # Imposta a Ready se non ci sono stati errori except Exception as e: self.logger.exception(f"Error during .gitignore editing or post-save action: {e}") status_final = "Error during .gitignore edit." self.main_frame.show_error("Editor Error", f"An error occurred:\n{e}") finally: if hasattr(self, 'main_frame'): # Aggiorna status bar solo se non è già stato impostato dal callback if self.main_frame.status_bar_var.get().startswith("Processing"): self.main_frame.update_status_bar(status_final) 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...") # Necessario ottenere di nuovo il percorso SVN # Usa la validazione base, perché il repo *deve* esistere a questo punto 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.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Error: Cannot untrack (invalid path).") return if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Checking files to untrack...") status_final = "Ready." # Default try: # Chiama la logica in ActionHandler untracked_committed = self.action_handler.execute_untrack_files_from_gitignore(svn_path) if untracked_committed: status_final = "Automatic untrack commit created." # Mostra info all'utente (opzionale, il log è già chiaro) # self.main_frame.show_info("Automatic Untrack", f"{status_final}\nCheck log for details.") else: status_final = "Automatic untrack check complete (no action needed)." self.logger.info(status_final) except (GitCommandError, ValueError) as e: self.logger.error(f"Automatic untracking failed: {e}", exc_info=True) status_final = f"Error during automatic untrack: {type(e).__name__}" 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}") status_final = "Error: Unexpected untracking failure." self.main_frame.show_error("Untrack Error", f"Unexpected error during untracking:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # È importante ricaricare la lista dei file modificati DOPO questa operazione self.refresh_changed_files_list() # --- Nuovi Metodi per Commit Tab --- def refresh_changed_files_list(self): """Refreshes the list of changed files in the Commit tab.""" self.logger.info("--- Action Triggered: Refresh Changed Files List ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Refreshing changed files...") # Usa la validazione base, il repo deve esistere svn_path = self._get_and_validate_svn_path("Refresh Changed Files") files_status_list = [] # Default lista vuota status_final = "Ready." if svn_path and self._is_repo_ready(svn_path): try: # Ottieni stato short da GitCommands files_status_list = self.git_commands.get_status_short(svn_path) count = len(files_status_list) self.logger.info(f"Found {count} changed/untracked items.") status_final = f"Ready ({count} changes detected)." if count > 0 else "Ready (No changes detected)." except Exception as e: self.logger.error(f"Failed refresh changed files list: {e}", exc_info=True) files_status_list = ["(Error refreshing list)"] # Mostra errore nella lista status_final = "Error refreshing changes list." if hasattr(self, 'main_frame'): self.main_frame.show_error("Error", f"Could not refresh changed files list:\n{e}") else: self.logger.debug("Refresh Changed Files skipped: Repository not ready.") files_status_list = ["(Repository not ready)"] status_final = "Ready (Repository not ready)." # Aggiorna la GUI Listbox if hasattr(self.main_frame, "update_changed_files_list"): self.main_frame.update_changed_files_list(files_status_list) # Aggiorna status bar alla fine if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) def open_diff_viewer(self, file_status_line): """Opens the Diff Viewer window for the selected file status line.""" # Logga la riga *originale* ricevuta self.logger.info(f"--- Action Triggered: Open Diff Viewer for '{file_status_line}' ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Processing: Opening diff viewer...") svn_path = self._get_and_validate_svn_path("Open Diff Viewer") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Error: Cannot open diff (invalid repo path).") return # --- MODIFICA: Rimuovi l'estrazione del path da qui --- # Pulisci solo null e spazi esterni dalla riga COMPLETA per il controllo status code cleaned_line_for_status_check = file_status_line.strip('\x00').strip() if not cleaned_line_for_status_check or len(cleaned_line_for_status_check) < 3: self.logger.warning(f"Invalid status line received: '{file_status_line}'") if hasattr(self, 'main_frame'): self.main_frame.show_warning("Error", "Invalid file status line selected.") self.main_frame.update_status_bar("Error: Invalid selection.") return # Estrai solo lo status code qui per il controllo preliminare status_code_part = cleaned_line_for_status_check[:2].strip() # Controlla se il file è adatto per il diff if status_code_part in ["??", "!!", "D"]: # Estrai il nome file SOLO per il messaggio di errore (usa la nuova logica qui per testarla) temp_cleaner = DiffViewerWindow._clean_relative_path # Metodo statico o di istanza temporanea display_path = temp_cleaner(self, cleaned_line_for_status_check) # Passa self se non statico msg = f"Cannot show diff for untracked, ignored, or deleted file:\n{display_path}" self.logger.info(f"Diff not applicable: {msg}") if hasattr(self, 'main_frame'): self.main_frame.show_info("Diff Not Applicable", msg) self.main_frame.update_status_bar("Diff not applicable.") return # --- FINE MODIFICA --- # Ora apri la finestra passando la RIGA DI STATO ORIGINALE self.logger.debug(f"Opening DiffViewerWindow with status line: '{file_status_line}'") status_final = "Ready." try: # --- MODIFICA: Passa file_status_line originale --- DiffViewerWindow( self.master, self.logger, self.git_commands, svn_path, file_status_line # <<< Passa la riga intera qui ) # --- FINE MODIFICA --- self.logger.debug("Diff viewer window closed.") status_final = "Ready." except Exception as e: self.logger.exception(f"Error opening or running diff viewer: {e}") status_final = "Error: Failed to open diff viewer." if hasattr(self, 'main_frame'): self.main_frame.show_error("Diff Viewer Error", f"Could not display diff:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- Action Callbacks (Synchronous) --- # (Aggiungere chiamate a update_status_bar e refresh_changed_files_list dove necessario) 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: # Errore già mostrato da _get_and_validate... if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Prepare failed: Invalid path.") return # Aggiorna Status Bar if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Preparing repository...") status_final = "Ready." # Default success = False try: # Chiamata diretta a ActionHandler success = self.action_handler.execute_prepare_repo(svn_path) if success: self.main_frame.show_info("Success", "Repository prepared successfully.") status_final = "Repository prepared." # Se execute_prepare_repo solleva ValueError("already prepared"), viene gestito sotto except ValueError as e: # Cattura "already prepared" self.logger.warning(f"Prepare Repo info: {e}") status_final = "Repository already prepared." self.main_frame.show_warning("Prepare Info", f"{e}") success = True # Consideralo un successo funzionale except (GitCommandError, IOError) as e: self.logger.error(f"Prepare Repo failed: {e}", exc_info=True) status_final = f"Error preparing repository: {type(e).__name__}" 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}") status_final = "Error: Unexpected preparation failure." self.main_frame.show_error("Unexpected Error", f"An unexpected error occurred:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # Aggiorna stato UI in ogni caso dopo l'operazione self.update_svn_status_indicator(svn_path) if success: # Aggiorna liste solo se preparato con successo (o già pronto) self.refresh_changed_files_list() 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.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create bundle failed: No profile.") return svn_path = self._get_and_validate_svn_path("Create Bundle") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create bundle failed: Invalid repo path.") return usb_path = self._get_and_validate_usb_path("Create Bundle") if not usb_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create bundle failed: Invalid target 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 cannot be empty.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create bundle failed: Empty filename.") return # Assicura estensione .bundle if not bundle_name.lower().endswith(".bundle"): bundle_name += ".bundle" # Aggiorna GUI se necessario (potrebbe causare ricorsione se trace è attivo?) # 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) # Salva profilo prima dell'azione if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create bundle cancelled (save failed).") return # Parse esclusioni e opzioni excluded_extensions, excluded_dirs = self._parse_exclusions() 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 if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Creating bundle...") status_final = "Ready." # Default per finally try: 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, ) if bundle_path_result: self.main_frame.show_info( "Bundle Created", f"Bundle created:\n{bundle_path_result}" ) status_final = f"Bundle created: {os.path.basename(bundle_path_result)}" else: self.main_frame.show_info( "Bundle Info", "Bundle creation finished, but no file generated (repo empty or no changes?).", ) status_final = "Bundle created (empty or no changes)." except (IOError, GitCommandError, ValueError) as e: self.logger.error(f"Create Bundle failed: {e}", exc_info=True) status_final = f"Error creating bundle: {type(e).__name__}" 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}") status_final = "Error: Unexpected failure creating bundle." self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # Aggiorna stato UI (importante se c'è stato autocommit) self.update_svn_status_indicator(svn_path) self.refresh_commit_history() # Mostra nuovo commit self.refresh_changed_files_list() # Aggiorna lista cambiamenti def fetch_from_git_bundle(self): """Handles the 'Fetch from Bundle' action (Synchronous).""" self.logger.info("--- Action Triggered: Fetch from Bundle ---") # Validazione Profilo profile = self.main_frame.profile_var.get() if not profile: self.main_frame.show_error("Error", "No profile selected.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Fetch failed: No profile.") return # Ottieni percorsi stringa (validazione interna ad ActionHandler) 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.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Fetch failed: Empty repo path.") return usb_path = self._get_and_validate_usb_path("Fetch from Bundle") # Valida percorso USB if not usb_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Fetch failed: Invalid target 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 cannot be empty.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Fetch failed: Empty filename.") return bundle_full_path = os.path.join(usb_path, bundle_name) # Salva profilo if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Fetch cancelled (save failed).") return # Parse opzioni excluded_extensions, excluded_dirs = self._parse_exclusions() autobackup_enabled = self.main_frame.autobackup_var.get() backup_base_dir = self.main_frame.backup_dir_var.get() # Esecuzione if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Fetching from bundle...") status_final = "Ready." try: # Chiama ActionHandler (che gestisce clone o fetch/merge) success = self.action_handler.execute_fetch_bundle( svn_path_str, bundle_full_path, profile, autobackup_enabled, backup_base_dir, excluded_extensions, excluded_dirs, ) if success: self.main_frame.show_info( "Operation Complete", "Fetch/Clone completed.\nCheck log for details.", ) status_final = "Fetch/Clone from bundle complete." # else: ActionHandler dovrebbe sollevare eccezione su fallimento # Gestione Errori Specifici except FileNotFoundError as e: self.logger.error(f"Fetch/Clone failed: {e}", exc_info=False) status_final = f"Error: Bundle file not found." self.main_frame.show_error("File Not Found", f"{e}") except IOError as e: self.logger.error(f"Fetch/Clone failed: {e}", exc_info=True) status_final = f"Error: Invalid destination or permissions?" self.main_frame.show_error("Operation Error", f"Operation failed:\n{e}") except GitCommandError as e: self.logger.error(f"Fetch/Clone failed: {e}", exc_info=True) if "merge conflict" in str(e).lower(): status_final = "Error: Merge conflict detected!" self.main_frame.show_error( "Merge Conflict", f"Conflict occurred during merge.\nPlease resolve conflicts manually in:\n{svn_path_str}\nThen commit the changes.", ) else: status_final = "Error: Git command failed during fetch/clone." self.main_frame.show_error("Operation Error (Git)", f"Operation failed:\n{e}") except ValueError as e: self.logger.error(f"Fetch/Clone failed: {e}", exc_info=True) status_final = f"Error: Invalid input for fetch/clone." self.main_frame.show_error("Input Error", f"Operation failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error during Fetch/Clone: {e}") status_final = "Error: Unexpected failure during fetch/clone." self.main_frame.show_error("Unexpected Error", f"An unexpected error occurred:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # Aggiorna stato UI e liste dopo l'operazione self.update_svn_status_indicator(svn_path_str) self.refresh_commit_history() self.refresh_tag_list() self.refresh_branch_list() self.refresh_changed_files_list() 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.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Backup failed: No profile.") return svn_path = self._get_and_validate_svn_path(f"Manual Backup ({profile})") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Backup failed: Invalid source 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 cannot be empty.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Backup failed: Empty backup directory.") return # Salva profilo (per salvare le esclusioni correnti) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Save Warning", "Could not save profile settings.\nContinue anyway?" ): if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Backup cancelled (save failed).") return # Parse esclusioni excluded_extensions, excluded_dirs = self._parse_exclusions() # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Creating manual backup...") status_final = "Ready." try: # Chiama direttamente BackupHandler 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, ) if backup_path_result: self.main_frame.show_info( "Backup Complete", f"Manual backup created:\n{backup_path_result}" ) timestamp = datetime.datetime.now().strftime('%H:%M:%S') # Solo ora per brevità status_final = f"Manual backup created ({timestamp})." else: self.main_frame.show_warning( "Backup Info", "Backup finished, but no file generated (source empty or all excluded?).", ) status_final = "Manual backup finished (no file generated)." except (IOError, ValueError, PermissionError) as e: self.logger.error(f"Manual backup failed: {e}", exc_info=True) status_final = f"Error creating backup: {type(e).__name__}" 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}") status_final = "Error: Unexpected backup failure." self.main_frame.show_error( "Unexpected Error", f"An unexpected error occurred:\n{e}" ) finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) 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: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Commit failed: Invalid 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." ) if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Commit failed: Empty message.") return # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Committing changes...") status_final = "Ready." 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() status_final = "Commit successful." # Aggiorna UI dopo successo self.refresh_commit_history() self.refresh_branch_list() self.refresh_tag_list() # Commit potrebbe abilitare nuovi tag self.refresh_changed_files_list() # Lista dovrebbe svuotarsi else: self.main_frame.show_info( "No Changes", "No changes were available to commit." ) status_final = "Commit finished (no changes)." # Aggiorna comunque lista cambiamenti self.refresh_changed_files_list() except (GitCommandError, ValueError) as e: self.logger.error(f"Commit failed: {e}", exc_info=True) status_final = f"Error during commit: {type(e).__name__}" self.main_frame.show_error("Commit Error", f"Failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected commit error: {e}") status_final = "Error: Unexpected commit failure." self.main_frame.show_error("Error", f"Unexpected:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- Tag Management Callbacks (Synchronous) --- def refresh_tag_list(self): """Refreshes the tag list (Synchronous).""" self.logger.info("--- Action Triggered: Refresh Tags ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Refreshing tags...") svn_path = self._get_and_validate_svn_path("Refresh Tags") tags_data = [] # Default a lista vuota status_final = "Ready." if svn_path and self._is_repo_ready(svn_path): try: tags_data = self.git_commands.list_tags(svn_path) count = len(tags_data) self.logger.info(f"Tag list refreshed ({count} tags found).") status_final = f"Tags refreshed ({count} found)." except Exception as e: self.logger.error(f"Failed refresh tags: {e}", exc_info=True) status_final = "Error refreshing tags." self.main_frame.show_error("Error", f"Could not refresh tags:\n{e}") tags_data = [("(Error loading tags)", "")] # Mostra errore nella lista else: self.logger.debug("Refresh Tags skipped: Repository not ready.") status_final = "Ready (Repository not ready)." tags_data = [("(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) if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) def _generate_next_tag_suggestion(self, svn_path): """Generates suggested tag name based on latest v.X.X.X.X tag.""" # (Implementazione come discussa precedentemente) self.logger.debug("Generating next tag suggestion...") default_suggestion = "v.0.0.0.1" latest_valid_tag = None # Pattern più robusto: permette v. (numeri separati da punti) tag_pattern = re.compile(r"^v\.(\d+)\.(\d+)\.(\d+)\.(\d+)$") try: 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 for tag_name, _ in tags_data: # Itera sui tag (dal più recente) 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 if not latest_valid_tag: self.logger.debug("No tags matched the pattern v.X.X.X.X. Suggesting default.") return default_suggestion match = tag_pattern.match(latest_valid_tag) if not match: self.logger.error(f"Internal error: Could not re-match tag {latest_valid_tag}") return default_suggestion # Incrementa i numeri, assumendo formato v1.v2.v3.v4 # Qui usiamo la logica con riporto a 99 come richiesto v1, v2, v3, v4 = map(int, match.groups()) limit = 99 # Limite per il riporto v4 += 1 if v4 > limit: v4 = 0 v3 += 1 if v3 > limit: v3 = 0 v2 += 1 if v2 > limit: v2 = 0 v1 += 1 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 def create_tag(self): """Handles 'Create Tag' (Synchronous after dialog), suggesting name and committing first.""" self.logger.info("--- Action Triggered: Create Tag ---") svn_path = self._get_and_validate_svn_path("Create Tag") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create tag failed: Invalid path.") return # Genera suggerimento PRIMA di aprire il dialogo if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Generating tag suggestion...") suggested_tag = self._generate_next_tag_suggestion(svn_path) if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Ready for tag input.") # Aggiorna dopo il calcolo # Ottieni input utente (Dialog sincrono) dialog = CreateTagDialog(self.master, suggested_tag_name=suggested_tag) tag_info = dialog.result # Attende chiusura dialogo if tag_info: tag_name, tag_message = tag_info self.logger.info(f"User provided tag name: '{tag_name}', message: '{tag_message[:50]}...'") # Non serve più salvare il profilo o ottenere commit_message dalla GUI # commit_message = self.main_frame.get_commit_message() # Rimosso # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Processing: Creating tag '{tag_name}'...") status_final = "Ready." try: # Chiama ActionHandler che ora fa commit + tag success = self.action_handler.execute_create_tag( svn_path, None, # Passa None per il messaggio commit GUI (ignorato) tag_name, tag_message ) if success: status_final = f"Tag '{tag_name}' created successfully." self.main_frame.show_info("Success", status_final) # Aggiorna UI dopo successo self.refresh_tag_list() self.refresh_commit_history() self.refresh_branch_list() self.refresh_changed_files_list() # Commit ha pulito i cambiamenti except (GitCommandError, ValueError) as e: self.logger.error(f"Failed create tag '{tag_name}': {e}", exc_info=True) status_final = f"Error creating tag: {type(e).__name__}" self.main_frame.show_error("Tag Error", f"Failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected tag error: {e}") status_final = "Error: Unexpected tag failure." self.main_frame.show_error("Error", f"Unexpected:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) else: self.logger.info("Tag creation cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("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: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Checkout failed: Invalid path.") return selected_tag = self.main_frame.get_selected_tag() if not selected_tag: self.main_frame.show_error("Selection Error", "No tag selected from the list.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Checkout failed: No tag selected.") return # Conferma utente (importante per checkout!) confirm_msg = (f"This will checkout the tag '{selected_tag}', potentially overwriting " "local changes and entering a 'detached HEAD' state.\n\n" "Make sure you have committed or stashed any important work.\n\n" "Proceed with checkout?") if not self.main_frame.ask_yes_no("Confirm Checkout Tag", confirm_msg): self.logger.info("Tag checkout cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Tag checkout cancelled.") return # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Processing: Checking out tag '{selected_tag}'...") status_final = "Ready." try: # ActionHandler ora dovrebbe verificare cambiamenti prima del checkout success = self.action_handler.execute_checkout_tag(svn_path, selected_tag) if success: status_final = f"Checked out tag '{selected_tag}' (Detached HEAD)." self.main_frame.show_info( "Checkout Successful", status_final + "\n\nNOTE: You are now in a 'detached HEAD' state." ) # Aggiorna UI dopo successo self.refresh_branch_list() # Mostra stato detached self.refresh_commit_history() self.refresh_changed_files_list() # Checkout cambia i file except ValueError as e: # Cattura errore "Uncommitted changes" da ActionHandler self.logger.error(f"Checkout tag '{selected_tag}' failed: {e}", exc_info=False) # Meno verboso per questo errore comune status_final = f"Checkout failed: Uncommitted changes." self.main_frame.show_warning("Checkout Blocked", f"{e}\nPlease commit or stash changes first.") except GitCommandError as e: self.logger.error(f"Failed checkout tag '{selected_tag}': {e}", exc_info=True) status_final = f"Error checking out tag: {type(e).__name__}" self.main_frame.show_error("Checkout Error", f"Checkout failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected checkout tag error: {e}") status_final = "Error: Unexpected checkout failure." self.main_frame.show_error("Error", f"Unexpected:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- Branch Management Callbacks (Synchronous) --- def refresh_branch_list(self): """Refreshes the branch list (Synchronous).""" self.logger.info("--- Action Triggered: Refresh Branches ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Refreshing branches...") svn_path = self._get_and_validate_svn_path("Refresh Branches") branches, current = [], None # Default status_final = "Ready." if svn_path and self._is_repo_ready(svn_path): try: branches, current = self.git_commands.list_branches(svn_path) count = len(branches) current_display = current if current else "None (Detached?)" self.logger.info(f"Branch list refreshed ({count} found). Current: {current_display}") status_final = f"Branches refreshed ({count} found). Current: {current_display}" except Exception as e: self.logger.error(f"Failed refresh branches: {e}", exc_info=True) status_final = "Error refreshing branches." self.main_frame.show_error("Error", f"Could not refresh branches:\n{e}") branches = ["(Error loading branches)"] else: self.logger.debug("Refresh Branches skipped: Repository not ready.") status_final = "Ready (Repository not ready)." branches = ["(Repository not ready)"] # Aggiorna GUI if hasattr(self.main_frame, "update_branch_list"): self.main_frame.update_branch_list(branches, current) # Aggiorna anche il filtro nella scheda History if hasattr(self.main_frame, "update_history_branch_filter"): # Passa lista pulita senza messaggi di errore/stato clean_branches = [b for b in branches if not b.startswith("(")] self.main_frame.update_history_branch_filter(clean_branches or [], current) if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) 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: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Create branch failed: Invalid path.") return if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Ready for new branch input.") # Ottieni input utente (Dialog sincrono) dialog = CreateBranchDialog(self.master) new_branch_name = dialog.result # Attende chiusura if new_branch_name: self.logger.info(f"User provided new branch name: '{new_branch_name}'") # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Processing: Creating branch '{new_branch_name}'...") status_final = "Ready." checkout_after = False try: success = self.action_handler.execute_create_branch( svn_path, new_branch_name ) if success: status_final = f"Branch '{new_branch_name}' created." self.main_frame.show_info("Success", status_final) # Aggiorna lista branch immediatamente self.refresh_branch_list() # Chiedi se fare checkout if self.main_frame.ask_yes_no( "Checkout New Branch?", f"Switch to the newly created branch '{new_branch_name}'?", ): checkout_after = True # Segna per fare checkout dopo else: # Aggiorna altre viste anche se non fai checkout self.refresh_commit_history() except (GitCommandError, ValueError) as e: self.logger.error(f"Failed create branch '{new_branch_name}': {e}", exc_info=True) status_final = f"Error creating branch: {type(e).__name__}" self.main_frame.show_error("Error", f"Create failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected create branch: {e}") status_final = "Error: Unexpected create branch failure." self.main_frame.show_error("Error", f"Unexpected:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # Esegui checkout se richiesto e la creazione è andata a buon fine if checkout_after and status_final.startswith("Branch"): self.checkout_branch( branch_to_checkout=new_branch_name, repo_path_override=svn_path # Passa il percorso già validato ) else: self.logger.info("Branch creation cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Branch creation cancelled.") def checkout_branch(self, branch_to_checkout=None, repo_path_override=None): """Handles 'Checkout Selected Branch' (Synchronous). Can be called internally after create_branch.""" log_target = branch_to_checkout if branch_to_checkout else "Selected" self.logger.info(f"--- Action Triggered: Checkout Branch (Target: {log_target}) ---") # Ottieni percorso repo (usa override se fornito) svn_path = repo_path_override or self._get_and_validate_svn_path("Checkout Branch") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Checkout failed: Invalid path.") return # Determina il branch da checkouttare selected_branch = branch_to_checkout # Usa quello passato se c'è needs_confirmation = False if not selected_branch: # Altrimenti prendilo dalla GUI selected_branch = self.main_frame.get_selected_branch() needs_confirmation = True # Richiedi conferma solo se l'utente ha cliccato if not selected_branch: self.main_frame.show_error("Selection Error", "No branch selected from the list.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Checkout failed: No branch selected.") return # Conferma (solo se triggerato da click utente) if needs_confirmation: if not self.main_frame.ask_yes_no( "Confirm Checkout Branch", f"Switch to branch '{selected_branch}'?" ): self.logger.info("Branch checkout cancelled by user.") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Branch checkout cancelled.") return # Esecuzione sincrona if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(f"Processing: Checking out branch '{selected_branch}'...") status_final = "Ready." try: # ActionHandler ora dovrebbe verificare cambiamenti prima dello switch success = self.action_handler.execute_switch_branch(svn_path, selected_branch) if success: status_final = f"Switched to branch '{selected_branch}'." self.main_frame.show_info("Success", status_final) # Aggiorna UI dopo successo self.refresh_branch_list() # Aggiorna la lista e la selezione corrente self.refresh_commit_history() # La storia potrebbe cambiare self.refresh_tag_list() # I tag visibili potrebbero cambiare (anche se raramente) self.refresh_changed_files_list() # Lo stato dei file cambia except ValueError as e: # Cattura errore "Uncommitted changes" da ActionHandler self.logger.error(f"Checkout branch '{selected_branch}' failed: {e}", exc_info=False) status_final = f"Checkout failed: Uncommitted changes." self.main_frame.show_warning("Checkout Blocked", f"{e}\nPlease commit or stash changes first.") except GitCommandError as e: self.logger.error(f"Failed checkout branch '{selected_branch}': {e}", exc_info=True) status_final = f"Error checking out branch: {type(e).__name__}" self.main_frame.show_error("Error", f"Checkout failed:\n{e}") except Exception as e: self.logger.exception(f"Unexpected checkout branch error: {e}") status_final = "Error: Unexpected checkout failure." self.main_frame.show_error("Error", f"Unexpected:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- History Method (Synchronous) --- def refresh_commit_history(self): """Refreshes the commit history display (Synchronous).""" self.logger.info("--- Action Triggered: Refresh History ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Refreshing history...") svn_path = self._get_and_validate_svn_path("Refresh History") log_data = [] # Default status_final = "Ready." branch_filter = None # Per il messaggio di stato if svn_path and self._is_repo_ready(svn_path): # Ottieni filtro da GUI 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 log_scope = f"'{branch_filter}'" if branch_filter else "All History" self.logger.debug(f"Refreshing history for filter: {log_scope}") try: # Ottieni log da GitCommands log_data = self.git_commands.get_commit_log( svn_path, max_count=200, branch=branch_filter ) count = len(log_data) self.logger.info(f"History refreshed ({count} entries for {log_scope}).") status_final = f"History refreshed ({count} entries for {log_scope})." except Exception as e: self.logger.error(f"Failed refresh history: {e}", exc_info=True) status_final = "Error refreshing history." self.main_frame.show_error("Error", f"Could not refresh history:\n{e}") log_data = ["(Error loading history)"] else: self.logger.debug("Refresh History skipped: Repository not ready.") status_final = "Ready (Repository not ready)." log_data = ["(Repository not ready)"] # Aggiorna GUI if hasattr(self.main_frame, "update_history_display"): self.main_frame.update_history_display(log_data) if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- GUI State Utilities --- def _clear_and_disable_fields(self): """Clears input fields and disables widgets when no valid profile/repo.""" if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): mf = self.main_frame self.logger.debug("Clearing/disabling GUI fields due to invalid profile/repo.") # Pulisci campi testo 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, "backup_dir_var"): mf.backup_dir_var.set("") if hasattr(mf, "backup_exclude_extensions_var"): mf.backup_exclude_extensions_var.set("") if hasattr(mf, "backup_exclude_dirs_var"): mf.backup_exclude_dirs_var.set("") if hasattr(mf, "clear_commit_message"): mf.clear_commit_message() # Pulisci variabili booleane if hasattr(mf, "autobackup_var"): mf.autobackup_var.set(False) if hasattr(mf, "autocommit_var"): mf.autocommit_var.set(False) # Aggiorna stato widget backup dir if hasattr(mf, "toggle_backup_dir"): mf.toggle_backup_dir() # Pulisci liste 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([]) if hasattr(mf, "update_changed_files_list"): mf.update_changed_files_list([]) # Disabilita la maggior parte dei controlli chiamando update_svn_status_indicator # con un percorso vuoto (che risulterà in is_repo_ready=False) self.update_svn_status_indicator("") # Disabilita anche i pulsanti specifici del profilo 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) # Aggiorna status bar self.main_frame.update_status_bar("No profile selected or repository not ready.") def show_fatal_error(self, message): """Shows a fatal error message box and attempts to close gracefully.""" self.logger.critical(f"FATAL ERROR: {message}") try: # Prova a mostrare il messaggio usando il master Toplevel se esiste parent = None if hasattr(self, "master") and self.master and self.master.winfo_exists(): parent = self.master messagebox.showerror("Fatal Error", message, parent=parent) except Exception as e: # Fallback se anche la messagebox fallisce (es. Tk non inizializzato) print(f"FATAL ERROR (GUI message failed: {e}): {message}") finally: # Tenta di distruggere la finestra master in ogni caso if hasattr(self, "master") and self.master and self.master.winfo_exists(): try: self.master.destroy() except: pass # Ignora errori durante la distruzione def add_selected_file(self, file_status_line): """Handles the 'Add to Staging Area' action from the context menu.""" self.logger.info(f"--- Action Triggered: Add File for '{file_status_line}' ---") if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Processing: Adding file to staging...") # Ottieni percorso repo svn_path = self._get_and_validate_svn_path("Add File") if not svn_path: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar("Add failed: Invalid path.") return # Estrai percorso relativo pulito # Usa la stessa logica di open_diff_viewer o il metodo da diff_viewer relative_path = "" try: # Assumi che _clean_relative_path sia in DiffViewerWindow, potremmo duplicarlo # o creare un helper comune se diventa troppo ripetitivo. # Per ora usiamo una logica simile a quella in open_diff_viewer line = file_status_line.strip('\x00').strip() if line.startswith("??") and len(line) > 3: relative_path = line[3:].strip() if relative_path.startswith('"') and relative_path.endswith('"'): relative_path = relative_path[1:-1] else: # Se non inizia con '??', l'opzione non doveva essere attiva. Logga errore. self.logger.error(f"Add action triggered for non-untracked file: '{file_status_line}'") if hasattr(self, 'main_frame'): self.main_frame.show_error("Error", "Cannot add non-untracked file.") self.main_frame.update_status_bar("Add failed: File not untracked.") return if not relative_path: raise ValueError("Could not extract file path.") except Exception as e: self.logger.error(f"Error parsing file path for add: {e}") if hasattr(self, 'main_frame'): self.main_frame.show_error("Error", f"Could not determine file to add from:\n{file_status_line}") self.main_frame.update_status_bar("Add failed: Parse error.") return # Esegui il comando git add status_final = "Ready." try: success = self.git_commands.add_file(svn_path, relative_path) if success: status_final = f"File '{os.path.basename(relative_path)}' added to staging." self.logger.info(status_final) # Aggiorna la lista dei file modificati per mostrare il nuovo stato self.refresh_changed_files_list() # else: add_file solleverà eccezione in caso di fallimento except (GitCommandError, ValueError) as e: self.logger.error(f"Failed to add file '{relative_path}': {e}", exc_info=True) status_final = f"Error adding file: {type(e).__name__}" if hasattr(self, 'main_frame'): self.main_frame.show_error("Add Error", f"Failed to add file:\n{e}") except Exception as e: self.logger.exception(f"Unexpected error adding file '{relative_path}': {e}") status_final = "Error: Unexpected add failure." if hasattr(self, 'main_frame'): self.main_frame.show_error("Error", f"Unexpected error:\n{e}") finally: if hasattr(self, 'main_frame'): self.main_frame.update_status_bar(status_final) # --- 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 per gestione errori app = None try: logger.debug("Creating Tkinter root window...") root = tk.Tk() # Imposta dimensioni minime per assicurare visibilità elementi base root.minsize(800, 700) # Aumentato leggermente per nuova lista file logger.info("Tkinter root window created.") # Crea l'applicazione principale logger.debug("Initializing GitSvnSyncApp...") app = GitSvnSyncApp(root) logger.debug("GitSvnSyncApp initialization attempt complete.") # Controlla se l'app (e la GUI) è stata inizializzata correttamente # prima di avviare mainloop. Necessario se __init__ può fallire e ritornare. if hasattr(app, "main_frame") and app.main_frame and app.main_frame.winfo_exists(): 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. Exiting." ) # Se root esiste ancora (errore dopo creazione root ma prima di mainloop), distruggila. if root and root.winfo_exists(): root.destroy() except Exception as e: logger.exception("Fatal error during application startup or main loop.") # Tenta di mostrare un errore finale all'utente, ma potrebbe fallire try: 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}") # Stampa su console come ultima risorsa finally: logging.info("Application exiting.") if __name__ == "__main__": main()