# --- 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, 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, 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, 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.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.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.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 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 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 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)