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
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(
settings.get("remote_name", DEFAULT_REMOTE_NAME)
)
@ -599,10 +600,26 @@ class GitSvnSyncApp:
self.refresh_branch_list() # Refreshes local branches
self.refresh_commit_history()
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
log_handler.log_debug("Remote URL found, initiating connection check.", func_name=func_name)
self.check_connection_auth() # Check auth/conn status
self.refresh_remote_status() # Check ahead/behind 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:
# If not ready, clear dynamic GUI lists
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.
Extracts the commit hash and starts an async worker to fetch details.
Callback triggered by clicking a line in the history view (now Treeview).
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"
commit_hash_short = history_line_or_hash.strip()
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
)
@ -2907,30 +2926,12 @@ class GitSvnSyncApp:
)
return
# --- Estrarre l'Hash del Commit dalla Riga di Log ---
# Assumiamo che il formato (%h) sia all'inizio della riga, seguito da spazio
commit_hash_short: Optional[str] = None
try:
parts: List[str] = history_line.split(maxsplit=1) # Divide solo al primo spazio
if parts and len(parts[0]) > 0: # Assumendo che l'hash sia la prima parte
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}"
)
# --- Validazione Hash (Opzionale ma consigliata) ---
if not commit_hash_short or not re.match(r"^[0-9a-fA-F]{7,}$", commit_hash_short):
log_handler.log_error(f"Invalid commit hash received: '{commit_hash_short}'", func_name=func_name)
self.main_frame.show_error("Input Error", f"Invalid commit hash format:\n{commit_hash_short}")
return
# --- Fine Validazione ---
# --- Start Async Worker to Get Commit Details ---
log_handler.log_info(
@ -2940,7 +2941,7 @@ class GitSvnSyncApp:
args: tuple = (self.git_commands, svn_path, commit_hash_short)
# Start the 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,
context_dict={
"context": "get_commit_details",

View File

@ -4,6 +4,7 @@ import os
import queue
import logging # Usato solo per i livelli, non per loggare direttamente
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
import log_handler
@ -1823,7 +1824,7 @@ def run_get_commit_details_async(
if show_result.returncode == 0 and show_result.stdout:
# --- Parsing dell'Output ---
# 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]
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]]] = []
i: int = 0
while i < len(file_entries):
# Ogni voce file dovrebbe avere status e path
# Rinominati/Copiati (R/C) hanno status, old_path, new_path
# Ottieni lo stato (primo elemento della sequenza per un file)
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()
if status_char.startswith(('R', 'C')): # Renamed/Copied
if i + 2 < len(file_entries):
# Rimuovi score (es. R100 -> R)
status_code = status_char[0]
# Necessita di status, old_path, new_path (3 elementi)
if i + 2 < len(file_entries): # <-- CONTROLLO INDICE
status_code = status_char[0] # Prendi solo R o C
old_path = file_entries[i+1]
new_path = file_entries[i+2]
# Verifica che i path non siano vuoti (ulteriore sicurezza)
if old_path and new_path:
parsed_files.append((status_code, old_path, new_path))
i += 3 # Avanza di 3 elementi
else:
log_handler.log_warning(f"[Worker] Incomplete R/C entry: {file_entries[i:]}", func_name=func_name)
break # Esce dal loop se i dati sono incompleti
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:
# Dati incompleti per R/C
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
if i + 1 < len(file_entries):
# Necessita di status, file_path (2 elementi)
if i + 1 < len(file_entries): # <-- CONTROLLO INDICE
file_path = file_entries[i+1]
parsed_files.append((status_char[0], file_path, None)) # None per new_path
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: {file_entries[i:]}", func_name=func_name)
break
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:
# Ignora elementi vuoti (potrebbero esserci NUL consecutivi?)
i += 1
# Dati incompleti per A/M/D/T
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:
# Questo caso non dovrebbe verificarsi con strip+split corretti, ma per sicurezza
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
log_handler.log_debug(
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
# Importa anche i livelli di logging se usati nei messaggi fallback
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) ---
# (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._scrolling_active: bool = False # Flag per scroll sincronizzato
self._configure_timer_id = None # ID per debounce resize minimap
self.loading_label: Optional[ttk.Label] = None
# --- Costruzione Interfaccia Grafica ---
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)
load_ok = False
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
if load_ok:
# Se il caricamento (anche parziale, es. file non trovato) è ok, calcola diff
@ -209,8 +214,11 @@ class DiffViewerWindow(tk.Toplevel):
# Disegna comunque minimappa vuota
self.minimap_canvas.after(50, self._draw_minimap)
self._hide_loading_message()
except Exception as load_err:
# 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)
messagebox.showerror("Fatal Error", f"Failed to display diff:\n{load_err}", parent=self)
# Chiudi la finestra se l'inizializzazione fallisce gravemente
@ -276,6 +284,17 @@ class DiffViewerWindow(tk.Toplevel):
# Ridisegna su 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)
self.text_pane1.tag_config("removed", background="#FFE0E0") # Righe solo a sinistra
self.text_pane1.tag_config("empty", background="#F5F5F5", foreground="#A0A0A0") # Filler
@ -560,6 +579,27 @@ class DiffViewerWindow(tk.Toplevel):
self._update_minimap_viewport()
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 ---
def _setup_scrolling(self):

View File

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

327
gui.py
View File

@ -499,6 +499,7 @@ class MainFrame(ttk.Frame):
self.delete_local_branch_callback = delete_local_branch_cb
self.merge_local_branch_callback = merge_local_branch_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.initial_profile_sections = profile_sections_list
@ -539,7 +540,8 @@ class MainFrame(ttk.Frame):
self.tags_tab_frame = self._create_tags_tab()
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.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)
self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ")
@ -752,6 +754,102 @@ class MainFrame(ttk.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):
"""Displays the context menu for the remote branches listbox."""
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.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.")
history_xscroll = ttk.Scrollbar(
@ -1734,12 +1832,19 @@ class MainFrame(ttk.Frame):
not callable(self.view_commit_details_callback):
return
widget_state = self.history_text.cget("state")
try:
if widget_state == tk.DISABLED:
self.history_text.config(state=tk.NORMAL)
# Get the index of the clicked character
index = self.history_text.index(f"@{event.x},{event.y}")
# Get the content of the line containing the index
line_content: str = self.history_text.get(f"{index} linestart", f"{index} lineend")
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
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:
# Log unexpected errors (consider using log_handler if available)
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):
"""Clears and populates the history ScrolledText widget."""
func_name = "update_history_display (GUI)" # Nome specifico per i log
# ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>---
log_handler.log_debug(
f"Received log_lines type={type(log_lines)}, count={len(log_lines) if isinstance(log_lines, list) else 'N/A'}. "
f"Sample: {repr(log_lines[:5]) if isinstance(log_lines, list) else repr(log_lines)}",
func_name=func_name,
)
history_widget = getattr(self, "history_text", None)
if not history_widget or not history_widget.winfo_exists():
log_handler.log_error(
"history_text widget not available for update.", func_name=func_name
def _on_history_double_click_tree(self, event: tk.Event) -> None:
"""
Handles the double-click event on the history Treeview.
Extracts the commit hash from the selected item and calls the view_commit_details callback.
"""
func_name = "_on_history_double_click_tree"
tree = getattr(self, "history_tree", None)
view_cb = getattr(self, "view_commit_details_callback", None)
# ---<<< AGGIUNTA: Logging Dettagliato per Debug >>>---
tree_exists = tree is not None and tree.winfo_exists()
cb_callable = callable(view_cb)
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
try:
history_widget.config(state=tk.NORMAL)
history_widget.delete("1.0", tk.END)
# Pulisci Treeview precedente
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 log_lines:
# Resetta colore (se era rosso o altro)
try:
# Potrebbe non esserci un colore foreground specifico per ScrolledText nello stile
# Potremmo impostare a nero o lasciare il default del widget
default_fg = "black" # Assumiamo nero come default sicuro
if history_widget.cget("fg") != default_fg:
history_widget.config(fg=default_fg)
except tk.TclError:
pass
# Processa ogni riga e inseriscila nella Treeview
for i, line in enumerate(log_lines):
line_str = str(line).strip()
# Ignora placeholder/errori inseriti precedentemente
if not line_str or line_str.startswith("("):
continue
# --- Parsing della riga di log ---
# Formato atteso: "hash datetime | author | (refs) subject"
commit_hash, commit_datetime, commit_author, commit_details = "", "", "", line_str # Fallback
try: # Aggiungi try/except per il parsing robusto
parts = line_str.split('|', 2) # Divide in max 3 parti
if len(parts) >= 3: # Formato completo atteso
part1 = parts[0].strip(); part2 = parts[1].strip(); part3 = parts[2].strip()
first_space = part1.find(' ')
if first_space != -1:
commit_hash = part1[:first_space]
# Prendi solo la data e ora (primi 16 caratteri)
commit_datetime = part1[first_space:].strip()[:16]
else: # Caso strano: solo hash?
commit_hash = part1
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
# Inserisci la riga parsata nella Treeview
tree.insert(
parent='', index=tk.END, iid=i, # Usa indice riga come IID
values=(commit_hash, commit_datetime, commit_author, commit_details)
)
# Unisci le linee (assicurati siano stringhe)
text_to_insert = "\n".join(map(str, log_lines))
history_widget.insert(tk.END, text_to_insert)
else: # Lista vuota valida
history_widget.insert(tk.END, "(No history found)")
history_widget.config(fg="grey") # Colore grigio per indicare vuoto
else: # log_lines non è una lista (errore?)
log_handler.log_warning(
f"Invalid data received for history: {repr(log_lines)}",
func_name=func_name,
)
history_widget.insert(
tk.END, f"(Invalid data received: {repr(log_lines)})"
)
history_widget.config(fg="orange") # Arancione per dato inatteso
# Inserisci un messaggio placeholder
tree.insert(parent='', index=tk.END, iid=0, values=("", "", "", "(No history found)"))
history_widget.config(state=tk.DISABLED) # Rendi read-only
history_widget.yview_moveto(0.0)
history_widget.xview_moveto(0.0)
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:
log_handler.log_exception(
f"Error updating history GUI: {e}", func_name=func_name
)
# Fallback: Mostra errore nel widget di testo
try:
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
log_handler.log_exception(f"Error updating history Treeview GUI: {e}", func_name=func_name)
try: # Fallback di visualizzazione errore nella treeview
if tree.winfo_exists():
for item_id in tree.get_children(): tree.delete(item_id)
tree.insert(parent='', index=tk.END, iid=0, values=("", "", "", "(Error displaying history)"))
except Exception as fallback_e:
log_handler.log_error(
f"Error displaying fallback error in history widget: {fallback_e}",
func_name=func_name,
)
log_handler.log_error(f"Error displaying fallback error in history Treeview: {fallback_e}", func_name=func_name)
# ---<<< FINE MODIFICA DEBUG & ERRORE >>>---
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")
# 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
listboxes = [
interactive_lists = [ # Lista di widget lista interattivi
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"]
]
for lb in listboxes:
if lb and lb.winfo_exists():
for lb in interactive_lists:
if lb and lb.winfo_exists() and isinstance(lb, tk.Listbox): # Controlla tipo
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(
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 ---