SXXXXXXX_GitUtility/GitUtility.py
2025-04-18 13:51:43 +02:00

1407 lines
64 KiB
Python

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