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