SXXXXXXX_GitUtility/GitUtility.py
2025-04-18 15:53:30 +02:00

1884 lines
92 KiB
Python

# 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(),
)
# --- 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
# --- 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()