# --- FILE: gitsync_tool/app.py --- import os import datetime import tkinter as tk from tkinter import messagebox, filedialog import logging import re import threading import queue import traceback import sys from typing import Callable, List, Dict, Any, Tuple, Optional, Set # --- Configuration --- from gitutility.config.config_manager import ( ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR, DEFAULT_REMOTE_NAME, ) # --- Core Logic Handlers --- from gitutility.core.action_handler import ActionHandler from gitutility.core.backup_handler import BackupHandler from gitutility.core.remote_actions import RemoteActionHandler from gitutility.core.wiki_updater import WikiUpdater from gitutility.core.history_cleaner import HistoryCleaner from gitutility.core.submodule_handler import SubmoduleHandler # --- Command Execution --- from gitutility.commands.git_commands import GitCommands, GitCommandError # --- Logging System --- from gitutility.logging_setup import log_handler from gitutility.logging_setup.logger_config import setup_file_logging # --- GUI Components --- from gitutility.gui.main_frame import MainFrame from gitutility.logic.window_handler import WindowHandler # --- Application Logic Handlers --- from gitutility.logic.profile_handler import ProfileHandler from gitutility.logic.repository_handler import RepositoryHandler from gitutility.logic.remote_handler import RemoteHandler from gitutility.logic.automation_handler import AutomationHandler from gitutility.logic.submodule_handler import SubmoduleLogicHandler # --- Asynchronous Operations --- from gitutility.async_tasks import async_workers from gitutility.async_tasks.async_result_handler import AsyncResultHandler # --- Versioning --- try: from gitutility import _version as wrapper_version WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" except ImportError: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" class GitSvnSyncApp: LOG_QUEUE_CHECK_INTERVAL_MS: int = 100 ASYNC_QUEUE_CHECK_INTERVAL_MS: int = 100 def __init__(self, master: tk.Tk): self.master: tk.Tk = master master.title( f"Git Utility (Bundle & Remote Manager) - {WRAPPER_APP_VERSION_STRING}" ) master.protocol("WM_DELETE_WINDOW", self.on_closing) log_handler.log_debug( "GitSvnSyncApp initialization started.", func_name="__init__" ) try: self.config_manager: ConfigManager = ConfigManager() self.git_commands: GitCommands = GitCommands() self.backup_handler: BackupHandler = BackupHandler() self.wiki_updater: WikiUpdater = WikiUpdater(self.git_commands) self.history_cleaner: HistoryCleaner = HistoryCleaner(self.git_commands) self.submodule_handler: SubmoduleHandler = SubmoduleHandler( self.git_commands ) self.action_handler: ActionHandler = ActionHandler( self.git_commands, self.backup_handler ) self.remote_action_handler: RemoteActionHandler = RemoteActionHandler( self.git_commands ) except Exception as e: self._handle_fatal_error(f"Failed to initialize core components: {e}") return self.remote_auth_status: str = "unknown" self.current_local_branch: Optional[str] = None self.profile_handler: Optional[ProfileHandler] = None self.repository_handler: Optional[RepositoryHandler] = None self.remote_handler: Optional[RemoteHandler] = None self.automation_handler: Optional[AutomationHandler] = None self.submodule_logic_handler: Optional[SubmoduleLogicHandler] = None self.window_handler: Optional[WindowHandler] = None try: self.main_frame: MainFrame = MainFrame( master=master, load_profile_settings_cb=lambda x: ( self.profile_handler.load_profile_settings(x) if self.profile_handler else None ), save_profile_cb=lambda: ( self.profile_handler.save_profile_settings() if self.profile_handler else False ), add_profile_cb=lambda: ( self.profile_handler.add_profile() if self.profile_handler else None ), remove_profile_cb=lambda: ( self.profile_handler.remove_profile() if self.profile_handler else None ), prepare_svn_for_git_cb=lambda: ( self.repository_handler.prepare_svn_for_git() if self.repository_handler else None ), create_git_bundle_cb=lambda: ( self.repository_handler.create_git_bundle() if self.repository_handler else None ), fetch_from_git_bundle_cb=lambda: ( self.repository_handler.fetch_from_git_bundle() if self.repository_handler else None ), commit_changes_cb=lambda: ( self.repository_handler.commit_changes() if self.repository_handler else None ), create_tag_cb=lambda: ( self.repository_handler.create_tag() if self.repository_handler else None ), checkout_tag_cb=lambda: ( self.repository_handler.checkout_tag() if self.repository_handler else None ), revert_to_tag_cb=lambda: ( self.repository_handler.revert_to_tag() if self.repository_handler else None ), create_branch_cb=lambda: ( self.repository_handler.create_branch() if self.repository_handler else None ), checkout_branch_cb=lambda x=None: ( self.repository_handler.checkout_branch(x) if self.repository_handler else None ), apply_remote_config_cb=lambda: ( self.remote_handler.apply_remote_config() if self.remote_handler else None ), check_connection_auth_cb=lambda: ( self.remote_handler.check_connection_auth() if self.remote_handler else None ), fetch_remote_cb=lambda: ( self.remote_handler.fetch_remote() if self.remote_handler else None ), pull_remote_cb=lambda: ( self.remote_handler.pull_remote() if self.remote_handler else None ), push_remote_cb=lambda: ( self.remote_handler.push_remote() if self.remote_handler else None ), push_tags_remote_cb=lambda: ( self.remote_handler.push_tags_remote() if self.remote_handler else None ), clone_remote_repo_cb=lambda: ( self.remote_handler.clone_remote_repo() if self.remote_handler else None ), checkout_remote_branch_cb=lambda x, y: ( self.remote_handler.checkout_remote_branch_as_local(x, y) if self.remote_handler else None ), refresh_submodules_cb=lambda: ( self.submodule_logic_handler.refresh_submodules() if self.submodule_logic_handler else None ), add_submodule_cb=lambda: ( self.submodule_logic_handler.add_submodule() if self.submodule_logic_handler else None ), view_submodule_changes_cb=lambda x: ( self.submodule_logic_handler.view_submodule_changes(x) if self.submodule_logic_handler else None ), sync_all_submodules_cb=lambda: ( self.submodule_logic_handler.sync_all_submodules() if self.submodule_logic_handler else None ), remove_submodule_cb=lambda x: ( self.submodule_logic_handler.remove_submodule(x) if self.submodule_logic_handler else None ), init_missing_submodules_cb=lambda: ( self.submodule_logic_handler.init_missing_submodules() if self.submodule_logic_handler else None ), update_gitea_wiki_cb=lambda: ( self.automation_handler.update_gitea_wiki() if self.automation_handler else None ), analyze_and_clean_history_cb=lambda: ( self.automation_handler.analyze_and_clean_history() if self.automation_handler else None ), clean_invalid_submodule_cb=lambda: ( self.automation_handler.clean_invalid_submodule() if self.automation_handler else None ), check_for_updates_cb=lambda: ( self.submodule_logic_handler.check_for_updates() if self.submodule_logic_handler else None ), browse_folder_cb=self.browse_folder, update_svn_status_cb=self.update_svn_status_indicator, open_gitignore_editor_cb=self.open_gitignore_editor, manual_backup_cb=self.manual_backup, refresh_changed_files_cb=self.refresh_changed_files_list, open_diff_viewer_cb=self.open_diff_viewer, add_selected_file_cb=self.add_selected_file, refresh_tags_cb=self.refresh_tag_list, refresh_branches_cb=self.refresh_branch_list, refresh_history_cb=self.refresh_commit_history, refresh_remote_status_cb=self.refresh_remote_status, refresh_remote_branches_cb=self.refresh_remote_branches, delete_local_branch_cb=self.delete_local_branch, merge_local_branch_cb=self.merge_local_branch, compare_branch_with_current_cb=self.compare_branch_with_current, view_commit_details_cb=self.view_commit_details, reset_to_commit_cb=( self.repository_handler.handle_reset_to_commit if self.repository_handler else None ), config_manager_instance=self.config_manager, profile_sections_list=self.config_manager.get_profile_sections(), ) except Exception as e: self._handle_fatal_error(f"Failed to initialize MainFrame GUI: {e}") return try: self.profile_handler = ProfileHandler(self) self.repository_handler = RepositoryHandler(self) self.remote_handler = RemoteHandler(self) self.automation_handler = AutomationHandler(self) self.submodule_logic_handler = SubmoduleLogicHandler(self) self.window_handler = WindowHandler(self) except Exception as e: self._handle_fatal_error(f"Failed to initialize logic handlers: {e}") return self._rewire_callbacks() self._setup_logging_processing() log_handler.log_info( "Git Utility Tool application starting up.", func_name="__init__" ) self._perform_initial_load() log_handler.log_info( "Git Utility Tool initialization complete and ready.", func_name="__init__" ) def _rewire_callbacks(self): self.main_frame.load_profile_settings_callback = ( self.profile_handler.load_profile_settings ) self.main_frame.save_profile_callback = ( self.profile_handler.save_profile_settings ) self.main_frame.add_profile_callback = self.profile_handler.add_profile self.main_frame.remove_profile_callback = self.profile_handler.remove_profile self.main_frame.clone_remote_repo_callback = ( self.remote_handler.clone_remote_repo ) self.main_frame.repo_tab.prepare_svn_for_git_callback = ( self.repository_handler.prepare_svn_for_git ) self.main_frame.repo_tab.create_git_bundle_callback = ( self.repository_handler.create_git_bundle ) self.main_frame.repo_tab.fetch_from_git_bundle_callback = ( self.repository_handler.fetch_from_git_bundle ) self.main_frame.commit_tab.commit_changes_callback = ( self.repository_handler.commit_changes ) self.main_frame.tags_tab.create_tag_callback = ( self.repository_handler.create_tag ) self.main_frame.tags_tab.checkout_tag_callback = ( self.repository_handler.checkout_tag ) self.main_frame.tags_tab.revert_to_tag_callback = ( self.repository_handler.revert_to_tag ) self.main_frame.history_tab.reset_to_commit_callback = ( self.repository_handler.handle_reset_to_commit ) self.main_frame.branch_tab.create_branch_callback = ( self.repository_handler.create_branch ) self.main_frame.branch_tab.checkout_branch_callback = ( self.repository_handler.checkout_branch ) self.main_frame.branch_tab.merge_local_branch_callback = ( self.repository_handler.handle_merge_local_branch ) self.main_frame.branch_tab.promote_to_main_callback = ( self.repository_handler.handle_promote_branch_to_main ) self.main_frame.branch_tab.delete_local_branch_callback = ( self.repository_handler.handle_delete_local_branch ) self.main_frame.branch_tab.compare_branch_with_current_callback = ( self.remote_handler.handle_compare_branch_with_current ) remote_tab = self.main_frame.remote_tab remote_tab.apply_remote_config_callback = ( self.remote_handler.apply_remote_config ) remote_tab.check_connection_auth_callback = ( self.remote_handler.check_connection_auth ) remote_tab.fetch_remote_callback = self.remote_handler.fetch_remote remote_tab.pull_remote_callback = self.remote_handler.pull_remote remote_tab.push_remote_callback = self.remote_handler.push_remote remote_tab.push_tags_remote_callback = self.remote_handler.push_tags_remote remote_tab.checkout_remote_branch_callback = ( self.remote_handler.checkout_remote_branch_as_local ) remote_tab.checkout_local_branch_callback = ( self.repository_handler.checkout_branch ) remote_tab.delete_local_branch_callback = ( self.repository_handler.handle_delete_local_branch ) remote_tab.merge_local_branch_callback = ( self.repository_handler.handle_merge_local_branch ) remote_tab.compare_branch_with_current_callback = ( self.remote_handler.handle_compare_branch_with_current ) sub_tab = self.main_frame.submodules_tab sub_tab.add_submodule_callback = self.submodule_logic_handler.add_submodule sub_tab.view_submodule_changes_callback = ( self.submodule_logic_handler.view_submodule_changes ) sub_tab.sync_all_submodules_callback = ( self.submodule_logic_handler.sync_all_submodules ) sub_tab.remove_submodule_callback = ( self.submodule_logic_handler.remove_submodule ) sub_tab.init_missing_submodules_callback = ( self.submodule_logic_handler.init_missing_submodules ) sub_tab.check_for_updates_callback = ( self.submodule_logic_handler.check_for_updates ) auto_tab = self.main_frame.automation_tab auto_tab.update_gitea_wiki_callback = self.automation_handler.update_gitea_wiki auto_tab.analyze_and_clean_history_callback = ( self.automation_handler.analyze_and_clean_history ) auto_tab.clean_invalid_submodule_callback = ( self.automation_handler.clean_invalid_submodule ) def _handle_fatal_error(self, message: str): log_handler.log_critical(message, func_name="_handle_fatal_error") traceback.print_exc(file=sys.stderr) try: parent = ( self.master if hasattr(self, "master") and self.master.winfo_exists() else None ) messagebox.showerror( "Fatal Application Error", f"{message}\n\nApplication will now close.", parent=parent, ) finally: if hasattr(self, "master") and self.master.winfo_exists(): self.master.after(10, self.on_closing) def on_closing(self): log_handler.log_info("Application closing initiated.", func_name="on_closing") if hasattr(self, "master") and self.master.winfo_exists(): self.master.destroy() log_handler.log_info("Application closed.", func_name="on_closing") def browse_folder(self, entry_widget: tk.Entry): current_path = entry_widget.get() initial_dir = ( os.path.dirname(current_path) if current_path and os.path.exists(os.path.dirname(current_path)) else os.path.expanduser("~") ) directory = filedialog.askdirectory( initialdir=initial_dir, title="Select Directory", parent=self.master ) if directory: entry_widget.delete(0, tk.END) entry_widget.insert(0, directory) if ( hasattr(self.main_frame, "repo_tab") and entry_widget == self.main_frame.repo_tab.svn_path_entry ): self.update_svn_status_indicator(directory) def _setup_logging_processing(self): func_name = "_setup_logging_processing" try: setup_file_logging(level=logging.DEBUG) if hasattr(self, "main_frame") and hasattr(self.main_frame, "log_text"): self.master.after( self.LOG_QUEUE_CHECK_INTERVAL_MS, self._process_log_queue ) except Exception as e: log_handler.log_exception( "Failed to setup logging processing.", func_name=func_name ) def _process_log_queue(self): log_widget = getattr(self.main_frame, "log_text", None) if not log_widget or not log_widget.winfo_exists(): return processed_count, max_proc_per_cycle = 0, 50 while not log_handler.log_queue.empty(): if processed_count >= max_proc_per_cycle: break try: log_entry = log_handler.log_queue.get_nowait() level, message = log_entry.get("level", logging.INFO), log_entry.get( "message", "" ) level_name = log_handler.get_log_level_name(level) logging.getLogger().log(level, message) processed_count += 1 state = log_widget.cget("state") log_widget.config(state=tk.NORMAL) log_widget.insert(tk.END, message + "\n", (level_name,)) log_widget.see(tk.END) log_widget.config(state=state) except queue.Empty: break except Exception: pass if self.master.winfo_exists(): self.master.after(self.LOG_QUEUE_CHECK_INTERVAL_MS, self._process_log_queue) def _perform_initial_load(self): initial_profile = self.main_frame.profile_var.get() if initial_profile: self.profile_handler.load_profile_settings(initial_profile) else: self._clear_and_disable_fields() self.main_frame.update_status_bar( "No profiles found. Please add or clone a profile." ) # --- GUI Update & Helper Methods (Centralized Logic) --- def _apply_profile_to_gui(self, profile_name: str): func_name = "_apply_profile_to_gui" log_handler.log_debug( f"Applying settings from profile '{profile_name}' to GUI.", func_name=func_name, ) cm = self.config_manager keys_with_defaults = cm._get_expected_keys_with_defaults() settings = { key: cm.get_profile_option(profile_name, key, fallback=default_value) for key, default_value in keys_with_defaults.items() } repo_tab = self.main_frame.repo_tab repo_tab.svn_path_entry.delete(0, tk.END) repo_tab.svn_path_entry.insert(0, settings.get("svn_working_copy_path", "")) repo_tab.usb_path_entry.delete(0, tk.END) repo_tab.usb_path_entry.insert(0, settings.get("usb_drive_path", "")) repo_tab.bundle_name_entry.delete(0, tk.END) repo_tab.bundle_name_entry.insert(0, settings.get("bundle_name", "")) repo_tab.bundle_updated_name_entry.delete(0, tk.END) repo_tab.bundle_updated_name_entry.insert( 0, settings.get("bundle_name_updated", "") ) backup_tab = self.main_frame.backup_tab backup_tab.autobackup_var.set( str(settings.get("autobackup", "False")).lower() == "true" ) backup_tab.backup_dir_var.set(settings.get("backup_dir", DEFAULT_BACKUP_DIR)) backup_tab.backup_exclude_extensions_var.set( settings.get("backup_exclude_extensions", "") ) backup_tab.backup_exclude_dirs_var.set(settings.get("backup_exclude_dirs", "")) backup_tab.toggle_backup_dir_widgets() commit_tab = self.main_frame.commit_tab commit_tab.autocommit_var.set( str(settings.get("autocommit", "False")).lower() == "true" ) commit_tab.clear_commit_message() commit_text_widget = commit_tab.commit_message_text commit_text_widget.config(state=tk.NORMAL) commit_text_widget.insert("1.0", settings.get("commit_message", "")) commit_text_widget.config(state=tk.DISABLED) remote_tab = self.main_frame.remote_tab remote_tab.remote_url_var.set(settings.get("remote_url", "")) remote_tab.remote_name_var.set(settings.get("remote_name", DEFAULT_REMOTE_NAME)) self._trigger_full_refresh(profile_name) def _clear_and_disable_fields(self): self.main_frame.set_action_widgets_state(tk.DISABLED) self.main_frame.repo_tab.svn_path_entry.delete(0, tk.END) self.main_frame.repo_tab.usb_path_entry.delete(0, tk.END) self.main_frame.repo_tab.bundle_name_entry.delete(0, tk.END) self.main_frame.repo_tab.bundle_updated_name_entry.delete(0, tk.END) self.main_frame.commit_tab.clear_commit_message() self.main_frame.backup_tab.autobackup_var.set(False) self.main_frame.backup_tab.toggle_backup_dir_widgets() self.main_frame.remote_tab.remote_url_var.set("") self.main_frame.remote_tab.remote_name_var.set("") self._update_gui_for_not_ready_state() self.update_svn_status_indicator("") self.main_frame.save_settings_button.config(state=tk.DISABLED) self.main_frame.remove_profile_button.config(state=tk.DISABLED) def _update_gui_for_not_ready_state(self): self.main_frame.tags_tab.update_tag_list([("(Repo not ready)", "")]) self.main_frame.branch_tab.update_branch_list([], None) self.main_frame.remote_tab.update_local_branch_list([], None) self.main_frame.history_tab.update_history_display(["(Repository not ready)"]) self.main_frame.commit_tab.update_changed_files_list(["(Repository not ready)"]) self.main_frame.remote_tab.update_remote_branches_list( ["(Repository not ready)"] ) self.main_frame.submodules_tab.update_submodules_list([]) self.main_frame.remote_tab.update_ahead_behind_status( status_text="Sync Status: (Repo not ready)" ) def _update_gui_for_detached_head(self, current_branch_name: Optional[str]): self.main_frame.remote_tab.update_ahead_behind_status( current_branch=current_branch_name, status_text="Sync Status: (Detached HEAD)", ) if hasattr(self.main_frame.remote_tab, "refresh_sync_status_button"): self.main_frame.remote_tab.refresh_sync_status_button.config( state=tk.DISABLED ) def _update_gui_for_no_upstream(self, current_branch_name: Optional[str]): self.main_frame.remote_tab.update_ahead_behind_status( current_branch=current_branch_name, status_text="Sync Status: Upstream not set", ) if hasattr(self.main_frame.remote_tab, "refresh_sync_status_button"): self.main_frame.remote_tab.refresh_sync_status_button.config( state=tk.DISABLED ) def _update_gui_for_status_error(self): self.main_frame.remote_tab.update_ahead_behind_status( status_text="Sync Status: Error getting info" ) if hasattr(self.main_frame.remote_tab, "refresh_sync_status_button"): self.main_frame.remote_tab.refresh_sync_status_button.config( state=tk.DISABLED ) def _update_gui_auth_status(self, status: str): self.remote_auth_status = status if ( hasattr(self, "main_frame") and self.main_frame.winfo_exists() and hasattr(self.main_frame, "remote_tab") ): self.main_frame.remote_tab.update_auth_status_indicator(status) if status != "ok": sync_status_text = f"Sync Status: ({status.replace('_', ' ').title()})" self.main_frame.remote_tab.update_ahead_behind_status( status_text=sync_status_text ) def _gather_settings_from_gui(self) -> Dict[str, Any]: """ Gathers all configurable settings from the GUI widgets. Returns: A dictionary of settings with keys matching the config file options. """ settings = {} # Repository Tab repo_tab = self.main_frame.repo_tab settings["svn_working_copy_path"] = repo_tab.svn_path_entry.get() settings["usb_drive_path"] = repo_tab.usb_path_entry.get() settings["bundle_name"] = repo_tab.bundle_name_entry.get() settings["bundle_name_updated"] = repo_tab.bundle_updated_name_entry.get() # Backup Tab backup_tab = self.main_frame.backup_tab settings["autobackup"] = str(backup_tab.autobackup_var.get()) settings["backup_dir"] = backup_tab.backup_dir_var.get() settings["backup_exclude_extensions"] = ( backup_tab.backup_exclude_extensions_var.get() ) settings["backup_exclude_dirs"] = backup_tab.backup_exclude_dirs_var.get() # Commit Tab commit_tab = self.main_frame.commit_tab settings["autocommit"] = str(commit_tab.autocommit_var.get()) settings["commit_message"] = commit_tab.get_commit_message() # Remote Tab remote_tab = self.main_frame.remote_tab settings["remote_url"] = remote_tab.remote_url_var.get() settings["remote_name"] = remote_tab.remote_name_var.get() return settings def _get_and_validate_svn_path( self, operation_name: str = "Operation" ) -> Optional[str]: path_str = self.main_frame.repo_tab.svn_path_entry.get().strip() if not path_str: self.main_frame.show_error( "Input Error", "Working Directory path cannot be empty." ) return None abs_path = os.path.abspath(path_str) if not os.path.isdir(abs_path): self.main_frame.show_error( "Path Error", f"Path is not a valid directory:\n{abs_path}" ) return None return abs_path def _get_and_validate_usb_path( self, operation_name: str = "Operation" ) -> Optional[str]: path_str = self.main_frame.repo_tab.usb_path_entry.get().strip() if not path_str: self.main_frame.show_error( "Input Error", "Bundle Target path cannot be empty." ) return None abs_path = os.path.abspath(path_str) if not os.path.isdir(abs_path): self.main_frame.show_error( "Path Error", f"Path is not a valid directory:\n{abs_path}" ) return None return abs_path def _is_repo_ready(self, repo_path: str) -> bool: return bool( repo_path and os.path.isdir(repo_path) and os.path.exists(os.path.join(repo_path, ".git")) ) def _parse_exclusions(self) -> Tuple[Set[str], Set[str]]: excluded_ext = set() excluded_dir = {".git", ".svn"} ext_str = self.main_frame.backup_tab.backup_exclude_extensions_var.get() dir_str = self.main_frame.backup_tab.backup_exclude_dirs_var.get() if ext_str: excluded_ext.update( "." + e.strip().lstrip(".").lower() for e in ext_str.split(",") if e.strip() ) if dir_str: excluded_dir.update( d.strip().lower().strip(os.path.sep + "/") for d in dir_str.split(",") if d.strip() ) return excluded_ext, excluded_dir def _extract_path_from_status_line(self, line: str) -> Optional[str]: cleaned_line = line.strip("\x00").strip() if not cleaned_line or len(cleaned_line) < 3: return None path_part = "" if "->" in cleaned_line: path_part = cleaned_line.split("->")[-1].strip() else: match = re.match(r"^[ MARCUD?!]{1,2}\s+(.*)", cleaned_line) if match: path_part = match.group(1) if not path_part: return None return ( path_part[1:-1] if path_part.startswith('"') and path_part.endswith('"') else path_part ) def _generate_next_tag_suggestion(self, svn_path: str) -> str: try: tags_data = self.git_commands.list_tags(svn_path) if not tags_data: return "v.0.0.0.1" tag_pattern = re.compile(r"^v\.(\d+)\.(\d+)\.(\d+)\.(\d+)$") for tag_name, _ in tags_data: match = tag_pattern.match(tag_name) if match: v = list(map(int, match.groups())) v[3] += 1 for i in range(3, 0, -1): if v[i] > 99: v[i], v[i - 1] = 0, v[i - 1] + 1 return f"v.{v[0]}.{v[1]}.{v[2]}.{v[3]}" return "v.0.0.0.1" except Exception: return "v.0.0.0.1" def show_comparison_summary( self, ref1: str, ref2: str, repo_path: str, changed_files: List[str] ): if self.window_handler: self.window_handler.handle_show_comparison_summary( ref1, ref2, repo_path, changed_files ) def show_commit_details(self, commit_details: Dict[str, Any]): if self.window_handler: self.window_handler.handle_show_commit_details(commit_details) def show_purge_confirmation_and_purge( self, repo_path: str, purgeable_files: List[Dict[str, Any]] ): if self.automation_handler: # This dialog is part of the automation flow self.automation_handler.show_purge_confirmation_and_purge( repo_path, purgeable_files ) # --- Direct/Shared Action Callbacks (che coordinano o aprono finestre) --- def update_svn_status_indicator(self, svn_path: str): is_valid_dir = bool(svn_path and os.path.isdir(svn_path)) is_repo_ready = is_valid_dir and os.path.exists(os.path.join(svn_path, ".git")) if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return self.main_frame.repo_tab.update_svn_indicator(is_repo_ready) state = tk.NORMAL if is_repo_ready else tk.DISABLED self.main_frame.set_action_widgets_state(state) prepare_state = tk.NORMAL if is_valid_dir and not is_repo_ready else tk.DISABLED if hasattr(self.main_frame.repo_tab, "prepare_svn_button"): self.main_frame.repo_tab.prepare_svn_button.config(state=prepare_state) valid_dir_state = tk.NORMAL if is_valid_dir else tk.DISABLED if hasattr(self.main_frame.backup_tab, "manual_backup_button"): self.main_frame.backup_tab.manual_backup_button.config( state=valid_dir_state ) def open_gitignore_editor(self): if self.window_handler: self.window_handler.handle_open_gitignore_editor() def manual_backup(self): if self.repository_handler: self.repository_handler.handle_manual_backup() def open_diff_viewer(self, file_status_line: str): if self.window_handler: self.window_handler.handle_open_diff_viewer(file_status_line) def add_selected_file(self, file_status_line: str): if self.repository_handler: self.repository_handler.handle_add_selected_file(file_status_line) def delete_local_branch(self, branch_name: str, force: bool): if self.repository_handler: self.repository_handler.handle_delete_local_branch(branch_name, force) def merge_local_branch(self, branch_to_merge: str): if self.repository_handler: self.repository_handler.handle_merge_local_branch(branch_to_merge) def compare_branch_with_current(self, other_branch_ref: str): if self.remote_handler: self.remote_handler.handle_compare_branch_with_current(other_branch_ref) def view_commit_details(self, commit_hash_short: str): if self.repository_handler: self.repository_handler.handle_view_commit_details(commit_hash_short) # --- Refresh Callbacks --- def _trigger_full_refresh(self, profile_name: str): repo_path = self.main_frame.repo_tab.svn_path_entry.get() self.update_svn_status_indicator(repo_path) is_ready = self._is_repo_ready(repo_path) if is_ready: log_handler.log_info( "Repo ready, triggering local async refreshes.", func_name="_trigger_full_refresh", ) self.refresh_tag_list() self.refresh_branch_list() self.refresh_commit_history() self.refresh_changed_files_list() if self.submodule_logic_handler: self.submodule_logic_handler.refresh_submodules() if self.main_frame.remote_tab.remote_url_var.get(): if self.remote_handler: self.remote_handler.check_connection_auth() else: self._update_gui_auth_status("unknown") self.main_frame.remote_tab.update_remote_branches_list([]) self.main_frame.remote_tab.update_ahead_behind_status( status_text="Sync Status: (Remote not configured)" ) else: log_handler.log_info( "Repo not ready, clearing dynamic lists.", func_name="_trigger_full_refresh", ) self._update_gui_for_not_ready_state() self.main_frame.update_status_bar(f"Profile '{profile_name}' loaded.") def _trigger_post_action_refreshes(self, sync_refresh_needed: bool): """ Triggers a sequence of GUI refreshes after a Git action has completed. This is called by the AsyncResultHandler. """ func_name = "_trigger_post_action_refreshes" repo_path = self._get_and_validate_svn_path("Post-Action Refresh") if not repo_path or not self._is_repo_ready(repo_path): log_handler.log_warning( "Post-action refresh skipped: Repo not ready.", func_name=func_name ) self._reenable_widgets_if_ready() return log_handler.log_info( "Triggering post-action GUI refreshes.", func_name=func_name ) # List of refresh functions to call refresh_funcs = [ self.refresh_commit_history, self.refresh_branch_list, self.refresh_tag_list, self.refresh_changed_files_list, self.refresh_remote_branches, ] if hasattr(self, "submodule_logic_handler") and self.submodule_logic_handler: refresh_funcs.append(self.submodule_logic_handler.refresh_submodules) delay = 50 for func in refresh_funcs: if callable(func): self.master.after(delay, func) delay += 75 if sync_refresh_needed: self.master.after(delay, self.refresh_remote_status) delay += 75 self.master.after(delay + 50, self._reenable_widgets_if_ready) def refresh_tag_list(self): svn_path = self._get_and_validate_svn_path("Refresh Tags") if svn_path and self._is_repo_ready(svn_path): self._start_async_operation( async_workers.run_refresh_tags_async, (self.git_commands, svn_path), {"context": "refresh_tags"}, ) def refresh_branch_list(self): svn_path = self._get_and_validate_svn_path("Refresh Branches") if svn_path and self._is_repo_ready(svn_path): self._start_async_operation( async_workers.run_refresh_branches_async, (self.git_commands, svn_path), {"context": "refresh_branches"}, ) def refresh_commit_history(self): svn_path = self._get_and_validate_svn_path("Refresh History") if svn_path and self._is_repo_ready(svn_path): branch_filter = self.main_frame.history_tab.history_branch_filter_var.get() if branch_filter == "-- All History --": branch_filter = None args = ( self.git_commands, svn_path, branch_filter, branch_filter or "All History", ) self._start_async_operation( async_workers.run_refresh_history_async, args, {"context": "refresh_history"}, ) def refresh_changed_files_list(self): svn_path = self._get_and_validate_svn_path("Refresh Changes") if svn_path and self._is_repo_ready(svn_path): self._start_async_operation( async_workers.run_refresh_changes_async, (self.git_commands, svn_path), {"context": "refresh_changes"}, ) def refresh_remote_branches(self): svn_path = self._get_and_validate_svn_path("Refresh Remote Branches") if svn_path and self._is_repo_ready(svn_path): remote_name = ( self.main_frame.remote_tab.remote_name_var.get() or DEFAULT_REMOTE_NAME ) self._start_async_operation( async_workers.run_refresh_remote_branches_async, (self.git_commands, svn_path, remote_name), {"context": "refresh_remote_branches"}, ) def refresh_remote_status(self): svn_path = self._get_and_validate_svn_path("Refresh Sync Status") if not svn_path or not self._is_repo_ready(svn_path): return try: current_branch = self.git_commands.get_current_branch_name(svn_path) if not current_branch: self._update_gui_for_detached_head(None) return upstream = self.git_commands.get_branch_upstream(svn_path, current_branch) if not upstream: self._update_gui_for_no_upstream(current_branch) return self.main_frame.remote_tab.update_ahead_behind_status( current_branch=current_branch, status_text="Sync Status: Checking..." ) args = (self.git_commands, svn_path, current_branch, upstream) self._start_async_operation( async_workers.run_get_ahead_behind_async, args, {"context": "get_ahead_behind", "local_branch": current_branch}, ) except Exception: self._update_gui_for_status_error() # --- Async Operation Handlers --- def _start_async_operation( self, worker_func: Callable, args_tuple: tuple, context_dict: dict ): context_name = context_dict.get("context", "unknown_op") status_msg = context_dict.get("status_msg", context_name) log_handler.log_info( f"--- Action Triggered: {context_name} ---", func_name=context_name ) self.main_frame.set_action_widgets_state(tk.DISABLED) self.main_frame.update_status_bar( f"Processing: {status_msg}...", bg_color=self.main_frame.STATUS_YELLOW ) results_queue = queue.Queue(maxsize=1) full_args = args_tuple + (results_queue,) try: worker_thread = threading.Thread( target=worker_func, args=full_args, daemon=True ) worker_thread.start() except Exception as e: self.main_frame.show_error( "Threading Error", f"Could not start background task: {e}" ) self.main_frame.set_action_widgets_state(tk.NORMAL) return self.master.after( self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context_dict, ) def _check_completion_queue(self, results_queue: queue.Queue, context: dict): try: result_data = results_queue.get_nowait() task_context = context.get("context", "unknown") status = result_data.get("status") if self._should_reenable_widgets_now(task_context, status): self._reenable_widgets_if_ready() self._update_status_bar_from_result(task_context, result_data) self._process_result_with_handler(result_data, context) except queue.Empty: if self.master.winfo_exists(): self.master.after( self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context, ) except Exception as e: self._handle_queue_check_error(e, context.get("context", "unknown")) def _should_reenable_widgets_now( self, task_context: str, status: Optional[str] ) -> bool: no_reenable_contexts = { ("check_connection", "auth_required"), ("clone_remote", "success"), ("checkout_tracking_branch", "success"), ("checkout_branch", "success"), ("checkout_tag", "success"), ("pull_remote", "conflict"), ("merge_local_branch", "conflict"), ("compare_branches", "success"), ("get_commit_details", "success"), ("analyze_history", "success"), } return (task_context, status) not in no_reenable_contexts def _reenable_widgets_if_ready(self): if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) def _update_status_bar_from_result(self, task_context: str, result_data: dict): status = result_data.get("status") message = result_data.get("message", "Operation finished.") skip_contexts = { ("clone_remote", "success"), ("checkout_tracking_branch", "success"), ("checkout_branch", "success"), ("checkout_tag", "success"), ("compare_branches", "success"), ("get_commit_details", "success"), ("analyze_history", "success"), } if (task_context, status) in skip_contexts or status in [ "conflict", "rejected", ]: return if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): color_map = { "success": (self.main_frame.STATUS_GREEN, 5000), "warning": (self.main_frame.STATUS_YELLOW, 7000), "auth_required": (self.main_frame.STATUS_YELLOW, 15000), "error": (self.main_frame.STATUS_RED, 10000), } bg_color, duration = color_map.get(status, (None, 5000)) self.main_frame.update_status_bar( message, bg_color=bg_color, duration_ms=duration ) def _process_result_with_handler(self, result_data: dict, context: dict): try: result_handler = AsyncResultHandler(self) result_handler.process(result_data, context) except Exception as e: self.main_frame.show_error( "Processing Error", f"Failed to handle task result:\n{e}" ) self._reenable_widgets_if_ready() def _handle_queue_check_error(self, error: Exception, task_context: str): log_handler.log_exception( f"Critical error in completion queue for {task_context}: {error}", func_name="_handle_queue_check_error", ) if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) self.main_frame.update_status_bar( "Error processing async result.", bg_color=self.main_frame.STATUS_RED )