fix history click details
This commit is contained in:
parent
ce36adddcc
commit
2da126cf2a
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -208,9 +213,12 @@ class DiffViewerWindow(tk.Toplevel):
|
|||||||
self._populate_text(self.text_pane2, [("error", f"<Error loading content for '{self.ref2_display_name}'>")])
|
self._populate_text(self.text_pane2, [("error", f"<Error loading content for '{self.ref2_display_name}'>")])
|
||||||
# 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
|
||||||
@ -275,6 +283,17 @@ class DiffViewerWindow(tk.Toplevel):
|
|||||||
self.minimap_canvas.grid(row=1, column=2, sticky="ns", padx=(5, 0))
|
self.minimap_canvas.grid(row=1, column=2, sticky="ns", padx=(5, 0))
|
||||||
# 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
|
||||||
@ -559,6 +578,27 @@ class DiffViewerWindow(tk.Toplevel):
|
|||||||
# Disegna indicatore viewport
|
# Disegna indicatore viewport
|
||||||
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 ---
|
||||||
|
|||||||
@ -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
329
gui.py
@ -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 ")
|
||||||
@ -751,6 +753,102 @@ class MainFrame(ttk.Frame):
|
|||||||
self.create_tooltip(self.refresh_local_branches_button_remote_tab, "Update the list of local branches.")
|
self.create_tooltip(self.refresh_local_branches_button_remote_tab, "Update the list of local branches.")
|
||||||
|
|
||||||
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."""
|
||||||
@ -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(
|
||||||
@ -1733,13 +1831,20 @@ class MainFrame(ttk.Frame):
|
|||||||
if not hasattr(self, "view_commit_details_callback") or \
|
if not hasattr(self, "view_commit_details_callback") or \
|
||||||
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 _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)
|
||||||
|
|
||||||
def update_history_display(self, log_lines):
|
# ---<<< AGGIUNTA: Logging Dettagliato per Debug >>>---
|
||||||
"""Clears and populates the history ScrolledText widget."""
|
tree_exists = tree is not None and tree.winfo_exists()
|
||||||
func_name = "update_history_display (GUI)" # Nome specifico per i log
|
cb_callable = callable(view_cb)
|
||||||
# ---<<< INIZIO MODIFICA DEBUG & ERRORE >>>---
|
log_handler.log_debug(f"Tree exists: {tree_exists}. Callback callable: {cb_callable}.", func_name=func_name)
|
||||||
log_handler.log_debug(
|
if view_cb and not cb_callable:
|
||||||
f"Received log_lines type={type(log_lines)}, count={len(log_lines) if isinstance(log_lines, list) else 'N/A'}. "
|
log_handler.log_warning(f"view_commit_details_callback is NOT callable. Type: {type(view_cb)}, Value: {repr(view_cb)}", func_name=func_name)
|
||||||
f"Sample: {repr(log_lines[:5]) if isinstance(log_lines, list) else repr(log_lines)}",
|
# ---<<< FINE AGGIUNTA >>>---
|
||||||
func_name=func_name,
|
|
||||||
)
|
# Verifica che la treeview e il callback esistano E siano validi
|
||||||
history_widget = getattr(self, "history_text", None)
|
if not tree_exists or not cb_callable: # Condizione aggiornata per usare le variabili debug
|
||||||
if not history_widget or not history_widget.winfo_exists():
|
# Il messaggio di warning esistente va bene
|
||||||
log_handler.log_error(
|
log_handler.log_warning(
|
||||||
"history_text widget not available for update.", func_name=func_name
|
"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,
|
||||||
|
|||||||
@ -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 ---
|
|
||||||
Loading…
Reference in New Issue
Block a user