versione con tab

This commit is contained in:
VALLONGOL 2025-04-09 13:39:00 +02:00
parent 7f15ca6caa
commit 59d32cb479
7 changed files with 620 additions and 1748 deletions

1324
GitTool.py

File diff suppressed because it is too large Load Diff

BIN
GitUtility.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,10 +1,12 @@
# GitUtility.py # GitUtility.py
import os import os
import sys
# import shutil # Not needed anymore
import datetime import datetime
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
import logging import logging
import zipfile # import zipfile # Not needed anymore
# Import application modules # Import application modules
from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR
@ -63,7 +65,7 @@ class GitSvnSyncApp:
self.main_frame = MainFrame( self.main_frame = MainFrame(
master, master,
load_profile_settings_cb=self.load_profile_settings, load_profile_settings_cb=self.load_profile_settings,
browse_folder_cb=self.browse_folder, # browse_folder_cb REMOVED - Handled within MainFrame now
update_svn_status_cb=self.update_svn_status_indicator, update_svn_status_cb=self.update_svn_status_indicator,
# Core Action Callbacks # Core Action Callbacks
prepare_svn_for_git_cb=self.ui_prepare_svn, prepare_svn_for_git_cb=self.ui_prepare_svn,
@ -132,10 +134,8 @@ class GitSvnSyncApp:
return None return None
if not hasattr(self.main_frame, 'svn_path_entry'): if not hasattr(self.main_frame, 'svn_path_entry'):
self.logger.error(f"{operation_name}: SVN path widget missing.") self.logger.error(f"{operation_name}: SVN path widget missing.")
# Try showing error if main_frame exists, otherwise just log
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
self.main_frame.show_error("Internal Error", self.main_frame.show_error("Internal Error", "SVN Path widget missing.")
"SVN Path widget not found.")
return None return None
svn_path_str = self.main_frame.svn_path_entry.get() svn_path_str = self.main_frame.svn_path_entry.get()
@ -147,13 +147,8 @@ class GitSvnSyncApp:
abs_path = os.path.abspath(svn_path_str) abs_path = os.path.abspath(svn_path_str)
if not os.path.isdir(abs_path): if not os.path.isdir(abs_path):
self.logger.error( self.logger.error(f"{operation_name}: Invalid directory path: {abs_path}")
f"{operation_name}: Invalid directory path: {abs_path}" self.main_frame.show_error("Input Error", f"Invalid SVN path:\n{abs_path}")
)
self.main_frame.show_error(
"Input Error",
f"Invalid SVN path (not a directory):\n{abs_path}"
)
return None return None
self.logger.debug(f"{operation_name}: Validated SVN path: {abs_path}") self.logger.debug(f"{operation_name}: Validated SVN path: {abs_path}")
@ -168,32 +163,72 @@ class GitSvnSyncApp:
if not hasattr(self.main_frame, 'usb_path_entry'): if not hasattr(self.main_frame, 'usb_path_entry'):
self.logger.error(f"{operation_name}: USB path widget missing.") self.logger.error(f"{operation_name}: USB path widget missing.")
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
self.main_frame.show_error("Internal Error", self.main_frame.show_error("Internal Error", "USB Path widget missing.")
"USB Path widget not found.")
return None return None
usb_path_str = self.main_frame.usb_path_entry.get() usb_path_str = self.main_frame.usb_path_entry.get()
usb_path_str = usb_path_str.strip() usb_path_str = usb_path_str.strip()
if not usb_path_str: if not usb_path_str:
self.logger.error(f"{operation_name}: Bundle Target Dir path empty.") self.logger.error(f"{operation_name}: Bundle Target Dir path empty.")
self.main_frame.show_error("Input Error", self.main_frame.show_error("Input Error", "Bundle Target Dir empty.")
"Bundle Target Directory cannot be empty.")
return None return None
abs_path = os.path.abspath(usb_path_str) abs_path = os.path.abspath(usb_path_str)
if not os.path.isdir(abs_path): if not os.path.isdir(abs_path):
self.logger.error( self.logger.error(f"{operation_name}: Invalid Bundle Target: {abs_path}")
f"{operation_name}: Invalid Bundle Target directory: {abs_path}" self.main_frame.show_error("Input Error", f"Invalid Bundle Target:\n{abs_path}")
)
self.main_frame.show_error(
"Input Error",
f"Invalid Bundle Target path (not a directory):\n{abs_path}"
)
return None return None
self.logger.debug(f"{operation_name}: Validated Bundle Target path: {abs_path}") self.logger.debug(f"{operation_name}: Validated Bundle Target path: {abs_path}")
return 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 --- # --- Profile Handling Wrappers ---
def load_profile_settings(self, profile_name): def load_profile_settings(self, profile_name):
@ -207,15 +242,14 @@ class GitSvnSyncApp:
profile_data = self.profile_handler.load_profile_data(profile_name) profile_data = self.profile_handler.load_profile_data(profile_name)
if not profile_data: if not profile_data:
# Handler logs error, show message and clear UI # Handler logs error, show message and clear UI
self.main_frame.show_error("Load Error", self.main_frame.show_error("Load Error", f"Could not load '{profile_name}'.")
f"Could not load profile '{profile_name}'.")
self._clear_and_disable_fields() self._clear_and_disable_fields()
return return
# Update GUI fields with loaded data if frame exists # Update GUI fields with loaded data if frame exists
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
mf = self.main_frame # Alias mf = self.main_frame # Alias
# Update Repository Frame widgets # Update all widgets based on loaded data
mf.svn_path_entry.delete(0, tk.END) mf.svn_path_entry.delete(0, tk.END)
mf.svn_path_entry.insert(0, profile_data.get("svn_working_copy_path", "")) 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.delete(0, tk.END)
@ -224,10 +258,8 @@ class GitSvnSyncApp:
mf.bundle_name_entry.insert(0, profile_data.get("bundle_name", "")) 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.delete(0, tk.END)
mf.bundle_updated_name_entry.insert(0, profile_data.get("bundle_name_updated", "")) mf.bundle_updated_name_entry.insert(0, profile_data.get("bundle_name_updated", ""))
# Update Commit/Tag Frame widgets
mf.autocommit_var.set(profile_data.get("autocommit", False)) # Bool mf.autocommit_var.set(profile_data.get("autocommit", False)) # Bool
mf.commit_message_var.set(profile_data.get("commit_message", "")) mf.commit_message_var.set(profile_data.get("commit_message", ""))
# Update Backup Frame widgets
mf.autobackup_var.set(profile_data.get("autobackup", False)) # Bool 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_dir_var.set(profile_data.get("backup_dir", DEFAULT_BACKUP_DIR))
mf.backup_exclude_extensions_var.set( mf.backup_exclude_extensions_var.set(
@ -264,24 +296,24 @@ class GitSvnSyncApp:
profile = self.main_frame.profile_var.get() profile = self.main_frame.profile_var.get()
if not profile: if not profile:
self.main_frame.show_error("Save Error", "No profile selected.") self.main_frame.show_error("Save Error", "No profile selected.")
return # Don't proceed return False # Indicate failure to caller if needed
# Gather data from GUI # Gather data from GUI
current_data = self._get_data_from_gui() current_data = self._get_data_from_gui()
if current_data is None: # Check if reading GUI failed if current_data is None: # Check if reading GUI failed
self.main_frame.show_error("Internal Error", "Could not read GUI data.") self.main_frame.show_error("Internal Error", "Could not read GUI data.")
return return False
# Delegate saving to ProfileHandler # Delegate saving to ProfileHandler
success = self.profile_handler.save_profile_data(profile, current_data) success = self.profile_handler.save_profile_data(profile, current_data)
if success: if success:
# Give positive feedback
self.main_frame.show_info("Saved", f"Settings saved for '{profile}'.") self.main_frame.show_info("Saved", f"Settings saved for '{profile}'.")
return True
else: else:
# Error message likely shown by handler/save method # Error message likely shown by handler/save method
self.logger.error(f"Saving settings failed for profile '{profile}'.") # self.main_frame.show_error("Save Error", f"Failed save settings.")
# self.main_frame.show_error("Save Error", "Failed to save settings.") return False
def _get_data_from_gui(self): def _get_data_from_gui(self):
@ -292,17 +324,16 @@ class GitSvnSyncApp:
mf = self.main_frame mf = self.main_frame
# Read values from all relevant widgets/variables # Read values from all relevant widgets/variables
data = { data = {}
"svn_working_copy_path": mf.svn_path_entry.get(), data["svn_working_copy_path"] = mf.svn_path_entry.get()
"usb_drive_path": mf.usb_path_entry.get(), data["usb_drive_path"] = mf.usb_path_entry.get()
"bundle_name": mf.bundle_name_entry.get(), data["bundle_name"] = mf.bundle_name_entry.get()
"bundle_name_updated": mf.bundle_updated_name_entry.get(), data["bundle_name_updated"] = mf.bundle_updated_name_entry.get()
"autocommit": mf.autocommit_var.get(), # Gets boolean data["autocommit"] = mf.autocommit_var.get() # Gets boolean
"commit_message": mf.commit_message_var.get(), data["commit_message"] = mf.commit_message_var.get()
"autobackup": mf.autobackup_var.get(), # Gets boolean data["autobackup"] = mf.autobackup_var.get() # Gets boolean
"backup_dir": mf.backup_dir_var.get(), data["backup_dir"] = mf.backup_dir_var.get()
"backup_exclude_extensions": mf.backup_exclude_extensions_var.get() data["backup_exclude_extensions"] = mf.backup_exclude_extensions_var.get()
}
return data return data
@ -315,19 +346,16 @@ class GitSvnSyncApp:
new_name = new_name.strip() new_name = new_name.strip()
if not new_name: if not new_name:
self.main_frame.show_error("Error", "Profile name cannot be empty."); return self.main_frame.show_error("Error", "Profile name empty."); return
# Delegate adding logic # Delegate adding logic
success = self.profile_handler.add_new_profile(new_name) success = self.profile_handler.add_new_profile(new_name)
if success: if success:
# Update GUI dropdown and select the new profile
sections = self.profile_handler.get_profile_list() sections = self.profile_handler.get_profile_list()
self.main_frame.update_profile_dropdown(sections) self.main_frame.update_profile_dropdown(sections)
self.main_frame.profile_var.set(new_name) # Triggers load self.main_frame.profile_var.set(new_name) # Triggers load
self.main_frame.show_info("Profile Added", f"Profile '{new_name}' created.") self.main_frame.show_info("Profile Added", f"Profile '{new_name}' created.")
else: else:
# Handler logged the reason (exists or error)
self.main_frame.show_error("Error", f"Could not add profile '{new_name}'.") self.main_frame.show_error("Error", f"Could not add profile '{new_name}'.")
@ -350,10 +378,9 @@ class GitSvnSyncApp:
self.main_frame.update_profile_dropdown(sections) # Triggers load self.main_frame.update_profile_dropdown(sections) # Triggers load
self.main_frame.show_info("Removed", f"Profile '{profile}' removed.") self.main_frame.show_info("Removed", f"Profile '{profile}' removed.")
else: else:
# Handler logged the reason
self.main_frame.show_error("Error", f"Failed to remove '{profile}'.") self.main_frame.show_error("Error", f"Failed to remove '{profile}'.")
else: else:
self.logger.info("Profile removal cancelled.") self.logger.info("Removal cancelled.")
# --- GUI Interaction Wrappers --- # --- GUI Interaction Wrappers ---
@ -369,7 +396,7 @@ class GitSvnSyncApp:
self.logger.debug(f"Selected: {directory}") self.logger.debug(f"Selected: {directory}")
entry_widget.delete(0, tk.END) entry_widget.delete(0, tk.END)
entry_widget.insert(0, directory) entry_widget.insert(0, directory)
# Update status if SVN path changed # Trigger status update if SVN path changed
if entry_widget == self.main_frame.svn_path_entry: if entry_widget == self.main_frame.svn_path_entry:
self.update_svn_status_indicator(directory) self.update_svn_status_indicator(directory)
else: else:
@ -385,10 +412,10 @@ class GitSvnSyncApp:
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
mf = self.main_frame # Alias mf = self.main_frame # Alias
# Update indicator & Prepare button via MainFrame method # Update indicator & Prepare button via GUI method
mf.update_svn_indicator(is_ready) mf.update_svn_indicator(is_ready)
# Determine states for other widgets based on validity/readiness # Determine states for other dependent widgets
gitignore_state = tk.NORMAL if is_valid else tk.DISABLED gitignore_state = tk.NORMAL if is_valid else tk.DISABLED
repo_ready_state = tk.NORMAL if is_ready else tk.DISABLED repo_ready_state = tk.NORMAL if is_ready else tk.DISABLED
@ -425,7 +452,7 @@ class GitSvnSyncApp:
"""Opens the modal editor window for .gitignore.""" """Opens the modal editor window for .gitignore."""
self.logger.info("--- Action Triggered: Edit .gitignore ---") self.logger.info("--- Action Triggered: Edit .gitignore ---")
svn_path = self._get_and_validate_svn_path("Edit .gitignore") svn_path = self._get_and_validate_svn_path("Edit .gitignore")
if not svn_path: return # Stop if path invalid if not svn_path: return
gitignore_path = os.path.join(svn_path, ".gitignore") gitignore_path = os.path.join(svn_path, ".gitignore")
self.logger.debug(f"Target .gitignore path: {gitignore_path}") self.logger.debug(f"Target .gitignore path: {gitignore_path}")
@ -449,9 +476,12 @@ class GitSvnSyncApp:
if not svn_path: return if not svn_path: return
# Save settings before action # Save settings before action
# Use ui_save_settings which returns True/False
if not self.ui_save_settings(): if not self.ui_save_settings():
self.logger.warning("Prepare SVN: Failed save settings first.") self.logger.warning("Prepare SVN: Failed to save settings first.")
# Ask user? # 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 # Delegate execution to ActionHandler
try: try:
@ -491,7 +521,7 @@ class GitSvnSyncApp:
# Ensure .bundle extension # Ensure .bundle extension
if not bundle_name.lower().endswith(".bundle"): if not bundle_name.lower().endswith(".bundle"):
bundle_name += ".bundle" bundle_name += ".bundle"
mf = self.main_frame mf = self.main_frame # Alias
mf.bundle_name_entry.delete(0, tk.END) mf.bundle_name_entry.delete(0, tk.END)
mf.bundle_name_entry.insert(0, bundle_name) mf.bundle_name_entry.insert(0, bundle_name)
bundle_full_path = os.path.join(usb_path, bundle_name) bundle_full_path = os.path.join(usb_path, bundle_name)
@ -669,24 +699,26 @@ class GitSvnSyncApp:
# Get commit message from the GUI entry # Get commit message from the GUI entry
commit_msg = self.main_frame.commit_message_var.get().strip() commit_msg = self.main_frame.commit_message_var.get().strip()
if not commit_msg: if not commit_msg:
self.main_frame.show_error("Commit Error", "Commit message cannot be empty.") self.main_frame.show_error("Commit Error", "Commit message empty.")
return return
# Save settings first? Optional, but saves the message if typed. # Save settings first? Optional, but saves the message if user typed it.
if not self.ui_save_settings(): if not self.ui_save_settings():
self.logger.warning("Manual Commit: Could not save settings.") self.logger.warning("Manual Commit: Could not save settings.")
# Ask user if they want to continue? # Ask user?
# Delegate commit execution to ActionHandler # Delegate commit execution to ActionHandler
try: try:
commit_made = self.action_handler.execute_manual_commit(svn_path, commit_msg) commit_made = self.action_handler.execute_manual_commit(
svn_path, commit_msg
)
if commit_made: if commit_made:
self.main_frame.show_info("Success", "Changes committed.") self.main_frame.show_info("Success", "Changes committed.")
# Optionally clear message field after successful commit # Clear message field after successful commit? Optional.
# self.main_frame.commit_message_var.set("") # self.main_frame.commit_message_var.set("")
else: else:
# git_commit already logged "nothing to commit" # git_commit already logged "nothing to commit"
self.main_frame.show_info("Info", "No changes were detected to commit.") self.main_frame.show_info("Info", "No changes to commit.")
except (GitCommandError, ValueError) as e: except (GitCommandError, ValueError) as e:
self.logger.error(f"Manual commit failed: {e}") self.logger.error(f"Manual commit failed: {e}")
@ -708,10 +740,10 @@ class GitSvnSyncApp:
self.main_frame.update_tag_list([]) # Clear list self.main_frame.update_tag_list([]) # Clear list
return return
# Fetch tags and update GUI
try: try:
# Get tag data (list of tuples) from GitCommands # list_tags returns list of tuples (name, subject)
tags_data = self.git_commands.list_tags(svn_path) tags_data = self.git_commands.list_tags(svn_path)
# Update the GUI listbox
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
self.main_frame.update_tag_list(tags_data) self.main_frame.update_tag_list(tags_data)
self.logger.info(f"Tag list updated ({len(tags_data)} tags).") self.logger.info(f"Tag list updated ({len(tags_data)} tags).")
@ -729,9 +761,10 @@ class GitSvnSyncApp:
if not svn_path: return if not svn_path: return
profile = self.main_frame.profile_var.get() profile = self.main_frame.profile_var.get()
if not profile: if not profile:
self.main_frame.show_error("Error", "No profile selected."); return self.main_frame.show_error("Error", "No profile selected.")
return
# Get commit message from GUI (for potential pre-commit) # Get commit message from GUI (needed by action handler for pre-commit)
commit_msg = self.main_frame.commit_message_var.get().strip() commit_msg = self.main_frame.commit_message_var.get().strip()
# Save settings before action (saves commit message) # Save settings before action (saves commit message)
@ -739,32 +772,40 @@ class GitSvnSyncApp:
self.logger.warning("Create Tag: Could not save settings first.") self.logger.warning("Create Tag: Could not save settings first.")
# Ask user? # Ask user?
# Open Dialog first to get Tag Name and Tag Message # --- Open Dialog to get Tag Name and Tag Message ---
self.logger.debug("Opening create tag dialog...") self.logger.debug("Opening create tag dialog...")
dialog = CreateTagDialog(self.master) dialog = CreateTagDialog(self.master) # Parent is the main Tk window
tag_info = dialog.result # Returns (tag_name, tag_message) or None tag_info = dialog.result # Returns (tag_name, tag_message) or None
if not tag_info: if not tag_info:
self.logger.info("Tag creation cancelled in dialog."); return # User cancelled the dialog
self.logger.info("Tag creation cancelled by user in dialog.")
return
tag_name, tag_message = tag_info tag_name, tag_message = tag_info
self.logger.info(f"User provided tag: '{tag_name}', msg: '{tag_message}'") self.logger.info(f"User provided tag: '{tag_name}', msg: '{tag_message}'")
# Delegate Execution (including potential pre-commit) to ActionHandler # --- Delegate Execution to ActionHandler ---
try: try:
# ActionHandler manages pre-commit logic based on commit_msg presence
success = self.action_handler.execute_create_tag( success = self.action_handler.execute_create_tag(
svn_path, commit_msg, tag_name, tag_message svn_path, commit_msg, tag_name, tag_message
) )
if success: # 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.logger.info(f"Tag '{tag_name}' created successfully.")
self.main_frame.show_info("Success", f"Tag '{tag_name}' created.") self.main_frame.show_info("Success", f"Tag '{tag_name}' created.")
self.refresh_tag_list() # Update list self.refresh_tag_list() # Update list after successful creation
except ValueError as e: # Catch specific errors like "commit message required" except ValueError as e:
# Catch specific errors like "commit message required"
self.logger.error(f"Tag creation validation failed: {e}") self.logger.error(f"Tag creation validation failed: {e}")
self.main_frame.show_error("Tag Error", str(e)) self.main_frame.show_error("Tag Error", str(e))
except GitCommandError as e: # Catch Git command errors (commit or tag) except GitCommandError as e:
# Catch Git command errors (from commit or tag)
self.logger.error(f"Tag creation failed (Git Error): {e}") self.logger.error(f"Tag creation failed (Git Error): {e}")
self.main_frame.show_error("Tag Error", f"Git command failed:\n{e}") self.main_frame.show_error("Tag Error", f"Git command failed:\n{e}")
except Exception as e: # Catch unexpected errors except Exception as e:
# Catch unexpected errors
self.logger.exception(f"Unexpected error creating tag: {e}") self.logger.exception(f"Unexpected error creating tag: {e}")
self.main_frame.show_error("Error", f"Unexpected error:\n{e}") self.main_frame.show_error("Error", f"Unexpected error:\n{e}")
@ -777,7 +818,8 @@ class GitSvnSyncApp:
selected_tag = self.main_frame.get_selected_tag() # Gets name selected_tag = self.main_frame.get_selected_tag() # Gets name
if not selected_tag: if not selected_tag:
self.main_frame.show_error("Selection Error", "Select a tag."); return self.main_frame.show_error("Selection Error", "Select a tag.")
return
self.logger.info(f"Attempting checkout for tag: {selected_tag}") self.logger.info(f"Attempting checkout for tag: {selected_tag}")
@ -790,7 +832,7 @@ class GitSvnSyncApp:
# Save settings before action? Optional. # Save settings before action? Optional.
if not self.ui_save_settings(): if not self.ui_save_settings():
self.logger.warning("Checkout Tag: Could not save settings.") self.logger.warning("Checkout Tag: Could not save profile settings.")
# Delegate execution to ActionHandler # Delegate execution to ActionHandler
try: try:
@ -798,7 +840,7 @@ class GitSvnSyncApp:
if success: if success:
self.main_frame.show_info("Success", self.main_frame.show_info("Success",
f"Checked out tag '{selected_tag}'.\n\nNOTE: In 'detached HEAD'.") f"Checked out tag '{selected_tag}'.\n\nNOTE: In 'detached HEAD'.")
# Update display after successful checkout # Update branch display after successful checkout
self.update_current_branch_display() self.update_current_branch_display()
except ValueError as e: # Catch specific errors like "uncommitted changes" except ValueError as e: # Catch specific errors like "uncommitted changes"
self.main_frame.show_error("Checkout Blocked", str(e)) self.main_frame.show_error("Checkout Blocked", str(e))
@ -820,7 +862,7 @@ class GitSvnSyncApp:
self.main_frame.show_error("Selection Error", "Select a tag to delete.") self.main_frame.show_error("Selection Error", "Select a tag to delete.")
return return
# Confirmation # Confirmation dialog
msg = f"Delete tag '{selected_tag}' permanently?\nCannot be easily undone." msg = f"Delete tag '{selected_tag}' permanently?\nCannot be easily undone."
if not self.main_frame.ask_yes_no("Confirm Delete Tag", msg): if not self.main_frame.ask_yes_no("Confirm Delete Tag", msg):
self.logger.info("Tag deletion cancelled.") self.logger.info("Tag deletion cancelled.")
@ -856,8 +898,10 @@ class GitSvnSyncApp:
# Fetch and update GUI # Fetch and update GUI
try: try:
# Assumes git_commands method exists and returns list of names
branches = self.git_commands.list_branches(svn_path) branches = self.git_commands.list_branches(svn_path)
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
# Update listbox and potentially current branch display implicitly
self.main_frame.update_branch_list(branches) self.main_frame.update_branch_list(branches)
self.logger.info(f"Branch list updated ({len(branches)} branches).") self.logger.info(f"Branch list updated ({len(branches)} branches).")
except Exception as e: except Exception as e:
@ -871,22 +915,25 @@ class GitSvnSyncApp:
"""Gets the current branch and updates the display label.""" """Gets the current branch and updates the display label."""
self.logger.debug("Updating current branch display...") self.logger.debug("Updating current branch display...")
svn_path = self._get_and_validate_svn_path("Update Branch Display") svn_path = self._get_and_validate_svn_path("Update Branch Display")
current_branch_name = "<N/A>" # Default current_branch_name = "<N/A>" # Default value
# Only query git if repo is ready # Only query git if repo is ready
if svn_path and os.path.exists(os.path.join(svn_path, ".git")): if svn_path and os.path.exists(os.path.join(svn_path, ".git")):
try: try:
# Use GitCommands to get branch name
branch_name = self.git_commands.get_current_branch(svn_path) branch_name = self.git_commands.get_current_branch(svn_path)
# Handle return values from get_current_branch # Update display text based on result
if branch_name == "(DETACHED HEAD)": if branch_name == "(DETACHED HEAD)":
current_branch_name = branch_name current_branch_name = branch_name # Show detached state clearly
elif branch_name == "<Error>": elif branch_name == "<Error>":
current_branch_name = "<Error>" current_branch_name = "<Error>" # Show error state
elif branch_name: elif branch_name:
current_branch_name = branch_name current_branch_name = branch_name # Show actual branch name
else: # Should not happen if logic in get_current_branch is correct else:
# Fallback if method returns None unexpectedly
current_branch_name = "<Unknown>" current_branch_name = "<Unknown>"
except Exception as e: except Exception as e:
# Handle exceptions during git command execution
self.logger.error(f"Failed to get current branch: {e}") self.logger.error(f"Failed to get current branch: {e}")
current_branch_name = "<Error>" current_branch_name = "<Error>"
@ -912,18 +959,15 @@ class GitSvnSyncApp:
# Delegate execution to ActionHandler # Delegate execution to ActionHandler
try: try:
# TODO: Add start_point logic if needed later # Assuming ActionHandler has execute_create_branch
# TODO: Add start_point logic later if needed
success = self.action_handler.execute_create_branch( success = self.action_handler.execute_create_branch(
svn_path, new_branch_name svn_path, new_branch_name
) )
if success: if success:
self.main_frame.show_info("Success", f"Branch '{new_branch_name}' created.") self.main_frame.show_info("Success", f"Branch '{new_branch_name}' created.")
self.refresh_branch_list() # Update list self.refresh_branch_list() # Update list
# Ask user if they want to switch? # Optionally ask to switch
# switch_q = f"Switch to new branch '{new_branch_name}'?"
# if self.main_frame.ask_yes_no("Switch Branch?", switch_q):
# self.ui_switch_branch(new_branch_name) # Needs method adjustment
except (GitCommandError, ValueError) as e: except (GitCommandError, ValueError) as e:
self.main_frame.show_error("Error", f"Could not create branch:\n{e}") self.main_frame.show_error("Error", f"Could not create branch:\n{e}")
except Exception as e: except Exception as e:
@ -941,7 +985,7 @@ class GitSvnSyncApp:
if not selected_branch: if not selected_branch:
self.main_frame.show_error("Error", "Select a branch."); return self.main_frame.show_error("Error", "Select a branch."); return
# Avoid switching to the same branch # Prevent switching to the same branch
current_branch = self.main_frame.current_branch_var.get() current_branch = self.main_frame.current_branch_var.get()
if selected_branch == current_branch: if selected_branch == current_branch:
self.main_frame.show_info("Info", f"Already on branch '{selected_branch}'.") self.main_frame.show_info("Info", f"Already on branch '{selected_branch}'.")
@ -952,14 +996,15 @@ class GitSvnSyncApp:
# Delegate execution (ActionHandler checks for changes) # Delegate execution (ActionHandler checks for changes)
try: try:
# Assuming ActionHandler has execute_switch_branch
success = self.action_handler.execute_switch_branch( success = self.action_handler.execute_switch_branch(
svn_path, selected_branch svn_path, selected_branch
) )
if success: if success:
self.main_frame.show_info("Success", f"Switched to branch '{selected_branch}'.") self.main_frame.show_info("Success", f"Switched to branch '{selected_branch}'.")
# Update UI after successful switch # Update UI after successful switch
self.update_current_branch_display() self.update_current_branch_display() # Update label immediately
self.refresh_branch_list() # Update highlight self.refresh_branch_list() # Update highlight in list
# else: Handler raises error # else: Handler raises error
except ValueError as e: # Catch specific errors like uncommitted changes except ValueError as e: # Catch specific errors like uncommitted changes
self.main_frame.show_error("Switch Blocked", str(e)) self.main_frame.show_error("Switch Blocked", str(e))
@ -997,13 +1042,15 @@ class GitSvnSyncApp:
# Delegate execution # Delegate execution
try: try:
# Attempt safe delete first
success = self.action_handler.execute_delete_branch( success = self.action_handler.execute_delete_branch(
svn_path, selected_branch, force=False # Attempt safe delete first svn_path, selected_branch, force=False
) )
if success: if success:
self.main_frame.show_info("Success", f"Branch '{selected_branch}' deleted.") self.main_frame.show_info("Success", f"Branch '{selected_branch}' deleted.")
self.refresh_branch_list() # Update list self.refresh_branch_list() # Update list
# else: Handler raises error # else: Handler should raise error
except GitCommandError as e: except GitCommandError as e:
# Handle specific errors, like 'not fully merged' # Handle specific errors, like 'not fully merged'
if "not fully merged" in str(e).lower(): if "not fully merged" in str(e).lower():
@ -1036,8 +1083,8 @@ class GitSvnSyncApp:
def _clear_and_disable_fields(self): def _clear_and_disable_fields(self):
"""Clears relevant GUI fields and disables most buttons.""" """Clears relevant GUI fields and disables most buttons."""
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
mf = self.main_frame mf = self.main_frame # Alias
# Clear Repo frame # Clear Repo frame fields
mf.svn_path_entry.delete(0, tk.END) mf.svn_path_entry.delete(0, tk.END)
mf.usb_path_entry.delete(0, tk.END) mf.usb_path_entry.delete(0, tk.END)
mf.bundle_name_entry.delete(0, tk.END) mf.bundle_name_entry.delete(0, tk.END)
@ -1045,12 +1092,13 @@ class GitSvnSyncApp:
# Clear Commit/Tag/Branch frame fields # Clear Commit/Tag/Branch frame fields
mf.commit_message_var.set("") mf.commit_message_var.set("")
mf.autocommit_var.set(False) mf.autocommit_var.set(False)
mf.update_tag_list([]) mf.update_tag_list([]) # Clear tag listbox
mf.update_branch_list([]) mf.update_branch_list([]) # Clear branch listbox
mf.set_current_branch_display("<N/A>") mf.set_current_branch_display("<N/A>")
# Reset indicator and dependent buttons # Reset indicator and dependent buttons/widgets
self.update_svn_status_indicator("") # Disables state-dependent # This handles disabling Prepare, EditGitignore, Commit/Tag/Branch widgets
# Disable general action buttons self.update_svn_status_indicator("")
# Disable general action buttons explicitly
self._disable_general_buttons() self._disable_general_buttons()
self.logger.debug("GUI fields cleared/reset. Buttons disabled.") self.logger.debug("GUI fields cleared/reset. Buttons disabled.")
@ -1058,7 +1106,7 @@ class GitSvnSyncApp:
def _disable_general_buttons(self): def _disable_general_buttons(self):
"""Disables buttons generally requiring only a loaded profile.""" """Disables buttons generally requiring only a loaded profile."""
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
# List of general button attribute names # List of general action button attribute names in main_frame
button_names = [ button_names = [
'create_bundle_button', 'fetch_bundle_button', 'create_bundle_button', 'fetch_bundle_button',
'manual_backup_button', 'save_settings_button' 'manual_backup_button', 'save_settings_button'
@ -1073,11 +1121,11 @@ class GitSvnSyncApp:
def _enable_function_buttons(self): def _enable_function_buttons(self):
""" """
Enables general action buttons. State-dependent buttons rely on Enables general action buttons. State-dependent buttons rely on
update_svn_status_indicator. update_svn_status_indicator for their state.
""" """
if hasattr(self, 'main_frame'): if hasattr(self, 'main_frame'):
general_state = tk.NORMAL general_state = tk.NORMAL
# List of general button attribute names # List of general action button attribute names
button_names = [ button_names = [
'create_bundle_button', 'fetch_bundle_button', 'create_bundle_button', 'fetch_bundle_button',
'manual_backup_button', 'save_settings_button' 'manual_backup_button', 'save_settings_button'
@ -1089,7 +1137,7 @@ class GitSvnSyncApp:
button.config(state=general_state) button.config(state=general_state)
# Ensure state-dependent buttons reflect the current status # Ensure state-dependent buttons reflect the current status
# This call handles Prepare, EditGitignore, Commit/Tag/Branch widgets # This call updates Prepare, EditGitignore, Commit/Tag/Branch widget states
current_svn_path = "" current_svn_path = ""
if hasattr(self.main_frame, 'svn_path_entry'): if hasattr(self.main_frame, 'svn_path_entry'):
current_svn_path = self.main_frame.svn_path_entry.get() current_svn_path = self.main_frame.svn_path_entry.get()
@ -1113,13 +1161,51 @@ class GitSvnSyncApp:
print(f"FATAL ERROR (and GUI error: {e}): {message}") 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 --- # --- Application Entry Point ---
def main(): def main():
"""Main function: Creates Tkinter root and runs the application.""" """Main function: Creates Tkinter root and runs the application."""
root = tk.Tk() root = tk.Tk()
# Adjust min size for the new layout with tabs # Adjust min size for the new layout with tabs
# May need adjustment based on final widget sizes root.minsize(750, 650) # Adjusted min height after tabbing
root.minsize(750, 650) # Adjusted height back down slightly
# --- 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 app = None # Initialize app variable
try: try:
app = GitSvnSyncApp(root) app = GitSvnSyncApp(root)
@ -1150,7 +1236,7 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
# Setup basic logging immediately at startup # Setup basic logging immediately at startup
# This ensures logs are captured even if setup_logger fails later
log_format = "%(asctime)s - %(levelname)s - [%(module)s:%(funcName)s:%(lineno)d] - %(message)s" log_format = "%(asctime)s - %(levelname)s - [%(module)s:%(funcName)s:%(lineno)d] - %(message)s"
logging.basicConfig(level=logging.INFO, format=log_format) # Consider level=logging.DEBUG for more detail # Consider level=logging.DEBUG for more detail during development
logging.basicConfig(level=logging.INFO, format=log_format)
main() main()

44
GitUtility.spec Normal file
View File

@ -0,0 +1,44 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['GitUtility.py'],
pathex=[],
binaries=[],
datas=[('git_svn_sync.ini', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='GitUtility',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='GitUtility',
)

View File

@ -22,6 +22,7 @@ class ActionHandler:
self.git_commands = git_commands self.git_commands = git_commands
self.backup_handler = backup_handler self.backup_handler = backup_handler
def _perform_backup_if_enabled(self, svn_path, profile_name, def _perform_backup_if_enabled(self, svn_path, profile_name,
autobackup_enabled, backup_base_dir, autobackup_enabled, backup_base_dir,
excluded_extensions, excluded_dirs): excluded_extensions, excluded_dirs):
@ -37,15 +38,16 @@ class ActionHandler:
excluded_dirs (set): Directory names to exclude. excluded_dirs (set): Directory names to exclude.
Raises: Raises:
IOError: If the backup process fails. IOError: If the backup process fails for any reason.
""" """
if not autobackup_enabled: if not autobackup_enabled:
self.logger.debug("Autobackup disabled, skipping backup.") self.logger.debug("Autobackup disabled, skipping backup.")
return # Backup not needed, proceed return # Backup not needed, proceed successfully
self.logger.info("Autobackup enabled. Starting backup...") self.logger.info("Autobackup enabled. Starting backup...")
try: try:
# Delegate backup creation to the BackupHandler instance # Delegate backup creation to the BackupHandler instance
# It will raise exceptions on failure
backup_path = self.backup_handler.create_zip_backup( backup_path = self.backup_handler.create_zip_backup(
svn_path, svn_path,
backup_base_dir, backup_base_dir,
@ -55,11 +57,12 @@ class ActionHandler:
) )
# Log success if backup handler doesn't raise an error # Log success if backup handler doesn't raise an error
self.logger.info(f"Backup completed successfully: {backup_path}") self.logger.info(f"Backup completed successfully: {backup_path}")
# No explicit return value needed on success, failure indicated by exception # No explicit return value needed on success
except Exception as backup_e: except Exception as backup_e:
# Log error and re-raise as IOError to signal critical failure # Log error and re-raise as IOError to signal critical failure
self.logger.error(f"Backup failed: {backup_e}", exc_info=True) self.logger.error(f"Backup failed: {backup_e}", exc_info=True)
# Standardize on IOError for backup failures passed up
raise IOError(f"Autobackup failed: {backup_e}") from backup_e raise IOError(f"Autobackup failed: {backup_e}") from backup_e
@ -85,17 +88,14 @@ class ActionHandler:
raise ValueError("Repository is already prepared.") raise ValueError("Repository is already prepared.")
# Attempt preparation using GitCommands # Attempt preparation using GitCommands
# Any exception (GitCommandError, ValueError, IOError) will be caught
try: try:
self.git_commands.prepare_svn_for_git(svn_path) self.git_commands.prepare_svn_for_git(svn_path)
self.logger.info("Repository prepared successfully.") self.logger.info("Repository prepared successfully.")
return True return True
except (GitCommandError, ValueError, IOError) as e:
# Log and re-raise known errors for the UI layer to handle
self.logger.error(f"Failed to prepare repository: {e}", exc_info=True)
raise
except Exception as e: except Exception as e:
# Log and re-raise unexpected errors # Log and re-raise any exception from prepare_svn_for_git
self.logger.exception(f"Unexpected error during preparation: {e}") self.logger.error(f"Failed to prepare repository: {e}", exc_info=True)
raise raise
@ -142,7 +142,7 @@ class ActionHandler:
commit_msg_to_use = commit_message if commit_message else \ commit_msg_to_use = commit_message if commit_message else \
f"Autocommit '{profile_name}' before bundle" f"Autocommit '{profile_name}' before bundle"
self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'") self.logger.debug(f"Using autocommit message: '{commit_msg_to_use}'")
# Perform commit (raises error on failure) # Perform commit (raises error on failure, returns bool)
self.git_commands.git_commit(svn_path, commit_msg_to_use) self.git_commands.git_commit(svn_path, commit_msg_to_use)
# Log based on return value? git_commit already logs detail. # Log based on return value? git_commit already logs detail.
self.logger.info("Autocommit attempt finished.") self.logger.info("Autocommit attempt finished.")
@ -172,7 +172,7 @@ class ActionHandler:
try: try:
os.remove(bundle_full_path) os.remove(bundle_full_path)
except OSError: except OSError:
self.logger.warning(f"Could not remove empty bundle file.") self.logger.warning("Could not remove empty bundle file.")
return None # Indicate non-fatal issue (empty bundle) return None # Indicate non-fatal issue (empty bundle)
except Exception as bundle_e: except Exception as bundle_e:
# Log and re-raise bundle creation errors # Log and re-raise bundle creation errors
@ -205,7 +205,7 @@ class ActionHandler:
# Delegate to GitCommands; it raises GitCommandError on conflict/failure # Delegate to GitCommands; it raises GitCommandError on conflict/failure
self.git_commands.fetch_from_git_bundle(svn_path, bundle_full_path) self.git_commands.fetch_from_git_bundle(svn_path, bundle_full_path)
self.logger.info("Fetch/merge process completed successfully.") self.logger.info("Fetch/merge process completed successfully.")
# No return value needed, success indicated by no exception # No return needed, success indicated by no exception
except Exception as fetch_e: except Exception as fetch_e:
# Log and re-raise any error from fetch/merge # Log and re-raise any error from fetch/merge
self.logger.error(f"Fetch/merge failed: {fetch_e}", exc_info=True) self.logger.error(f"Fetch/merge failed: {fetch_e}", exc_info=True)
@ -221,13 +221,13 @@ class ActionHandler:
commit_message (str): The commit message (must not be empty). commit_message (str): The commit message (must not be empty).
Returns: Returns:
bool: True if commit made, False if no changes. bool: True if a commit was made, False if no changes were committed.
Raises: Raises:
ValueError: If commit_message is empty. ValueError: If commit_message is empty.
GitCommandError/Exception: If commit command fails. GitCommandError/Exception: If commit command fails.
""" """
if not commit_message: if not commit_message:
# Should be validated by caller (UI layer) # This validation should ideally happen before calling this method
self.logger.error("Manual commit attempt with empty message.") self.logger.error("Manual commit attempt with empty message.")
raise ValueError("Commit message cannot be empty.") raise ValueError("Commit message cannot be empty.")
@ -274,9 +274,10 @@ class ActionHandler:
# Perform the pre-tag commit with the provided message # Perform the pre-tag commit with the provided message
self.logger.debug(f"Performing pre-tag commit: '{commit_message}'") self.logger.debug(f"Performing pre-tag commit: '{commit_message}'")
# git_commit raises error on failure, returns bool on success/no changes # git_commit raises error on failure, returns bool otherwise
self.git_commands.git_commit(svn_path, commit_message) commit_made = self.git_commands.git_commit(svn_path, commit_message)
self.logger.info("Pre-tag commit attempt finished.") # Log based on return value? git_commit logs details.
self.logger.info(f"Pre-tag commit attempt finished (made={commit_made}).")
else: else:
self.logger.info("No uncommitted changes detected before tagging.") self.logger.info("No uncommitted changes detected before tagging.")
@ -303,11 +304,11 @@ class ActionHandler:
def execute_checkout_tag(self, svn_path, tag_name): def execute_checkout_tag(self, svn_path, tag_name):
""" """
Executes checkout for the specified tag after checking for changes. Executes checkout for the specified tag after checking changes.
Args: Args:
svn_path (str): Validated path to repository. svn_path (str): Validated path to repository.
tag_name (str): The tag name to check out (already validated by caller). tag_name (str): The tag name to check out (validated by caller).
Returns: Returns:
bool: True on successful checkout. bool: True on successful checkout.
@ -316,7 +317,7 @@ class ActionHandler:
GitCommandError/Exception: If status check or checkout fails. GitCommandError/Exception: If status check or checkout fails.
""" """
if not tag_name: if not tag_name:
raise ValueError("Tag name required for checkout.") # Should be caught earlier raise ValueError("Tag name required for checkout.")
self.logger.info(f"Executing checkout tag '{tag_name}' in: {svn_path}") self.logger.info(f"Executing checkout tag '{tag_name}' in: {svn_path}")
@ -336,11 +337,15 @@ class ActionHandler:
# --- Execute Checkout --- # --- Execute Checkout ---
try: try:
# git_commands.checkout_tag raises GitCommandError on failure # git_commands.checkout_tag raises error on failure
checkout_success = self.git_commands.checkout_tag(svn_path, tag_name) checkout_success = self.git_commands.checkout_tag(svn_path, tag_name)
# If no exception, assume success if checkout_success:
self.logger.info(f"Tag '{tag_name}' checked out.") self.logger.info(f"Tag '{tag_name}' checked out.")
return True return True
else:
# This path should theoretically not be reached
self.logger.error("Checkout command reported failure unexpectedly.")
raise GitCommandError("Checkout failed for unknown reason.")
except Exception as e: except Exception as e:
# Catch errors during checkout (e.g., tag not found) # Catch errors during checkout (e.g., tag not found)
self.logger.error(f"Failed to checkout tag '{tag_name}': {e}", self.logger.error(f"Failed to checkout tag '{tag_name}': {e}",
@ -365,6 +370,7 @@ class ActionHandler:
self.git_commands.create_branch(svn_path, branch_name, start_point) self.git_commands.create_branch(svn_path, branch_name, start_point)
return True return True
except Exception as e: except Exception as e:
# Log and re-raise error
self.logger.error(f"Failed to create branch: {e}", exc_info=True) self.logger.error(f"Failed to create branch: {e}", exc_info=True)
raise raise
@ -388,9 +394,10 @@ class ActionHandler:
if has_changes: if has_changes:
msg = "Uncommitted changes exist. Commit or stash first." msg = "Uncommitted changes exist. Commit or stash first."
self.logger.error(f"Switch blocked: {msg}") self.logger.error(f"Switch blocked: {msg}")
raise ValueError(msg) # Raise specific error raise ValueError(msg) # Raise specific error for UI
self.logger.debug("No uncommitted changes found.") self.logger.debug("No uncommitted changes found.")
except Exception as e: except Exception as e:
# Catch errors during status check
self.logger.error(f"Status check error before switch: {e}", self.logger.error(f"Status check error before switch: {e}",
exc_info=True) exc_info=True)
raise # Re-raise status check errors raise # Re-raise status check errors
@ -401,6 +408,7 @@ class ActionHandler:
success = self.git_commands.checkout_branch(svn_path, branch_name) success = self.git_commands.checkout_branch(svn_path, branch_name)
return success return success
except Exception as e: except Exception as e:
# Catch errors during switch (e.g., branch not found)
self.logger.error(f"Failed switch to branch '{branch_name}': {e}", self.logger.error(f"Failed switch to branch '{branch_name}': {e}",
exc_info=True) exc_info=True)
raise raise
@ -417,27 +425,27 @@ class ActionHandler:
""" """
if not branch_name: if not branch_name:
raise ValueError("Branch name required for delete.") raise ValueError("Branch name required for delete.")
# Add checks for main/master? Done in UI layer. # Add checks for main/master? UI layer handles this.
self.logger.info(f"Executing delete branch '{branch_name}' (force={force}).") self.logger.info(f"Executing delete branch '{branch_name}' (force={force}).")
try: try:
# Delegate to git_commands, raises error on failure # Delegate to git_commands, raises error on failure
success = self.git_commands.delete_branch(svn_path, branch_name, force) success = self.git_commands.delete_branch(svn_path, branch_name, force)
return success return success
except Exception as e: except Exception as e:
# Catch errors (like not fully merged if force=False) # Catch errors (like not merged, needs force)
self.logger.error(f"Failed delete branch '{branch_name}': {e}", self.logger.error(f"Failed delete branch '{branch_name}': {e}",
exc_info=True) exc_info=True)
raise raise
# --- ADDED: Delete Tag Action ---
def execute_delete_tag(self, svn_path, tag_name): def execute_delete_tag(self, svn_path, tag_name):
""" """
Executes deletion for the specified tag. Executes deletion for the specified tag.
Args: Args:
svn_path (str): Validated path to repository. svn_path (str): Validated path to repository.
tag_name (str): The tag name to delete (validated by caller). tag_name (str): The tag name to delete (already validated).
Returns: Returns:
bool: True on successful deletion. bool: True on successful deletion.
@ -445,7 +453,7 @@ class ActionHandler:
GitCommandError/ValueError/Exception: If delete fails. GitCommandError/ValueError/Exception: If delete fails.
""" """
if not tag_name: if not tag_name:
# Should be validated by caller (UI layer) # Should be validated by caller
raise ValueError("Tag name required for deletion.") raise ValueError("Tag name required for deletion.")
self.logger.info(f"Executing delete tag '{tag_name}' in: {svn_path}") self.logger.info(f"Executing delete tag '{tag_name}' in: {svn_path}")

0
create_exe.bat Normal file
View File

622
gui.py

File diff suppressed because it is too large Load Diff