365 lines
17 KiB
Python
365 lines
17 KiB
Python
# --- FILE: commit_detail_window.py ---
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
from tkinter import scrolledtext # Per il corpo del messaggio
|
|
import os
|
|
from typing import Dict, Any, List, Tuple, Callable, Optional
|
|
|
|
# Import moduli locali (se necessari, come Tooltip o log_handler)
|
|
import log_handler
|
|
# Importa la classe Tooltip (assumendo sia definita in gui.py o altrove)
|
|
try:
|
|
# Prova a importare da gui.py se non è in un modulo separato
|
|
from gui import Tooltip
|
|
except ImportError:
|
|
# Fallback se Tooltip non è disponibile (potrebbe causare errori se usato)
|
|
log_handler.log_warning("Tooltip class not found, tooltips may be disabled.", func_name="import")
|
|
# Definisci una classe fittizia per evitare errori immediati
|
|
class Tooltip:
|
|
def __init__(self, widget, text): pass
|
|
|
|
|
|
class CommitDetailWindow(tk.Toplevel):
|
|
"""
|
|
Toplevel window to display detailed information about a specific Git commit,
|
|
including metadata and a list of changed files. Allows opening a diff viewer
|
|
for selected files.
|
|
"""
|
|
# Mappa per tradurre gli stati dei file (simile a DiffSummaryWindow)
|
|
STATUS_MAP: Dict[str, str] = {
|
|
'A': 'Added',
|
|
'M': 'Modified',
|
|
'D': 'Deleted',
|
|
'R': 'Renamed',
|
|
'C': 'Copied',
|
|
'T': 'Type Change',
|
|
'U': 'Unmerged',
|
|
'X': 'Unknown',
|
|
'B': 'Broken',
|
|
# Aggiungi altri se necessario
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
master: tk.Misc, # Parent window
|
|
commit_data: Dict[str, Any], # Dati ricevuti dal worker
|
|
open_diff_callback: Callable[[str, str, str, Optional[str]], None] # Callback per aprire il diff
|
|
):
|
|
"""
|
|
Initializes the Commit Detail window.
|
|
|
|
Args:
|
|
master: The parent widget.
|
|
commit_data (Dict[str, Any]): Dictionary containing commit details:
|
|
'hash_full', 'author_name', 'author_email', 'author_date',
|
|
'subject', 'body', 'files_changed' (List[Tuple[status, path1, path2]])
|
|
open_diff_callback (Callable): Function to call when a file is selected
|
|
for diffing. Expects args:
|
|
(commit_hash, file_status, file_path, old_file_path)
|
|
"""
|
|
super().__init__(master)
|
|
func_name: str = "__init__ (CommitDetail)" # Nome per logging
|
|
|
|
# --- Validazione Input ---
|
|
if not isinstance(commit_data, dict):
|
|
raise TypeError("commit_data must be a dictionary.")
|
|
if not callable(open_diff_callback):
|
|
raise TypeError("open_diff_callback must be callable.")
|
|
|
|
# Memorizza dati e callback
|
|
self.commit_data: Dict[str, Any] = commit_data
|
|
self.open_diff_callback: Callable[[str, str, str, Optional[str]], None] = open_diff_callback
|
|
# Estrai hash per usarlo nel titolo e nel callback diff
|
|
self.commit_hash: Optional[str] = commit_data.get('hash_full')
|
|
self.short_hash: str = self.commit_hash[:7] if self.commit_hash else "N/A"
|
|
|
|
log_handler.log_info(f"Opening Commit Detail window for {self.short_hash}", func_name=func_name)
|
|
|
|
# --- Configurazione Finestra ---
|
|
self.title(f"Commit Details - {self.short_hash}")
|
|
self.geometry("800x600") # Dimensioni suggerite
|
|
self.minsize(550, 400)
|
|
# Rendi modale rispetto al parent
|
|
self.grab_set()
|
|
self.transient(master)
|
|
|
|
# --- Creazione Widget ---
|
|
self._create_widgets()
|
|
# Popola i widget con i dati del commit
|
|
self._populate_details()
|
|
|
|
# Centra la finestra
|
|
self._center_window(master)
|
|
|
|
# Imposta focus iniziale sulla lista dei file
|
|
if hasattr(self, "files_tree"):
|
|
self.files_tree.focus_set()
|
|
|
|
|
|
def _create_widgets(self):
|
|
""" Creates the main widgets for the commit detail view. """
|
|
# Frame principale con padding
|
|
main_frame = ttk.Frame(self, padding="10")
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
# Configura espansione righe/colonne
|
|
main_frame.columnconfigure(0, weight=1)
|
|
main_frame.rowconfigure(1, weight=0) # Riga metadati non si espande molto
|
|
main_frame.rowconfigure(3, weight=1) # Riga corpo messaggio si espande
|
|
main_frame.rowconfigure(5, weight=2) # Riga lista file si espande di più
|
|
|
|
# --- Frame Metadati ---
|
|
meta_frame = ttk.LabelFrame(main_frame, text="Commit Info", padding=(10, 5))
|
|
meta_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5))
|
|
meta_frame.columnconfigure(1, weight=1) # Colonna valori si espande
|
|
|
|
row_idx: int = 0
|
|
# Hash Completo
|
|
ttk.Label(meta_frame, text="Commit Hash:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2)
|
|
self.hash_label = ttk.Label(meta_frame, text="", anchor="w", font=("Consolas", 9))
|
|
self.hash_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2)
|
|
Tooltip(self.hash_label, "Full commit SHA-1 hash.")
|
|
row_idx += 1
|
|
# Autore
|
|
ttk.Label(meta_frame, text="Author:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2)
|
|
self.author_label = ttk.Label(meta_frame, text="", anchor="w")
|
|
self.author_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2)
|
|
Tooltip(self.author_label, "Commit author name and email.")
|
|
row_idx += 1
|
|
# Data
|
|
ttk.Label(meta_frame, text="Date:").grid(row=row_idx, column=0, sticky="nw", padx=5, pady=2)
|
|
self.date_label = ttk.Label(meta_frame, text="", anchor="w")
|
|
self.date_label.grid(row=row_idx, column=1, sticky="ew", padx=5, pady=2)
|
|
Tooltip(self.date_label, "Author date.")
|
|
row_idx += 1
|
|
|
|
# --- Subject (Titolo Commit) ---
|
|
ttk.Label(main_frame, text="Subject:").grid(row=1, column=0, sticky="w", padx=5, pady=(5, 0))
|
|
self.subject_label = ttk.Label(
|
|
main_frame, text="", anchor="w", font=("Segoe UI", 10, "bold"), relief=tk.GROOVE, padding=5
|
|
)
|
|
self.subject_label.grid(row=2, column=0, sticky="ew", padx=5, pady=(0, 5))
|
|
Tooltip(self.subject_label, "Commit subject line.")
|
|
|
|
# --- Corpo Messaggio Commit ---
|
|
ttk.Label(main_frame, text="Message Body:").grid(row=3, column=0, sticky="w", padx=5, pady=(0, 0))
|
|
self.body_text = scrolledtext.ScrolledText(
|
|
master=main_frame,
|
|
height=6, # Altezza iniziale
|
|
wrap=tk.WORD,
|
|
font=("Segoe UI", 9),
|
|
state=tk.DISABLED, # Read-only
|
|
padx=5,
|
|
pady=5,
|
|
borderwidth=1,
|
|
relief=tk.SUNKEN,
|
|
)
|
|
self.body_text.grid(row=4, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
|
Tooltip(self.body_text, "Full commit message body.")
|
|
|
|
# --- Lista File Modificati ---
|
|
files_frame = ttk.LabelFrame(main_frame, text="Changed Files", padding=(10, 5))
|
|
files_frame.grid(row=5, column=0, sticky="nsew", pady=(5, 0))
|
|
files_frame.rowconfigure(0, weight=1)
|
|
files_frame.columnconfigure(0, weight=1)
|
|
|
|
# Treeview per i file (migliore per colonne)
|
|
columns: Tuple[str, str] = ('status', 'file')
|
|
self.files_tree = ttk.Treeview(
|
|
master=files_frame,
|
|
columns=columns,
|
|
show='headings', # Mostra solo headings, non colonna #0
|
|
selectmode='browse', # Seleziona solo una riga
|
|
height=7 # Altezza iniziale
|
|
)
|
|
# Definisci headings
|
|
self.files_tree.heading('status', text='Status', anchor='w')
|
|
self.files_tree.heading('file', text='File Path', anchor='w')
|
|
# Definisci colonne
|
|
self.files_tree.column('status', width=80, stretch=tk.NO, anchor='w')
|
|
self.files_tree.column('file', width=600, stretch=tk.YES, anchor='w')
|
|
|
|
# Scrollbar verticale
|
|
tree_scrollbar = ttk.Scrollbar(
|
|
master=files_frame,
|
|
orient=tk.VERTICAL,
|
|
command=self.files_tree.yview
|
|
)
|
|
self.files_tree.configure(yscrollcommand=tree_scrollbar.set)
|
|
|
|
# Layout Treeview e Scrollbar
|
|
self.files_tree.grid(row=0, column=0, sticky="nsew")
|
|
tree_scrollbar.grid(row=0, column=1, sticky="ns")
|
|
|
|
# Binding per doppio click sulla lista file -> apre diff
|
|
self.files_tree.bind("<Double-Button-1>", self._on_file_double_click)
|
|
Tooltip(self.files_tree, "Double-click a file to view changes in this commit.")
|
|
|
|
|
|
def _populate_details(self):
|
|
""" Populates the widgets with data from self.commit_data. """
|
|
func_name: str = "_populate_details"
|
|
# --- Popola Metadati ---
|
|
if hasattr(self, "hash_label"):
|
|
self.hash_label.config(text=self.commit_data.get('hash_full', 'N/A'))
|
|
if hasattr(self, "author_label"):
|
|
name: str = self.commit_data.get('author_name', 'N/A')
|
|
email: str = self.commit_data.get('author_email', '')
|
|
author_text: str = f"{name} <{email}>" if email else name
|
|
self.author_label.config(text=author_text)
|
|
if hasattr(self, "date_label"):
|
|
self.date_label.config(text=self.commit_data.get('author_date', 'N/A'))
|
|
if hasattr(self, "subject_label"):
|
|
self.subject_label.config(text=self.commit_data.get('subject', '(No Subject)'))
|
|
|
|
# --- Popola Corpo Messaggio ---
|
|
if hasattr(self, "body_text"):
|
|
body_content: str = self.commit_data.get('body', '(No Message Body)')
|
|
try:
|
|
self.body_text.config(state=tk.NORMAL) # Abilita per modifica
|
|
self.body_text.delete("1.0", tk.END) # Pulisci
|
|
self.body_text.insert(tk.END, body_content) # Inserisci testo
|
|
self.body_text.config(state=tk.DISABLED) # Disabilita di nuovo
|
|
except Exception as e:
|
|
log_handler.log_error(f"Error populating commit body text: {e}", func_name=func_name)
|
|
# Mostra errore nel widget stesso
|
|
try:
|
|
self.body_text.config(state=tk.NORMAL)
|
|
self.body_text.delete("1.0", tk.END)
|
|
self.body_text.insert(tk.END, f"(Error displaying body: {e})")
|
|
self.body_text.config(state=tk.DISABLED)
|
|
except Exception: pass # Ignora errori nel fallback
|
|
|
|
# --- Popola Lista File Modificati ---
|
|
if hasattr(self, "files_tree"):
|
|
# Pulisci treeview precedente
|
|
for item in self.files_tree.get_children():
|
|
self.files_tree.delete(item)
|
|
|
|
# Ottieni lista file dal dizionario dati
|
|
files_changed: List[Tuple[str, str, Optional[str]]] = self.commit_data.get('files_changed', [])
|
|
|
|
if files_changed:
|
|
# Inserisci ogni file nella treeview
|
|
for i, (status_char, path1, path2) in enumerate(files_changed):
|
|
display_status: str = self.STATUS_MAP.get(status_char, status_char) # Traduci stato
|
|
display_path: str = ""
|
|
# Gestisci visualizzazione per rinominati/copiati
|
|
if status_char in ('R', 'C') and path2:
|
|
display_path = f"{path1} -> {path2}"
|
|
else:
|
|
display_path = path1 # Usa path1 per A, M, D, T
|
|
|
|
# Memorizza dati completi per il callback nel tag dell'item
|
|
# (Alternativa: usare un dizionario separato mappato per iid)
|
|
item_data_tag = f"file_{i}"
|
|
self.files_tree.insert(
|
|
parent='',
|
|
index=tk.END,
|
|
iid=i, # Usa indice come ID interno
|
|
values=(display_status, display_path),
|
|
tags=(item_data_tag,) # Associa un tag univoco
|
|
)
|
|
# Salva i dati necessari per il diff nel tag config (un po' hacky ma funziona)
|
|
self.files_tree.tag_configure(
|
|
item_data_tag,
|
|
foreground='black' # Dummy config, usiamo solo per storage
|
|
)
|
|
# Allega i dati veri al tag
|
|
self.files_tree.tag_bind(
|
|
item_data_tag, "<Set>",
|
|
lambda e, sc=status_char, p1=path1, p2=path2:
|
|
setattr(e.widget, f"_data_{e.widget.focus()}", (sc, p1, p2))
|
|
)
|
|
|
|
|
|
else:
|
|
# Mostra messaggio se non ci sono file cambiati (raro per un commit!)
|
|
self.files_tree.insert(parent='', index=tk.END, values=("", "(No files changed in commit data)"))
|
|
|
|
|
|
def _on_file_double_click(self, event: tk.Event):
|
|
""" Handles double-click on the files Treeview. """
|
|
func_name: str = "_on_file_double_click"
|
|
if not hasattr(self, "files_tree"): return
|
|
|
|
# Ottieni l'ID dell'item selezionato
|
|
selected_iid = self.files_tree.focus() # focus() restituisce l'iid dell'elemento con focus
|
|
if not selected_iid: return # Nessun item selezionato
|
|
|
|
try:
|
|
# Recupera i dati associati all'item (dalla tupla originale)
|
|
item_index = int(selected_iid)
|
|
files_list: list = self.commit_data.get('files_changed', [])
|
|
|
|
if 0 <= item_index < len(files_list):
|
|
status_char, path1, path2 = files_list[item_index]
|
|
|
|
# Determina quale path passare e se c'era un path vecchio
|
|
file_path_to_diff: str = path2 if status_char in ('R', 'C') and path2 else path1
|
|
old_file_path_for_diff: Optional[str] = path1 if status_char in ('R', 'C') else None
|
|
|
|
# ---<<< MODIFICA: Non passare None come hash commit >>>---
|
|
commit_hash_full: Optional[str] = self.commit_data.get('hash_full')
|
|
if commit_hash_full:
|
|
log_handler.log_debug(
|
|
f"Calling open_diff_callback for file: {file_path_to_diff}, commit: {commit_hash_full[:7]}",
|
|
func_name=func_name
|
|
)
|
|
# Chiama il callback passato da GitUtility
|
|
self.open_diff_callback(
|
|
commit_hash_full, # Hash completo del commit
|
|
status_char, # Stato del file (A, M, D...)
|
|
file_path_to_diff, # Path del file nel commit
|
|
old_file_path_for_diff # Path vecchio (solo per R/C)
|
|
)
|
|
else:
|
|
log_handler.log_error("Cannot open diff: Commit hash is missing.", func_name=func_name)
|
|
messagebox.showerror("Error", "Internal error: Commit hash not available.", parent=self)
|
|
# ---<<< FINE MODIFICA >>>---
|
|
|
|
else:
|
|
log_handler.log_error(
|
|
f"Selected Treeview item IID '{selected_iid}' out of range for data.",
|
|
func_name=func_name
|
|
)
|
|
messagebox.showerror("Error", "Internal error: Selected item data not found.", parent=self)
|
|
|
|
except ValueError:
|
|
log_handler.log_error(f"Invalid Treeview item IID: '{selected_iid}'", func_name=func_name)
|
|
messagebox.showerror("Error", "Internal error: Invalid item selected.", parent=self)
|
|
except Exception as e:
|
|
log_handler.log_exception(f"Error handling file double-click: {e}", func_name=func_name)
|
|
messagebox.showerror("Error", f"Could not process file selection:\n{e}", parent=self)
|
|
|
|
|
|
def _center_window(self, parent: tk.Misc):
|
|
""" Centers the Toplevel window relative to its parent. """
|
|
func_name: str = "_center_window (CommitDetail)"
|
|
try:
|
|
self.update_idletasks() # Ensure window dimensions are calculated
|
|
# Get parent geometry
|
|
parent_x: int = parent.winfo_rootx()
|
|
parent_y: int = parent.winfo_rooty()
|
|
parent_w: int = parent.winfo_width()
|
|
parent_h: int = parent.winfo_height()
|
|
# Get self geometry
|
|
win_w: int = self.winfo_width()
|
|
win_h: int = self.winfo_height()
|
|
# Calculate position
|
|
pos_x: int = parent_x + (parent_w // 2) - (win_w // 2)
|
|
pos_y: int = parent_y + (parent_h // 2) - (win_h // 2)
|
|
# Keep window on screen
|
|
screen_w: int = self.winfo_screenwidth()
|
|
screen_h: int = self.winfo_screenheight()
|
|
pos_x = max(0, min(pos_x, screen_w - win_w))
|
|
pos_y = max(0, min(pos_y, screen_h - win_h))
|
|
# Set geometry
|
|
self.geometry(f"+{pos_x}+{pos_y}")
|
|
except Exception as e:
|
|
# Log error if centering fails
|
|
log_handler.log_error(
|
|
f"Could not center CommitDetailWindow: {e}", func_name=func_name
|
|
)
|
|
|
|
# --- FINE FILE commit_detail_window.py --- |