SXXXXXXX_GitUtility/gitutility/app.py

1150 lines
46 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.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", "<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
)