729 lines
40 KiB
Python
729 lines
40 KiB
Python
# --- 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.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", "<empty>")
|
|
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) |