fix history click details
This commit is contained in:
parent
ce36adddcc
commit
2da126cf2a
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
327
gui.py
@ -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,
|
||||
|
||||
@ -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