1242 lines
56 KiB
Python
1242 lines
56 KiB
Python
# GitUtility.py
|
|
import os
|
|
import sys
|
|
# import shutil # Not needed anymore
|
|
import datetime
|
|
import tkinter as tk
|
|
from tkinter import messagebox
|
|
import logging
|
|
# import zipfile # Not needed anymore
|
|
|
|
# Import application modules
|
|
from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR
|
|
from git_commands import GitCommands, GitCommandError
|
|
from logger_config import setup_logger
|
|
# Import GUI classes
|
|
from gui import MainFrame, GitignoreEditorWindow, CreateTagDialog, CreateBranchDialog
|
|
# Import Handler classes
|
|
from backup_handler import BackupHandler
|
|
from profile_handler import ProfileHandler
|
|
from action_handler import ActionHandler
|
|
|
|
|
|
class GitSvnSyncApp:
|
|
"""
|
|
Main application class: Coordinates GUI, configuration, and actions.
|
|
Delegates logic to specific handler classes.
|
|
"""
|
|
|
|
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)
|
|
|
|
# --- Early Logger Setup ---
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(levelname)s - %(message)s"
|
|
)
|
|
self.logger = logging.getLogger("GitSvnSyncApp")
|
|
|
|
# --- Initialize Core Components ---
|
|
try:
|
|
self.config_manager = ConfigManager(self.logger)
|
|
self.git_commands = GitCommands(self.logger)
|
|
# Initialize Handlers
|
|
self.profile_handler = ProfileHandler(self.logger, self.config_manager)
|
|
self.backup_handler = BackupHandler(self.logger)
|
|
self.action_handler = ActionHandler(
|
|
self.logger, self.git_commands, self.backup_handler
|
|
)
|
|
except Exception as e:
|
|
self.logger.critical(f"Failed component init: {e}", exc_info=True)
|
|
self.show_fatal_error(f"Initialization Error:\n{e}\nApp cannot start.")
|
|
master.destroy()
|
|
return
|
|
|
|
# --- Create GUI Main Frame ---
|
|
try:
|
|
self.main_frame = MainFrame(
|
|
master,
|
|
load_profile_settings_cb=self.load_profile_settings,
|
|
# browse_folder_cb REMOVED - Handled within MainFrame now
|
|
update_svn_status_cb=self.update_svn_status_indicator,
|
|
# Core Action Callbacks
|
|
prepare_svn_for_git_cb=self.ui_prepare_svn,
|
|
create_git_bundle_cb=self.ui_create_bundle,
|
|
fetch_from_git_bundle_cb=self.ui_fetch_bundle,
|
|
manual_backup_cb=self.ui_manual_backup,
|
|
# Profile/Settings Callbacks
|
|
add_profile_cb=self.ui_add_profile,
|
|
remove_profile_cb=self.ui_remove_profile,
|
|
save_profile_cb=self.ui_save_settings,
|
|
# Commit/Gitignore Callbacks
|
|
manual_commit_cb=self.ui_manual_commit,
|
|
open_gitignore_editor_cb=self.open_gitignore_editor,
|
|
# Tag Callbacks
|
|
refresh_tags_cb=self.refresh_tag_list,
|
|
create_tag_cb=self.ui_create_tag,
|
|
checkout_tag_cb=self.ui_checkout_tag,
|
|
delete_tag_cb=self.ui_delete_tag, # Pass new callback
|
|
# Branch Callbacks
|
|
refresh_branches_cb=self.refresh_branch_list,
|
|
create_branch_cb=self.ui_create_branch,
|
|
switch_branch_cb=self.ui_switch_branch,
|
|
delete_branch_cb=self.ui_delete_branch,
|
|
# Pass instances/data if needed by GUI
|
|
config_manager_instance=self.config_manager,
|
|
profile_sections_list=self.profile_handler.get_profile_list()
|
|
)
|
|
except Exception as e:
|
|
self.logger.critical(f"Failed init MainFrame: {e}", exc_info=True)
|
|
self.show_fatal_error(f"GUI Error:\n{e}\nApp cannot start.")
|
|
master.destroy()
|
|
return
|
|
|
|
# --- Enhanced Logger Setup ---
|
|
self.logger = setup_logger(self.main_frame.log_text)
|
|
# Ensure all components use the final logger
|
|
self.config_manager.logger = self.logger
|
|
self.git_commands.logger = self.logger
|
|
self.profile_handler.logger = self.logger
|
|
self.backup_handler.logger = self.logger
|
|
self.action_handler.logger = self.logger
|
|
|
|
# --- Initial Application State ---
|
|
self.logger.info("Application initializing...")
|
|
initial_profile = self.main_frame.profile_var.get()
|
|
if initial_profile:
|
|
self.logger.debug(f"Initial profile: '{initial_profile}'. Loading...")
|
|
# Load settings (called automatically by trace on profile_var)
|
|
else:
|
|
self.logger.warning("No profile selected on startup.")
|
|
self._clear_and_disable_fields() # Set initial disabled state
|
|
self.logger.info("Application started successfully.")
|
|
|
|
|
|
def on_closing(self):
|
|
"""Handles window close event."""
|
|
self.logger.info("Application closing.")
|
|
self.master.destroy()
|
|
|
|
|
|
# --- Helper methods to get/validate paths from GUI ---
|
|
def _get_and_validate_svn_path(self, operation_name="Operation"):
|
|
"""Retrieves and validates the SVN path from the GUI."""
|
|
if not hasattr(self, 'main_frame'):
|
|
self.logger.error(f"{operation_name}: GUI component unavailable.")
|
|
return None
|
|
if not hasattr(self.main_frame, 'svn_path_entry'):
|
|
self.logger.error(f"{operation_name}: SVN path widget missing.")
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.show_error("Internal Error", "SVN Path widget missing.")
|
|
return None
|
|
|
|
svn_path_str = self.main_frame.svn_path_entry.get()
|
|
svn_path_str = svn_path_str.strip()
|
|
if not svn_path_str:
|
|
self.logger.error(f"{operation_name}: SVN Path is empty.")
|
|
self.main_frame.show_error("Input Error", "SVN Path cannot be empty.")
|
|
return None
|
|
|
|
abs_path = os.path.abspath(svn_path_str)
|
|
if not os.path.isdir(abs_path):
|
|
self.logger.error(f"{operation_name}: Invalid directory path: {abs_path}")
|
|
self.main_frame.show_error("Input Error", f"Invalid SVN path:\n{abs_path}")
|
|
return None
|
|
|
|
self.logger.debug(f"{operation_name}: Validated SVN path: {abs_path}")
|
|
return abs_path
|
|
|
|
|
|
def _get_and_validate_usb_path(self, operation_name="Operation"):
|
|
"""Retrieves and validates the USB/Bundle Target path from the GUI."""
|
|
if not hasattr(self, 'main_frame'):
|
|
self.logger.error(f"{operation_name}: GUI component unavailable.")
|
|
return None
|
|
if not hasattr(self.main_frame, 'usb_path_entry'):
|
|
self.logger.error(f"{operation_name}: USB path widget missing.")
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.show_error("Internal Error", "USB Path widget missing.")
|
|
return None
|
|
|
|
usb_path_str = self.main_frame.usb_path_entry.get()
|
|
usb_path_str = usb_path_str.strip()
|
|
if not usb_path_str:
|
|
self.logger.error(f"{operation_name}: Bundle Target Dir path empty.")
|
|
self.main_frame.show_error("Input Error", "Bundle Target Dir empty.")
|
|
return None
|
|
|
|
abs_path = os.path.abspath(usb_path_str)
|
|
if not os.path.isdir(abs_path):
|
|
self.logger.error(f"{operation_name}: Invalid Bundle Target: {abs_path}")
|
|
self.main_frame.show_error("Input Error", f"Invalid Bundle Target:\n{abs_path}")
|
|
return None
|
|
|
|
self.logger.debug(f"{operation_name}: Validated Bundle Target path: {abs_path}")
|
|
return abs_path
|
|
|
|
# --- ADDED: Helper to parse exclusions directly here ---
|
|
def _parse_exclusions(self, profile_name):
|
|
"""
|
|
Parses exclusion string from config for the given profile.
|
|
Needed here because ActionHandler might not have direct ConfigManager access,
|
|
and exclusions are needed for backup steps within actions.
|
|
|
|
Args:
|
|
profile_name (str): The name of the profile.
|
|
|
|
Returns:
|
|
tuple: (set of excluded extensions (lowercase, starting with '.'),
|
|
set of excluded base directory names (lowercase))
|
|
Raises:
|
|
ValueError: If exclusion string parsing fails.
|
|
"""
|
|
try:
|
|
# Get exclusion string from config
|
|
exclude_str = self.config_manager.get_profile_option(
|
|
profile_name, "backup_exclude_extensions", fallback=""
|
|
)
|
|
excluded_extensions = set()
|
|
# Define standard directories to always exclude (lowercase for comparison)
|
|
excluded_dirs_base = {".git", ".svn"}
|
|
|
|
if exclude_str:
|
|
raw_extensions = exclude_str.split(',')
|
|
for ext in raw_extensions:
|
|
clean_ext = ext.strip().lower()
|
|
if not clean_ext:
|
|
continue # Skip empty parts
|
|
# Ensure extension starts with a dot
|
|
if not clean_ext.startswith('.'):
|
|
clean_ext = '.' + clean_ext
|
|
excluded_extensions.add(clean_ext)
|
|
|
|
self.logger.debug(
|
|
f"Parsed Exclusions '{profile_name}' - "
|
|
f"Ext: {excluded_extensions}, Dirs: {excluded_dirs_base}"
|
|
)
|
|
return excluded_extensions, excluded_dirs_base
|
|
except Exception as e:
|
|
self.logger.error(f"Error parsing exclusions for '{profile_name}': {e}",
|
|
exc_info=True)
|
|
# Raise a specific error to indicate parsing failure
|
|
raise ValueError(f"Could not parse backup exclusions: {e}") from e
|
|
|
|
|
|
# --- Profile Handling Wrappers ---
|
|
def load_profile_settings(self, profile_name):
|
|
"""Loads profile settings into GUI when profile selection changes."""
|
|
self.logger.info(f"UI Request: Load profile '{profile_name}'")
|
|
if not profile_name:
|
|
self._clear_and_disable_fields()
|
|
return
|
|
|
|
# Delegate loading to ProfileHandler
|
|
profile_data = self.profile_handler.load_profile_data(profile_name)
|
|
if not profile_data:
|
|
# Handler logs error, show message and clear UI
|
|
self.main_frame.show_error("Load Error", f"Could not load '{profile_name}'.")
|
|
self._clear_and_disable_fields()
|
|
return
|
|
|
|
# Update GUI fields with loaded data if frame exists
|
|
if hasattr(self, 'main_frame'):
|
|
mf = self.main_frame # Alias
|
|
# Update all widgets based on loaded data
|
|
mf.svn_path_entry.delete(0, tk.END)
|
|
mf.svn_path_entry.insert(0, profile_data.get("svn_working_copy_path", ""))
|
|
mf.usb_path_entry.delete(0, tk.END)
|
|
mf.usb_path_entry.insert(0, profile_data.get("usb_drive_path", ""))
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.insert(0, profile_data.get("bundle_name", ""))
|
|
mf.bundle_updated_name_entry.delete(0, tk.END)
|
|
mf.bundle_updated_name_entry.insert(0, profile_data.get("bundle_name_updated", ""))
|
|
mf.autocommit_var.set(profile_data.get("autocommit", False)) # Bool
|
|
mf.commit_message_var.set(profile_data.get("commit_message", ""))
|
|
mf.autobackup_var.set(profile_data.get("autobackup", False)) # Bool
|
|
mf.backup_dir_var.set(profile_data.get("backup_dir", DEFAULT_BACKUP_DIR))
|
|
mf.backup_exclude_extensions_var.set(
|
|
profile_data.get("backup_exclude_extensions", ".log,.tmp")
|
|
)
|
|
mf.toggle_backup_dir() # Update backup dir entry state
|
|
|
|
# Update status indicators and enable/disable buttons based on path
|
|
svn_path = profile_data.get("svn_working_copy_path", "")
|
|
self.update_svn_status_indicator(svn_path) # Updates state widgets
|
|
self._enable_function_buttons() # Enable general action buttons
|
|
|
|
# Refresh tag and branch lists if repo ready
|
|
repo_ready = (svn_path and os.path.isdir(svn_path) and
|
|
os.path.exists(os.path.join(svn_path, ".git")))
|
|
if repo_ready:
|
|
self.refresh_tag_list()
|
|
self.refresh_branch_list()
|
|
self.update_current_branch_display() # Update display label
|
|
else:
|
|
# Clear lists if repo not ready
|
|
mf.update_tag_list([])
|
|
mf.update_branch_list([])
|
|
mf.set_current_branch_display("<N/A>") # Reset display label
|
|
|
|
self.logger.info(f"Settings loaded successfully for '{profile_name}'.")
|
|
else:
|
|
self.logger.error("Cannot load settings: Main frame missing.")
|
|
|
|
|
|
def ui_save_settings(self):
|
|
"""Callback for the 'Save Settings' button."""
|
|
self.logger.debug("UI Request: Save Settings button clicked.")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Save Error", "No profile selected.")
|
|
return False # Indicate failure to caller if needed
|
|
|
|
# Gather data from GUI
|
|
current_data = self._get_data_from_gui()
|
|
if current_data is None: # Check if reading GUI failed
|
|
self.main_frame.show_error("Internal Error", "Could not read GUI data.")
|
|
return False
|
|
|
|
# Delegate saving to ProfileHandler
|
|
success = self.profile_handler.save_profile_data(profile, current_data)
|
|
|
|
if success:
|
|
self.main_frame.show_info("Saved", f"Settings saved for '{profile}'.")
|
|
return True
|
|
else:
|
|
# Error message likely shown by handler/save method
|
|
# self.main_frame.show_error("Save Error", f"Failed save settings.")
|
|
return False
|
|
|
|
|
|
def _get_data_from_gui(self):
|
|
"""Helper to gather current settings from GUI widgets into a dict."""
|
|
if not hasattr(self, 'main_frame'):
|
|
self.logger.error("Cannot get GUI data: Main frame missing.")
|
|
return None # Indicate failure
|
|
|
|
mf = self.main_frame
|
|
# Read values from all relevant widgets/variables
|
|
data = {}
|
|
data["svn_working_copy_path"] = mf.svn_path_entry.get()
|
|
data["usb_drive_path"] = mf.usb_path_entry.get()
|
|
data["bundle_name"] = mf.bundle_name_entry.get()
|
|
data["bundle_name_updated"] = mf.bundle_updated_name_entry.get()
|
|
data["autocommit"] = mf.autocommit_var.get() # Gets boolean
|
|
data["commit_message"] = mf.commit_message_var.get()
|
|
data["autobackup"] = mf.autobackup_var.get() # Gets boolean
|
|
data["backup_dir"] = mf.backup_dir_var.get()
|
|
data["backup_exclude_extensions"] = mf.backup_exclude_extensions_var.get()
|
|
return data
|
|
|
|
|
|
def ui_add_profile(self):
|
|
"""Callback for the 'Add Profile' button."""
|
|
self.logger.debug("UI Request: Add Profile.")
|
|
new_name = self.main_frame.ask_new_profile_name()
|
|
if not new_name:
|
|
self.logger.info("Add profile cancelled."); return
|
|
|
|
new_name = new_name.strip()
|
|
if not new_name:
|
|
self.main_frame.show_error("Error", "Profile name empty."); return
|
|
|
|
# Delegate adding logic
|
|
success = self.profile_handler.add_new_profile(new_name)
|
|
if success:
|
|
sections = self.profile_handler.get_profile_list()
|
|
self.main_frame.update_profile_dropdown(sections)
|
|
self.main_frame.profile_var.set(new_name) # Triggers load
|
|
self.main_frame.show_info("Profile Added", f"Profile '{new_name}' created.")
|
|
else:
|
|
self.main_frame.show_error("Error", f"Could not add profile '{new_name}'.")
|
|
|
|
|
|
def ui_remove_profile(self):
|
|
"""Callback for the 'Remove Profile' button."""
|
|
self.logger.debug("UI Request: Remove Profile.")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Error", "No profile selected."); return
|
|
if profile == DEFAULT_PROFILE:
|
|
self.main_frame.show_error("Error", f"Cannot remove '{DEFAULT_PROFILE}'."); return
|
|
|
|
# Confirm with user
|
|
msg = f"Remove profile '{profile}'?"
|
|
if self.main_frame.ask_yes_no("Remove Profile", msg):
|
|
# Delegate removal
|
|
success = self.profile_handler.remove_existing_profile(profile)
|
|
if success:
|
|
sections = self.profile_handler.get_profile_list()
|
|
self.main_frame.update_profile_dropdown(sections) # Triggers load
|
|
self.main_frame.show_info("Removed", f"Profile '{profile}' removed.")
|
|
else:
|
|
self.main_frame.show_error("Error", f"Failed to remove '{profile}'.")
|
|
else:
|
|
self.logger.info("Removal cancelled.")
|
|
|
|
|
|
# --- GUI Interaction Wrappers ---
|
|
def browse_folder(self, entry_widget):
|
|
"""Opens folder dialog to update an entry widget."""
|
|
self.logger.debug("Browse folder requested.")
|
|
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:
|
|
self.logger.debug(f"Selected: {directory}")
|
|
entry_widget.delete(0, tk.END)
|
|
entry_widget.insert(0, directory)
|
|
# Trigger status update if SVN path changed
|
|
if entry_widget == self.main_frame.svn_path_entry:
|
|
self.update_svn_status_indicator(directory)
|
|
else:
|
|
self.logger.debug("Browse cancelled.")
|
|
|
|
|
|
def update_svn_status_indicator(self, svn_path):
|
|
"""Checks repo status and updates all dependent GUI widget states."""
|
|
is_valid = bool(svn_path and os.path.isdir(svn_path))
|
|
is_ready = is_valid and os.path.exists(os.path.join(svn_path, ".git"))
|
|
self.logger.debug(f"Updating status indicators for '{svn_path}'. "
|
|
f"Valid:{is_valid}, Ready:{is_ready}")
|
|
|
|
if hasattr(self, 'main_frame'):
|
|
mf = self.main_frame # Alias
|
|
# Update indicator & Prepare button via GUI method
|
|
mf.update_svn_indicator(is_ready)
|
|
|
|
# Determine states for other dependent widgets
|
|
gitignore_state = tk.NORMAL if is_valid else tk.DISABLED
|
|
repo_ready_state = tk.NORMAL if is_ready else tk.DISABLED
|
|
|
|
# Apply states to relevant widgets if they exist
|
|
widget_states = {
|
|
'edit_gitignore_button': gitignore_state,
|
|
'commit_message_entry': repo_ready_state,
|
|
'autocommit_checkbox': repo_ready_state,
|
|
'commit_button': repo_ready_state,
|
|
'refresh_branches_button': repo_ready_state,
|
|
'create_branch_button': repo_ready_state,
|
|
'switch_branch_button': repo_ready_state,
|
|
'delete_branch_button': repo_ready_state,
|
|
'refresh_tags_button': repo_ready_state,
|
|
'create_tag_button': repo_ready_state,
|
|
'checkout_tag_button': repo_ready_state,
|
|
'delete_tag_button': repo_ready_state,
|
|
}
|
|
for widget_name, state in widget_states.items():
|
|
widget = getattr(mf, widget_name, None)
|
|
if widget:
|
|
# Ensure config method is called safely
|
|
try:
|
|
widget.config(state=state)
|
|
except tk.TclError as e:
|
|
self.logger.warning(f"TclError configuring {widget_name}: {e}")
|
|
|
|
# Update current branch display if repo not ready
|
|
if not is_ready:
|
|
mf.set_current_branch_display("<N/A>")
|
|
|
|
|
|
def open_gitignore_editor(self):
|
|
"""Opens the modal editor window for .gitignore."""
|
|
self.logger.info("--- Action Triggered: 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:
|
|
# Create and run the modal editor window
|
|
editor = GitignoreEditorWindow(self.master, gitignore_path, self.logger)
|
|
# Execution blocks here until editor is closed
|
|
self.logger.debug("Gitignore editor finished.")
|
|
except Exception as e:
|
|
self.logger.exception(f"Error opening .gitignore editor: {e}")
|
|
self.main_frame.show_error("Editor Error",
|
|
f"Could not open editor:\n{e}")
|
|
|
|
|
|
# --- Core Action Wrappers (GUI Callbacks) ---
|
|
|
|
def ui_prepare_svn(self):
|
|
"""Callback for 'Prepare SVN Repo' button."""
|
|
self.logger.info("--- Action Triggered: Prepare SVN Repo ---")
|
|
svn_path = self._get_and_validate_svn_path("Prepare SVN")
|
|
if not svn_path: return
|
|
|
|
# Save settings before action
|
|
# Use ui_save_settings which returns True/False
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Prepare SVN: Failed to save settings first.")
|
|
# Ask user if they want to continue?
|
|
# if not self.main_frame.ask_yes_no("Warning", "Could not save settings.\nContinue anyway?"):
|
|
# return
|
|
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
self.action_handler.execute_prepare_repo(svn_path)
|
|
self.main_frame.show_info("Success", "Repository prepared.")
|
|
# Update GUI state after successful preparation
|
|
self.update_svn_status_indicator(svn_path)
|
|
except ValueError as e: # Catch specific "already prepared" error
|
|
self.main_frame.show_info("Info", str(e))
|
|
self.update_svn_status_indicator(svn_path) # Ensure UI reflects state
|
|
except (GitCommandError, IOError) as e:
|
|
self.main_frame.show_error("Error", f"Failed prepare:\n{e}")
|
|
self.update_svn_status_indicator(svn_path) # Update state after failure
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected prepare error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
self.update_svn_status_indicator(svn_path)
|
|
|
|
|
|
def ui_create_bundle(self):
|
|
"""Callback for 'Create Bundle' button."""
|
|
self.logger.info("--- Action Triggered: Create Git Bundle ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Error", "No profile selected.")
|
|
return
|
|
|
|
# Validate inputs
|
|
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 name empty.")
|
|
return
|
|
# Ensure .bundle extension
|
|
if not bundle_name.lower().endswith(".bundle"):
|
|
bundle_name += ".bundle"
|
|
mf = self.main_frame # Alias
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.insert(0, bundle_name)
|
|
bundle_full_path = os.path.join(usb_path, bundle_name)
|
|
|
|
# Get settings needed by action handler
|
|
settings = self._get_data_from_gui()
|
|
if not settings: return
|
|
backup_needed = settings.get("autobackup", False)
|
|
commit_needed = settings.get("autocommit", False)
|
|
commit_msg = settings.get("commit_message", "")
|
|
backup_dir = settings.get("backup_dir", "")
|
|
try:
|
|
# Parse exclusions needed for backup step
|
|
excluded_ext, excluded_dir = self._parse_exclusions(profile)
|
|
except ValueError as e:
|
|
self.main_frame.show_error("Config Error", str(e))
|
|
return
|
|
|
|
# Save settings before action
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Create Bundle: Could not save settings.")
|
|
# Ask user?
|
|
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
created_path = self.action_handler.execute_create_bundle(
|
|
svn_path, bundle_full_path, profile, backup_needed, backup_dir,
|
|
commit_needed, commit_msg, excluded_ext, excluded_dir
|
|
)
|
|
# Show feedback based on result
|
|
if created_path:
|
|
self.main_frame.show_info("Success", f"Bundle created:\n{created_path}")
|
|
else:
|
|
# Non-fatal issue (e.g., empty bundle)
|
|
self.main_frame.show_warning("Info", "Bundle empty or not created.")
|
|
except Exception as e:
|
|
# Handle errors raised by action_handler
|
|
self.logger.error(f"Bundle process error: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Failed create bundle:\n{e}")
|
|
|
|
|
|
def ui_fetch_bundle(self):
|
|
"""Callback for 'Fetch Bundle' button."""
|
|
self.logger.info("--- Action Triggered: Fetch from Git Bundle ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Error", "No profile selected.")
|
|
return
|
|
|
|
# Validate inputs
|
|
svn_path = self._get_and_validate_svn_path("Fetch Bundle")
|
|
if not svn_path: return
|
|
usb_path = self._get_and_validate_usb_path("Fetch 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 name empty.")
|
|
return
|
|
bundle_full_path = os.path.join(usb_path, bundle_name)
|
|
if not os.path.isfile(bundle_full_path):
|
|
self.main_frame.show_error("Error", f"Bundle not found:\n{bundle_full_path}")
|
|
return
|
|
|
|
# Get settings needed by action handler
|
|
settings = self._get_data_from_gui()
|
|
if not settings: return
|
|
backup_needed = settings.get("autobackup", False)
|
|
backup_dir = settings.get("backup_dir", "")
|
|
try:
|
|
excluded_ext, excluded_dir = self._parse_exclusions(profile)
|
|
except ValueError as e:
|
|
self.main_frame.show_error("Config Error", str(e))
|
|
return
|
|
|
|
# Save settings before action
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Fetch Bundle: Could not save settings.")
|
|
# Ask user?
|
|
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
self.action_handler.execute_fetch_bundle(
|
|
svn_path, bundle_full_path, profile, backup_needed, backup_dir,
|
|
excluded_ext, excluded_dir
|
|
)
|
|
# Show generic success, conflicts handled by error message below
|
|
self.main_frame.show_info("Fetch Complete",
|
|
f"Fetch complete.\nCheck logs for status.")
|
|
# Refresh state after fetch completes successfully
|
|
self.refresh_branch_list()
|
|
self.update_current_branch_display()
|
|
self.refresh_tag_list()
|
|
except GitCommandError as e:
|
|
# Handle specific Git errors like merge conflicts
|
|
self.logger.error(f"Fetch/merge error: {e}", exc_info=False)
|
|
if "merge conflict" in str(e).lower():
|
|
self.main_frame.show_error(
|
|
"Merge Conflict",
|
|
f"Conflict occurred.\nResolve manually in:\n{svn_path}\nThen commit."
|
|
)
|
|
else:
|
|
self.main_frame.show_error("Error", f"Fetch/Merge Failed:\n{e}")
|
|
except Exception as e:
|
|
# Handle other errors (backup, unexpected)
|
|
self.logger.error(f"Fetch process error: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Fetch failed:\n{e}")
|
|
|
|
|
|
def ui_manual_backup(self):
|
|
"""Callback for 'Backup Now' button."""
|
|
self.logger.info("--- Action Triggered: Manual Backup ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Error", "No profile selected.")
|
|
return
|
|
|
|
# Validate paths
|
|
svn_path = self._get_and_validate_svn_path(f"Manual Backup ({profile})")
|
|
if not svn_path: return
|
|
backup_dir = self.main_frame.backup_dir_var.get().strip()
|
|
if not backup_dir:
|
|
self.main_frame.show_error("Backup Error", "Backup directory empty.")
|
|
return
|
|
|
|
# Check/create backup dir
|
|
if not os.path.isdir(backup_dir):
|
|
create_q = f"Create directory:\n{backup_dir}?"
|
|
if self.main_frame.ask_yes_no("Create Directory?", create_q):
|
|
try:
|
|
os.makedirs(backup_dir, exist_ok=True)
|
|
except OSError as e:
|
|
self.main_frame.show_error("Error", f"Cannot create dir:\n{e}")
|
|
return # Stop if cannot create dir
|
|
else:
|
|
self.logger.info("User cancelled backup dir creation.")
|
|
return # Stop if user cancels creation
|
|
|
|
# Parse exclusions needed for backup handler
|
|
try:
|
|
excluded_ext, excluded_dir = self._parse_exclusions(profile)
|
|
except ValueError as e:
|
|
self.main_frame.show_error("Config Error", str(e))
|
|
return
|
|
|
|
# Save settings first (important for exclusions and backup dir)
|
|
if not self.ui_save_settings():
|
|
confirm_q = "Could not save settings.\nContinue backup anyway?"
|
|
if not self.main_frame.ask_yes_no("Warning", confirm_q):
|
|
self.logger.warning("Manual backup aborted by user.")
|
|
return
|
|
|
|
# Delegate backup creation to BackupHandler
|
|
try:
|
|
backup_path = self.backup_handler.create_zip_backup(
|
|
svn_path, backup_dir, profile, excluded_ext, excluded_dir
|
|
)
|
|
# Show success message if path is returned
|
|
if backup_path:
|
|
self.main_frame.show_info(
|
|
"Backup Complete", f"Backup created:\n{backup_path}"
|
|
)
|
|
# else: BackupHandler logs issues, maybe return specific status?
|
|
except Exception as e:
|
|
# Handle any exception raised by backup handler
|
|
self.logger.error(f"Manual backup failed: {e}", exc_info=True)
|
|
self.main_frame.show_error("Backup Error", f"Failed:\n{e}")
|
|
|
|
|
|
def ui_manual_commit(self):
|
|
"""Callback for the 'Commit Changes' button."""
|
|
self.logger.info("--- Action Triggered: Manual Commit ---")
|
|
svn_path = self._get_and_validate_svn_path("Manual Commit")
|
|
if not svn_path: return
|
|
|
|
# Get commit message from the GUI entry
|
|
commit_msg = self.main_frame.commit_message_var.get().strip()
|
|
if not commit_msg:
|
|
self.main_frame.show_error("Commit Error", "Commit message empty.")
|
|
return
|
|
|
|
# Save settings first? Optional, but saves the message if user typed it.
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Manual Commit: Could not save settings.")
|
|
# Ask user?
|
|
|
|
# Delegate commit execution to ActionHandler
|
|
try:
|
|
commit_made = self.action_handler.execute_manual_commit(
|
|
svn_path, commit_msg
|
|
)
|
|
if commit_made:
|
|
self.main_frame.show_info("Success", "Changes committed.")
|
|
# Clear message field after successful commit? Optional.
|
|
# self.main_frame.commit_message_var.set("")
|
|
else:
|
|
# git_commit already logged "nothing to commit"
|
|
self.main_frame.show_info("Info", "No changes to commit.")
|
|
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Manual commit failed: {e}")
|
|
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 commit error:\n{e}")
|
|
|
|
|
|
# --- Tag Management Callbacks ---
|
|
def refresh_tag_list(self):
|
|
"""Refreshes tag list in GUI."""
|
|
self.logger.info("--- Action: Refresh Tag List ---")
|
|
svn_path = self._get_and_validate_svn_path("Refresh Tags")
|
|
# Repo must be ready to list tags
|
|
repo_ready = svn_path and os.path.exists(os.path.join(svn_path, ".git"))
|
|
if not repo_ready:
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.update_tag_list([]) # Clear list
|
|
return
|
|
|
|
# Fetch tags and update GUI
|
|
try:
|
|
# list_tags returns list of tuples (name, subject)
|
|
tags_data = self.git_commands.list_tags(svn_path)
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.update_tag_list(tags_data)
|
|
self.logger.info(f"Tag list updated ({len(tags_data)} tags).")
|
|
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}")
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.update_tag_list([])
|
|
|
|
|
|
def ui_create_tag(self):
|
|
"""Callback for 'Create Tag' button."""
|
|
self.logger.info("--- Action Triggered: Create Tag ---")
|
|
svn_path = self._get_and_validate_svn_path("Create Tag")
|
|
if not svn_path: return
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.main_frame.show_error("Error", "No profile selected.")
|
|
return
|
|
|
|
# Get commit message from GUI (needed by action handler for pre-commit)
|
|
commit_msg = self.main_frame.commit_message_var.get().strip()
|
|
|
|
# Save settings before action (saves commit message)
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Create Tag: Could not save settings first.")
|
|
# Ask user?
|
|
|
|
# --- Open Dialog to get Tag Name and Tag Message ---
|
|
self.logger.debug("Opening create tag dialog...")
|
|
dialog = CreateTagDialog(self.master) # Parent is the main Tk window
|
|
tag_info = dialog.result # Returns (tag_name, tag_message) or None
|
|
|
|
if not tag_info:
|
|
# User cancelled the dialog
|
|
self.logger.info("Tag creation cancelled by user in dialog.")
|
|
return
|
|
|
|
tag_name, tag_message = tag_info
|
|
self.logger.info(f"User provided tag: '{tag_name}', msg: '{tag_message}'")
|
|
|
|
# --- Delegate Execution to ActionHandler ---
|
|
try:
|
|
# ActionHandler manages pre-commit logic based on commit_msg presence
|
|
success = self.action_handler.execute_create_tag(
|
|
svn_path, commit_msg, tag_name, tag_message
|
|
)
|
|
# execute_create_tag raises exceptions on failure
|
|
if success: # Should be true if no exception
|
|
self.logger.info(f"Tag '{tag_name}' created successfully.")
|
|
self.main_frame.show_info("Success", f"Tag '{tag_name}' created.")
|
|
self.refresh_tag_list() # Update list after successful creation
|
|
except ValueError as e:
|
|
# Catch specific errors like "commit message required"
|
|
self.logger.error(f"Tag creation validation failed: {e}")
|
|
self.main_frame.show_error("Tag Error", str(e))
|
|
except GitCommandError as e:
|
|
# Catch Git command errors (from commit or tag)
|
|
self.logger.error(f"Tag creation failed (Git Error): {e}")
|
|
self.main_frame.show_error("Tag Error", f"Git command failed:\n{e}")
|
|
except Exception as e:
|
|
# Catch unexpected errors
|
|
self.logger.exception(f"Unexpected error creating tag: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
|
|
|
|
def ui_checkout_tag(self):
|
|
"""Callback for 'Checkout Selected Tag' button."""
|
|
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() # Gets name
|
|
if not selected_tag:
|
|
self.main_frame.show_error("Selection Error", "Select a tag.")
|
|
return
|
|
|
|
self.logger.info(f"Attempting checkout for tag: {selected_tag}")
|
|
|
|
# Confirmation dialog first
|
|
msg = (f"Checkout tag '{selected_tag}'?\n\n"
|
|
f"WARNINGS:\n- Files WILL BE OVERWRITTEN.\n- NO backup created.\n"
|
|
f"- Enters 'detached HEAD' state.")
|
|
if not self.main_frame.ask_yes_no("Confirm Checkout", msg):
|
|
self.logger.info("Tag checkout cancelled."); return
|
|
|
|
# Save settings before action? Optional.
|
|
if not self.ui_save_settings():
|
|
self.logger.warning("Checkout Tag: Could not save profile settings.")
|
|
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
success = self.action_handler.execute_checkout_tag(svn_path, selected_tag)
|
|
if success:
|
|
self.main_frame.show_info("Success",
|
|
f"Checked out tag '{selected_tag}'.\n\nNOTE: In 'detached HEAD'.")
|
|
# Update branch display after successful checkout
|
|
self.update_current_branch_display()
|
|
except ValueError as e: # Catch specific errors like "uncommitted changes"
|
|
self.main_frame.show_error("Checkout Blocked", str(e))
|
|
except GitCommandError as e: # Catch Git command errors
|
|
self.main_frame.show_error("Error", f"Could not checkout:\n{e}")
|
|
except Exception as e: # Catch unexpected errors
|
|
self.logger.exception(f"Unexpected checkout error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected checkout error:\n{e}")
|
|
|
|
|
|
def ui_delete_tag(self):
|
|
"""Callback for 'Delete Selected Tag' button."""
|
|
self.logger.info("--- Action Triggered: Delete Tag ---")
|
|
svn_path = self._get_and_validate_svn_path("Delete Tag")
|
|
if not svn_path: return
|
|
|
|
selected_tag = self.main_frame.get_selected_tag() # Gets name
|
|
if not selected_tag:
|
|
self.main_frame.show_error("Selection Error", "Select a tag to delete.")
|
|
return
|
|
|
|
# Confirmation dialog
|
|
msg = f"Delete tag '{selected_tag}' permanently?\nCannot be easily undone."
|
|
if not self.main_frame.ask_yes_no("Confirm Delete Tag", msg):
|
|
self.logger.info("Tag deletion cancelled.")
|
|
return
|
|
|
|
self.logger.info(f"Attempting delete tag: {selected_tag}")
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
success = self.action_handler.execute_delete_tag(svn_path, selected_tag)
|
|
if success:
|
|
self.main_frame.show_info("Success", f"Tag '{selected_tag}' deleted.")
|
|
self.refresh_tag_list() # Update list
|
|
# else: action_handler raises error on failure
|
|
except (GitCommandError, ValueError) as e:
|
|
# Handle known errors (not found, invalid name format?)
|
|
self.main_frame.show_error("Error", f"Could not delete tag:\n{e}")
|
|
except Exception as e:
|
|
# Handle unexpected errors
|
|
self.logger.exception(f"Unexpected error deleting tag: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
|
|
|
|
# --- Branch Management Callbacks ---
|
|
def refresh_branch_list(self):
|
|
"""Refreshes the branch list in the GUI."""
|
|
self.logger.info("--- Action: Refresh Branch List ---")
|
|
svn_path = self._get_and_validate_svn_path("Refresh Branches")
|
|
# Repo must be ready to list branches
|
|
is_ready = svn_path and os.path.exists(os.path.join(svn_path, ".git"))
|
|
if not is_ready:
|
|
if hasattr(self, 'main_frame'): self.main_frame.update_branch_list([])
|
|
return
|
|
|
|
# Fetch and update GUI
|
|
try:
|
|
# Assumes git_commands method exists and returns list of names
|
|
branches = self.git_commands.list_branches(svn_path)
|
|
if hasattr(self, 'main_frame'):
|
|
# Update listbox and potentially current branch display implicitly
|
|
self.main_frame.update_branch_list(branches)
|
|
self.logger.info(f"Branch list updated ({len(branches)} branches).")
|
|
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}")
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.update_branch_list([])
|
|
|
|
|
|
def update_current_branch_display(self):
|
|
"""Gets the current branch and updates the display label."""
|
|
self.logger.debug("Updating current branch display...")
|
|
svn_path = self._get_and_validate_svn_path("Update Branch Display")
|
|
current_branch_name = "<N/A>" # Default value
|
|
|
|
# Only query git if repo is ready
|
|
if svn_path and os.path.exists(os.path.join(svn_path, ".git")):
|
|
try:
|
|
# Use GitCommands to get branch name
|
|
branch_name = self.git_commands.get_current_branch(svn_path)
|
|
# Update display text based on result
|
|
if branch_name == "(DETACHED HEAD)":
|
|
current_branch_name = branch_name # Show detached state clearly
|
|
elif branch_name == "<Error>":
|
|
current_branch_name = "<Error>" # Show error state
|
|
elif branch_name:
|
|
current_branch_name = branch_name # Show actual branch name
|
|
else:
|
|
# Fallback if method returns None unexpectedly
|
|
current_branch_name = "<Unknown>"
|
|
except Exception as e:
|
|
# Handle exceptions during git command execution
|
|
self.logger.error(f"Failed to get current branch: {e}")
|
|
current_branch_name = "<Error>"
|
|
|
|
# Update the GUI label via MainFrame method
|
|
if hasattr(self, 'main_frame'):
|
|
self.main_frame.set_current_branch_display(current_branch_name)
|
|
|
|
|
|
def ui_create_branch(self):
|
|
"""Callback for 'Create Branch' button."""
|
|
self.logger.info("--- Action Triggered: Create Branch ---")
|
|
svn_path = self._get_and_validate_svn_path("Create Branch")
|
|
if not svn_path: return
|
|
|
|
# Use custom dialog to get new branch name
|
|
dialog = CreateBranchDialog(self.master)
|
|
new_branch_name = dialog.result # Returns name or None
|
|
if not new_branch_name:
|
|
self.logger.info("Branch creation cancelled."); return
|
|
|
|
self.logger.info(f"Attempting create branch: '{new_branch_name}'")
|
|
# Save settings? Optional.
|
|
|
|
# Delegate execution to ActionHandler
|
|
try:
|
|
# Assuming ActionHandler has execute_create_branch
|
|
# TODO: Add start_point logic later if needed
|
|
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() # Update list
|
|
# Optionally ask to switch
|
|
except (GitCommandError, ValueError) as e:
|
|
self.main_frame.show_error("Error", f"Could not create branch:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected error creating branch: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
|
|
|
|
def ui_switch_branch(self):
|
|
"""Callback for 'Switch to Selected Branch' button."""
|
|
self.logger.info("--- Action Triggered: Switch Branch ---")
|
|
svn_path = self._get_and_validate_svn_path("Switch Branch")
|
|
if not svn_path: return
|
|
|
|
selected_branch = self.main_frame.get_selected_branch() # Gets name
|
|
if not selected_branch:
|
|
self.main_frame.show_error("Error", "Select a branch."); return
|
|
|
|
# Prevent switching to the same branch
|
|
current_branch = self.main_frame.current_branch_var.get()
|
|
if selected_branch == current_branch:
|
|
self.main_frame.show_info("Info", f"Already on branch '{selected_branch}'.")
|
|
return
|
|
|
|
self.logger.info(f"Attempting switch to branch: {selected_branch}")
|
|
# Save settings? Optional.
|
|
|
|
# Delegate execution (ActionHandler checks for changes)
|
|
try:
|
|
# Assuming ActionHandler has execute_switch_branch
|
|
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}'.")
|
|
# Update UI after successful switch
|
|
self.update_current_branch_display() # Update label immediately
|
|
self.refresh_branch_list() # Update highlight in list
|
|
# else: Handler raises error
|
|
except ValueError as e: # Catch specific errors like uncommitted changes
|
|
self.main_frame.show_error("Switch Blocked", str(e))
|
|
except GitCommandError as e: # Catch errors like branch not found
|
|
self.main_frame.show_error("Error", f"Could not switch branch:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected error switching branch: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
|
|
|
|
def ui_delete_branch(self):
|
|
"""Callback for 'Delete Selected Branch' button."""
|
|
self.logger.info("--- Action Triggered: Delete Branch ---")
|
|
svn_path = self._get_and_validate_svn_path("Delete Branch")
|
|
if not svn_path: return
|
|
|
|
selected_branch = self.main_frame.get_selected_branch() # Gets name
|
|
if not selected_branch:
|
|
self.main_frame.show_error("Error", "Select a branch."); return
|
|
|
|
# Prevent deleting current or main branches
|
|
current_branch = self.main_frame.current_branch_var.get()
|
|
if selected_branch == current_branch:
|
|
self.main_frame.show_error("Error", "Cannot delete current branch."); return
|
|
if selected_branch in ["main", "master"]:
|
|
self.main_frame.show_error("Error", f"Cannot delete '{selected_branch}'."); return
|
|
|
|
# Confirmation
|
|
msg = f"Delete local branch '{selected_branch}'?\nCannot be undone easily!"
|
|
if not self.main_frame.ask_yes_no("Confirm Delete Branch", msg):
|
|
self.logger.info("Branch deletion cancelled."); return
|
|
|
|
self.logger.info(f"Attempting delete branch: {selected_branch}")
|
|
# Save settings? Unlikely needed.
|
|
|
|
# Delegate execution
|
|
try:
|
|
# Attempt safe delete first
|
|
success = self.action_handler.execute_delete_branch(
|
|
svn_path, selected_branch, force=False
|
|
)
|
|
if success:
|
|
self.main_frame.show_info("Success", f"Branch '{selected_branch}' deleted.")
|
|
self.refresh_branch_list() # Update list
|
|
# else: Handler should raise error
|
|
|
|
except GitCommandError as e:
|
|
# Handle specific errors, like 'not fully merged'
|
|
if "not fully merged" in str(e).lower():
|
|
force_msg = (f"Branch '{selected_branch}' not fully merged.\n"
|
|
f"Force delete anyway (irreversible)?")
|
|
if self.main_frame.ask_yes_no("Force Delete?", force_msg):
|
|
try:
|
|
# Attempt force delete via ActionHandler
|
|
force_success = self.action_handler.execute_delete_branch(
|
|
svn_path, selected_branch, force=True
|
|
)
|
|
if force_success:
|
|
self.main_frame.show_info("Success", f"Branch '{selected_branch}' force deleted.")
|
|
self.refresh_branch_list()
|
|
# else: Handler should raise error if force delete fails
|
|
except Exception as force_e:
|
|
self.logger.error(f"Force delete failed: {force_e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Force delete failed:\n{force_e}")
|
|
else:
|
|
self.logger.info("Force delete cancelled.")
|
|
else:
|
|
# Other Git errors
|
|
self.main_frame.show_error("Error", f"Could not delete branch:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected error deleting branch: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
|
|
|
|
|
|
# --- GUI State Utilities ---
|
|
def _clear_and_disable_fields(self):
|
|
"""Clears relevant GUI fields and disables most buttons."""
|
|
if hasattr(self, 'main_frame'):
|
|
mf = self.main_frame # Alias
|
|
# Clear Repo frame fields
|
|
mf.svn_path_entry.delete(0, tk.END)
|
|
mf.usb_path_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_updated_name_entry.delete(0, tk.END)
|
|
# Clear Commit/Tag/Branch frame fields
|
|
mf.commit_message_var.set("")
|
|
mf.autocommit_var.set(False)
|
|
mf.update_tag_list([]) # Clear tag listbox
|
|
mf.update_branch_list([]) # Clear branch listbox
|
|
mf.set_current_branch_display("<N/A>")
|
|
# Reset indicator and dependent buttons/widgets
|
|
# This handles disabling Prepare, EditGitignore, Commit/Tag/Branch widgets
|
|
self.update_svn_status_indicator("")
|
|
# Disable general action buttons explicitly
|
|
self._disable_general_buttons()
|
|
self.logger.debug("GUI fields cleared/reset. Buttons disabled.")
|
|
|
|
|
|
def _disable_general_buttons(self):
|
|
"""Disables buttons generally requiring only a loaded profile."""
|
|
if hasattr(self, 'main_frame'):
|
|
# List of general action button attribute names in main_frame
|
|
button_names = [
|
|
'create_bundle_button', 'fetch_bundle_button',
|
|
'manual_backup_button', 'save_settings_button'
|
|
]
|
|
# Iterate and disable if the button exists
|
|
for name in button_names:
|
|
button = getattr(self.main_frame, name, None)
|
|
if button:
|
|
button.config(state=tk.DISABLED)
|
|
|
|
|
|
def _enable_function_buttons(self):
|
|
"""
|
|
Enables general action buttons. State-dependent buttons rely on
|
|
update_svn_status_indicator for their state.
|
|
"""
|
|
if hasattr(self, 'main_frame'):
|
|
general_state = tk.NORMAL
|
|
# List of general action button attribute names
|
|
button_names = [
|
|
'create_bundle_button', 'fetch_bundle_button',
|
|
'manual_backup_button', 'save_settings_button'
|
|
]
|
|
# Iterate and enable if the button exists
|
|
for name in button_names:
|
|
button = getattr(self.main_frame, name, None)
|
|
if button:
|
|
button.config(state=general_state)
|
|
|
|
# Ensure state-dependent buttons reflect the current status
|
|
# This call updates Prepare, EditGitignore, Commit/Tag/Branch widget states
|
|
current_svn_path = ""
|
|
if hasattr(self.main_frame, 'svn_path_entry'):
|
|
current_svn_path = self.main_frame.svn_path_entry.get()
|
|
self.update_svn_status_indicator(current_svn_path)
|
|
self.logger.debug("General buttons enabled. State buttons updated.")
|
|
|
|
|
|
def show_fatal_error(self, message):
|
|
"""Shows a fatal error message."""
|
|
try:
|
|
# Determine parent window safely
|
|
parent = None
|
|
if hasattr(self, 'master') and self.master and self.master.winfo_exists():
|
|
parent = self.master
|
|
messagebox.showerror("Fatal Error", message, parent=parent)
|
|
except tk.TclError:
|
|
# Fallback if GUI is not ready or fails
|
|
print(f"FATAL ERROR: {message}")
|
|
except Exception as e:
|
|
# Log error showing the message box itself
|
|
print(f"FATAL ERROR (and GUI error: {e}): {message}")
|
|
|
|
|
|
def resource_path(relative_path):
|
|
""" Ottiene il percorso assoluto della risorsa, funziona per dev e per PyInstaller """
|
|
try:
|
|
# PyInstaller crea una cartella temporanea e salva il percorso in _MEIPASS
|
|
base_path = sys._MEIPASS
|
|
except Exception:
|
|
# _MEIPASS non esiste, siamo in modalità sviluppo normale
|
|
base_path = os.path.abspath(".") # Usa la directory corrente
|
|
|
|
return os.path.join(base_path, relative_path)
|
|
|
|
# --- Application Entry Point ---
|
|
def main():
|
|
"""Main function: Creates Tkinter root and runs the application."""
|
|
root = tk.Tk()
|
|
# Adjust min size for the new layout with tabs
|
|
root.minsize(750, 650) # Adjusted min height after tabbing
|
|
|
|
# --- Imposta l'icona della finestra ---
|
|
try:
|
|
# Assumendo che 'app_icon.ico' sia nella stessa dir dello script
|
|
# o aggiunto correttamente a PyInstaller
|
|
icon_path = resource_path("GitUtility.ico")
|
|
# Usa wm_iconbitmap per Windows
|
|
if os.path.exists(icon_path):
|
|
# wm_iconbitmap si aspetta un file .ico su Windows
|
|
# Per Linux/Mac, si userebbe iconphoto con un PhotoImage (PNG)
|
|
if os.name == 'nt': # Solo per Windows
|
|
root.wm_iconbitmap(icon_path)
|
|
else:
|
|
# Su Linux/Mac potresti usare iconphoto con un file PNG
|
|
# icon_img = tk.PhotoImage(file=resource_path("app_icon.png"))
|
|
# root.iconphoto(True, icon_img) # 'True' per default icon
|
|
# Nota: Dovresti aggiungere app_icon.png con --add-data
|
|
pass # Per ora non facciamo nulla su altri OS
|
|
else:
|
|
# Log se l'icona non viene trovata nel percorso atteso
|
|
logging.warning(f"Window icon file not found at: {icon_path}")
|
|
except tk.TclError as e:
|
|
# Logga se c'è un errore nel caricare/impostare l'icona
|
|
logging.warning(f"Could not set window icon: {e}")
|
|
except Exception as e:
|
|
# Logga altri errori imprevisti
|
|
logging.warning(f"Unexpected error setting window icon: {e}", exc_info=True)
|
|
|
|
app = None # Initialize app variable
|
|
try:
|
|
app = GitSvnSyncApp(root)
|
|
# Start main loop only if initialization likely succeeded
|
|
if hasattr(app, 'main_frame') and app.main_frame:
|
|
root.mainloop()
|
|
else:
|
|
# Initialization failed before GUI setup could complete
|
|
print("Application initialization failed, exiting.")
|
|
# Ensure window closes if init failed but window was created
|
|
if root and root.winfo_exists():
|
|
root.destroy()
|
|
except Exception as e:
|
|
# Catch-all for unexpected errors during startup or main loop
|
|
logging.exception("Fatal error during application startup or main loop.")
|
|
# Try showing message box, fallback to print
|
|
try:
|
|
parent_window = root if root and root.winfo_exists() else None
|
|
messagebox.showerror("Fatal Error",
|
|
f"Application failed unexpectedly:\n{e}",
|
|
parent=parent_window)
|
|
except Exception as msg_e:
|
|
print(f"FATAL ERROR (GUI error: {msg_e}): App failed:\n{e}")
|
|
finally:
|
|
# Log application exit regardless of success or failure
|
|
logging.info("Application exiting.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Setup basic logging immediately at startup
|
|
log_format = "%(asctime)s - %(levelname)s - [%(module)s:%(funcName)s:%(lineno)d] - %(message)s"
|
|
# Consider level=logging.DEBUG for more detail during development
|
|
logging.basicConfig(level=logging.INFO, format=log_format)
|
|
main() |