# --- START OF FILE GitUtility.py --- import os import datetime import tkinter as tk from tkinter import messagebox, filedialog import logging import re import threading import queue import traceback # Per log eccezioni in main # 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 # Importa la nuova gestione log basata su coda import log_handler # Importa solo la funzione per configurare il file logger from logger_config import setup_file_logging # Importa GUI from gui import ( MainFrame, GitignoreEditorWindow, CreateTagDialog, CreateBranchDialog, ) from diff_viewer import DiffViewerWindow except ImportError as e: critical_msg = f"Critical Error: Failed to import required application modules: {e}" print(f"FATAL IMPORT ERROR: {critical_msg}") try: root = tk.Tk() root.withdraw() messagebox.showerror( "Startup Error", f"Failed to load components:\n{e}\n\nApplication cannot start.", ) root.destroy() except: pass exit(1) class GitSvnSyncApp: """ Main application class for the Git Sync Tool. Orchestrates GUI and backend actions using asynchronous operations and a centralized logging queue. """ LOG_QUEUE_CHECK_INTERVAL_MS = 100 # Poll log queue every 100ms ASYNC_QUEUE_CHECK_INTERVAL_MS = 100 # Poll result queues every 100ms def __init__(self, master): """Initializes the application.""" self.master = master master.title("Git Sync Tool (Bundle Manager)") master.protocol("WM_DELETE_WINDOW", self.on_closing) # Log iniziale (console) print("Initializing GitSvnSyncApp...") # --- Initialize Core Components (NO logger passed) --- try: # Pass None for logger; components will use log_handler directly self.config_manager = ConfigManager(None) self.git_commands = GitCommands(None) self.backup_handler = BackupHandler(None) # ActionHandler needs git_commands and backup_handler self.action_handler = ActionHandler( self.git_commands, self.backup_handler ) print("Core components initialized.") except Exception as e: print(f"FATAL: Failed to initialize core components: {e}") self.show_fatal_error( f"Initialization Error:\n{e}\n\nApplication cannot start." ) return # Stop initialization # --- Initialize GUI --- try: print("Creating MainFrame...") self.main_frame = MainFrame( master, # Callbacks (connection points between GUI and this class) 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, 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, open_gitignore_editor_cb=self.open_gitignore_editor, manual_backup_cb=self.manual_backup, commit_changes_cb=self.commit_changes, refresh_tags_cb=self.refresh_tag_list, create_tag_cb=self.create_tag, checkout_tag_cb=self.checkout_tag, refresh_branches_cb=self.refresh_branch_list, checkout_branch_cb=self.checkout_branch, create_branch_cb=self.create_branch, refresh_history_cb=self.refresh_commit_history, refresh_changed_files_cb=self.refresh_changed_files_list, open_diff_viewer_cb=self.open_diff_viewer, # Stays sync add_selected_file_cb=self.add_selected_file, # Instances/Data config_manager_instance=self.config_manager, profile_sections_list=self.config_manager.get_profile_sections(), ) print("MainFrame GUI created.") except Exception as e: print(f"FATAL: Failed to initialize MainFrame GUI: {e}") self.show_fatal_error( f"GUI Initialization Error:\n{e}\n\nApplication cannot start." ) return # Stop initialization # --- Setup Logging Processing (File + Queue) --- self._setup_logging_processing() # Sets up file and starts queue polling # --- Log Application Start (via Queue) --- log_handler.log_info( "Git Sync Tool initialization sequence started.", func_name="init" ) # --- Initial Profile Load --- self._perform_initial_load() # Load default/first profile settings log_handler.log_info( "Git Sync Tool initialization sequence complete.", func_name="init" ) def _setup_logging_processing(self): """Configures file logging and starts the log queue processing loop.""" # 1. Configure file logging only. Level determines what goes to file. setup_file_logging(level=logging.DEBUG) # 2. Start the log queue polling loop if GUI widget exists if hasattr(self, "main_frame") and hasattr(self.main_frame, "log_text"): log_handler.log_info( "Starting log queue processing.", func_name="_setup_logging_processing" ) self.master.after(self.LOG_QUEUE_CHECK_INTERVAL_MS, self._process_log_queue) else: print( "ERROR: Cannot start log queue processing - GUI log widget not found." ) # Cannot use log_handler here reliably if GUI failed before this point def _process_log_queue(self): """Processes messages from the log queue to update file and GUI log.""" log_widget = getattr(self.main_frame, "log_text", None) if not log_widget or not log_widget.winfo_exists(): return # Stop processing if GUI is gone # Process all available messages processed_count = 0 while not log_handler.log_queue.empty(): if ( processed_count > 50 ): # Limit processing per cycle to prevent blocking GUI break try: log_entry = log_handler.log_queue.get_nowait() level = log_entry.get("level", logging.INFO) message = log_entry.get("message", "") level_name = log_handler.get_log_level_name(level) # 1. Write to root logger (handled by FileHandler setup in logger_config) # Ensures message goes to file if level allows logging.getLogger().log(level, message) processed_count += 1 # 2. Update GUI widget if level is appropriate for GUI (e.g., DEBUG and up) # We assume the GUI log widget always shows DEBUG level and up. if level >= logging.DEBUG: # Check level try: original_state = log_widget.cget("state") log_widget.config(state=tk.NORMAL) log_widget.insert(tk.END, message + "\n", (level_name,)) log_widget.see(tk.END) log_widget.config(state=original_state) except tk.TclError as e_gui: print( f"TclError updating log widget: {e_gui} - Message: {message}", file=sys.stderr, ) except Exception as e_gui: print( f"Error updating log widget: {e_gui} - Message: {message}", file=sys.stderr, ) except queue.Empty: break # Should not happen with check above, but safety first except Exception as e_proc: print(f"Error processing log queue item: {e_proc}", file=sys.stderr) # Reschedule the next check self.master.after(self.LOG_QUEUE_CHECK_INTERVAL_MS, self._process_log_queue) def _perform_initial_load(self): """Loads the initially selected profile settings.""" log_handler.log_debug( "Performing initial profile load.", func_name="_perform_initial_load" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): log_handler.log_error( "Cannot perform initial load: MainFrame not ready.", func_name="_perform_initial_load", ) return initial_profile = self.main_frame.profile_var.get() if initial_profile: self.load_profile_settings(initial_profile) else: log_handler.log_warning( "No initial profile set during load.", func_name="_perform_initial_load" ) self._clear_and_disable_fields() self.main_frame.update_status_bar("No profile selected.") def on_closing(self): """Handles the window close event.""" log_handler.log_info("Application closing initiated.", func_name="on_closing") if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): try: self.main_frame.update_status_bar("Exiting...") except Exception: pass if self.master and self.master.winfo_exists(): self.master.destroy() log_handler.log_info("Application closed.", func_name="on_closing") # --- Profile Management Callbacks (Sync, use log_handler) --- def load_profile_settings(self, profile_name): log_handler.log_info( f"Loading settings for profile: '{profile_name}'", func_name="load_profile_settings", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): log_handler.log_error( "Cannot load profile: Main frame not available.", func_name="load_profile_settings", ) return self.main_frame.update_status_bar(f"Loading profile: {profile_name}...") if ( not profile_name or profile_name not in self.config_manager.get_profile_sections() ): log_handler.log_warning( f"Profile '{profile_name}' invalid/not found.", func_name="load_profile_settings", ) self._clear_and_disable_fields() if profile_name: self.main_frame.show_error( "Profile Load Error", f"Profile '{profile_name}' not found." ) self.main_frame.update_status_bar( f"Error: Profile '{profile_name}' not found." if profile_name else "No profile selected." ) return cm = self.config_manager settings = { k: cm.get_profile_option(profile_name, k, fallback=d) for k, d in cm._get_expected_keys_with_defaults().items() } mf = self.main_frame status_final = f"Profile '{profile_name}' loaded." repo_path_for_refresh = "" try: mf.svn_path_entry.delete(0, tk.END) mf.svn_path_entry.insert(0, settings.get("svn_working_copy_path", "")) repo_path_for_refresh = 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", "") ) 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", "")) mf.toggle_backup_dir() mf.autocommit_var.set( str(settings.get("autocommit", "False")).lower() == "true" ) mf.clear_commit_message() if mf.commit_message_text.winfo_exists(): state = mf.commit_message_text.cget("state") if state == tk.DISABLED: mf.commit_message_text.config(state=tk.NORMAL) mf.commit_message_text.insert("1.0", settings.get("commit_message", "")) if state == tk.DISABLED: mf.commit_message_text.config(state=tk.DISABLED) log_handler.log_info( f"Applied settings from '{profile_name}' to GUI.", func_name="load_profile_settings", ) self.update_svn_status_indicator(repo_path_for_refresh) # Sync update if self._is_repo_ready(repo_path_for_refresh): log_handler.log_info( "Repo ready, triggering async refreshes.", func_name="load_profile_settings", ) # Trigger async calls - they will manage status bar self.refresh_tag_list() self.refresh_branch_list() self.refresh_commit_history() self.refresh_changed_files_list() else: log_handler.log_info( "Repo not ready, clearing lists.", func_name="load_profile_settings" ) 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(["(Repo not ready)"]) status_final = f"Profile '{profile_name}' loaded (Repo not ready)." mf.update_status_bar(status_final) # Update status now except Exception as e: log_handler.log_exception( f"Error applying settings for '{profile_name}': {e}", func_name="load_profile_settings", ) status_final = f"Error loading profile '{profile_name}'." mf.show_error("Profile Load Error", f"Failed to apply settings:\n{e}") mf.update_status_bar(status_final) def save_profile_settings(self): """Saves current GUI values to the selected profile (Synchronous).""" profile_name = self.main_frame.profile_var.get() if not profile_name: log_handler.log_warning( "Save failed: No profile selected.", func_name="save_profile_settings" ) if hasattr(self, "main_frame"): self.main_frame.update_status_bar("Save failed: No profile selected.") return False # ... (resto come prima, usando log_handler.log_*) ... log_handler.log_info( f"Saving settings for profile: '{profile_name}'", func_name="save_profile_settings", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): log_handler.log_error( "Cannot save profile: Main frame not available.", func_name="save_profile_settings", ) return False mf = self.main_frame cm = self.config_manager status_final = "Ready." success = False try: settings = { # Raccolta valori "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(), } for key, value in settings.items(): cm.set_profile_option( profile_name, key, str(value) if value is not None else "" ) cm.save_config() log_handler.log_info( f"Settings saved successfully for '{profile_name}'.", func_name="save_profile_settings", ) status_final = f"Profile '{profile_name}' saved." success = True except Exception as e: log_handler.log_exception( f"Error saving profile '{profile_name}': {e}", func_name="save_profile_settings", ) status_final = f"Error saving profile '{profile_name}'." mf.show_error("Save Error", f"Failed:\n{e}") success = False finally: mf.update_status_bar(status_final) return success def add_profile(self): """Handles adding a new profile (Synchronous).""" # ... (Logica invariata, ma usa log_handler.log_*) ... log_handler.log_debug("'Add Profile' button clicked.", func_name="add_profile") if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return self.main_frame.update_status_bar("Adding new profile...") name = self.main_frame.ask_new_profile_name() if not name: log_handler.log_info("Add profile cancelled.", func_name="add_profile") self.main_frame.update_status_bar("Add profile cancelled.") return name = name.strip() if not name: log_handler.log_warning("Add failed: Name empty.", func_name="add_profile") self.main_frame.show_error("Input Error", "Profile name cannot be empty.") self.main_frame.update_status_bar("Add failed: Empty name.") return if name in self.config_manager.get_profile_sections(): log_handler.log_warning( f"Add failed: '{name}' exists.", func_name="add_profile" ) self.main_frame.show_error("Error", f"Profile '{name}' already exists.") self.main_frame.update_status_bar(f"Add failed: '{name}' exists.") return log_handler.log_info( f"Attempting to add new profile: '{name}'", func_name="add_profile" ) status_final = "Ready." 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 key, value in defaults.items(): self.config_manager.set_profile_option(name, key, str(value)) self.config_manager.save_config() log_handler.log_info( f"Profile '{name}' added successfully.", func_name="add_profile" ) sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) self.main_frame.profile_var.set(name) # Trigger load except Exception as e: log_handler.log_exception( f"Error adding profile '{name}': {e}", func_name="add_profile" ) status_final = f"Error adding profile '{name}'." self.main_frame.show_error("Add Error", f"Failed:\n{e}") self.main_frame.update_status_bar(status_final) def remove_profile(self): """Handles removing the selected profile (Synchronous).""" # ... (Logica invariata, ma usa log_handler.log_*) ... log_handler.log_debug( "'Remove Profile' button clicked.", func_name="remove_profile" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return profile = self.main_frame.profile_var.get() if not profile: log_handler.log_warning( "Remove failed: No profile selected.", func_name="remove_profile" ) self.main_frame.show_error("Error", "No profile selected.") self.main_frame.update_status_bar("Remove failed: No profile.") return try: from config_manager import DEFAULT_PROFILE except ImportError: DEFAULT_PROFILE = "default" if profile == DEFAULT_PROFILE: log_handler.log_warning( "Attempt remove default denied.", func_name="remove_profile" ) self.main_frame.show_error( "Denied", f"Cannot remove default ('{DEFAULT_PROFILE}')." ) self.main_frame.update_status_bar("Cannot remove default.") return if self.main_frame.ask_yes_no("Confirm Remove", f"Remove profile '{profile}'?"): log_handler.log_info( f"Attempting remove profile: '{profile}'", func_name="remove_profile" ) self.main_frame.update_status_bar(f"Removing profile: {profile}...") status_final = "Ready." try: removed = self.config_manager.remove_profile_section(profile) if removed: self.config_manager.save_config() log_handler.log_info( f"Profile '{profile}' removed.", func_name="remove_profile" ) status_final = f"Profile '{profile}' removed." sections = self.config_manager.get_profile_sections() self.main_frame.update_profile_dropdown(sections) # Trigger load else: log_handler.log_error( f"Failed remove profile '{profile}' (backend).", func_name="remove_profile", ) status_final = f"Error removing profile '{profile}'." self.main_frame.show_error( "Error", f"Could not remove '{profile}'." ) self.main_frame.update_status_bar(status_final) except Exception as e: log_handler.log_exception( f"Error removing profile '{profile}': {e}", func_name="remove_profile", ) status_final = f"Error removing profile '{profile}'." self.main_frame.show_error("Error", f"Failed:\n{e}") self.main_frame.update_status_bar(status_final) else: log_handler.log_info( "Profile removal cancelled.", func_name="remove_profile" ) self.main_frame.update_status_bar("Removal cancelled.") # --- GUI Interaction & Helpers (Sync, use log_handler) --- def browse_folder(self, entry_widget): current_path = entry_widget.get() initial_dir = ( current_path if os.path.isdir(current_path) else os.path.expanduser("~") ) log_handler.log_debug( f"Opening folder browser. Initial: {initial_dir}", func_name="browse_folder" ) directory = filedialog.askdirectory( initialdir=initial_dir, title="Select Directory", parent=self.master ) if directory: log_handler.log_debug( f"Directory selected: {directory}", func_name="browse_folder" ) 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) # Trigger status update else: log_handler.log_debug("Folder browse cancelled.", func_name="browse_folder") def update_svn_status_indicator(self, svn_path): """ Checks repo status, updates GUI indicator, and enables/disables relevant action widgets (Synchronous update of widget states). """ is_valid_dir = bool(svn_path and os.path.isdir(svn_path)) is_repo_ready = is_valid_dir and os.path.exists(os.path.join(svn_path, ".git")) log_handler.log_debug(f"Updating status indicator. Path='{svn_path}', Valid={is_valid_dir}, Ready={is_repo_ready}", func_name="update_svn_status_indicator") if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return mf = self.main_frame mf.update_svn_indicator(is_repo_ready) # Update color/tooltip # --- Determine Widget States --- repo_ready_state = tk.NORMAL if is_repo_ready else tk.DISABLED valid_path_state = tk.NORMAL if is_valid_dir else tk.DISABLED prepare_state = tk.NORMAL if is_valid_dir and not is_repo_ready else tk.DISABLED # ... (logica fetch_button_state invariata) ... fetch_button_state = tk.DISABLED try: # Logic per fetch button 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_use_svn_dir=False if os.path.isdir(svn_path_str): if not os.listdir(svn_path_str): can_use_svn_dir=True elif svn_path_str: parent_dir=os.path.dirname(svn_path_str); can_use_svn_dir=(parent_dir and os.path.isdir(parent_dir)) or (not parent_dir) 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_use_svn_dir and bundle_file_exists): fetch_button_state = tk.NORMAL except Exception as e: log_handler.log_error(f"Error checking fetch state: {e}",func_name="update_svn_status_indicator"); fetch_button_state=tk.DISABLED # --- Update Widget States --- try: # ... (Aggiorna tutti gli altri widget come prima) ... 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) # ... etc per tutti gli altri widget ... # <<< MODIFICA: Non cancellare la lista changes qui se repo è pronto >>> if hasattr(mf, "changed_files_listbox"): # Cancella la lista SOLO se il repo NON è pronto. # Se è pronto, lascia che sia refresh_changed_files_list a popolarla. if repo_ready_state == tk.DISABLED: log_handler.log_debug("Repo not ready, clearing changes list via status update.", func_name="update_svn_status_indicator") mf.update_changed_files_list(["(Repository not ready)"]) # else: Non fare nulla qui se repo è pronto # <<< FINE MODIFICA >>> # ... (Aggiorna altri widget come prima) ... 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, mf.tag_listbox, mf.branch_listbox, mf.refresh_changes_button, mf.commit_button, mf.autocommit_checkbox, mf.commit_message_text, mf.edit_gitignore_button] # Lista aggiornata for widget in widgets_require_ready: name_attr=getattr(widget,'winfo_name',None) if name_attr and hasattr(mf,name_attr()): target=getattr(mf,name_attr()) if target and target.winfo_exists(): state=repo_ready_state; try: if isinstance(target,ttk.Combobox): target.config(state="readonly" if state==tk.NORMAL else tk.DISABLED) elif isinstance(target,(tk.Text,scrolledtext.ScrolledText)): target.config(state=state) elif isinstance(target,tk.Listbox): target.config(state=state) else: target.config(state=state) except tk.TclError: pass # Ignora errori Tcl rari except Exception as e: log_handler.log_error(f"Error updating widget states: {e}", func_name="update_svn_status_indicator") def _is_repo_ready(self, repo_path): return bool( repo_path and os.path.isdir(repo_path) and os.path.exists(os.path.join(repo_path, ".git")) ) def _parse_exclusions(self): # Usa log_handler exts = set() dirs = {".git", ".svn"} if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return exts, dirs mf = self.main_frame ext_str = mf.backup_exclude_extensions_var.get() dir_str = mf.backup_exclude_dirs_var.get() if ext_str: for ext in ext_str.split(","): clean = ext.strip().lower() if clean: exts.add("." + clean if not clean.startswith(".") else clean) if dir_str: for dname in dir_str.split(","): clean = dname.strip().lower().strip(os.path.sep + "/") if clean and clean not in {".", ".."}: dirs.add(clean) log_handler.log_debug( f"Parsed Exclusions - Exts: {exts}, Dirs: {dirs}", func_name="_parse_exclusions", ) return exts, dirs def _get_and_validate_svn_path(self, op="Op"): # Usa log_handler if not hasattr(self, "main_frame") or not hasattr( mf := self.main_frame, "svn_path_entry" ): log_handler.log_error(f"{op} failed: SVN entry missing.", func_name="v_svn") return None p = mf.svn_path_entry.get().strip() if not p: log_handler.log_warning(f"{op} failed: Path empty.", func_name="v_svn") mf.show_error("Error", "WD Path empty.") return None ap = os.path.abspath(p) if not os.path.isdir(ap): log_handler.log_warning(f"{op} failed: Not dir: {ap}", func_name="v_svn") mf.show_error("Error", f"Not dir:\n{ap}") return None log_handler.log_debug(f"{op}: Using validated WD path: {ap}", func_name="v_svn") return ap def _get_and_validate_usb_path(self, op="Op"): # Usa log_handler if not hasattr(self, "main_frame") or not hasattr( mf := self.main_frame, "usb_path_entry" ): log_handler.log_error(f"{op} failed: USB entry missing.", func_name="v_usb") return None p = mf.usb_path_entry.get().strip() if not p: log_handler.log_warning(f"{op} failed: Path empty.", func_name="v_usb") mf.show_error("Error", "Bundle Target Path empty.") return None ap = os.path.abspath(p) if not os.path.isdir(ap): log_handler.log_warning(f"{op} failed: Not dir: {ap}", func_name="v_usb") mf.show_error("Error", f"Not dir:\n{ap}") return None log_handler.log_debug( f"{op}: Using validated Bundle Target path: {ap}", func_name="v_usb" ) return ap def _clear_and_disable_fields(self): # Usa log_handler if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return mf = self.main_frame log_handler.log_debug("Clearing/disabling fields.", func_name="_clear") if hasattr(mf, "svn_path_entry"): mf.svn_path_entry.delete(0, tk.END) # etc... (altri clear) 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, "clear_commit_message"): mf.clear_commit_message() 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, "autobackup_var"): mf.autobackup_var.set(False) if hasattr(mf, "autocommit_var"): mf.autocommit_var.set(False) if hasattr(mf, "toggle_backup_dir"): mf.toggle_backup_dir() 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([]) self.update_svn_status_indicator("") 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) mf.update_status_bar("No profile or repo not ready.") def show_fatal_error( self, message ): # Usa log_handler (anche se potrebbe fallire qui) log_handler.log_critical( f"FATAL ERROR: {message}", func_name="show_fatal_error" ) 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}", file=sys.stderr) finally: self.on_closing() # --- ==== ASYNCHRONOUS ACTION IMPLEMENTATIONS ==== --- def _start_async_operation(self, worker_func, args_tuple, context_dict): """Generic helper to start an async operation with UI feedback.""" # ... (controllo main_frame esistente) ... if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): log_handler.log_error("Cannot start async op: Main frame missing.", func_name="_start_async_operation") return context_name = context_dict.get('context', 'unknown_op') status_msg = context_dict.get('status_msg', context_name) log_handler.log_info(f"--- Action Triggered: {context_name} (Async Queue) ---", func_name=context_name) # --- Update UI: Disable widgets and set PROCESSING status --- self.main_frame.set_action_widgets_state(tk.DISABLED) # <<< MODIFICA: Imposta colore giallo per "in corso" >>> self.main_frame.update_status_bar(f"Processing: {status_msg}...", bg_color=self.main_frame.STATUS_YELLOW) results_queue = queue.Queue(maxsize=1) # --- Start Worker Thread --- full_args = args_tuple + (results_queue,) log_handler.log_debug(f"Creating worker thread for {context_name}.", func_name="_start_async_operation") worker_thread = threading.Thread(target=worker_func, args=full_args, daemon=True) log_handler.log_debug(f"Starting worker thread for {context_name}.", func_name="_start_async_operation") worker_thread.start() # --- Schedule Completion Check --- log_handler.log_debug(f"Scheduling completion check for {context_name}.", func_name="_start_async_operation") self.master.after( self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context_dict ) # --- Specific Action Wrappers --- def refresh_tag_list(self): svn_path = self._get_and_validate_svn_path("Refresh Tags") if not svn_path or not self._is_repo_ready(svn_path): log_handler.log_debug( "Refresh Tags skipped: Repo not ready.", func_name="refresh_tag_list" ) self.main_frame.update_tag_list([("(Repo not ready)", "")]) self.main_frame.update_status_bar("Ready (Repo not ready).") return self._start_async_operation( self._run_refresh_tags_async, (svn_path,), {"context": "refresh_tags", "status_msg": "Refreshing tags"}, ) def refresh_branch_list(self): svn_path = self._get_and_validate_svn_path("Refresh Branches") if not svn_path or not self._is_repo_ready(svn_path): log_handler.log_debug( "Refresh Branches skipped: Repo not ready.", func_name="refresh_branch_list", ) self.main_frame.update_branch_list([], None) self.main_frame.update_history_branch_filter([]) self.main_frame.update_status_bar("Ready (Repo not ready).") return self._start_async_operation( self._run_refresh_branches_async, (svn_path,), {"context": "refresh_branches", "status_msg": "Refreshing branches"}, ) def refresh_commit_history(self): svn_path = self._get_and_validate_svn_path("Refresh History") if not svn_path or not self._is_repo_ready(svn_path): log_handler.log_debug( "Refresh History skipped: Repo not ready.", func_name="refresh_commit_history", ) self.main_frame.update_history_display([]) self.main_frame.update_status_bar("Ready (Repo not ready).") return branch_filter = None log_scope = "All History" if hasattr(self.main_frame, "history_branch_filter_var"): filter_sel = self.main_frame.history_branch_filter_var.get() if filter_sel and filter_sel != "-- All History --": branch_filter = filter_sel log_scope = f"'{branch_filter}'" self._start_async_operation( self._run_refresh_history_async, (svn_path, branch_filter, log_scope), { "context": "refresh_history", "status_msg": f"Refreshing history for {log_scope}", }, ) def refresh_changed_files_list(self): svn_path = self._get_and_validate_svn_path("Refresh Changed Files") if not svn_path or not self._is_repo_ready(svn_path): log_handler.log_debug( "Refresh Changes skipped: Repo not ready.", func_name="refresh_changed" ) self.main_frame.update_changed_files_list(["(Repo not ready)"]) self.main_frame.update_status_bar("Ready (Repo not ready).") return self._start_async_operation( self._run_refresh_changes_async, (svn_path,), {"context": "refresh_changes", "status_msg": "Refreshing changed files"}, ) def open_diff_viewer(self, file_status_line): """Opens the Diff Viewer window for the selected file (Synchronous GUI action).""" func_name = "open_diff_viewer" log_handler.log_info(f"--- Action Triggered: Open Diff Viewer for '{file_status_line}' ---", func_name=func_name) # Controlla se main_frame esiste if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): log_handler.log_error("Cannot open diff viewer: Main frame not available.", func_name=func_name) return # Aggiorna status bar iniziale self.main_frame.update_status_bar("Processing: Opening diff viewer...") # Ottieni e valida il percorso del repository svn_path = self._get_and_validate_svn_path("Open Diff Viewer") if not svn_path: # Errore già loggato e mostrato da _get_and_validate_svn_path self.main_frame.update_status_bar("Error: Cannot open diff (invalid repo path).") return # --- Validazione preliminare della riga di stato --- # Pulisci eventuali caratteri null e spazi esterni cleaned_line = file_status_line.strip('\x00').strip() if not cleaned_line or len(cleaned_line) < 2: # Deve avere almeno codice stato + spazio log_handler.log_warning(f"Invalid status line received for diff: '{file_status_line}'", func_name=func_name) self.main_frame.show_warning("Diff Error", "Invalid file status line selected.") self.main_frame.update_status_bar("Error: Invalid selection for diff.") return # Estrai codice stato (prime due colonne) status_code = cleaned_line[:2].strip() # Impedisci diff per stati non appropriati (Untracked, Ignored, Deleted) # 'D ' (Deleted) è problematico perché il file non c'è nel workdir # '??' (Untracked) e '!!' (Ignored) non hanno una versione in HEAD da confrontare if status_code in ["??", "!!", "D"]: # Tenta di estrarre il path solo per il messaggio di errore display_path = "(Could not parse path)" try: # Usa la logica di pulizia (potrebbe essere necessario adattarla o estrarla) # Assumiamo una logica semplice qui per il messaggio if "->" in cleaned_line: # Gestione rinomine display_path = cleaned_line.split("->")[-1].strip().strip('"') else: display_path = cleaned_line[len(status_code):].lstrip().strip('"') except Exception: pass msg = f"Cannot show diff for file with status '{status_code}':\n{display_path}\n\n(Untracked, Ignored, or Deleted files cannot be diffed against HEAD)." log_handler.log_info(f"Diff not applicable for status '{status_code}'.", func_name=func_name) self.main_frame.show_info("Diff Not Applicable", msg) self.main_frame.update_status_bar("Ready (Diff not applicable).") return # --- Apri la finestra DiffViewer --- log_handler.log_debug(f"Opening DiffViewerWindow with status line: '{file_status_line}'", func_name=func_name) status_final = "Ready." # Status di default dopo chiusura finestra try: # La creazione della finestra Toplevel è sincrona # DiffViewerWindow gestirà il caricamento del contenuto internamente DiffViewerWindow( self.master, # Parent window self.git_commands, # Istanza GitCommands svn_path, # Percorso repo validato file_status_line # Riga di stato originale # Nota: Non passiamo più il logger ) # Il codice qui attende la chiusura della finestra DiffViewer log_handler.log_debug("Diff viewer window closed.", func_name=func_name) status_final = "Ready." except Exception as e: # Gestisci errori durante l'inizializzazione o l'esecuzione di DiffViewerWindow log_handler.log_exception(f"Error opening or running diff viewer: {e}", func_name=func_name) status_final = "Error: Failed to open diff viewer." self.main_frame.show_error("Diff Viewer Error", f"Could not display diff:\n{e}") finally: # Aggiorna status bar finale self.main_frame.update_status_bar(status_final) def prepare_svn_for_git(self): svn_path = self._get_and_validate_svn_path("Prepare Repository") if not svn_path: self.main_frame.update_status_bar("Prepare failed: Invalid path.") return # Add check if already prepared before starting async? Optional. # if self._is_repo_ready(svn_path): self.main_frame.show_warning("Info", "Repo already prepared."); self.update_svn_status_indicator(svn_path); return self._start_async_operation( self._run_prepare_async, (svn_path,), {"context": "prepare_repo", "status_msg": "Preparing repository"}, ) def create_git_bundle(self): profile = self.main_frame.profile_var.get() svn_path = self._get_and_validate_svn_path("Create Bundle") usb_path = self._get_and_validate_usb_path("Create Bundle") bundle_name = self.main_frame.bundle_name_entry.get().strip() if not profile or not svn_path or not usb_path or not bundle_name: return if not bundle_name.lower().endswith(".bundle"): bundle_name += ".bundle" bundle_full_path = os.path.join(usb_path, bundle_name) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Warning", "Could not save profile.\nContinue?" ): self.main_frame.update_status_bar("Cancelled.") return exts, dirs = self._parse_exclusions() backup = self.main_frame.autobackup_var.get() bk_dir = self.main_frame.backup_dir_var.get() commit = self.main_frame.autocommit_var.get() msg = self.main_frame.get_commit_message() args = ( svn_path, bundle_full_path, profile, backup, bk_dir, commit, msg, exts, dirs, ) self._start_async_operation( self._run_create_bundle_async, args, { "context": "create_bundle", "status_msg": "Creating bundle", "committed_flag_possible": True, }, ) # Mark commit possible def fetch_from_git_bundle(self): profile = self.main_frame.profile_var.get() svn_path_str = self.main_frame.svn_path_entry.get().strip() usb_path = self._get_and_validate_usb_path("Fetch Bundle") bundle_name = self.main_frame.bundle_updated_name_entry.get().strip() if not profile or not svn_path_str or not usb_path or not bundle_name: return bundle_full_path = os.path.join(usb_path, bundle_name) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Warning", "Could not save profile.\nContinue?" ): self.main_frame.update_status_bar("Cancelled.") return exts, dirs = self._parse_exclusions() backup = self.main_frame.autobackup_var.get() bk_dir = self.main_frame.backup_dir_var.get() args = (svn_path_str, bundle_full_path, profile, backup, bk_dir, exts, dirs) self._start_async_operation( self._run_fetch_bundle_async, args, { "context": "fetch_bundle", "status_msg": "Fetching from bundle", "repo_path": svn_path_str, }, ) def manual_backup(self): profile = self.main_frame.profile_var.get() svn_path = self._get_and_validate_svn_path(f"Backup ({profile})") bk_dir_str = self.main_frame.backup_dir_var.get().strip() if not profile or not svn_path or not bk_dir_str: return bk_dir = os.path.abspath(bk_dir_str) if not self.save_profile_settings(): if not self.main_frame.ask_yes_no( "Warning", "Could not save profile.\nContinue?" ): self.main_frame.update_status_bar("Cancelled.") return exts, dirs = self._parse_exclusions() args = (svn_path, bk_dir, profile, exts, dirs) self._start_async_operation( self._run_manual_backup_async, args, {"context": "manual_backup", "status_msg": "Creating manual backup"}, ) def commit_changes(self): svn_path = self._get_and_validate_svn_path("Commit") msg = self.main_frame.get_commit_message() if not svn_path or not msg: return args = (svn_path, msg) self._start_async_operation( self._run_commit_async, args, { "context": "commit", "status_msg": "Committing changes", "committed_flag_possible": True, }, ) def open_gitignore_editor(self): # Sync GUI part log_handler.log_info( "--- Action: Edit .gitignore ---", func_name="open_gitignore_editor" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return self.main_frame.update_status_bar("Opening .gitignore editor...") svn_path = self._get_and_validate_svn_path("Edit .gitignore") if not svn_path or not self._is_repo_ready(svn_path): log_handler.log_warning( "Cannot edit: Repo path invalid/not ready.", func_name="open_gitignore" ) self.main_frame.show_error("Error", "Select valid/prepared repo.") self.main_frame.update_status_bar("Edit failed: Repo not ready.") return gitignore_path = os.path.join(svn_path, ".gitignore") log_handler.log_debug(f"Target: {gitignore_path}", func_name="open_gitignore") status = "Ready." try: log_handler.log_debug( "Opening editor window...", func_name="open_gitignore" ) GitignoreEditorWindow( self.master, gitignore_path, None, on_save_success_callback=self._handle_gitignore_save_async, ) log_handler.log_debug("Editor closed.", func_name="open_gitignore") if not self.main_frame.status_bar_var.get().startswith("Processing"): status = "Ready." except Exception as e: log_handler.log_exception(f"Error editing: {e}", func_name="open_gitignore") status = "Error editing." self.main_frame.show_error("Editor Error", f"Error:\n{e}") finally: if not self.main_frame.status_bar_var.get().startswith("Processing"): self.main_frame.update_status_bar(status) def _handle_gitignore_save_async(self): # Starts async untrack check log_handler.log_info( "Callback: .gitignore saved. Starting async untrack check.", func_name="_handle_gitignore_save_async", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = self._get_and_validate_svn_path("Untrack Check") if not svn_path: log_handler.log_error( "Cannot untrack: Invalid path.", func_name="_handle_gitignore_save_async", ) self.main_frame.update_status_bar("Error: Untrack failed (path).") return args = (svn_path,) self._start_async_operation( self._run_untrack_async, args, { "context": "_handle_gitignore_save_async", "status_msg": "Checking files to untrack", "committed_flag_possible": True, }, ) def add_selected_file(self, file_status_line): log_handler.log_info( f"--- Action: Add File for '{file_status_line}' (Async) ---", func_name="add_selected_file", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = self._get_and_validate_svn_path("Add File") if not svn_path: self.main_frame.update_status_bar("Add failed: Invalid path.") return relative_path = "" try: # Extract path line = file_status_line.strip("\x00").strip() if line.startswith("??"): rp_raw = line[2:].lstrip() relative_path = ( rp_raw[1:-1] if len(rp_raw) >= 2 and rp_raw.startswith('"') and rp_raw.endswith('"') else rp_raw ) if not relative_path: raise ValueError("Extracted path empty.") if not line.startswith("??"): log_handler.log_error( f"Add invalid for non-untracked: {line}", func_name="add_selected_file", ) self.main_frame.show_error("Error", "Cannot add non-untracked file.") self.main_frame.update_status_bar("Add failed: Not untracked.") return except Exception as e: log_handler.log_error( f"Error parsing path for add: {e}", func_name="add_selected_file" ) self.main_frame.show_error("Error", f"Cannot parse:\n{file_status_line}") self.main_frame.update_status_bar("Add failed: Parse error.") return args = (svn_path, relative_path) base_name = os.path.basename(relative_path) self._start_async_operation( self._run_add_file_async, args, {"context": "add_file", "status_msg": f"Adding '{base_name}'"}, ) def create_tag(self): # Dialog sync, action async log_handler.log_info( "--- Action: Create Tag (Dialog then Async) ---", func_name="create_tag" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = self._get_and_validate_svn_path("Create Tag") if not svn_path: return self.main_frame.update_status_bar("Generating tag suggestion...") suggested = self._generate_next_tag_suggestion(svn_path) self.main_frame.update_status_bar("Ready for tag input.") dialog = CreateTagDialog(self.master, suggested_tag_name=suggested) tag_info = dialog.result if tag_info: tag_name, tag_message = tag_info log_handler.log_info( f"User provided tag: '{tag_name}'", func_name="create_tag" ) args = (svn_path, tag_name, tag_message) self._start_async_operation( self._run_create_tag_async, args, { "context": "create_tag", "status_msg": f"Creating tag '{tag_name}'", "committed_flag_possible": True, }, ) else: log_handler.log_info("Tag creation cancelled.", func_name="create_tag") self.main_frame.update_status_bar("Cancelled.") def checkout_tag(self): # Confirm sync, action async log_handler.log_info( "--- Action: Checkout Tag (Async) ---", func_name="checkout_tag" ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = self._get_and_validate_svn_path("Checkout Tag") if not svn_path: return tag = self.main_frame.get_selected_tag() if not tag: self.main_frame.show_error("Error", "No tag selected.") self.main_frame.update_status_bar("Checkout failed: No tag.") return msg = f"Checkout tag '{tag}'?\nWarning: Enters 'detached HEAD' state." if not self.main_frame.ask_yes_no("Confirm Checkout", msg): log_handler.log_info("Checkout cancelled.", func_name="checkout_tag") self.main_frame.update_status_bar("Cancelled.") return args = (svn_path, tag) self._start_async_operation( self._run_checkout_tag_async, args, {"context": "checkout_tag", "status_msg": f"Checking out tag '{tag}'"}, ) def create_branch(self): # Dialog sync, action async log_handler.log_info( "--- Action: Create Branch (Dialog then Async) ---", func_name="create_branch", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = self._get_and_validate_svn_path("Create Branch") if not svn_path: return self.main_frame.update_status_bar("Ready for branch input.") dialog = CreateBranchDialog(self.master) branch_name = dialog.result if branch_name: log_handler.log_info( f"User provided branch: '{branch_name}'", func_name="create_branch" ) args = (svn_path, branch_name) # Pass branch name in context for potential checkout later context = { "context": "create_branch", "status_msg": f"Creating branch '{branch_name}'", "new_branch_name": branch_name, } self._start_async_operation(self._run_create_branch_async, args, context) else: log_handler.log_info( "Branch creation cancelled.", func_name="create_branch" ) self.main_frame.update_status_bar("Cancelled.") def checkout_branch( self, branch_to_checkout=None, repo_path_override=None ): # Confirm sync (sometimes), action async target = branch_to_checkout if branch_to_checkout else "Selected" log_handler.log_info( f"--- Action: Checkout Branch (Target: {target}, Async) ---", func_name="checkout_branch", ) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return svn_path = repo_path_override or self._get_and_validate_svn_path( "Checkout Branch" ) if not svn_path: return branch = branch_to_checkout confirm = False if not branch: branch = self.main_frame.get_selected_branch() confirm = True if not branch: self.main_frame.show_error("Error", "No branch selected.") self.main_frame.update_status_bar("Checkout failed: No branch.") return if confirm: if not self.main_frame.ask_yes_no( "Confirm Checkout", f"Switch to branch '{branch}'?" ): log_handler.log_info("Checkout cancelled.", func_name="checkout_branch") self.main_frame.update_status_bar("Cancelled.") return args = (svn_path, branch) self._start_async_operation( self._run_checkout_branch_async, args, { "context": "checkout_branch", "status_msg": f"Checking out branch '{branch}'", }, ) # --- ==== Worker Methods (_run_*_async) ==== --- # (Questi sono eseguiti nel thread separato) def _run_refresh_tags_async(self, svn_path, results_queue): log_handler.log_debug( "[Worker] Started: Refresh Tags", func_name="_run_refresh_tags_async" ) try: tags_data = self.git_commands.list_tags(svn_path) count = len(tags_data) message = f"Tags refreshed ({count} found)." results_queue.put( {"status": "success", "result": tags_data, "message": message} ) except Exception as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_tags_async" ) results_queue.put( { "status": "error", "exception": e, "result": [("(Error)", "")], "message": "Error refreshing tags.", } ) log_handler.log_debug( "[Worker] Finished: Refresh Tags", func_name="_run_refresh_tags_async" ) def _run_refresh_branches_async(self, svn_path, results_queue): log_handler.log_debug( "[Worker] Started: Refresh Branches", func_name="_run_refresh_branches_async", ) try: branches, current = self.git_commands.list_branches(svn_path) count = len(branches) curr_disp = current if current else "None (Detached?)" message = f"Branches refreshed ({count} found). Current: {curr_disp}" results_queue.put( {"status": "success", "result": (branches, current), "message": message} ) except Exception as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_branches_async" ) results_queue.put( { "status": "error", "exception": e, "result": (["(Error)"], None), "message": "Error refreshing branches.", } ) log_handler.log_debug( "[Worker] Finished: Refresh Branches", func_name="_run_refresh_branches_async", ) def _run_refresh_history_async( self, svn_path, branch_filter, log_scope, results_queue ): log_handler.log_debug( f"[Worker] Started: Refresh History ({log_scope})", func_name="_run_refresh_history_async", ) try: log_data = self.git_commands.get_commit_log( svn_path, max_count=200, branch=branch_filter ) count = len(log_data) message = f"History refreshed ({count} entries for {log_scope})." results_queue.put( {"status": "success", "result": log_data, "message": message} ) except Exception as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_history_async" ) results_queue.put( { "status": "error", "exception": e, "result": ["(Error)"], "message": "Error refreshing history.", } ) log_handler.log_debug( "[Worker] Finished: Refresh History", func_name="_run_refresh_history_async" ) def _run_refresh_changes_async(self, svn_path, results_queue): func_name = "_run_refresh_changes_async" log_handler.log_debug("[Worker] Started: Refresh Changes", func_name=func_name) files_status_list = ["(Worker Error Default)"] try: log_handler.log_debug("[Worker] Calling git_commands.get_status_short...", func_name=func_name) files_status_list = self.git_commands.get_status_short(svn_path) # <<< NUOVO LOG >>> log_handler.log_debug(f"[Worker] Received list from get_status_short: {files_status_list}", func_name=func_name) # <<< FINE NUOVO LOG >>> count = len(files_status_list) log_handler.log_info(f"[Worker] Found {count} changes.", func_name=func_name) message = f"Ready ({count} changes detected)." if count > 0 else "Ready (No changes detected)." log_handler.log_debug(f"[Worker] Preparing to put result in queue. Data: status='success', count={count}", func_name=func_name) result_dict = {'status': 'success', 'result': files_status_list, 'message': message, 'context': 'refresh_changes'} log_handler.log_debug(f"[Worker] Data prepared: {result_dict}", func_name=func_name) results_queue.put(result_dict) log_handler.log_debug("[Worker] Successfully PUT result in queue.", func_name=func_name) except (GitCommandError, ValueError) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_changes_async" ) results_queue.put( { "status": "error", "exception": e, "result": ["(Error)"], "message": "Error refreshing changes.", } ) except Exception as e: log_handler.log_exception( f"[Worker] UNEXPECTED EXCEPTION: {e}", func_name="_run_refresh_changes_async", ) results_queue.put( { "status": "error", "exception": e, "result": ["(Error)"], "message": "Unexpected error refreshing changes.", } ) log_handler.log_debug(f"[Worker] Reached end of function.", func_name=func_name) def _run_prepare_async(self, svn_path, results_queue): log_handler.log_debug( "[Worker] Started: Prepare Repo", func_name="_run_prepare_async" ) try: success = self.action_handler.execute_prepare_repo(svn_path) message = "Repository prepared successfully." results_queue.put( {"status": "success", "result": success, "message": message} ) except ValueError as e: # Handle "already prepared" if "already prepared" in str(e).lower(): results_queue.put( {"status": "warning", "result": True, "message": str(e)} ) else: raise # Re-raise other ValueErrors except (GitCommandError, IOError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_prepare_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error preparing: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Prepare Repo", func_name="_run_prepare_async" ) def _run_create_bundle_async( self, svn_path, bundle_path, profile, backup, bk_dir, commit, msg, exts, dirs, results_queue, ): log_handler.log_debug( "[Worker] Started: Create Bundle", func_name="_run_create_bundle_async" ) try: result_path = self.action_handler.execute_create_bundle( svn_path, bundle_path, profile, backup, bk_dir, commit, msg, exts, dirs ) message = ( f"Bundle created: {os.path.basename(result_path)}" if result_path else "Bundle created (empty/no changes)." ) # Pass flag indicating if commit might have happened results_queue.put( { "status": "success", "result": result_path, "message": message, "committed": commit, } ) except (IOError, GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_create_bundle_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error creating bundle: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Create Bundle", func_name="_run_create_bundle_async" ) def _run_fetch_bundle_async( self, svn_path, bundle_path, profile, backup, bk_dir, exts, dirs, results_queue ): log_handler.log_debug( "[Worker] Started: Fetch Bundle", func_name="_run_fetch_bundle_async" ) try: success = self.action_handler.execute_fetch_bundle( svn_path, bundle_path, profile, backup, bk_dir, exts, dirs ) message = "Fetch/Clone from bundle completed successfully." results_queue.put( {"status": "success", "result": success, "message": message} ) except ( FileNotFoundError, IOError, GitCommandError, ValueError, Exception, ) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_fetch_bundle_async" ) conflict = ( "merge conflict" in str(e).lower() if isinstance(e, GitCommandError) else False ) results_queue.put( { "status": "error", "exception": e, "message": f"Error fetching: {type(e).__name__}", "conflict": conflict, "repo_path": svn_path, } ) log_handler.log_debug( "[Worker] Finished: Fetch Bundle", func_name="_run_fetch_bundle_async" ) def _run_manual_backup_async( self, svn_path, bk_dir, profile, exts, dirs, results_queue ): log_handler.log_debug( "[Worker] Started: Manual Backup", func_name="_run_manual_backup_async" ) try: result_path = self.backup_handler.create_zip_backup( svn_path, bk_dir, profile, exts, dirs ) ts = datetime.datetime.now().strftime("%H:%M:%S") message = ( f"Manual backup created ({ts})." if result_path else f"Manual backup finished (no file, {ts})." ) results_queue.put( {"status": "success", "result": result_path, "message": message} ) except (IOError, ValueError, PermissionError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_manual_backup_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error creating backup: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Manual Backup", func_name="_run_manual_backup_async" ) def _run_commit_async(self, svn_path, msg, results_queue): log_handler.log_debug("[Worker] Started: Commit", func_name="_run_commit_async") try: committed = self.action_handler.execute_manual_commit(svn_path, msg) message = ( "Commit successful." if committed else "Commit finished (no changes)." ) results_queue.put( { "status": "success", "result": committed, "message": message, "committed": committed, } ) except (GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_commit_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error committing: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Commit", func_name="_run_commit_async" ) def _run_untrack_async(self, svn_path, results_queue): log_handler.log_debug( "[Worker] Started: Untrack Files", func_name="_run_untrack_async" ) try: committed = self.action_handler.execute_untrack_files_from_gitignore( svn_path ) message = ( "Automatic untrack commit created." if committed else "Untrack check complete (no action needed)." ) results_queue.put( { "status": "success", "result": committed, "message": message, "committed": committed, } ) except (GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_untrack_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error untracking: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Untrack Files", func_name="_run_untrack_async" ) def _run_add_file_async(self, svn_path, relative_path, results_queue): log_handler.log_debug( f"[Worker] Started: Add File '{relative_path}'", func_name="_run_add_file_async", ) try: success = self.git_commands.add_file(svn_path, relative_path) message = f"File '{os.path.basename(relative_path)}' added to staging." results_queue.put( {"status": "success", "result": success, "message": message} ) except (GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_add_file_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error adding file: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Add File", func_name="_run_add_file_async" ) def _run_create_tag_async(self, svn_path, tag_name, tag_message, results_queue): log_handler.log_debug( f"[Worker] Started: Create Tag '{tag_name}'", func_name="_run_create_tag_async", ) try: success = self.action_handler.execute_create_tag( svn_path, None, tag_name, tag_message ) message = f"Tag '{tag_name}' created successfully." results_queue.put( { "status": "success", "result": success, "message": message, "committed": True, } ) # Assume commit might have happened except (GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_create_tag_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error creating tag: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Create Tag", func_name="_run_create_tag_async" ) def _run_checkout_tag_async(self, svn_path, tag_name, results_queue): log_handler.log_debug( f"[Worker] Started: Checkout Tag '{tag_name}'", func_name="_run_checkout_tag_async", ) try: success = self.action_handler.execute_checkout_tag(svn_path, tag_name) message = f"Checked out tag '{tag_name}' (Detached HEAD)." results_queue.put( {"status": "success", "result": success, "message": message} ) except ( ValueError, GitCommandError, Exception, ) as e: # ValueError for uncommitted changes log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_checkout_tag_async" ) msg = ( f"Checkout failed: Uncommitted changes." if isinstance(e, ValueError) else f"Error checking out tag: {type(e).__name__}" ) results_queue.put({"status": "error", "exception": e, "message": msg}) log_handler.log_debug( "[Worker] Finished: Checkout Tag", func_name="_run_checkout_tag_async" ) def _run_create_branch_async(self, svn_path, branch_name, results_queue): log_handler.log_debug( f"[Worker] Started: Create Branch '{branch_name}'", func_name="_run_create_branch_async", ) try: success = self.action_handler.execute_create_branch(svn_path, branch_name) message = f"Branch '{branch_name}' created successfully." results_queue.put( {"status": "success", "result": success, "message": message} ) except (GitCommandError, ValueError, Exception) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_create_branch_async" ) results_queue.put( { "status": "error", "exception": e, "message": f"Error creating branch: {type(e).__name__}", } ) log_handler.log_debug( "[Worker] Finished: Create Branch", func_name="_run_create_branch_async" ) def _run_checkout_branch_async(self, svn_path, branch_name, results_queue): log_handler.log_debug( f"[Worker] Started: Checkout Branch '{branch_name}'", func_name="_run_checkout_branch_async", ) try: success = self.action_handler.execute_switch_branch(svn_path, branch_name) message = f"Switched to branch '{branch_name}'." results_queue.put( {"status": "success", "result": success, "message": message} ) except ( ValueError, GitCommandError, Exception, ) as e: # ValueError for uncommitted changes log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_checkout_branch_async" ) msg = ( f"Checkout failed: Uncommitted changes." if isinstance(e, ValueError) else f"Error checking out branch: {type(e).__name__}" ) results_queue.put({"status": "error", "exception": e, "message": msg}) log_handler.log_debug( "[Worker] Finished: Checkout Branch", func_name="_run_checkout_branch_async" ) # --- ==== Gestione Coda Risultati ==== --- def _check_completion_queue(self, results_queue, context): """Checks result queue, updates GUI (incl. status bar color).""" task_context = context.get('context', 'unknown') # log_handler.log_debug(f"Checking completion queue for context: {task_context}", func_name="_check_completion_queue") # Mantenuto commentato per ora try: result_data = results_queue.get_nowait() log_handler.log_info(f"Result received for '{task_context}'. Status: {result_data.get('status')}", func_name="_check_completion_queue") # --- 1. Re-enable GUI Widgets --- log_handler.log_debug("Re-enabling widgets.", func_name="_check_completion_queue") self.main_frame.set_action_widgets_state(tk.NORMAL) # --- 2. Extract Details --- status = result_data.get('status') message = result_data.get('message') result = result_data.get('result') exception = result_data.get('exception') committed = result_data.get('committed', False) is_conflict = result_data.get('conflict', False) repo_path_conflict = result_data.get('repo_path') new_branch_context = context.get('new_branch_name') # --- 3. Update Status Bar (con colore e reset temporizzato) --- # (Logica status bar invariata) status_color = None reset_duration = 5000 # Resetta colore dopo 5 secondi if status == 'success': status_color = self.main_frame.STATUS_GREEN elif status == 'warning': status_color = self.main_frame.STATUS_YELLOW reset_duration = 7000 elif status == 'error': status_color = self.main_frame.STATUS_RED reset_duration = 10000 self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration) # --- 4. Process Result & Trigger Updates --- repo_path_for_updates = self._get_and_validate_svn_path("Post-Action Update") if status == 'success': refresh_list = [] # (Logica per popolare refresh_list invariata) if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', 'checkout_tag', 'create_branch', 'checkout_branch', '_handle_gitignore_save_async', 'add_file']: if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save_async']: refresh_list.append(self.refresh_commit_history) if task_context != 'refresh_changes': refresh_list.append(self.refresh_changed_files_list) if task_context not in ['refresh_tags','checkout_tag'] or committed: refresh_list.append(self.refresh_tag_list) if task_context != 'refresh_branches': refresh_list.append(self.refresh_branch_list) # Gestione aggiornamenti diretti post-refresh if task_context == 'refresh_tags': self.main_frame.update_tag_list(result if result else []) elif task_context == 'refresh_branches': branches, current = result if result else ([], None) self.main_frame.update_branch_list(branches, current) self.main_frame.update_history_branch_filter(branches) elif task_context == 'refresh_history': self.main_frame.update_history_display(result if result else []) elif task_context == 'refresh_changes': # ---<<< INIZIO MODIFICA DEBUG >>>--- # Logga esattamente cosa sta per essere passato a update_changed_files_list log_handler.log_debug( f"Preparing to call update_changed_files_list. " f"Task Context: '{task_context}'. Result type: {type(result)}. Result value: {repr(result)}", func_name="_check_completion_queue" ) # ---<<< FINE MODIFICA DEBUG >>>--- self.main_frame.update_changed_files_list(result if result else []) # (Altre gestioni di successo invariate: commit, create_branch checkout, etc.) if task_context == 'commit' and committed: self.main_frame.clear_commit_message() if task_context == 'create_branch' and new_branch_context: if self.main_frame.ask_yes_no("Checkout?", f"Switch to new branch '{new_branch_context}'?"): self.checkout_branch(branch_to_checkout=new_branch_context) else: # Refresh history if not checking out if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) # Trigger collected async refreshes if repo_path_for_updates and refresh_list: log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes for '{task_context}'", func_name="_check_completion_queue") for refresh_func in refresh_list: try: refresh_func() except Exception as ref_e: log_handler.log_error(f"Error triggering {refresh_func.__name__}: {ref_e}", func_name="_check_completion_queue") elif refresh_list: log_handler.log_warning("Cannot trigger UI refreshes: Repo path unavailable.", func_name="_check_completion_queue") elif status == 'warning': # (gestione warning invariata) self.main_frame.show_warning("Operation Info", message) if "already prepared" in message: self.refresh_changed_files_list() elif status == 'error': # (gestione errore invariata) error_details = f"{message}\n({exception})" if exception else message if is_conflict and repo_path_conflict: self.main_frame.show_error("Merge Conflict", f"Conflict occurred.\nResolve in:\n{repo_path_conflict}\nThen commit.") elif "Uncommitted changes" in message: self.main_frame.show_warning("Action Blocked", f"{exception}\nCommit or stash first.") else: self.main_frame.show_error("Error: Operation Failed", error_details) # Aggiornamento liste con errore (opzionale, dipende dal task) if task_context == 'refresh_tags': self.main_frame.update_tag_list([("(Error)", "")]) elif task_context == 'refresh_branches': self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([]) elif task_context == 'refresh_history': self.main_frame.update_history_display(["(Error retrieving history)"]) elif task_context == 'refresh_changes': self.main_frame.update_changed_files_list(["(Error refreshing changes)"]) log_handler.log_debug(f"Finished processing result for context '{task_context}'.", func_name="_check_completion_queue") except queue.Empty: # Reschedule check self.master.after(self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context) except Exception as e: log_handler.log_exception(f"Critical error processing completion queue for {task_context}: {e}", func_name="_check_completion_queue") try: self.main_frame.set_action_widgets_state(tk.NORMAL) # Attempt recovery except: pass self.main_frame.update_status_bar("Error processing async result.", bg_color=self.main_frame.STATUS_RED, duration_ms=10000) # --- Punto di Ingresso (invariato) --- def main(): # ... (invariato) ... # setup_file_logging è chiamato da GitSvnSyncApp ora logger = None root = None app = None try: print("Creating Tkinter root window...") root = tk.Tk() root.minsize(850, 750) print("Tkinter root window created.") print("Initializing GitSvnSyncApp...") app = GitSvnSyncApp(root) print("GitSvnSyncApp initialization attempt complete.") if ( hasattr(app, "main_frame") and app.main_frame and app.main_frame.winfo_exists() ): print("Starting Tkinter main event loop.") root.mainloop() print("Tkinter main event loop finished.") else: print("CRITICAL: App init failed before mainloop. Exiting.") if root and root.winfo_exists(): root.destroy() except Exception as e: print(f"FATAL error during startup/mainloop: {e}") traceback.print_exc() try: parent = root if root and root.winfo_exists() else None messagebox.showerror("Fatal Error", f"App failed:\n{e}", parent=parent) except Exception as msg_e: print(f"FATAL (GUI error failed: {msg_e}):\n{e}") finally: print("Application exiting.") if __name__ == "__main__": main() # --- END OF FILE GitUtility.py ---