1249 lines
56 KiB
Python
1249 lines
56 KiB
Python
# GitTool.py
|
|
import os
|
|
import shutil
|
|
import datetime
|
|
import tkinter as tk
|
|
from tkinter import messagebox
|
|
import logging
|
|
import zipfile
|
|
|
|
# Import application modules
|
|
from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR
|
|
from git_commands import GitCommands, GitCommandError
|
|
from logger_config import setup_logger
|
|
|
|
# Import GUI classes
|
|
from gui import (
|
|
MainFrame,
|
|
GitignoreEditorWindow,
|
|
CreateTagDialog,
|
|
CreateBranchDialog,
|
|
) # Include necessary dialogs
|
|
|
|
|
|
class GitSvnSyncApp:
|
|
"""
|
|
Main application class for the Git SVN Sync Tool.
|
|
Coordinates the GUI (now tabbed), configuration, and Git commands.
|
|
"""
|
|
|
|
def __init__(self, master):
|
|
"""
|
|
Initializes the GitSvnSyncApp.
|
|
|
|
Args:
|
|
master (tk.Tk): The main Tkinter root window.
|
|
"""
|
|
self.master = master
|
|
master.title("Git SVN Sync Tool")
|
|
master.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
# Basic logging setup first
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s %(levelname)s: %(message)s"
|
|
)
|
|
self.logger = logging.getLogger("GitSvnSyncApp")
|
|
|
|
# Initialize Configuration Manager
|
|
try:
|
|
self.config_manager = ConfigManager(self.logger)
|
|
except Exception as e:
|
|
self.logger.critical(f"Failed init ConfigManager: {e}", exc_info=True)
|
|
self.show_fatal_error(f"Config Error:\n{e}")
|
|
master.destroy()
|
|
return
|
|
|
|
# Initialize MainFrame (GUI) with all necessary callbacks
|
|
try:
|
|
self.main_frame = MainFrame(
|
|
master,
|
|
# Profile Load
|
|
load_profile_settings_cb=self.load_profile_settings,
|
|
# Browse
|
|
browse_folder_cb=self.browse_folder,
|
|
# Status Update (triggers UI state changes)
|
|
update_svn_status_cb=self.update_svn_status_indicator,
|
|
# Actions from Repo Tab
|
|
prepare_svn_for_git_cb=self.prepare_svn_for_git,
|
|
create_git_bundle_cb=self.create_git_bundle,
|
|
fetch_from_git_bundle_cb=self.fetch_from_git_bundle,
|
|
open_gitignore_editor_cb=self.open_gitignore_editor,
|
|
# Action from Backup Tab
|
|
manual_backup_cb=self.manual_backup,
|
|
# Config Manager related
|
|
config_manager_instance=self.config_manager,
|
|
profile_sections_list=self.config_manager.get_profile_sections(),
|
|
add_profile_cb=self.add_profile,
|
|
remove_profile_cb=self.remove_profile,
|
|
save_profile_cb=self.save_profile_settings,
|
|
# Action from Commit Tab
|
|
commit_changes_cb=self.commit_changes,
|
|
# Actions from Tags Tab
|
|
refresh_tags_cb=self.refresh_tag_list,
|
|
create_tag_cb=self.create_tag,
|
|
checkout_tag_cb=self.checkout_tag,
|
|
# Actions from Branch Tab
|
|
refresh_branches_cb=self.refresh_branch_list,
|
|
checkout_branch_cb=self.checkout_branch,
|
|
create_branch_cb=self.create_branch,
|
|
# Action from History Tab
|
|
refresh_history_cb=self.refresh_commit_history,
|
|
)
|
|
except Exception as e:
|
|
self.logger.critical(f"Failed init MainFrame: {e}", exc_info=True)
|
|
self.show_fatal_error(f"GUI Error:\n{e}")
|
|
master.destroy()
|
|
return
|
|
|
|
# Finalize Logger Setup using the GUI text widget
|
|
self.logger = setup_logger(self.main_frame.log_text)
|
|
self.config_manager.logger = self.logger # Ensure manager uses final logger
|
|
|
|
# Initialize Git Commands Handler
|
|
self.git_commands = GitCommands(self.logger)
|
|
|
|
# --- Initial Application State ---
|
|
self.logger.info("Application initializing...")
|
|
initial_profile = self.main_frame.profile_var.get()
|
|
if initial_profile:
|
|
self.logger.debug(f"Initial profile: '{initial_profile}'. Loading...")
|
|
# Settings load triggers refresh of tags, branches, history via trace
|
|
else:
|
|
self.logger.warning("No profile selected on startup.")
|
|
self._clear_and_disable_fields() # Disable everything initially
|
|
self.logger.info("Application started successfully.")
|
|
|
|
def on_closing(self):
|
|
"""Handles the window close event."""
|
|
self.logger.info("Application closing.")
|
|
# Add cleanup or checks for unsaved work here if needed
|
|
self.master.destroy()
|
|
|
|
# --- Profile Management ---
|
|
def load_profile_settings(self, profile_name):
|
|
"""Loads settings for the selected profile into the GUI."""
|
|
self.logger.info(f"Loading settings for profile: '{profile_name}'")
|
|
if not profile_name:
|
|
self.logger.warning("No profile selected to load.")
|
|
self._clear_and_disable_fields() # Reset UI if no profile
|
|
return
|
|
if profile_name not in self.config_manager.get_profile_sections():
|
|
self.logger.error(f"Profile '{profile_name}' not found.")
|
|
self.main_frame.show_error("Error", f"Profile '{profile_name}' not found.")
|
|
self._clear_and_disable_fields()
|
|
return
|
|
|
|
# Load data from config manager
|
|
cm = self.config_manager
|
|
svn_path = cm.get_profile_option(profile_name, "svn_working_copy_path", "")
|
|
usb_path = cm.get_profile_option(profile_name, "usb_drive_path", "")
|
|
bundle_name = cm.get_profile_option(profile_name, "bundle_name", "")
|
|
bundle_upd = cm.get_profile_option(profile_name, "bundle_name_updated", "")
|
|
autocommit = cm.get_profile_option(profile_name, "autocommit", "False")
|
|
commit_msg = cm.get_profile_option(profile_name, "commit_message", "")
|
|
autobackup = cm.get_profile_option(profile_name, "autobackup", "False")
|
|
backup_dir = cm.get_profile_option(
|
|
profile_name, "backup_dir", DEFAULT_BACKUP_DIR
|
|
)
|
|
excludes = cm.get_profile_option(
|
|
profile_name, "backup_exclude_extensions", ".log,.tmp"
|
|
)
|
|
|
|
# Update GUI widgets
|
|
if hasattr(self, "main_frame"):
|
|
mf = self.main_frame
|
|
# Repo Tab
|
|
mf.svn_path_entry.delete(0, tk.END)
|
|
mf.svn_path_entry.insert(0, svn_path)
|
|
mf.usb_path_entry.delete(0, tk.END)
|
|
mf.usb_path_entry.insert(0, usb_path)
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.insert(0, bundle_name)
|
|
mf.bundle_updated_name_entry.delete(0, tk.END)
|
|
mf.bundle_updated_name_entry.insert(0, bundle_upd)
|
|
# Backup Tab
|
|
mf.autobackup_var.set(autobackup.lower() == "true")
|
|
mf.backup_dir_var.set(backup_dir)
|
|
mf.backup_exclude_extensions_var.set(excludes)
|
|
mf.toggle_backup_dir()
|
|
# Commit Tab
|
|
mf.autocommit_var.set(autocommit.lower() == "true")
|
|
mf.clear_commit_message() # Clear first
|
|
if hasattr(mf, "commit_message_text"):
|
|
if mf.commit_message_text.cget("state") == tk.DISABLED:
|
|
mf.commit_message_text.config(state=tk.NORMAL)
|
|
mf.commit_message_text.insert("1.0", commit_msg) # Insert loaded msg
|
|
# State will be set by update_svn_status_indicator
|
|
|
|
# Update Status & Enable/Disable Buttons
|
|
self.update_svn_status_indicator(svn_path)
|
|
self._enable_function_buttons()
|
|
|
|
# Refresh dynamic lists if repo is ready
|
|
repo_ready = (
|
|
svn_path
|
|
and os.path.isdir(svn_path)
|
|
and os.path.exists(os.path.join(svn_path, ".git"))
|
|
)
|
|
if repo_ready:
|
|
self.refresh_tag_list()
|
|
self.refresh_branch_list()
|
|
self.refresh_commit_history()
|
|
else:
|
|
# Clear lists if repo not ready
|
|
mf.update_tag_list([])
|
|
mf.update_branch_list([], None)
|
|
mf.update_history_display([])
|
|
mf.update_history_branch_filter([]) # Clear history filter too
|
|
|
|
self.logger.info(f"Settings loaded for '{profile_name}'.")
|
|
else:
|
|
self.logger.error("Cannot load settings: Main frame missing.")
|
|
|
|
def save_profile_settings(self):
|
|
"""Saves current GUI values to the selected profile."""
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.logger.warning("Cannot save: No profile selected.")
|
|
return False
|
|
self.logger.info(f"Saving settings for profile: '{profile}'")
|
|
try:
|
|
cm = self.config_manager
|
|
mf = self.main_frame
|
|
# Repo Tab
|
|
cm.set_profile_option(
|
|
profile, "svn_working_copy_path", mf.svn_path_entry.get()
|
|
)
|
|
cm.set_profile_option(profile, "usb_drive_path", mf.usb_path_entry.get())
|
|
cm.set_profile_option(profile, "bundle_name", mf.bundle_name_entry.get())
|
|
cm.set_profile_option(
|
|
profile, "bundle_name_updated", mf.bundle_updated_name_entry.get()
|
|
)
|
|
# Commit Tab
|
|
cm.set_profile_option(profile, "autocommit", str(mf.autocommit_var.get()))
|
|
commit_msg = mf.get_commit_message() # Get from ScrolledText
|
|
cm.set_profile_option(profile, "commit_message", commit_msg)
|
|
# Backup Tab
|
|
cm.set_profile_option(profile, "autobackup", str(mf.autobackup_var.get()))
|
|
cm.set_profile_option(profile, "backup_dir", mf.backup_dir_var.get())
|
|
cm.set_profile_option(
|
|
profile,
|
|
"backup_exclude_extensions",
|
|
mf.backup_exclude_extensions_var.get(),
|
|
)
|
|
# Save to file
|
|
cm.save_config()
|
|
self.logger.info(f"Settings saved for '{profile}'.")
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"Error saving '{profile}': {e}", exc_info=True)
|
|
self.main_frame.show_error("Save Error", f"Failed save:\n{e}")
|
|
return False
|
|
|
|
def add_profile(self):
|
|
"""Handles adding a new profile."""
|
|
self.logger.debug("'Add Profile' clicked.")
|
|
name = self.main_frame.ask_new_profile_name()
|
|
if not name:
|
|
self.logger.info("Add cancelled.")
|
|
return
|
|
name = name.strip()
|
|
if not name:
|
|
self.logger.warning("Empty name.")
|
|
self.main_frame.show_error("Error", "Name empty.")
|
|
return
|
|
if name in self.config_manager.get_profile_sections():
|
|
self.logger.warning(f"Exists: '{name}'")
|
|
self.main_frame.show_error("Error", f"'{name}' exists.")
|
|
return
|
|
self.logger.info(f"Adding profile: '{name}'")
|
|
try:
|
|
defaults = self.config_manager._get_expected_keys_with_defaults()
|
|
defaults["bundle_name"] = f"{name}_repo.bundle"
|
|
defaults["bundle_name_updated"] = f"{name}_update.bundle"
|
|
defaults["svn_working_copy_path"] = ""
|
|
defaults["usb_drive_path"] = ""
|
|
for k, v in defaults.items():
|
|
self.config_manager.set_profile_option(name, k, v)
|
|
self.config_manager.save_config()
|
|
sections = self.config_manager.get_profile_sections()
|
|
self.main_frame.update_profile_dropdown(sections)
|
|
self.main_frame.profile_var.set(name) # Select new profile
|
|
self.logger.info(f"Profile '{name}' added.")
|
|
except Exception as e:
|
|
self.logger.error(f"Error adding '{name}': {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Failed add:\n{e}")
|
|
|
|
def remove_profile(self):
|
|
"""Handles removing the selected profile."""
|
|
self.logger.debug("'Remove Profile' clicked.")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.logger.warning("No profile.")
|
|
self.main_frame.show_error("Error", "No profile.")
|
|
return
|
|
if profile == DEFAULT_PROFILE:
|
|
self.logger.warning("Cannot remove default.")
|
|
self.main_frame.show_error("Error", f"Cannot remove '{DEFAULT_PROFILE}'.")
|
|
return
|
|
if self.main_frame.ask_yes_no("Remove Profile", f"Remove '{profile}'?"):
|
|
self.logger.info(f"Removing: '{profile}'")
|
|
try:
|
|
if self.config_manager.remove_profile_section(profile):
|
|
self.config_manager.save_config()
|
|
self.logger.info("Removed.")
|
|
sections = self.config_manager.get_profile_sections()
|
|
self.main_frame.update_profile_dropdown(
|
|
sections
|
|
) # Update list, selection changes automatically
|
|
else:
|
|
self.main_frame.show_error("Error", "Failed remove.")
|
|
except Exception as e:
|
|
self.logger.error(f"Error removing: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Error removing:\n{e}")
|
|
else:
|
|
self.logger.info("Removal cancelled.")
|
|
|
|
# --- GUI Interaction ---
|
|
def browse_folder(self, entry_widget):
|
|
"""Opens folder dialog to update an entry widget."""
|
|
self.logger.debug("Browse folder requested.")
|
|
current = entry_widget.get()
|
|
initial = current if os.path.isdir(current) else os.path.expanduser("~")
|
|
directory = filedialog.askdirectory(
|
|
initialdir=initial, title="Select Directory", parent=self.master
|
|
)
|
|
if directory:
|
|
self.logger.debug(f"Selected: {directory}")
|
|
entry_widget.delete(0, tk.END)
|
|
entry_widget.insert(0, directory)
|
|
if entry_widget == self.main_frame.svn_path_entry:
|
|
self.update_svn_status_indicator(directory)
|
|
else:
|
|
self.logger.debug("Browse cancelled.")
|
|
|
|
def update_svn_status_indicator(self, svn_path):
|
|
"""Checks repo status and updates states of dependent GUI widgets."""
|
|
is_valid = bool(svn_path and os.path.isdir(svn_path))
|
|
is_ready = is_valid and os.path.exists(os.path.join(svn_path, ".git"))
|
|
self.logger.debug(
|
|
f"Updating status for '{svn_path}'. Valid:{is_valid}, Ready:{is_ready}"
|
|
)
|
|
|
|
if hasattr(self, "main_frame"):
|
|
mf = self.main_frame
|
|
# Update indicator & Prepare button (Repo Tab)
|
|
mf.update_svn_indicator(is_ready)
|
|
|
|
# Determine states based on validity and readiness
|
|
gitignore_state = tk.NORMAL if is_valid else tk.DISABLED
|
|
repo_ready_state = tk.NORMAL if is_ready else tk.DISABLED
|
|
|
|
# Update states: Repo Tab
|
|
if hasattr(mf, "edit_gitignore_button"):
|
|
mf.edit_gitignore_button.config(state=gitignore_state)
|
|
if hasattr(mf, "create_bundle_button"):
|
|
mf.create_bundle_button.config(state=repo_ready_state)
|
|
if hasattr(mf, "fetch_bundle_button"):
|
|
mf.fetch_bundle_button.config(state=repo_ready_state)
|
|
# Update states: Backup Tab
|
|
if hasattr(mf, "manual_backup_button"):
|
|
mf.manual_backup_button.config(state=repo_ready_state)
|
|
# Update states: Commit Tab
|
|
if hasattr(mf, "autocommit_checkbox"):
|
|
mf.autocommit_checkbox.config(state=repo_ready_state)
|
|
if hasattr(mf, "commit_message_text"):
|
|
mf.commit_message_text.config(state=repo_ready_state)
|
|
if hasattr(mf, "commit_button"):
|
|
mf.commit_button.config(state=repo_ready_state)
|
|
# Update states: Tags Tab
|
|
if hasattr(mf, "refresh_tags_button"):
|
|
mf.refresh_tags_button.config(state=repo_ready_state)
|
|
if hasattr(mf, "create_tag_button"):
|
|
mf.create_tag_button.config(state=repo_ready_state)
|
|
if hasattr(mf, "checkout_tag_button"):
|
|
mf.checkout_tag_button.config(state=repo_ready_state)
|
|
# Update states: Branches Tab
|
|
if hasattr(mf, "refresh_branches_button"):
|
|
mf.refresh_branches_button.config(state=repo_ready_state)
|
|
if hasattr(mf, "create_branch_button"):
|
|
mf.create_branch_button.config(state=repo_ready_state)
|
|
if hasattr(mf, "checkout_branch_button"):
|
|
mf.checkout_branch_button.config(state=repo_ready_state)
|
|
# Update states: History Tab
|
|
if hasattr(mf, "history_branch_filter_combo"):
|
|
mf.history_branch_filter_combo.config(state=repo_ready_state)
|
|
if hasattr(mf, "refresh_history_button"):
|
|
mf.refresh_history_button.config(state=repo_ready_state)
|
|
|
|
def open_gitignore_editor(self):
|
|
"""Opens the editor window for .gitignore."""
|
|
self.logger.info("--- Action: Edit .gitignore ---")
|
|
svn_path = self._get_and_validate_svn_path("Edit .gitignore")
|
|
if not svn_path:
|
|
return
|
|
gitignore_path = os.path.join(svn_path, ".gitignore")
|
|
self.logger.debug(f"Target: {gitignore_path}")
|
|
try:
|
|
editor = GitignoreEditorWindow(self.master, gitignore_path, self.logger)
|
|
self.logger.debug("Editor opened.")
|
|
except Exception as e:
|
|
self.logger.exception(f"Editor error: {e}")
|
|
self.main_frame.show_error("Error", f"Editor error:\n{e}")
|
|
|
|
def _get_and_validate_svn_path(self, operation_name="Operation"):
|
|
"""
|
|
Retrieves the SVN path from the GUI entry, validates its existence
|
|
as a directory, shows errors in GUI, and logs issues.
|
|
|
|
Args:
|
|
operation_name (str): Name of the operation requesting the path
|
|
(for logging/error messages).
|
|
|
|
Returns:
|
|
str: The validated absolute path if valid, None otherwise.
|
|
"""
|
|
# Check if main_frame and the specific widget exist first
|
|
if not hasattr(self, 'main_frame') or \
|
|
not self.main_frame.winfo_exists() or \
|
|
not hasattr(self.main_frame, 'svn_path_entry'):
|
|
self.logger.error(f"{operation_name}: GUI component unavailable.")
|
|
# Avoid showing messagebox if GUI isn't fully ready
|
|
# self.main_frame.show_error("GUI Error", "Cannot access path field.")
|
|
return None
|
|
|
|
svn_path_str = self.main_frame.svn_path_entry.get()
|
|
svn_path_str = svn_path_str.strip()
|
|
|
|
if not svn_path_str:
|
|
self.logger.error(f"{operation_name}: SVN Working Copy Path is empty.")
|
|
self.main_frame.show_error(
|
|
"Input Error",
|
|
"Please specify the SVN Working Copy Path."
|
|
)
|
|
return None
|
|
|
|
# Convert to absolute path and check if it's a directory
|
|
abs_path = os.path.abspath(svn_path_str)
|
|
if not os.path.isdir(abs_path):
|
|
self.logger.error(
|
|
f"{operation_name}: Specified SVN path is not a valid "
|
|
f"directory: {abs_path}"
|
|
)
|
|
self.main_frame.show_error(
|
|
"Input Error",
|
|
f"Invalid SVN path (not a directory):\n{abs_path}"
|
|
)
|
|
return None
|
|
|
|
# If all checks pass
|
|
self.logger.debug(f"{operation_name}: Using validated SVN path: {abs_path}")
|
|
return abs_path
|
|
|
|
|
|
def _get_and_validate_usb_path(self, operation_name="Operation"):
|
|
"""
|
|
Retrieves the USB/Bundle Target path from the GUI entry, validates
|
|
its existence as a directory, shows errors, and logs issues.
|
|
|
|
Args:
|
|
operation_name (str): Name of the operation requesting the path.
|
|
|
|
Returns:
|
|
str: The validated absolute path if valid, None otherwise.
|
|
"""
|
|
# Check if main_frame and the specific widget exist first
|
|
if not hasattr(self, 'main_frame') or \
|
|
not self.main_frame.winfo_exists() or \
|
|
not hasattr(self.main_frame, 'usb_path_entry'):
|
|
self.logger.error(f"{operation_name}: GUI component unavailable.")
|
|
return None
|
|
|
|
usb_path_str = self.main_frame.usb_path_entry.get()
|
|
usb_path_str = usb_path_str.strip()
|
|
|
|
if not usb_path_str:
|
|
self.logger.error(f"{operation_name}: Bundle Target Dir path empty.")
|
|
self.main_frame.show_error(
|
|
"Input Error",
|
|
"Please specify the Bundle Target Directory."
|
|
)
|
|
return None
|
|
|
|
# Convert to absolute path and check if it's a directory
|
|
abs_path = os.path.abspath(usb_path_str)
|
|
if not os.path.isdir(abs_path):
|
|
self.logger.error(
|
|
f"{operation_name}: Specified Bundle Target path is not a "
|
|
f"valid directory: {abs_path}"
|
|
)
|
|
self.main_frame.show_error(
|
|
"Input Error",
|
|
f"Invalid Bundle Target path (not a directory):\n{abs_path}"
|
|
)
|
|
return None
|
|
|
|
# If all checks pass
|
|
self.logger.debug(f"{operation_name}: Using validated Bundle Target path: {abs_path}")
|
|
return abs_path
|
|
|
|
# --- Core Repo/Bundle/Backup Actions (now triggered from specific tabs) ---
|
|
|
|
def prepare_svn_for_git(self):
|
|
"""Handles the 'Prepare SVN Repo' action (Repo Tab)."""
|
|
self.logger.info("--- Action: Prepare Repo ---")
|
|
svn_path = self._get_and_validate_svn_path("Prepare")
|
|
if not svn_path:
|
|
return
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Prepare: Save failed.")
|
|
if os.path.exists(os.path.join(svn_path, ".git")):
|
|
self.logger.info("Already prepared.")
|
|
self.main_frame.show_info("Info", "Already prepared.")
|
|
self.update_svn_status_indicator(svn_path)
|
|
return
|
|
self.logger.info(f"Preparing: {svn_path}")
|
|
try:
|
|
self.git_commands.prepare_svn_for_git(svn_path)
|
|
self.logger.info("Prepared.")
|
|
self.main_frame.show_info("Success", "Repo prepared.")
|
|
self.update_svn_status_indicator(svn_path)
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Prepare error: {e}")
|
|
self.main_frame.show_error("Error", f"Failed:\n{e}")
|
|
self.update_svn_status_indicator(svn_path)
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected prepare error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
self.update_svn_status_indicator(svn_path)
|
|
|
|
def create_git_bundle(self):
|
|
"""Handles the 'Create Bundle' action (Repo Tab)."""
|
|
self.logger.info("--- Action: Create Bundle ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.logger.error("Bundle: No profile.")
|
|
self.main_frame.show_error("Error", "No profile.")
|
|
return
|
|
svn_path = self._get_and_validate_svn_path("Bundle")
|
|
if not svn_path:
|
|
return
|
|
usb_path = self._get_and_validate_usb_path("Bundle")
|
|
if not usb_path:
|
|
return
|
|
name = self.main_frame.bundle_name_entry.get().strip()
|
|
if not name:
|
|
self.logger.error("Bundle: Name empty.")
|
|
self.main_frame.show_error("Error", "Bundle name empty.")
|
|
return
|
|
if not name.lower().endswith(".bundle"):
|
|
self.logger.warning(f"Adding .bundle")
|
|
name += ".bundle"
|
|
mf = self.main_frame
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.insert(0, name)
|
|
full_path = os.path.join(usb_path, name)
|
|
self.logger.debug(f"Target: {full_path}")
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Bundle: Save failed.")
|
|
# Backup Step
|
|
if self.main_frame.autobackup_var.get():
|
|
self.logger.info("Autobackup...")
|
|
if not self.create_backup(svn_path, profile):
|
|
self.logger.error("Aborted: backup fail.")
|
|
return
|
|
# Autocommit Step (using checkbox and message from Commit Tab)
|
|
if self.main_frame.autocommit_var.get():
|
|
self.logger.info("Autocommit before bundle...")
|
|
try:
|
|
if self.git_commands.git_status_has_changes(svn_path):
|
|
self.logger.info("Changes detected for autocommit...")
|
|
msg = (
|
|
self.main_frame.get_commit_message()
|
|
or f"Autocommit '{profile}' before bundle"
|
|
)
|
|
self.logger.debug(f"Autocommit msg: '{msg}'")
|
|
if self.git_commands.git_commit(svn_path, msg):
|
|
self.logger.info("Autocommit ok.")
|
|
else:
|
|
self.logger.info("No changes for autocommit.")
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Autocommit error: {e}")
|
|
self.main_frame.show_error("Error", f"Autocommit Failed:\n{e}")
|
|
return
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected autocommit: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
return
|
|
# Create Bundle Step
|
|
self.logger.info(f"Creating bundle: {full_path}")
|
|
try:
|
|
self.git_commands.create_git_bundle(svn_path, full_path)
|
|
if os.path.exists(full_path) and os.path.getsize(full_path) > 0:
|
|
self.logger.info("Bundle created.")
|
|
self.main_frame.show_info("Success", f"Bundle created:\n{full_path}")
|
|
else:
|
|
self.logger.warning("Bundle empty/not created.")
|
|
self.main_frame.show_warning("Info", "Bundle empty/not created.")
|
|
if os.path.exists(full_path):
|
|
try:
|
|
os.remove(full_path)
|
|
self.logger.info("Removed empty.")
|
|
except OSError as e:
|
|
self.logger.warning(f"Cannot remove empty: {e}")
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Bundle error: {e}")
|
|
self.main_frame.show_error("Error", f"Failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected bundle error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
|
|
def fetch_from_git_bundle(self):
|
|
"""Handles the 'Fetch from Bundle' action (Repo Tab)."""
|
|
self.logger.info("--- Action: Fetch Bundle ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.logger.error("Fetch: No profile.")
|
|
self.main_frame.show_error("Error", "No profile.")
|
|
return
|
|
svn_path = self._get_and_validate_svn_path("Fetch")
|
|
if not svn_path:
|
|
return
|
|
usb_path = self._get_and_validate_usb_path("Fetch")
|
|
if not usb_path:
|
|
return
|
|
name = self.main_frame.bundle_updated_name_entry.get().strip()
|
|
if not name:
|
|
self.logger.error("Fetch: Name empty.")
|
|
self.main_frame.show_error("Error", "Fetch name empty.")
|
|
return
|
|
full_path = os.path.join(usb_path, name)
|
|
self.logger.debug(f"Source: {full_path}")
|
|
if not os.path.isfile(full_path):
|
|
self.logger.error(f"Fetch: Not found: {full_path}")
|
|
self.main_frame.show_error("Error", f"Not found:\n{full_path}")
|
|
return
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Fetch: Could not save.")
|
|
if self.main_frame.autobackup_var.get():
|
|
self.logger.info("Autobackup...")
|
|
if not self.create_backup(svn_path, profile):
|
|
self.logger.error("Aborted: backup fail.")
|
|
return
|
|
self.logger.info(f"Fetching into '{svn_path}' from: {full_path}")
|
|
try:
|
|
self.git_commands.fetch_from_git_bundle(svn_path, full_path)
|
|
self.logger.info("Fetch/merge completed.")
|
|
self.main_frame.show_info("Fetch Complete", "Fetch complete.\nCheck logs.")
|
|
except GitCommandError as e:
|
|
self.logger.error(f"Fetch/merge error: {e}")
|
|
if "merge conflict" in str(e).lower():
|
|
self.main_frame.show_error(
|
|
"Merge Conflict",
|
|
f"Conflict.\nResolve in:\n{svn_path}\nThen commit.",
|
|
)
|
|
else:
|
|
self.main_frame.show_error("Error", f"Failed:\n{e}")
|
|
except ValueError as e:
|
|
self.logger.error(f"Validation error fetch: {e}")
|
|
self.main_frame.show_error("Error", f"Invalid:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected fetch error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
|
|
# --- Backup Logic (from Backup Tab) ---
|
|
def _parse_exclusions(self, profile_name):
|
|
"""Parses exclusion string from config."""
|
|
exclude_str = self.config_manager.get_profile_option(
|
|
profile_name, "backup_exclude_extensions", ""
|
|
)
|
|
extensions = set()
|
|
dirs = {".git", ".svn"}
|
|
if exclude_str:
|
|
for ext in exclude_str.split(","):
|
|
clean = ext.strip().lower()
|
|
if clean:
|
|
extensions.add("." + clean if not clean.startswith(".") else clean)
|
|
self.logger.debug(
|
|
f"Exclusions '{profile_name}' - Ext:{extensions}, Dirs:{dirs}"
|
|
)
|
|
return extensions, dirs
|
|
|
|
def create_backup(self, source_path, profile):
|
|
"""Creates a timestamped ZIP backup."""
|
|
self.logger.info(f"Creating ZIP backup for '{profile}' from '{source_path}'")
|
|
base_dir = self.main_frame.backup_dir_var.get().strip()
|
|
if not base_dir:
|
|
self.logger.error("Backup Fail: Dir empty.")
|
|
self.main_frame.show_error("Error", "Backup dir empty.")
|
|
return False
|
|
if not os.path.isdir(base_dir):
|
|
self.logger.info(f"Creating backup dir: {base_dir}")
|
|
try:
|
|
os.makedirs(base_dir, exist_ok=True)
|
|
except OSError as e:
|
|
self.logger.error(f"Cannot create dir: {e}")
|
|
self.main_frame.show_error("Error", f"Cannot create dir:\n{e}")
|
|
return False
|
|
try:
|
|
excluded_ext, excluded_dir = self._parse_exclusions(profile)
|
|
except Exception as e:
|
|
self.logger.error(f"Parse error: {e}")
|
|
self.main_frame.show_error("Error", "Cannot parse exclusions.")
|
|
return False
|
|
now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
safe_prof = (
|
|
"".join(c for c in profile if c.isalnum() or c in "_-").rstrip()
|
|
or "profile"
|
|
)
|
|
filename = f"{now}_backup_{safe_prof}.zip"
|
|
full_path = os.path.join(base_dir, filename)
|
|
self.logger.info(f"Target ZIP: {full_path}")
|
|
added, excl_f, excl_d = 0, 0, 0
|
|
zip_f = None
|
|
try:
|
|
zip_f = zipfile.ZipFile(full_path, "w", zipfile.ZIP_DEFLATED, True)
|
|
for root, dirs, files in os.walk(source_path, topdown=True):
|
|
orig_dirs = list(dirs)
|
|
dirs[:] = [d for d in dirs if d.lower() not in excluded_dir]
|
|
excl_now = set(orig_dirs) - set(dirs)
|
|
if excl_now:
|
|
excl_d += len(excl_now)
|
|
for d in excl_now:
|
|
self.logger.debug(f"Excl Dir: {os.path.join(root, d)}")
|
|
for file in files:
|
|
_, ext = os.path.splitext(file)
|
|
ext_low = ext.lower()
|
|
if file.lower() in excluded_dir or ext_low in excluded_ext:
|
|
self.logger.debug(f"Excl File: {os.path.join(root, file)}")
|
|
excl_f += 1
|
|
continue
|
|
file_path = os.path.join(root, file)
|
|
arc_name = os.path.relpath(file_path, source_path)
|
|
try:
|
|
zip_f.write(file_path, arc_name)
|
|
added += 1
|
|
if added % 500 == 0:
|
|
self.logger.debug(f"Added {added}...")
|
|
except Exception as we:
|
|
self.logger.error(
|
|
f"Write error '{file_path}': {we}", exc_info=True
|
|
)
|
|
self.logger.info(
|
|
f"ZIP finished: {full_path} (Add:{added}, ExF:{excl_f}, ExD:{excl_d})"
|
|
)
|
|
return True
|
|
except OSError as e:
|
|
self.logger.error(f"OS error ZIP: {e}")
|
|
self.main_frame.show_error("Error", f"OS Error:\n{e}")
|
|
return False
|
|
except zipfile.BadZipFile as e:
|
|
self.logger.error(f"ZIP format error: {e}")
|
|
self.main_frame.show_error("Error", f"ZIP format:\n{e}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected ZIP error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected ZIP:\n{e}")
|
|
return False
|
|
finally:
|
|
if zip_f:
|
|
zip_f.close()
|
|
self.logger.debug("ZIP closed.")
|
|
if os.path.exists(full_path) and added == 0:
|
|
self.logger.warning(f"Empty ZIP: {full_path}")
|
|
try:
|
|
os.remove(full_path)
|
|
self.logger.info("Removed empty ZIP.")
|
|
except OSError as e:
|
|
self.logger.error(f"Failed remove empty ZIP: {e}")
|
|
|
|
def manual_backup(self):
|
|
"""Handles the 'Backup Now' button click (Backup Tab)."""
|
|
self.logger.info("--- Action: Manual Backup ---")
|
|
profile = self.main_frame.profile_var.get()
|
|
if not profile:
|
|
self.logger.warning("Backup: No profile.")
|
|
self.main_frame.show_error("Error", "No profile.")
|
|
return
|
|
svn_path = self._get_and_validate_svn_path(f"Manual Backup ({profile})")
|
|
if not svn_path:
|
|
return
|
|
self.logger.info("Saving settings before backup...")
|
|
if not self.save_profile_settings():
|
|
self.logger.error("Backup: Could not save settings.")
|
|
if not self.main_frame.ask_yes_no(
|
|
"Error", "Could not save.\nContinue anyway?"
|
|
):
|
|
self.logger.warning("Backup aborted.")
|
|
return
|
|
self.logger.info(f"Starting manual backup for '{profile}'...")
|
|
success = self.create_backup(svn_path, profile) # Calls ZIP backup
|
|
if success:
|
|
self.main_frame.show_info("Success", "Manual ZIP backup completed.")
|
|
else:
|
|
self.logger.error(
|
|
f"Manual backup failed for '{profile}'."
|
|
) # Error shown by create_backup
|
|
|
|
# --- Commit Action (from Commit Tab) ---
|
|
def commit_changes(self):
|
|
"""Handles the 'Commit Changes' button click (Commit Tab)."""
|
|
self.logger.info("--- Action: Commit Changes ---")
|
|
svn_path = self._get_and_validate_svn_path("Commit Changes")
|
|
if not svn_path:
|
|
return
|
|
|
|
# Get commit message from the ScrolledText widget
|
|
commit_msg = self.main_frame.get_commit_message()
|
|
if not commit_msg:
|
|
self.logger.warning("Commit failed: Message is empty.")
|
|
self.main_frame.show_error(
|
|
"Commit Error", "Commit message cannot be empty."
|
|
)
|
|
return
|
|
|
|
# Save settings before commit (optional, ensures consistency)
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Commit Changes: Could not save settings first.")
|
|
|
|
# Perform the commit
|
|
try:
|
|
commit_made = self.git_commands.git_commit(svn_path, commit_msg)
|
|
if commit_made:
|
|
self.logger.info("Commit successful.")
|
|
self.main_frame.show_info("Success", "Changes committed.")
|
|
# Clear the commit message box after successful commit
|
|
self.main_frame.clear_commit_message()
|
|
# Refresh history view after commit
|
|
self.refresh_commit_history()
|
|
else:
|
|
# git_commit logs "nothing to commit"
|
|
self.main_frame.show_info("No Changes", "No changes to commit.")
|
|
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Commit failed: {e}")
|
|
self.main_frame.show_error("Commit Error", f"Failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected commit error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
|
|
# --- Tag Management (from Tags Tab) ---
|
|
def refresh_tag_list(self):
|
|
"""Fetches tags with subjects and updates GUI listbox."""
|
|
self.logger.info("--- Action: Refresh Tags ---")
|
|
svn_path = self._get_and_validate_svn_path("Refresh Tags")
|
|
if not svn_path or not os.path.exists(os.path.join(svn_path, ".git")):
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_tag_list([])
|
|
return # Silently return if repo not ready
|
|
try:
|
|
tags_data = self.git_commands.list_tags(svn_path) # Returns tuples
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_tag_list(tags_data)
|
|
self.logger.info(f"Tag list updated ({len(tags_data)} tags).")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed refresh tags: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Could not refresh tags:\n{e}")
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_tag_list([])
|
|
|
|
def create_tag(self):
|
|
"""Handles 'Create Tag': commits (if needed), shows dialog, creates tag."""
|
|
self.logger.info("--- Action: Create Tag ---")
|
|
svn_path = self._get_and_validate_svn_path("Create Tag")
|
|
if not svn_path:
|
|
return
|
|
profile = self.main_frame.profile_var.get() # Needed for commit msg?
|
|
if not profile:
|
|
self.logger.error("Tag Error: No profile")
|
|
self.main_frame.show_error("Error", "No profile.")
|
|
return
|
|
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Tag: Could not save.")
|
|
|
|
# 1. Commit outstanding changes IF message is provided
|
|
try:
|
|
if self.git_commands.git_status_has_changes(svn_path):
|
|
self.logger.info("Uncommitted changes detected.")
|
|
commit_msg = self.main_frame.get_commit_message()
|
|
if not commit_msg:
|
|
self.logger.error("Tag blocked: Changes exist, no commit msg.")
|
|
self.main_frame.show_error(
|
|
"Commit Required", "Changes exist.\nEnter commit message first."
|
|
)
|
|
return
|
|
confirm_msg = f"Commit changes with message:\n'{commit_msg}'?"
|
|
if not self.main_frame.ask_yes_no("Confirm Commit", confirm_msg):
|
|
self.logger.info("Commit cancelled.")
|
|
self.main_frame.show_warning("Cancelled", "Tag creation cancelled.")
|
|
return
|
|
if self.git_commands.git_commit(svn_path, commit_msg):
|
|
self.logger.info("Pre-tag commit successful.")
|
|
self.main_frame.clear_commit_message() # Clear after use
|
|
else:
|
|
self.logger.info("No changes detected before tagging.")
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Pre-tag commit error: {e}")
|
|
self.main_frame.show_error("Commit Error", f"Failed:\n{e}")
|
|
return
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected pre-tag commit error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
return
|
|
|
|
# 2. Open Dialog for Tag Name and Tag Message
|
|
self.logger.debug("Opening create tag dialog...")
|
|
dialog = CreateTagDialog(self.master)
|
|
tag_info = dialog.result # (name, message) or None
|
|
|
|
# 3. Create Tag if user provided info
|
|
if tag_info:
|
|
tag_name, tag_message = tag_info
|
|
self.logger.info(f"Creating tag '{tag_name}'...")
|
|
try:
|
|
self.git_commands.create_tag(svn_path, tag_name, tag_message)
|
|
self.logger.info(f"Tag '{tag_name}' created.")
|
|
self.main_frame.show_info("Success", f"Tag '{tag_name}' created.")
|
|
self.refresh_tag_list() # Refresh UI list
|
|
self.refresh_commit_history() # Show new tag in history
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Failed create tag '{tag_name}': {e}")
|
|
self.main_frame.show_error("Tag Error", f"Failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected tag error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
else:
|
|
self.logger.info("Tag creation cancelled in dialog.")
|
|
|
|
def checkout_tag(self):
|
|
"""Handles the 'Checkout Selected Tag' action."""
|
|
self.logger.info("--- Action: Checkout Tag ---")
|
|
svn_path = self._get_and_validate_svn_path("Checkout Tag")
|
|
if not svn_path:
|
|
return
|
|
selected_tag = self.main_frame.get_selected_tag() # Gets name
|
|
if not selected_tag:
|
|
self.logger.warning("Checkout: No tag selected.")
|
|
self.main_frame.show_error("Error", "Select tag.")
|
|
return
|
|
self.logger.info(f"Attempting checkout: {selected_tag}")
|
|
try: # Check changes
|
|
if self.git_commands.git_status_has_changes(svn_path):
|
|
self.logger.error("Checkout blocked: Changes.")
|
|
self.main_frame.show_error("Blocked", "Changes exist.")
|
|
return
|
|
self.logger.debug("No changes found.")
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Status check error: {e}")
|
|
self.main_frame.show_error("Error", f"Status check:\n{e}")
|
|
return
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected status check: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected status:\n{e}")
|
|
return
|
|
# Confirm
|
|
confirm_msg = f"Checkout tag '{selected_tag}'?\n\nWARNINGS:\n- Files overwritten.\n- NO backup.\n- Detached HEAD state."
|
|
if not self.main_frame.ask_yes_no("Confirm Checkout", confirm_msg):
|
|
self.logger.info("Checkout cancelled.")
|
|
return
|
|
self.logger.info(f"Confirmed checkout: {selected_tag}")
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Checkout: Save failed.")
|
|
# Execute
|
|
try:
|
|
if self.git_commands.checkout_tag(svn_path, selected_tag):
|
|
self.logger.info(f"Tag '{selected_tag}' checked out.")
|
|
self.main_frame.show_info(
|
|
"Success",
|
|
f"Checked out '{selected_tag}'.\n\nNOTE: Detached HEAD state.",
|
|
)
|
|
self.refresh_branch_list() # Update current branch display
|
|
self.refresh_commit_history() # Update history view
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Failed checkout '{selected_tag}': {e}")
|
|
self.main_frame.show_error("Error", f"Checkout failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected checkout error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
|
|
# --- Branch Management Methods (from Branch Tab) ---
|
|
def refresh_branch_list(self):
|
|
"""Fetches local branches and updates the Branch tab listbox."""
|
|
self.logger.info("--- Action: Refresh Branches ---")
|
|
svn_path = self._get_and_validate_svn_path("Refresh Branches")
|
|
current_branch_for_history = None # Keep track for history filter update
|
|
if not svn_path or not os.path.exists(os.path.join(svn_path, ".git")):
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_branch_list([], None)
|
|
self.main_frame.update_history_branch_filter(
|
|
[]
|
|
) # Clear history filter too
|
|
return # Silently return if repo not ready
|
|
try:
|
|
branches, current = self.git_commands.list_branches(svn_path)
|
|
current_branch_for_history = current # Store current branch
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_branch_list(branches, current)
|
|
# Update the history filter dropdown as well
|
|
self.main_frame.update_history_branch_filter(branches, current)
|
|
self.logger.info(
|
|
f"Branch list updated ({len(branches)} branches). Current: {current}"
|
|
)
|
|
except Exception as e:
|
|
self.logger.error(f"Failed refresh branches: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Could not refresh branches:\n{e}")
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_branch_list([], None)
|
|
self.main_frame.update_history_branch_filter([])
|
|
|
|
def checkout_branch(self):
|
|
"""Handles the 'Checkout Selected Branch' action."""
|
|
self.logger.info("--- Action: Checkout Branch ---")
|
|
svn_path = self._get_and_validate_svn_path("Checkout Branch")
|
|
if not svn_path:
|
|
return
|
|
|
|
selected_branch = self.main_frame.get_selected_branch()
|
|
if not selected_branch:
|
|
self.logger.warning("Checkout Branch: No branch selected.")
|
|
self.main_frame.show_error("Selection Error", "Select a branch.")
|
|
return
|
|
|
|
self.logger.info(f"Attempting checkout for branch: {selected_branch}")
|
|
|
|
# Check for uncommitted changes
|
|
try:
|
|
if self.git_commands.git_status_has_changes(svn_path):
|
|
self.logger.error("Checkout blocked: Uncommitted changes.")
|
|
self.main_frame.show_error(
|
|
"Blocked", "Uncommitted changes exist.\nCommit or stash first."
|
|
)
|
|
return
|
|
self.logger.debug("No uncommitted changes found.")
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Status check error: {e}")
|
|
self.main_frame.show_error("Error", f"Cannot check status:\n{e}")
|
|
return
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected status error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected status:\n{e}")
|
|
return
|
|
|
|
# Confirm checkout
|
|
if not self.main_frame.ask_yes_no(
|
|
"Confirm Checkout", f"Switch to branch '{selected_branch}'?"
|
|
):
|
|
self.logger.info("Branch checkout cancelled.")
|
|
return
|
|
|
|
self.logger.info(f"Confirmed checkout: {selected_branch}")
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Checkout: Save failed.")
|
|
|
|
# Execute checkout
|
|
try:
|
|
if self.git_commands.checkout_branch(svn_path, selected_branch):
|
|
self.logger.info(f"Branch '{selected_branch}' checked out.")
|
|
self.main_frame.show_info(
|
|
"Success", f"Switched to branch '{selected_branch}'."
|
|
)
|
|
self.refresh_branch_list() # Update list highlighting
|
|
self.refresh_commit_history() # Update history view
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Failed checkout '{selected_branch}': {e}")
|
|
self.main_frame.show_error("Error", f"Checkout failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected checkout error: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
|
|
def create_branch(self):
|
|
"""Handles the 'Create Branch' action."""
|
|
self.logger.info("--- Action: Create Branch ---")
|
|
svn_path = self._get_and_validate_svn_path("Create Branch")
|
|
if not svn_path:
|
|
return
|
|
|
|
# Open dialog to get new branch name
|
|
dialog = CreateBranchDialog(self.master)
|
|
new_branch_name = dialog.result
|
|
|
|
if new_branch_name:
|
|
self.logger.info(f"User wants new branch: '{new_branch_name}'")
|
|
if not self.save_profile_settings():
|
|
self.logger.warning("Create Branch: Save failed.")
|
|
|
|
# Execute create command
|
|
try:
|
|
if self.git_commands.create_branch(svn_path, new_branch_name):
|
|
self.logger.info(f"Branch '{new_branch_name}' created.")
|
|
self.main_frame.show_info(
|
|
"Success", f"Branch '{new_branch_name}' created."
|
|
)
|
|
self.refresh_branch_list() # Update list
|
|
# Ask user if they want to switch to the new branch
|
|
if self.main_frame.ask_yes_no(
|
|
"Checkout New Branch?",
|
|
f"Switch to new branch '{new_branch_name}'?",
|
|
):
|
|
# Perform checkout (no need for change check or confirmation here)
|
|
try:
|
|
if self.git_commands.checkout_branch(
|
|
svn_path, new_branch_name
|
|
):
|
|
self.logger.info(
|
|
f"Checked out new branch '{new_branch_name}'."
|
|
)
|
|
self.refresh_branch_list() # Update highlight again
|
|
self.refresh_commit_history()
|
|
# else: checkout failed, error logged by command
|
|
except (GitCommandError, ValueError) as chk_e:
|
|
self.logger.error(f"Failed checkout new branch: {chk_e}")
|
|
self.main_frame.show_error(
|
|
"Checkout Error", f"Failed switch:\n{chk_e}"
|
|
)
|
|
except Exception as chk_e_unex:
|
|
self.logger.exception(
|
|
f"Unexpected checkout error: {chk_e_unex}"
|
|
)
|
|
self.main_frame.show_error(
|
|
"Error", f"Unexpected switch:\n{chk_e_unex}"
|
|
)
|
|
else:
|
|
# Refresh history even if not checking out
|
|
self.refresh_commit_history()
|
|
|
|
except (GitCommandError, ValueError) as e:
|
|
self.logger.error(f"Failed create branch '{new_branch_name}': {e}")
|
|
self.main_frame.show_error("Error", f"Create failed:\n{e}")
|
|
except Exception as e:
|
|
self.logger.exception(f"Unexpected create branch: {e}")
|
|
self.main_frame.show_error("Error", f"Unexpected:\n{e}")
|
|
else:
|
|
self.logger.info("Branch creation cancelled.")
|
|
|
|
# --- History Method (from History Tab) ---
|
|
def refresh_commit_history(self):
|
|
"""Fetches commit log based on filters and updates display."""
|
|
self.logger.info("--- Action: Refresh History ---")
|
|
svn_path = self._get_and_validate_svn_path("Refresh History")
|
|
if not svn_path or not os.path.exists(os.path.join(svn_path, ".git")):
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_history_display([])
|
|
return # Silently return if repo not ready
|
|
|
|
# Get selected branch filter from GUI
|
|
branch_filter = None
|
|
if hasattr(self.main_frame, "history_branch_filter_var"):
|
|
selected_filter = self.main_frame.history_branch_filter_var.get()
|
|
if selected_filter and selected_filter != "-- All History --":
|
|
branch_filter = selected_filter # Use selected branch name
|
|
|
|
self.logger.debug(f"Refreshing history with filter: {branch_filter}")
|
|
|
|
try:
|
|
# Get log entries with optional filter
|
|
log_data = self.git_commands.get_commit_log(
|
|
svn_path, max_count=200, branch=branch_filter # Get more entries?
|
|
)
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_history_display(log_data)
|
|
self.logger.info(f"History updated ({len(log_data)} entries).")
|
|
except Exception as e:
|
|
self.logger.error(f"Failed refresh history: {e}", exc_info=True)
|
|
self.main_frame.show_error("Error", f"Could not refresh history:\n{e}")
|
|
if hasattr(self, "main_frame"):
|
|
self.main_frame.update_history_display([])
|
|
|
|
# --- GUI State Utilities ---
|
|
def _clear_and_disable_fields(self):
|
|
"""Clears fields and disables most buttons when no profile/repo."""
|
|
if hasattr(self, "main_frame"):
|
|
mf = self.main_frame
|
|
# Clear Repo Tab
|
|
mf.svn_path_entry.delete(0, tk.END)
|
|
mf.usb_path_entry.delete(0, tk.END)
|
|
mf.bundle_name_entry.delete(0, tk.END)
|
|
mf.bundle_updated_name_entry.delete(0, tk.END)
|
|
# Clear Commit Tab
|
|
mf.clear_commit_message()
|
|
mf.autocommit_var.set(False)
|
|
# Clear Tags Tab
|
|
mf.update_tag_list([])
|
|
# Clear Branch Tab
|
|
mf.update_branch_list([], None)
|
|
# Clear History Tab
|
|
mf.update_history_display([])
|
|
mf.update_history_branch_filter([])
|
|
# Update status (disables state-dependent widgets)
|
|
self.update_svn_status_indicator("")
|
|
# Disable general buttons
|
|
self._disable_general_buttons()
|
|
self.logger.debug("GUI fields cleared/reset. Buttons disabled.")
|
|
|
|
def _disable_general_buttons(self):
|
|
"""Disables general buttons requiring a profile."""
|
|
if hasattr(self, "main_frame"):
|
|
# Only Save Settings is truly general now
|
|
button = getattr(self.main_frame, "save_settings_button", None)
|
|
if button:
|
|
button.config(state=tk.DISABLED)
|
|
|
|
def _enable_function_buttons(self):
|
|
"""Enables general buttons. State buttons handled by status update."""
|
|
if hasattr(self, "main_frame"):
|
|
general_state = tk.NORMAL
|
|
button = getattr(self.main_frame, "save_settings_button", None)
|
|
if button:
|
|
button.config(state=general_state)
|
|
# Trigger state update for all repo-dependent widgets
|
|
self.update_svn_status_indicator(self.main_frame.svn_path_entry.get())
|
|
self.logger.debug("General buttons enabled. State buttons updated.")
|
|
|
|
def show_fatal_error(self, message):
|
|
"""Shows a fatal error message."""
|
|
try:
|
|
parent = (
|
|
self.master
|
|
if hasattr(self, "master")
|
|
and self.master
|
|
and self.master.winfo_exists()
|
|
else None
|
|
)
|
|
messagebox.showerror("Fatal Error", message, parent=parent)
|
|
except Exception as e:
|
|
print(f"FATAL ERROR (+GUI error {e}): {message}")
|
|
|
|
|
|
# --- Application Entry Point ---
|
|
def main():
|
|
"""Main function: Creates Tkinter root and runs the application."""
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s %(levelname)s:%(message)s"
|
|
)
|
|
root = tk.Tk()
|
|
# Adjust min height slightly for history
|
|
root.minsize(750, 800)
|
|
app = None
|
|
try:
|
|
app = GitSvnSyncApp(root)
|
|
if hasattr(app, "main_frame") and app.main_frame:
|
|
root.mainloop()
|
|
else:
|
|
print("App init failed.")
|
|
if root and root.winfo_exists():
|
|
root.destroy()
|
|
except Exception as e:
|
|
logging.exception("Fatal error during startup/mainloop.")
|
|
try:
|
|
parent = root if root and root.winfo_exists() else None
|
|
messagebox.showerror("Fatal Error", f"App failed:\n{e}", parent=parent)
|
|
except Exception as msg_e:
|
|
print(f"FATAL ERROR (GUI error: {msg_e}):\n{e}")
|
|
finally:
|
|
logging.info("Application exiting.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|