fix history click details

This commit is contained in:
VALLONGOL 2025-04-28 11:30:35 +02:00
parent ce36adddcc
commit 2da126cf2a
6 changed files with 387 additions and 428 deletions

View File

@ -568,7 +568,8 @@ class GitSvnSyncApp:
# Load remote repository settings # Load remote repository settings
if hasattr(mf, "remote_url_var") and hasattr(mf, "remote_name_var"): if hasattr(mf, "remote_url_var") and hasattr(mf, "remote_name_var"):
mf.remote_url_var.set(settings.get("remote_url", "")) remote_url_loaded = settings.get("remote_url", "") # Ottieni l'URL
mf.remote_url_var.set(remote_url_loaded)
mf.remote_name_var.set( mf.remote_name_var.set(
settings.get("remote_name", DEFAULT_REMOTE_NAME) settings.get("remote_name", DEFAULT_REMOTE_NAME)
) )
@ -599,10 +600,26 @@ class GitSvnSyncApp:
self.refresh_branch_list() # Refreshes local branches self.refresh_branch_list() # Refreshes local branches
self.refresh_commit_history() self.refresh_commit_history()
self.refresh_changed_files_list() self.refresh_changed_files_list()
# Also check remote status after loading a ready profile if remote_url_loaded: # Controlla se l'URL caricato NON è vuoto
self.check_connection_auth() # Check auth/conn status log_handler.log_debug("Remote URL found, initiating connection check.", func_name=func_name)
self.refresh_remote_status() # Check ahead/behind status self.check_connection_auth() # Check auth/conn status
# Status bar will be updated by the results of these async operations self.refresh_remote_status() # Check ahead/behind status (richiede upstream valido)
else:
# Se l'URL è vuoto, non tentare la connessione
log_handler.log_info("Remote URL is empty. Skipping connection check and remote status update.", func_name=func_name)
# Imposta lo stato auth/sync come sconosciuto/non configurato
self._update_gui_auth_status("unknown") # O un nuovo stato 'not_configured'? 'unknown' va bene per ora.
if hasattr(self.main_frame, "update_ahead_behind_status"):
self.main_frame.update_ahead_behind_status(
status_text="Sync Status: (Remote not configured)"
)
# Assicurati che il bottone refresh sync sia disabilitato
if hasattr(self.main_frame, "refresh_sync_status_button"):
self.main_frame.refresh_sync_status_button.config(state=tk.DISABLED)
# Aggiorna la status bar
mf.update_status_bar(
f"Profile '{profile_name}' loaded (Remote not configured)."
)
else: else:
# If not ready, clear dynamic GUI lists # If not ready, clear dynamic GUI lists
log_handler.log_info( log_handler.log_info(
@ -2883,14 +2900,16 @@ class GitSvnSyncApp:
} }
) )
def view_commit_details(self, history_line: str): def view_commit_details(self, history_line_or_hash: str): # Nome parametro aggiornabile per chiarezza
""" """
Callback triggered by double-clicking a line in the history view. Callback triggered by clicking a line in the history view (now Treeview).
Extracts the commit hash and starts an async worker to fetch details. Extracts the commit hash (if needed, ma ora riceve solo l'hash)
and starts an async worker to fetch details.
""" """
func_name: str = "view_commit_details" func_name: str = "view_commit_details"
commit_hash_short = history_line_or_hash.strip()
log_handler.log_info( log_handler.log_info(
f"--- Action Triggered: View Commit Details for line: '{history_line}' ---", f"--- Action Triggered: View Commit Details for hash: '{commit_hash_short}' ---",
func_name=func_name func_name=func_name
) )
@ -2907,30 +2926,12 @@ class GitSvnSyncApp:
) )
return return
# --- Estrarre l'Hash del Commit dalla Riga di Log --- # --- Validazione Hash (Opzionale ma consigliata) ---
# Assumiamo che il formato (%h) sia all'inizio della riga, seguito da spazio if not commit_hash_short or not re.match(r"^[0-9a-fA-F]{7,}$", commit_hash_short):
commit_hash_short: Optional[str] = None log_handler.log_error(f"Invalid commit hash received: '{commit_hash_short}'", func_name=func_name)
try: self.main_frame.show_error("Input Error", f"Invalid commit hash format:\n{commit_hash_short}")
parts: List[str] = history_line.split(maxsplit=1) # Divide solo al primo spazio return
if parts and len(parts[0]) > 0: # Assumendo che l'hash sia la prima parte # --- Fine Validazione ---
commit_hash_short = parts[0]
# Potremmo validare che sia un hash valido (es. 7+ caratteri esadecimali)
if not re.match(r"^[0-9a-fA-F]{7,}$", commit_hash_short):
raise ValueError(f"Extracted part '{commit_hash_short}' doesn't look like a commit hash.")
else:
raise ValueError("Could not split history line to find hash.")
log_handler.log_debug(f"Extracted commit hash: {commit_hash_short}", func_name=func_name)
except Exception as e:
log_handler.log_error(
f"Could not extract commit hash from history line '{history_line}': {e}",
func_name=func_name
)
self.main_frame.show_error(
"Parsing Error", f"Could not identify commit hash in selected line:\n{history_line}"
)
return
# --- Start Async Worker to Get Commit Details --- # --- Start Async Worker to Get Commit Details ---
log_handler.log_info( log_handler.log_info(
@ -2940,7 +2941,7 @@ class GitSvnSyncApp:
args: tuple = (self.git_commands, svn_path, commit_hash_short) args: tuple = (self.git_commands, svn_path, commit_hash_short)
# Start the async operation # Start the async operation
self._start_async_operation( self._start_async_operation(
worker_func=async_workers.run_get_commit_details_async, # NUOVO WORKER worker_func=async_workers.run_get_commit_details_async, # Worker corretto
args_tuple=args, args_tuple=args,
context_dict={ context_dict={
"context": "get_commit_details", "context": "get_commit_details",

View File

@ -4,6 +4,7 @@ import os
import queue import queue
import logging # Usato solo per i livelli, non per loggare direttamente import logging # Usato solo per i livelli, non per loggare direttamente
import datetime # Necessario per alcuni messaggi import datetime # Necessario per alcuni messaggi
from typing import Tuple, Dict, List, Callable, Optional, Any
# Importa i moduli necessari per la logica interna e le dipendenze # Importa i moduli necessari per la logica interna e le dipendenze
import log_handler import log_handler
@ -1823,7 +1824,7 @@ def run_get_commit_details_async(
if show_result.returncode == 0 and show_result.stdout: if show_result.returncode == 0 and show_result.stdout:
# --- Parsing dell'Output --- # --- Parsing dell'Output ---
# L'output sarà: Metadati formattati\n\nLista file separati da NUL\0 # L'output sarà: Metadati formattati\n\nLista file separati da NUL\0
output_parts: List[str] = show_result.stdout.split('\n\n', 1) output_parts: List[str] = show_result.stdout.split('\n', 1)
metadata_line: str = output_parts[0] metadata_line: str = output_parts[0]
files_part_raw: str = output_parts[1] if len(output_parts) > 1 else "" files_part_raw: str = output_parts[1] if len(output_parts) > 1 else ""
@ -1852,31 +1853,47 @@ def run_get_commit_details_async(
parsed_files: List[Tuple[str, str, Optional[str]]] = [] parsed_files: List[Tuple[str, str, Optional[str]]] = []
i: int = 0 i: int = 0
while i < len(file_entries): while i < len(file_entries):
# Ogni voce file dovrebbe avere status e path # Ottieni lo stato (primo elemento della sequenza per un file)
# Rinominati/Copiati (R/C) hanno status, old_path, new_path if not file_entries[i]: # Salta stringhe vuote risultanti da split (dovuto a \x00 finali?)
i += 1
continue
status_char: str = file_entries[i].strip() status_char: str = file_entries[i].strip()
if status_char.startswith(('R', 'C')): # Renamed/Copied if status_char.startswith(('R', 'C')): # Renamed/Copied
if i + 2 < len(file_entries): # Necessita di status, old_path, new_path (3 elementi)
# Rimuovi score (es. R100 -> R) if i + 2 < len(file_entries): # <-- CONTROLLO INDICE
status_code = status_char[0] status_code = status_char[0] # Prendi solo R o C
old_path = file_entries[i+1] old_path = file_entries[i+1]
new_path = file_entries[i+2] new_path = file_entries[i+2]
parsed_files.append((status_code, old_path, new_path)) # Verifica che i path non siano vuoti (ulteriore sicurezza)
i += 3 # Avanza di 3 elementi if old_path and new_path:
parsed_files.append((status_code, old_path, new_path))
else:
log_handler.log_warning(f"[Worker] Incomplete R/C entry (empty path?): {file_entries[i:i+3]}", func_name=func_name)
i += 3 # Avanza di 3
else: else:
log_handler.log_warning(f"[Worker] Incomplete R/C entry: {file_entries[i:]}", func_name=func_name) # Dati incompleti per R/C
break # Esce dal loop se i dati sono incompleti log_handler.log_warning(f"[Worker] Incomplete R/C entry (not enough parts): {file_entries[i:]}", func_name=func_name)
break # Interrompi il loop se la struttura dati è corrotta
elif status_char: # Added, Modified, Deleted, Type Changed elif status_char: # Added, Modified, Deleted, Type Changed
if i + 1 < len(file_entries): # Necessita di status, file_path (2 elementi)
file_path = file_entries[i+1] if i + 1 < len(file_entries): # <-- CONTROLLO INDICE
parsed_files.append((status_char[0], file_path, None)) # None per new_path file_path = file_entries[i+1]
i += 2 # Avanza di 2 elementi # Verifica che il path non sia vuoto
if file_path:
# Status: prendi solo il primo carattere (es. M, A, D, T)
parsed_files.append((status_char[0], file_path, None)) # Usa None per new_path
else:
log_handler.log_warning(f"[Worker] Incomplete A/M/D/T entry (empty path?): {file_entries[i:i+2]}", func_name=func_name)
i += 2 # Avanza di 2
else: else:
log_handler.log_warning(f"[Worker] Incomplete A/M/D/T entry: {file_entries[i:]}", func_name=func_name) # Dati incompleti per A/M/D/T
break log_handler.log_warning(f"[Worker] Incomplete A/M/D/T entry (not enough parts): {file_entries[i:]}", func_name=func_name)
break # Interrompi loop
else: else:
# Ignora elementi vuoti (potrebbero esserci NUL consecutivi?) # Questo caso non dovrebbe verificarsi con strip+split corretti, ma per sicurezza
i += 1 log_handler.log_warning(f"[Worker] Unexpected empty status_char at index {i}", func_name=func_name)
i += 1 # Avanza comunque per evitare loop infinito
commit_details['files_changed'] = parsed_files commit_details['files_changed'] = parsed_files
log_handler.log_debug( log_handler.log_debug(
f"[Worker] Parsed {len(parsed_files)} changed files.", func_name=func_name f"[Worker] Parsed {len(parsed_files)} changed files.", func_name=func_name

View File

@ -9,7 +9,7 @@ import log_handler
from git_commands import GitCommands, GitCommandError from git_commands import GitCommands, GitCommandError
# Importa anche i livelli di logging se usati nei messaggi fallback # Importa anche i livelli di logging se usati nei messaggi fallback
import logging import logging
from typing import List # Per type hints from typing import List, Optional # Per type hints
# --- Tooltip Class Definition (Copiata da gui.py per standalone, o importata) --- # --- Tooltip Class Definition (Copiata da gui.py per standalone, o importata) ---
# (Assumiamo sia definita qui o importata correttamente se messa in un file separato) # (Assumiamo sia definita qui o importata correttamente se messa in un file separato)
@ -186,6 +186,7 @@ class DiffViewerWindow(tk.Toplevel):
self.diff_map: List[int] = [] # Mappa per minimap self.diff_map: List[int] = [] # Mappa per minimap
self._scrolling_active: bool = False # Flag per scroll sincronizzato self._scrolling_active: bool = False # Flag per scroll sincronizzato
self._configure_timer_id = None # ID per debounce resize minimap self._configure_timer_id = None # ID per debounce resize minimap
self.loading_label: Optional[ttk.Label] = None
# --- Costruzione Interfaccia Grafica --- # --- Costruzione Interfaccia Grafica ---
log_handler.log_debug("Creating diff viewer widgets...", func_name=func_name) log_handler.log_debug("Creating diff viewer widgets...", func_name=func_name)
@ -195,6 +196,10 @@ class DiffViewerWindow(tk.Toplevel):
log_handler.log_debug("Loading content and computing diff...", func_name=func_name) log_handler.log_debug("Loading content and computing diff...", func_name=func_name)
load_ok = False load_ok = False
try: try:
self._show_loading_message()
# Forza l'aggiornamento della GUI per mostrare il messaggio prima del blocco
self.update_idletasks()
load_ok = self._load_content() # Usa self.ref1 e self.ref2 internamente load_ok = self._load_content() # Usa self.ref1 e self.ref2 internamente
if load_ok: if load_ok:
# Se il caricamento (anche parziale, es. file non trovato) è ok, calcola diff # Se il caricamento (anche parziale, es. file non trovato) è ok, calcola diff
@ -209,8 +214,11 @@ class DiffViewerWindow(tk.Toplevel):
# Disegna comunque minimappa vuota # Disegna comunque minimappa vuota
self.minimap_canvas.after(50, self._draw_minimap) self.minimap_canvas.after(50, self._draw_minimap)
self._hide_loading_message()
except Exception as load_err: except Exception as load_err:
# Errore imprevisto durante caricamento o calcolo diff # Errore imprevisto durante caricamento o calcolo diff
self._hide_loading_message()
log_handler.log_exception(f"Unexpected error during diff setup for '{self.relative_file_path}': {load_err}", func_name=func_name) log_handler.log_exception(f"Unexpected error during diff setup for '{self.relative_file_path}': {load_err}", func_name=func_name)
messagebox.showerror("Fatal Error", f"Failed to display diff:\n{load_err}", parent=self) messagebox.showerror("Fatal Error", f"Failed to display diff:\n{load_err}", parent=self)
# Chiudi la finestra se l'inizializzazione fallisce gravemente # Chiudi la finestra se l'inizializzazione fallisce gravemente
@ -276,6 +284,17 @@ class DiffViewerWindow(tk.Toplevel):
# Ridisegna su resize # Ridisegna su resize
self.minimap_canvas.bind("<Configure>", self._on_minimap_resize) self.minimap_canvas.bind("<Configure>", self._on_minimap_resize)
self.loading_label = ttk.Label(
main_frame, # Appoggia al main_frame per essere sopra gli altri
text="Loading file versions, please wait...", # Testo in inglese
font=("Segoe UI", 12, "italic"),
background="#FFFACD", # Giallo chiaro
foreground="#555555", # Grigio scuro
relief=tk.SOLID,
borderwidth=1,
padding=(10, 5)
)
# Configurazione Tag Highlighting (aggiornato nomi widget) # Configurazione Tag Highlighting (aggiornato nomi widget)
self.text_pane1.tag_config("removed", background="#FFE0E0") # Righe solo a sinistra self.text_pane1.tag_config("removed", background="#FFE0E0") # Righe solo a sinistra
self.text_pane1.tag_config("empty", background="#F5F5F5", foreground="#A0A0A0") # Filler self.text_pane1.tag_config("empty", background="#F5F5F5", foreground="#A0A0A0") # Filler
@ -560,6 +579,27 @@ class DiffViewerWindow(tk.Toplevel):
self._update_minimap_viewport() self._update_minimap_viewport()
log_handler.log_debug("Minimap drawing complete.", func_name=func_name) log_handler.log_debug("Minimap drawing complete.", func_name=func_name)
def _hide_loading_message(self):
"""Removes the loading label from view."""
if self.loading_label and self.loading_label.winfo_exists():
try:
self.loading_label.place_forget()
log_handler.log_debug("Loading message hidden.", func_name="_hide_loading_message")
except Exception as e:
log_handler.log_error(f"Failed to hide loading label: {e}", func_name="_hide_loading_message")
def _show_loading_message(self):
"""Places and lifts the loading label to make it visible."""
if self.loading_label and self.loading_label.winfo_exists():
try:
# Place al centro del main_frame (primo figlio di self)
container = self.winfo_children()[0] if self.winfo_children() else self
self.loading_label.place(in_=container, relx=0.5, rely=0.5, anchor=tk.CENTER)
self.loading_label.lift()
log_handler.log_debug("Loading message shown.", func_name="_show_loading_message")
except Exception as e:
log_handler.log_error(f"Failed to place/lift loading label: {e}", func_name="_show_loading_message")
# --- Scrolling Logic --- # --- Scrolling Logic ---
def _setup_scrolling(self): def _setup_scrolling(self):

View File

@ -62,10 +62,8 @@ class GitCommands:
working_directory: str, working_directory: str,
check: bool = True, check: bool = True,
log_output_level: int = logging.INFO, log_output_level: int = logging.INFO,
# ---<<< NUOVI PARAMETRI >>>---
capture: bool = True, # Cattura stdout/stderr? capture: bool = True, # Cattura stdout/stderr?
hide_console: bool = True, # Nascondi finestra console (Windows)? hide_console: bool = True, # Nascondi finestra console (Windows)?
# ---<<< FINE NUOVI PARAMETRI >>>---
) -> subprocess.CompletedProcess: ) -> subprocess.CompletedProcess:
""" """
Executes a shell command, logs details, handles errors, with options Executes a shell command, logs details, handles errors, with options
@ -115,7 +113,7 @@ class GitCommands:
# --- Esecuzione Comando --- # --- Esecuzione Comando ---
try: try:
# ---<<< MODIFICA: Configurazione startupinfo/flags >>>--- # ---Configurazione startupinfo/flags ---
startupinfo = None startupinfo = None
creationflags = 0 creationflags = 0
# Applica solo se richiesto E siamo su Windows # Applica solo se richiesto E siamo su Windows
@ -130,7 +128,6 @@ class GitCommands:
# per isolare l'input/output del comando interattivo # per isolare l'input/output del comando interattivo
creationflags = subprocess.CREATE_NEW_CONSOLE creationflags = subprocess.CREATE_NEW_CONSOLE
# Su Linux/macOS, non impostiamo nulla di speciale per la finestra # Su Linux/macOS, non impostiamo nulla di speciale per la finestra
# ---<<< FINE MODIFICA >>>---
# Timeout diagnostico (mantenuto) # Timeout diagnostico (mantenuto)
timeout_seconds = 60 # Aumentato leggermente per operazioni remote timeout_seconds = 60 # Aumentato leggermente per operazioni remote
@ -157,18 +154,13 @@ class GitCommands:
# --- Log Output di Successo (solo se catturato) --- # --- Log Output di Successo (solo se catturato) ---
if capture and (result.returncode == 0 or not check): if capture and (result.returncode == 0 or not check):
stdout_log_debug = ( stdout_repr = repr(result.stdout) if result.stdout else "<no stdout>"
result.stdout.strip() if result.stdout else "<no stdout>" stderr_repr = repr(result.stderr) if result.stderr else "<no stderr>"
)
stderr_log_debug = (
result.stderr.strip() if result.stderr else "<no stderr>"
)
# Logga sempre a DEBUG
log_handler.log_debug( log_handler.log_debug(
f"Command successful (RC={result.returncode}). Output:\n" f"Command successful (RC={result.returncode}). RAW Output:\n"
f"--- stdout ---\n{stdout_log_debug}\n" f"--- stdout (repr) ---\n{stdout_repr}\n"
f"--- stderr ---\n{stderr_log_debug}\n" f"--- stderr (repr) ---\n{stderr_repr}\n"
f"--- End Output ---", f"--- End RAW Output ---",
func_name=func_name, func_name=func_name,
) )
# Logga anche al livello richiesto se diverso da DEBUG # Logga anche al livello richiesto se diverso da DEBUG
@ -184,17 +176,13 @@ class GitCommands:
# --- Gestione Errori (modificata per CalledProcessError quando capture=False) --- # --- Gestione Errori (modificata per CalledProcessError quando capture=False) ---
except subprocess.TimeoutExpired as e: except subprocess.TimeoutExpired as e:
# (Gestione Timeout invariata) # (Gestione Timeout invariata)
log_handler.log_error( stderr_repr = repr(e.stderr) if capture and e.stderr else "<stderr not captured>"
f"Command timed out after {timeout_seconds}s: {command_str}", stdout_repr = repr(e.stdout) if capture and e.stdout else "<stdout not captured>"
func_name=func_name, err_msg = (
f"Command failed (RC {e.returncode}) in '{effective_cwd}'.\n"
f"CMD: {command_str}\nRAW_STDERR: {stderr_repr}\nRAW_STDOUT: {stdout_repr}"
) )
stderr_out = ( log_handler.log_error(err_msg, func_name=func_name)
e.stderr.strip() if capture and e.stderr else "<stderr not captured>"
)
stdout_out = (
e.stdout.strip() if capture and e.stdout else "<stdout not captured>"
)
log_handler.log_error(f"Timeout stderr: {stderr_out}", func_name=func_name)
raise GitCommandError( raise GitCommandError(
f"Timeout after {timeout_seconds}s.", f"Timeout after {timeout_seconds}s.",
command=safe_command_parts, command=safe_command_parts,

329
gui.py
View File

@ -499,6 +499,7 @@ class MainFrame(ttk.Frame):
self.delete_local_branch_callback = delete_local_branch_cb self.delete_local_branch_callback = delete_local_branch_cb
self.merge_local_branch_callback = merge_local_branch_cb self.merge_local_branch_callback = merge_local_branch_cb
self.compare_branch_with_current_callback = compare_branch_with_current_cb self.compare_branch_with_current_callback = compare_branch_with_current_cb
self.view_commit_details_callback = view_commit_details_cb
self.config_manager = config_manager_instance self.config_manager = config_manager_instance
self.initial_profile_sections = profile_sections_list self.initial_profile_sections = profile_sections_list
@ -539,7 +540,8 @@ class MainFrame(ttk.Frame):
self.tags_tab_frame = self._create_tags_tab() self.tags_tab_frame = self._create_tags_tab()
self.branch_tab_frame = self._create_branch_tab() # Crea la listbox/button originale self.branch_tab_frame = self._create_branch_tab() # Crea la listbox/button originale
self.remote_tab_frame = self._create_remote_tab() # Crea la nuova tab con le liste affiancate self.remote_tab_frame = self._create_remote_tab() # Crea la nuova tab con le liste affiancate
self.history_tab_frame = self._create_history_tab() #self.history_tab_frame = self._create_history_tab()
self.history_tab_frame = self._create_history_tab_treeview()
# Aggiunta delle tab al Notebook (ordine di visualizzazione) # Aggiunta delle tab al Notebook (ordine di visualizzazione)
self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ") self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ")
@ -752,6 +754,102 @@ class MainFrame(ttk.Frame):
return frame return frame
def _create_history_tab_treeview(self):
"""Creates the widgets for the History tab using ttk.Treeview."""
frame = ttk.Frame(self.notebook, padding=(10, 10))
# Configura righe/colonne per espansione
frame.rowconfigure(1, weight=1) # Riga per Treeview si espande
frame.columnconfigure(0, weight=1) # Colonna per Treeview si espande
# Frame per controlli (filtro, refresh)
controls_frame = ttk.Frame(frame)
controls_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
controls_frame.columnconfigure(1, weight=1) # Combobox si espande
ttk.Label(controls_frame, text="Filter History by Branch/Tag:").pack(side=tk.LEFT, padx=(0, 5))
self.history_branch_filter_var = tk.StringVar()
self.history_branch_filter_combo = ttk.Combobox(
controls_frame,
textvariable=self.history_branch_filter_var,
state="readonly", # Inizia readonly, diventa disabled se repo non pronto
width=40,
)
self.history_branch_filter_combo.pack(
side=tk.LEFT, expand=True, fill=tk.X, padx=5
)
# Assicurati che il callback esista nel controller
if hasattr(self, 'refresh_history_callback') and callable(self.refresh_history_callback):
self.history_branch_filter_combo.bind(
"<<ComboboxSelected>>", lambda e: self.refresh_history_callback()
)
self.create_tooltip(
self.history_branch_filter_combo, "Select a branch or tag to filter the commit history."
)
self.refresh_history_button = ttk.Button(
controls_frame,
text="Refresh History",
state=tk.DISABLED, # Inizia disabilitato
)
# Assicurati che il callback esista nel controller
if hasattr(self, 'refresh_history_callback') and callable(self.refresh_history_callback):
self.refresh_history_button.config(command=self.refresh_history_callback)
self.refresh_history_button.pack(side=tk.LEFT, padx=5)
self.create_tooltip(self.refresh_history_button, "Reload commit history based on the selected filter.")
# Frame per Treeview e Scrollbars
content_frame = ttk.Frame(frame)
content_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
content_frame.rowconfigure(0, weight=1) # Riga Treeview si espande
content_frame.columnconfigure(0, weight=1) # Colonna Treeview si espande
# Definisci le colonne per la Treeview
# ('hash', 'datetime', 'author', 'details') <-- 'details' conterrà soggetto e refs
columns = ('hash', 'datetime', 'author', 'details')
self.history_tree = ttk.Treeview(
content_frame,
columns=columns,
show='headings', # Mostra solo le intestazioni, non la colonna #0
selectmode='browse', # Seleziona solo una riga alla volta
height=15 # Altezza suggerita in righe
)
# Definisci Intestazioni (headings)
self.history_tree.heading('hash', text='Hash', anchor='w')
self.history_tree.heading('datetime', text='Date/Time', anchor='w')
self.history_tree.heading('author', text='Author', anchor='w')
self.history_tree.heading('details', text='Subject / Refs', anchor='w') # Titolo colonna aggiornato
# Definisci Colonne (larghezza e ancoraggio)
self.history_tree.column('hash', width=80, stretch=tk.NO, anchor='w')
self.history_tree.column('datetime', width=140, stretch=tk.NO, anchor='w')
self.history_tree.column('author', width=150, stretch=tk.NO, anchor='w')
self.history_tree.column('details', width=450, stretch=tk.YES, anchor='w') # Questa colonna si espande
# Scrollbars Verticale e Orizzontale
tree_scrollbar_y = ttk.Scrollbar(
content_frame, orient=tk.VERTICAL, command=self.history_tree.yview
)
tree_scrollbar_x = ttk.Scrollbar(
content_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview
)
self.history_tree.configure(
yscrollcommand=tree_scrollbar_y.set,
xscrollcommand=tree_scrollbar_x.set
)
# Layout Treeview e Scrollbars usando grid nel content_frame
self.history_tree.grid(row=0, column=0, sticky="nsew")
tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew") # Span su 2 colonne
# Binding Doppio Click (chiama nuovo handler _on_history_double_click_tree)
# Assicurati che il metodo _on_history_double_click_tree sia definito più avanti
self.history_tree.bind("<Double-Button-1>", self._on_history_double_click_tree)
self.create_tooltip(self.history_tree, "Double-click a commit line to view details.")
return frame
def _show_remote_branches_context_menu(self, event): def _show_remote_branches_context_menu(self, event):
"""Displays the context menu for the remote branches listbox.""" """Displays the context menu for the remote branches listbox."""
func_name = "_show_remote_branches_context_menu" func_name = "_show_remote_branches_context_menu"
@ -1487,7 +1585,7 @@ class MainFrame(ttk.Frame):
) )
self.history_text.grid(row=2, column=0, sticky="nsew", padx=5, pady=(0, 5)) self.history_text.grid(row=2, column=0, sticky="nsew", padx=5, pady=(0, 5))
self.history_text.bind("<Double-Button-1>", self._on_history_double_click) self.history_text.bind("<Button-1>", self._on_history_double_click)
self.create_tooltip(self.history_text, "Double-click a commit to view details.") self.create_tooltip(self.history_text, "Double-click a commit to view details.")
history_xscroll = ttk.Scrollbar( history_xscroll = ttk.Scrollbar(
@ -1734,12 +1832,19 @@ class MainFrame(ttk.Frame):
not callable(self.view_commit_details_callback): not callable(self.view_commit_details_callback):
return return
widget_state = self.history_text.cget("state")
try: try:
if widget_state == tk.DISABLED:
self.history_text.config(state=tk.NORMAL)
# Get the index of the clicked character # Get the index of the clicked character
index = self.history_text.index(f"@{event.x},{event.y}") index = self.history_text.index(f"@{event.x},{event.y}")
# Get the content of the line containing the index # Get the content of the line containing the index
line_content: str = self.history_text.get(f"{index} linestart", f"{index} lineend") line_content: str = self.history_text.get(f"{index} linestart", f"{index} lineend")
line_content = line_content.strip() # Remove leading/trailing whitespace line_content = line_content.strip() # Remove leading/trailing whitespace
log_handler.log_debug(f"History clicked. Line content: '{line_content}'", func_name="_on_history_double_click")
# Pass the extracted line content to the callback function # Pass the extracted line content to the callback function
if line_content and not line_content.startswith("("): # Avoid acting on "(Error..)" lines if line_content and not line_content.startswith("("): # Avoid acting on "(Error..)" lines
@ -1750,78 +1855,150 @@ class MainFrame(ttk.Frame):
except Exception as e: except Exception as e:
# Log unexpected errors (consider using log_handler if available) # Log unexpected errors (consider using log_handler if available)
print(f"Error handling history double-click: {e}", file=sys.stderr) print(f"Error handling history double-click: {e}", file=sys.stderr)
finally:
# ---<<< MODIFICA: Ripristina stato originale >>>---
# Always restore the original state if it was changed
if widget_state == tk.DISABLED and self.history_text.winfo_exists():
try:
self.history_text.config(state=tk.DISABLED)
except Exception: # Ignore errors restoring state
pass
def update_history_display(self, log_lines): def _on_history_double_click_tree(self, event: tk.Event) -> None:
"""Clears and populates the history ScrolledText widget.""" """
func_name = "update_history_display (GUI)" # Nome specifico per i log Handles the double-click event on the history Treeview.
# ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>--- Extracts the commit hash from the selected item and calls the view_commit_details callback.
log_handler.log_debug( """
f"Received log_lines type={type(log_lines)}, count={len(log_lines) if isinstance(log_lines, list) else 'N/A'}. " func_name = "_on_history_double_click_tree"
f"Sample: {repr(log_lines[:5]) if isinstance(log_lines, list) else repr(log_lines)}", tree = getattr(self, "history_tree", None)
func_name=func_name, view_cb = getattr(self, "view_commit_details_callback", None)
)
history_widget = getattr(self, "history_text", None) # ---<<< AGGIUNTA: Logging Dettagliato per Debug >>>---
if not history_widget or not history_widget.winfo_exists(): tree_exists = tree is not None and tree.winfo_exists()
log_handler.log_error( cb_callable = callable(view_cb)
"history_text widget not available for update.", func_name=func_name log_handler.log_debug(f"Tree exists: {tree_exists}. Callback callable: {cb_callable}.", func_name=func_name)
if view_cb and not cb_callable:
log_handler.log_warning(f"view_commit_details_callback is NOT callable. Type: {type(view_cb)}, Value: {repr(view_cb)}", func_name=func_name)
# ---<<< FINE AGGIUNTA >>>---
# Verifica che la treeview e il callback esistano E siano validi
if not tree_exists or not cb_callable: # Condizione aggiornata per usare le variabili debug
# Il messaggio di warning esistente va bene
log_handler.log_warning(
"History tree or view commit details callback not configured or available.",
func_name=func_name
) )
return # Esce dalla funzione
# Se siamo qui, sia tree che view_cb sono validi
try:
selected_iid = tree.focus()
if not selected_iid:
log_handler.log_debug("No item selected in history tree on double click.", func_name=func_name)
return
item_data = tree.item(selected_iid)
item_values = item_data.get("values")
if item_values and len(item_values) > 0:
commit_hash_short = str(item_values[0]).strip()
if commit_hash_short and not commit_hash_short.startswith("("):
log_handler.log_info(f"History tree double-clicked. Requesting details for hash: '{commit_hash_short}'", func_name=func_name)
view_cb(commit_hash_short) # Chiama il callback validato
else:
log_handler.log_debug(f"Ignoring double-click on placeholder/invalid history item: {item_values}", func_name=func_name)
else:
log_handler.log_warning(f"Could not get values for selected history item IID: {selected_iid}", func_name=func_name)
except Exception as e:
log_handler.log_exception(f"Error handling history tree double-click: {e}", func_name=func_name)
messagebox.showerror("Error", f"Could not process history selection:\n{e}", parent=self)
def update_history_display(self, log_lines: List[str]):
"""Clears and populates the history Treeview widget."""
func_name = "update_history_display_(Tree)" # Identificatore per i log
log_handler.log_debug(f"Updating history display (Treeview) with {len(log_lines) if isinstance(log_lines, list) else 'N/A'} lines.", func_name=func_name)
tree = getattr(self, "history_tree", None)
if not tree or not tree.winfo_exists():
log_handler.log_error("history_tree widget not available for update.", func_name=func_name)
return return
try: try:
history_widget.config(state=tk.NORMAL) # Pulisci Treeview precedente
history_widget.delete("1.0", tk.END) for item_id in tree.get_children():
tree.delete(item_id)
# Assicurati che log_lines sia una lista prima di fare join # Processa le righe di log ricevute
if isinstance(log_lines, list): if isinstance(log_lines, list):
if log_lines: if log_lines:
# Resetta colore (se era rosso o altro) # Processa ogni riga e inseriscila nella Treeview
try: for i, line in enumerate(log_lines):
# Potrebbe non esserci un colore foreground specifico per ScrolledText nello stile line_str = str(line).strip()
# Potremmo impostare a nero o lasciare il default del widget # Ignora placeholder/errori inseriti precedentemente
default_fg = "black" # Assumiamo nero come default sicuro if not line_str or line_str.startswith("("):
if history_widget.cget("fg") != default_fg: continue
history_widget.config(fg=default_fg)
except tk.TclError:
pass
# Unisci le linee (assicurati siano stringhe) # --- Parsing della riga di log ---
text_to_insert = "\n".join(map(str, log_lines)) # Formato atteso: "hash datetime | author | (refs) subject"
history_widget.insert(tk.END, text_to_insert) commit_hash, commit_datetime, commit_author, commit_details = "", "", "", line_str # Fallback
else: # Lista vuota valida try: # Aggiungi try/except per il parsing robusto
history_widget.insert(tk.END, "(No history found)") parts = line_str.split('|', 2) # Divide in max 3 parti
history_widget.config(fg="grey") # Colore grigio per indicare vuoto if len(parts) >= 3: # Formato completo atteso
else: # log_lines non è una lista (errore?) part1 = parts[0].strip(); part2 = parts[1].strip(); part3 = parts[2].strip()
log_handler.log_warning( first_space = part1.find(' ')
f"Invalid data received for history: {repr(log_lines)}", if first_space != -1:
func_name=func_name, commit_hash = part1[:first_space]
) # Prendi solo la data e ora (primi 16 caratteri)
history_widget.insert( commit_datetime = part1[first_space:].strip()[:16]
tk.END, f"(Invalid data received: {repr(log_lines)})" else: # Caso strano: solo hash?
) commit_hash = part1
history_widget.config(fg="orange") # Arancione per dato inatteso commit_datetime = "N/A" # O lascia vuoto
commit_author = part2
commit_details = part3 # Contiene soggetto e refs
elif len(parts) == 2: # Manca autore? (meno probabile con il formato usato)
part1 = parts[0].strip(); part3 = parts[1].strip()
first_space = part1.find(' ')
if first_space != -1:
commit_hash = part1[:first_space]; commit_datetime = part1[first_space:].strip()[:16]
else: commit_hash = part1; commit_datetime = "N/A"
commit_author = "N/A" # Autore mancante
commit_details = part3
elif len(parts) == 1: # Formato irriconoscibile, mostra come details
commit_details = line_str # Mostra l'intera riga come dettagli
commit_hash = "N/A"; commit_datetime = "N/A"; commit_author = "N/A"
except Exception as parse_err:
log_handler.log_warning(f"Could not parse history line '{line_str}': {parse_err}", func_name=func_name)
# Imposta valori di fallback per mostrare qualcosa
commit_hash = "Error"; commit_datetime = ""; commit_author=""
commit_details = line_str # Mostra riga originale
history_widget.config(state=tk.DISABLED) # Rendi read-only # Inserisci la riga parsata nella Treeview
history_widget.yview_moveto(0.0) tree.insert(
history_widget.xview_moveto(0.0) parent='', index=tk.END, iid=i, # Usa indice riga come IID
values=(commit_hash, commit_datetime, commit_author, commit_details)
)
else: # Lista vuota valida
# Inserisci un messaggio placeholder
tree.insert(parent='', index=tk.END, iid=0, values=("", "", "", "(No history found)"))
else: # Dati non validi (non è una lista)
log_handler.log_warning(f"Invalid data received for history (Treeview): {repr(log_lines)}", func_name=func_name)
tree.insert(parent='', index=tk.END, iid=0, values=("", "", "", "(Error: Invalid data received)"))
# Scroll all'inizio dopo aver popolato
tree.yview_moveto(0.0)
tree.xview_moveto(0.0)
except Exception as e: except Exception as e:
log_handler.log_exception( log_handler.log_exception(f"Error updating history Treeview GUI: {e}", func_name=func_name)
f"Error updating history GUI: {e}", func_name=func_name try: # Fallback di visualizzazione errore nella treeview
) if tree.winfo_exists():
# Fallback: Mostra errore nel widget di testo for item_id in tree.get_children(): tree.delete(item_id)
try: tree.insert(parent='', index=tk.END, iid=0, values=("", "", "", "(Error displaying history)"))
if history_widget.winfo_exists():
history_widget.config(state=tk.NORMAL)
history_widget.delete("1.0", tk.END)
history_widget.insert(tk.END, "(Error displaying history)")
history_widget.config(
state=tk.DISABLED, fg="red"
) # Rosso e disabilitato
except Exception as fallback_e: except Exception as fallback_e:
log_handler.log_error( log_handler.log_error(f"Error displaying fallback error in history Treeview: {fallback_e}", func_name=func_name)
f"Error displaying fallback error in history widget: {fallback_e}",
func_name=func_name,
)
# ---<<< FINE MODIFICA DEBUG & ERRORE >>>--- # ---<<< FINE MODIFICA DEBUG & ERRORE >>>---
def update_history_branch_filter(self, branches_tags, current_ref=None): def update_history_branch_filter(self, branches_tags, current_ref=None):
@ -2140,16 +2317,36 @@ class MainFrame(ttk.Frame):
except Exception as e: log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state") except Exception as e: log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state")
# Gestione stato liste (devono essere selezionabili se abilitate) # Gestione stato liste (devono essere selezionabili se abilitate)
# Le liste devono essere utilizzabili (NORMAL) quando le azioni sono abilitate,
# e visivamente indicate come disabilitate altrimenti (potremmo cambiare colore o stile?)
# Per ora, impostiamo lo stato NORMAL/DISABLED dove applicabile.
# Per Treeview, controlliamo i binding.
list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED
listboxes = [ interactive_lists = [ # Lista di widget lista interattivi
getattr(self, name, None) for name in getattr(self, name, None) for name in
["tag_listbox", "branch_listbox", "history_text", "changed_files_listbox", ["tag_listbox", "branch_listbox", "changed_files_listbox",
"remote_branches_listbox", "local_branches_listbox_remote_tab"] "remote_branches_listbox", "local_branches_listbox_remote_tab"]
] ]
for lb in listboxes: for lb in interactive_lists:
if lb and lb.winfo_exists(): if lb and lb.winfo_exists() and isinstance(lb, tk.Listbox): # Controlla tipo
try: lb.config(state=list_state) try: lb.config(state=list_state)
except Exception: pass # Ignora errori specifici widget (es. Text non ha 'readonly') except Exception: pass # Ignora errori
# Gestione binding per History Treeview
history_tree = getattr(self, "history_tree", None)
if history_tree and history_tree.winfo_exists():
try:
if state == tk.DISABLED:
# Unbind - Rimuovi il binding per doppio click
history_tree.unbind("<Double-Button-1>")
else:
# Rebind - Riaggiungi il binding se non già presente
# Controllare se il binding esiste già potrebbe essere complesso,
# riassociare è generalmente sicuro.
if hasattr(self, "_on_history_double_click_tree"):
history_tree.bind("<Double-Button-1>", self._on_history_double_click_tree)
except Exception as e:
log_handler.log_error(f"Error configuring history tree bindings: {e}", func_name="set_action_widgets_state")
def update_ahead_behind_status( def update_ahead_behind_status(
self, self,

View File

@ -1,284 +0,0 @@
# --- START OF FILE profile_handler.py ---
# profile_handler.py
# Rimosso import logging
# Importa il nuovo gestore della coda log
import log_handler
# Importa ConfigManager e costanti associate
from config_manager import ConfigManager, DEFAULT_PROFILE, DEFAULT_BACKUP_DIR
class ProfileHandler:
"""
Handles loading, saving, adding, removing profiles via ConfigManager.
Uses log_handler for logging.
(Nota: Alcune logiche qui potrebbero essere duplicate o gestite
direttamente in GitUtility.py. Questo modulo potrebbe necessitare
di revisione o rimozione se non più centrale.)
"""
# Rimosso logger da __init__
def __init__(self, config_manager: ConfigManager):
"""
Initializes the ProfileHandler.
Args:
config_manager (ConfigManager): Instance to interact with config file.
"""
# Validazione tipo input
if not isinstance(config_manager, ConfigManager):
# Log critico e solleva errore se config_manager non è valido
msg = "ProfileHandler requires a valid ConfigManager instance."
# Usa log_handler per errore critico (se già configurato altrove)
# o print/raise direttamente
print(f"CRITICAL ERROR: {msg}")
# log_handler.log_critical(msg, func_name="__init__") # Potrebbe non funzionare qui
raise TypeError(msg)
self.config_manager = config_manager
log_handler.log_debug("ProfileHandler initialized.", func_name="__init__")
def get_profile_list(self):
"""Returns the list of available profile names."""
# Delega direttamente a config_manager
return self.config_manager.get_profile_sections()
def load_profile_data(self, profile_name):
"""
Loads all relevant data for a given profile name.
Args:
profile_name (str): The name of the profile to load.
Returns:
dict or None: A dictionary containing all profile settings,
or None if the profile doesn't exist.
"""
func_name = "load_profile_data"
log_handler.log_info(
f"Loading data for profile: '{profile_name}'", func_name=func_name
)
# Verifica esistenza profilo tramite config_manager
if profile_name not in self.config_manager.get_profile_sections():
log_handler.log_error(
f"Cannot load: Profile '{profile_name}' not found.", func_name=func_name
)
return None
# Ottieni chiavi e default da config_manager (fonte unica di verità)
keys_with_defaults = self.config_manager._get_expected_keys_with_defaults()
profile_data = {}
# Carica ogni opzione usando ConfigManager
log_handler.log_debug(
f"Loading options for profile '{profile_name}'...", func_name=func_name
)
for key, default_value in keys_with_defaults.items():
# Passa il default corretto
profile_data[key] = self.config_manager.get_profile_option(
profile_name, key, fallback=default_value
)
# --- Conversione Tipi (Opzionale qui, forse meglio nel chiamante) ---
# Se si decide di fare conversioni qui, assicurarsi che i valori di default
# in _get_expected_keys_with_defaults siano stringhe come in .ini
try:
profile_data["autocommit"] = (
str(profile_data.get("autocommit", "False")).lower() == "true"
)
profile_data["autobackup"] = (
str(profile_data.get("autobackup", "False")).lower() == "true"
)
# Aggiungere altre conversioni se necessario (es. per interi o float)
except Exception as e_conv:
log_handler.log_warning(
f"Error converting loaded profile data types for '{profile_name}': {e_conv}",
func_name=func_name,
)
# Procedi con i dati stringa originali in caso di errore di conversione
log_handler.log_debug(
f"Loaded data for '{profile_name}': {profile_data}", func_name=func_name
)
return profile_data
def save_profile_data(self, profile_name, profile_data):
"""
Saves the provided data dictionary to the specified profile name.
Args:
profile_name (str): The name of the profile to save.
profile_data (dict): Dictionary containing the settings to save.
Returns:
bool: True on success, False on failure.
"""
func_name = "save_profile_data"
# Validazione input
if not profile_name:
log_handler.log_warning(
"Cannot save: No profile name provided.", func_name=func_name
)
return False
if not isinstance(profile_data, dict):
log_handler.log_error(
"Cannot save: Invalid profile data (not a dict).", func_name=func_name
)
return False
log_handler.log_info(
f"Saving data for profile: '{profile_name}'", func_name=func_name
)
try:
# Itera sui dati da salvare
for key, value in profile_data.items():
# Converte tipi Python in stringhe per configparser
if isinstance(value, bool):
value_to_save = str(value) # "True" o "False"
# Aggiungere altre conversioni se necessario (es. lista in stringa separata da virgola)
# elif isinstance(value, list):
# value_to_save = ",".join(map(str, value))
else:
# Converte in stringa, gestendo None
value_to_save = str(value) if value is not None else ""
# Imposta l'opzione tramite ConfigManager
self.config_manager.set_profile_option(profile_name, key, value_to_save)
# Salva le modifiche sul file .ini
self.config_manager.save_config()
log_handler.log_info(
f"Profile '{profile_name}' saved successfully.", func_name=func_name
)
return True
except Exception as e:
# Logga errore durante il salvataggio
log_handler.log_exception(
f"Error saving profile '{profile_name}': {e}", func_name=func_name
)
# L'errore verrà probabilmente mostrato dal chiamante (GitUtility)
return False
def add_new_profile(self, profile_name):
"""
Adds a new profile with default settings.
Args:
profile_name (str): The name for the new profile.
Returns:
bool: True if added successfully, False otherwise.
"""
func_name = "add_new_profile"
log_handler.log_info(
f"Attempting to add new profile: '{profile_name}'", func_name=func_name
)
# Validazione nome
if not profile_name or profile_name.isspace():
log_handler.log_warning(
"Add profile failed: Name cannot be empty.", func_name=func_name
)
return False
if profile_name in self.config_manager.get_profile_sections():
log_handler.log_warning(
f"Add profile failed: '{profile_name}' already exists.",
func_name=func_name,
)
return False
try:
# Ottieni i default da ConfigManager
defaults = self.config_manager._get_expected_keys_with_defaults()
# Personalizza alcuni default per il nuovo profilo
defaults["bundle_name"] = f"{profile_name}_repo.bundle"
defaults["bundle_name_updated"] = f"{profile_name}_update.bundle"
defaults["svn_working_copy_path"] = "" # Inizia vuoto
defaults["usb_drive_path"] = "" # Inizia vuoto
defaults["commit_message"] = (
f"Initial commit for profile {profile_name}" # Esempio messaggio
)
# Aggiungi la sezione (se non esiste già - per sicurezza)
self.config_manager.add_section(profile_name)
# Imposta tutte le opzioni per la nuova sezione
for key, value in defaults.items():
self.config_manager.set_profile_option(
profile_name, key, value
) # set_profile_option gestisce la conversione a stringa
# Salva il file di configurazione
self.config_manager.save_config()
log_handler.log_info(
f"Profile '{profile_name}' added successfully with defaults.",
func_name=func_name,
)
return True
except Exception as e:
# Logga errori imprevisti durante l'aggiunta
log_handler.log_exception(
f"Error adding profile '{profile_name}': {e}", func_name=func_name
)
return False
def remove_existing_profile(self, profile_name):
"""
Removes an existing profile (cannot remove the default profile).
Args:
profile_name (str): The name of the profile to remove.
Returns:
bool: True if removed successfully, False otherwise.
"""
func_name = "remove_existing_profile"
log_handler.log_info(
f"Attempting to remove profile: '{profile_name}'", func_name=func_name
)
# Validazioni
if not profile_name:
log_handler.log_warning(
"Remove profile failed: No name provided.", func_name=func_name
)
return False
if profile_name == DEFAULT_PROFILE:
log_handler.log_warning(
"Remove profile failed: Cannot remove default profile.",
func_name=func_name,
)
return False
if profile_name not in self.config_manager.get_profile_sections():
log_handler.log_warning(
f"Remove profile failed: '{profile_name}' not found.",
func_name=func_name,
)
return False
try:
# Delega la rimozione a ConfigManager
success = self.config_manager.remove_profile_section(profile_name)
if success:
# Salva la configurazione solo se la rimozione è andata a buon fine
self.config_manager.save_config()
log_handler.log_info(
f"Profile '{profile_name}' removed successfully.",
func_name=func_name,
)
return True
else:
# ConfigManager dovrebbe aver loggato il motivo del fallimento
log_handler.log_error(
f"ConfigManager reported failure removing '{profile_name}'.",
func_name=func_name,
)
return False
except Exception as e:
# Logga errori imprevisti durante la rimozione
log_handler.log_exception(
f"Error removing profile '{profile_name}': {e}", func_name=func_name
)
return False
# --- END OF FILE profile_handler.py ---