fix problem with compare remote/locale branch
This commit is contained in:
parent
3193bcd7eb
commit
ce36adddcc
228
GitUtility.py
228
GitUtility.py
@ -47,6 +47,7 @@ try:
|
||||
from gui import CloneFromRemoteDialog
|
||||
from diff_viewer import DiffViewerWindow
|
||||
from diff_summary_viewer import DiffSummaryWindow
|
||||
from commit_detail_window import CommitDetailWindow
|
||||
|
||||
# Asynchronous operations
|
||||
import async_workers
|
||||
@ -171,9 +172,9 @@ class GitSvnSyncApp:
|
||||
delete_local_branch_cb=self.delete_local_branch,
|
||||
merge_local_branch_cb=self.merge_local_branch,
|
||||
compare_branch_with_current_cb=self.compare_branch_with_current,
|
||||
# Instance and initial data for the GUI
|
||||
config_manager_instance=self.config_manager,
|
||||
profile_sections_list=self.config_manager.get_profile_sections(),
|
||||
view_commit_details_cb=self.view_commit_details,
|
||||
)
|
||||
print("MainFrame GUI created.")
|
||||
log_handler.log_debug("MainFrame GUI created successfully.", func_name="__init__")
|
||||
@ -1941,6 +1942,165 @@ class GitSvnSyncApp:
|
||||
}
|
||||
)
|
||||
|
||||
def show_commit_details(self, commit_details: Dict[str, Any]):
|
||||
"""
|
||||
Opens the CommitDetailWindow to display details of a specific commit.
|
||||
Called by the AsyncResultHandler after fetching commit data.
|
||||
|
||||
Args:
|
||||
commit_details (Dict[str, Any]): A dictionary containing commit metadata
|
||||
(hash, author, date, subject, body)
|
||||
and a list of changed files.
|
||||
"""
|
||||
func_name: str = "show_commit_details"
|
||||
log_handler.log_debug(
|
||||
f"Attempting to show commit details for hash: {commit_details.get('hash_full', 'N/A')}",
|
||||
func_name=func_name
|
||||
)
|
||||
|
||||
# Validazione base dei dati ricevuti
|
||||
if not isinstance(commit_details, dict) or not commit_details.get('hash_full'):
|
||||
log_handler.log_error(
|
||||
"Invalid or incomplete commit details received.", func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error(
|
||||
"Display Error", "Internal error: Received invalid commit data."
|
||||
)
|
||||
# Riabilita widget se i dati non sono validi
|
||||
self._reenable_widgets_after_modal()
|
||||
return
|
||||
|
||||
try:
|
||||
# Crea e mostra la finestra modale CommitDetailWindow
|
||||
log_handler.log_debug(
|
||||
f"Opening CommitDetailWindow for commit {commit_details.get('hash_full')[:7]}...",
|
||||
func_name=func_name
|
||||
)
|
||||
CommitDetailWindow(
|
||||
master=self.master, # Parent è la finestra root
|
||||
commit_data=commit_details, # Passa il dizionario dei dati
|
||||
# Passa il callback per aprire il diff di un file specifico
|
||||
open_diff_callback=self._open_commit_file_diff
|
||||
)
|
||||
# Il codice attende qui finché la finestra CommitDetailWindow non viene chiusa
|
||||
log_handler.log_info("Commit Detail window closed by user.", func_name=func_name)
|
||||
# Ripristina la status bar dopo la chiusura della finestra
|
||||
if hasattr(self.main_frame, "update_status_bar"):
|
||||
self.main_frame.update_status_bar("Ready.")
|
||||
|
||||
except Exception as e_detail:
|
||||
# Gestisci errori durante la creazione/visualizzazione della finestra
|
||||
log_handler.log_exception(
|
||||
f"Error opening commit detail window: {e_detail}", func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error") and hasattr(self.main_frame, "update_status_bar"):
|
||||
self.main_frame.show_error(
|
||||
"Display Error", f"Could not display commit details:\n{e_detail}"
|
||||
)
|
||||
self.main_frame.update_status_bar("Error displaying commit details.")
|
||||
finally:
|
||||
# Assicurati che i widget vengano riabilitati dopo che la finestra
|
||||
# (o il messaggio di errore) è stata chiusa.
|
||||
self._reenable_widgets_after_modal()
|
||||
|
||||
def _open_commit_file_diff(
|
||||
self,
|
||||
commit_hash: str,
|
||||
file_status: str,
|
||||
file_path: str,
|
||||
old_file_path: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Opens the DiffViewerWindow to show the changes for a specific file
|
||||
within a given commit compared to its parent.
|
||||
|
||||
Args:
|
||||
commit_hash (str): The full hash of the commit being viewed.
|
||||
file_status (str): The status character (A, M, D, R, T...).
|
||||
file_path (str): The relative path of the file in the commit.
|
||||
old_file_path (Optional[str]): The old path (for Renamed files).
|
||||
"""
|
||||
func_name: str = "_open_commit_file_diff"
|
||||
log_handler.log_info(
|
||||
f"Requesting diff for file '{file_path}' in commit '{commit_hash[:7]}'",
|
||||
func_name=func_name
|
||||
)
|
||||
|
||||
# Validazione repo path corrente
|
||||
svn_path: Optional[str] = self._get_and_validate_svn_path("Open Commit File Diff")
|
||||
if not svn_path or not self._is_repo_ready(svn_path):
|
||||
log_handler.log_error(
|
||||
"Cannot open diff: Repository path invalid or not ready.", func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error("Error", "Cannot open diff: Repository not available.")
|
||||
return
|
||||
|
||||
# Determina i riferimenti per il diff viewer
|
||||
ref1: str = f"{commit_hash}^" # Genitore del commit
|
||||
ref2: str = commit_hash # Il commit stesso
|
||||
|
||||
# --- Gestione Casi Speciali ---
|
||||
# 1. File Aggiunto (A) o Copiato (C): Confronta con "stato vuoto" (come fare?)
|
||||
# Git diff <parent> <commit> -- <file> mostra l'intero file come aggiunto.
|
||||
# Possiamo passare i ref standard, DiffViewerWindow lo mostrerà correttamente.
|
||||
# 2. File Cancellato (D): Confronta il file nel parent con "stato vuoto".
|
||||
# DiffViewerWindow dovrebbe mostrare il file come cancellato.
|
||||
# Per Deleted files, il path da usare è quello *prima* della cancellazione.
|
||||
# 3. File Rinominato (R): Dobbiamo usare old_file_path per ref1 e file_path per ref2?
|
||||
# No, `git diff commit^ commit -- file` gestisce la rinomina. Usiamo il *nuovo* path.
|
||||
# 4. Commit Iniziale (Root Commit): Non ha genitore (`commit^` fallisce).
|
||||
# Dobbiamo rilevarlo. Possiamo provare `git rev-parse commit^`. Se fallisce,
|
||||
# è un root commit. In tal caso, mostriamo il file solo in ref2 (vs "empty").
|
||||
# Per semplicità iniziale, potremmo anche solo mostrare un errore se `git show`
|
||||
# per `commit^:path` fallisce.
|
||||
|
||||
path_to_diff: str = file_path
|
||||
if file_status == 'D' and old_file_path:
|
||||
# Per file cancellati, in realtà vogliamo vedere il contenuto *prima*
|
||||
# ma il diff tra parent e commit con il path nuovo non funziona.
|
||||
# È più corretto mostrare il file come esisteva nel parent.
|
||||
# Potremmo aprire DiffViewer in modo speciale solo con ref1?
|
||||
# O semplicemente non permettere il diff per file cancellati in questa vista?
|
||||
# Per ora, mostriamo un messaggio e non apriamo il diff per 'D'.
|
||||
log_handler.log_info(
|
||||
f"Diff view skipped for deleted file '{file_path}' in commit '{commit_hash[:7]}'.",
|
||||
func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_info"):
|
||||
self.main_frame.show_info(
|
||||
"Diff Not Applicable",
|
||||
f"Cannot show diff for file deleted in this commit:\n{file_path}"
|
||||
)
|
||||
return
|
||||
|
||||
# --- Apri DiffViewerWindow ---
|
||||
log_handler.log_debug(
|
||||
f"Opening DiffViewerWindow: Ref1='{ref1}', Ref2='{ref2}', Path='{path_to_diff}'",
|
||||
func_name=func_name
|
||||
)
|
||||
try:
|
||||
# Istanzia e mostra la finestra di diff modale
|
||||
DiffViewerWindow(
|
||||
master=self.master, # Usa la finestra root come parent
|
||||
git_commands=self.git_commands,
|
||||
repo_path=svn_path,
|
||||
relative_file_path=path_to_diff, # Usa il path corretto
|
||||
ref1=ref1, # Commit genitore
|
||||
ref2=ref2 # Commit selezionato
|
||||
)
|
||||
log_handler.log_debug("Commit file diff viewer closed.", func_name=func_name)
|
||||
# Non serve aggiornare status bar qui, la finestra CommitDetail è ancora aperta
|
||||
except Exception as e_diff:
|
||||
log_handler.log_exception(
|
||||
f"Error opening commit file diff viewer: {e_diff}", func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error(
|
||||
"Diff Viewer Error", f"Could not display file changes:\n{e_diff}"
|
||||
)
|
||||
|
||||
def manual_backup(self):
|
||||
""" Starts async operation for creating a manual backup ZIP. """
|
||||
func_name: str ="manual_backup"
|
||||
@ -2723,6 +2883,72 @@ class GitSvnSyncApp:
|
||||
}
|
||||
)
|
||||
|
||||
def view_commit_details(self, history_line: str):
|
||||
"""
|
||||
Callback triggered by double-clicking a line in the history view.
|
||||
Extracts the commit hash and starts an async worker to fetch details.
|
||||
"""
|
||||
func_name: str = "view_commit_details"
|
||||
log_handler.log_info(
|
||||
f"--- Action Triggered: View Commit Details for line: '{history_line}' ---",
|
||||
func_name=func_name
|
||||
)
|
||||
|
||||
# Ensure main frame exists
|
||||
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return
|
||||
# Validate repo path and readiness
|
||||
svn_path: Optional[str] = self._get_and_validate_svn_path("View Commit Details")
|
||||
if not svn_path or not self._is_repo_ready(svn_path):
|
||||
log_handler.log_warning(
|
||||
"View Commit Details skipped: Repo not ready.", func_name=func_name
|
||||
)
|
||||
self.main_frame.show_error(
|
||||
"Action Failed", "Repository path is not valid or not prepared."
|
||||
)
|
||||
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}"
|
||||
)
|
||||
return
|
||||
|
||||
# --- Start Async Worker to Get Commit Details ---
|
||||
log_handler.log_info(
|
||||
f"Fetching details for commit '{commit_hash_short}'...", func_name=func_name
|
||||
)
|
||||
# Prepare arguments for the worker
|
||||
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
|
||||
args_tuple=args,
|
||||
context_dict={
|
||||
"context": "get_commit_details",
|
||||
"status_msg": f"Loading details for commit {commit_hash_short}",
|
||||
"commit_hash": commit_hash_short # Passa l'hash nel contesto
|
||||
}
|
||||
)
|
||||
|
||||
# --- Remote Action Launchers ---
|
||||
def apply_remote_config(self):
|
||||
""" Callback for 'Apply Config' button. Starts async worker. """
|
||||
|
||||
@ -122,6 +122,7 @@ class AsyncResultHandler:
|
||||
'push_tags_remote': self._handle_push_tags_remote_result,
|
||||
'clone_remote': self._handle_clone_remote_result,
|
||||
'checkout_tracking_branch': self._handle_checkout_tracking_branch_result,
|
||||
'get_commit_details': self._handle_get_commit_details_result,
|
||||
}
|
||||
|
||||
# Get the appropriate handler method from the map
|
||||
@ -587,6 +588,61 @@ class AsyncResultHandler:
|
||||
|
||||
return trigger_refreshes, sync_refresh
|
||||
|
||||
def _handle_get_commit_details_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Handles the result of the 'get_commit_details' async task.
|
||||
If successful, calls the main app to display the commit detail window.
|
||||
"""
|
||||
func_name: str = "_handle_get_commit_details_result"
|
||||
status: Optional[str] = result_data.get('status')
|
||||
message: Optional[str] = result_data.get('message')
|
||||
# result_value qui è il dizionario commit_details dal worker
|
||||
commit_details: Optional[Dict[str, Any]] = result_data.get('result')
|
||||
|
||||
if status == 'success':
|
||||
log_handler.log_info(
|
||||
"Commit details retrieved successfully. Requesting detail window display...",
|
||||
func_name=func_name
|
||||
)
|
||||
# Verifica che i dettagli siano un dizionario valido
|
||||
if isinstance(commit_details, dict):
|
||||
# Chiama un nuovo metodo sull'app principale per mostrare la finestra
|
||||
if hasattr(self.app, "show_commit_details") and callable(self.app.show_commit_details):
|
||||
# Passa il dizionario completo dei dettagli
|
||||
self.app.show_commit_details(commit_details)
|
||||
# Nota: show_commit_details in GitUtility si occuperà di
|
||||
# riabilitare i widget dopo la chiusura della finestra modale.
|
||||
else:
|
||||
log_handler.log_error(
|
||||
"Cannot display commit details: 'show_commit_details' method missing on app.",
|
||||
func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error(
|
||||
"Internal Error", "Cannot display commit details window."
|
||||
)
|
||||
# Riabilita subito i widget in caso di errore interno
|
||||
self._reenable_widgets_after_modal()
|
||||
else:
|
||||
# Errore: il risultato non è nel formato atteso
|
||||
log_handler.log_error(
|
||||
f"Received success status for commit details, but result is not a dict: {type(commit_details)}",
|
||||
func_name=func_name
|
||||
)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error("Data Error", "Failed to process commit details.")
|
||||
self._reenable_widgets_after_modal()
|
||||
|
||||
elif status == 'error':
|
||||
# Gestisce l'errore (es. commit hash non valido)
|
||||
log_handler.log_error(f"Failed to get commit details: {message}", func_name=func_name)
|
||||
if hasattr(self.main_frame, "show_error"):
|
||||
self.main_frame.show_error("Commit Details Error", f"Could not get details:\n{message}")
|
||||
# Widgets sono già riabilitati dal chiamante per stato 'error'
|
||||
|
||||
# Ottenere dettagli commit non triggera altri refresh standard
|
||||
return False, False
|
||||
|
||||
def _handle_add_file_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
|
||||
""" Handles the result of the 'add_file' async task. """
|
||||
func_name: str = "_handle_add_file_result"
|
||||
|
||||
196
async_workers.py
196
async_workers.py
@ -1758,4 +1758,198 @@ def run_compare_branches_async(
|
||||
|
||||
log_handler.log_debug(f"[Worker] Finished: Compare Branches '{ref1}' vs '{ref2}'", func_name=func_name)
|
||||
|
||||
# --- END OF FILE async_workers.py ---
|
||||
def run_get_commit_details_async(
|
||||
git_commands: GitCommands,
|
||||
repo_path: str,
|
||||
commit_hash: str, # Hash del commit (può essere breve)
|
||||
results_queue: queue.Queue
|
||||
):
|
||||
"""
|
||||
Worker to fetch detailed information about a specific commit, including
|
||||
metadata and the list of changed files. Executed in a separate thread.
|
||||
"""
|
||||
func_name: str = "run_get_commit_details_async"
|
||||
log_handler.log_debug(
|
||||
f"[Worker] Started: Get details for commit '{commit_hash}' in '{repo_path}'",
|
||||
func_name=func_name
|
||||
)
|
||||
|
||||
# Dizionario per contenere i risultati
|
||||
commit_details: Dict[str, Any] = {
|
||||
'hash_full': None,
|
||||
'author_name': None,
|
||||
'author_email': None,
|
||||
'author_date': None,
|
||||
'subject': None,
|
||||
'body': "", # Messaggio completo (potrebbe essere multilinea)
|
||||
'files_changed': [] # Lista di tuple (status, path1, [path2 for R/C])
|
||||
}
|
||||
status: str = "error" # Default a errore
|
||||
message: str = f"Could not retrieve details for commit '{commit_hash}'."
|
||||
exception_obj: Optional[Exception] = None
|
||||
|
||||
try:
|
||||
# --- 1. Ottieni Metadati e Lista File con 'git show --name-status' ---
|
||||
# Usiamo un formato specifico per estrarre facilmente i metadati principali
|
||||
# Usiamo %H per hash completo, %an/%ae per autore, %ad per data autore, %s per soggetto
|
||||
# Separatori specifici (es. |||) per dividere i campi in modo affidabile
|
||||
# --name-status alla fine per ottenere la lista dei file
|
||||
# -z usa NUL come terminatore per i nomi file (più robusto)
|
||||
separator: str = "|||---|||" # Separatore improbabile nel contenuto
|
||||
pretty_format: str = f"%H{separator}%an{separator}%ae{separator}%ad{separator}%s"
|
||||
show_cmd: List[str] = [
|
||||
"git",
|
||||
"show",
|
||||
f"--pretty=format:{pretty_format}",
|
||||
"--name-status",
|
||||
"-z", # Usa NUL terminators per i file
|
||||
commit_hash # Usa l'hash fornito (breve o completo)
|
||||
]
|
||||
log_handler.log_debug(
|
||||
f"[Worker] Executing git show command: {' '.join(show_cmd)}",
|
||||
func_name=func_name
|
||||
)
|
||||
# Esegui catturando l'output, check=False per gestire commit non validi
|
||||
show_result = git_commands.log_and_execute(
|
||||
command=show_cmd,
|
||||
working_directory=repo_path,
|
||||
check=False, # Gestiamo noi se il commit non viene trovato
|
||||
capture=True,
|
||||
hide_console=True,
|
||||
log_output_level=logging.DEBUG
|
||||
)
|
||||
|
||||
# Controlla se il comando 'git show' ha avuto successo
|
||||
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)
|
||||
metadata_line: str = output_parts[0]
|
||||
files_part_raw: str = output_parts[1] if len(output_parts) > 1 else ""
|
||||
|
||||
# Estrai metadati dalla prima parte
|
||||
meta_parts: List[str] = metadata_line.split(separator)
|
||||
if len(meta_parts) == 5:
|
||||
commit_details['hash_full'] = meta_parts[0].strip()
|
||||
commit_details['author_name'] = meta_parts[1].strip()
|
||||
commit_details['author_email'] = meta_parts[2].strip()
|
||||
commit_details['author_date'] = meta_parts[3].strip()
|
||||
commit_details['subject'] = meta_parts[4].strip()
|
||||
log_handler.log_debug(
|
||||
f"[Worker] Parsed metadata: Hash={commit_details['hash_full']}, Author={commit_details['author_name']}",
|
||||
func_name=func_name
|
||||
)
|
||||
else:
|
||||
log_handler.log_warning(
|
||||
f"[Worker] Could not parse metadata line correctly: {metadata_line}",
|
||||
func_name=func_name
|
||||
)
|
||||
# Non considerarlo un errore fatale, potremmo avere solo i file
|
||||
|
||||
# Estrai lista file dalla seconda parte (gestione NUL terminator)
|
||||
if files_part_raw:
|
||||
file_entries: List[str] = files_part_raw.strip('\x00').split('\x00')
|
||||
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
|
||||
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]
|
||||
old_path = file_entries[i+1]
|
||||
new_path = file_entries[i+2]
|
||||
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
|
||||
elif status_char: # Added, Modified, Deleted, Type Changed
|
||||
if i + 1 < len(file_entries):
|
||||
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
|
||||
else:
|
||||
log_handler.log_warning(f"[Worker] Incomplete A/M/D/T entry: {file_entries[i:]}", func_name=func_name)
|
||||
break
|
||||
else:
|
||||
# Ignora elementi vuoti (potrebbero esserci NUL consecutivi?)
|
||||
i += 1
|
||||
commit_details['files_changed'] = parsed_files
|
||||
log_handler.log_debug(
|
||||
f"[Worker] Parsed {len(parsed_files)} changed files.", func_name=func_name
|
||||
)
|
||||
|
||||
# --- 2. Ottieni Corpo Messaggio Commit con 'git show -s --format=%B' ---
|
||||
# Questo comando estrae solo il messaggio completo (soggetto + corpo)
|
||||
body_cmd: List[str] = ["git", "show", "-s", "--format=%B", commit_hash]
|
||||
body_result = git_commands.log_and_execute(
|
||||
command=body_cmd,
|
||||
working_directory=repo_path,
|
||||
check=False, # Dovrebbe funzionare se show precedente ha funzionato
|
||||
capture=True,
|
||||
hide_console=True
|
||||
)
|
||||
if body_result.returncode == 0:
|
||||
commit_details['body'] = body_result.stdout.strip()
|
||||
# Subject è già nel body, ma lo teniamo separato per potenziali usi futuri
|
||||
log_handler.log_debug("[Worker] Fetched full commit message body.", func_name=func_name)
|
||||
else:
|
||||
log_handler.log_warning(
|
||||
f"[Worker] Could not fetch commit body (RC={body_result.returncode}).",
|
||||
func_name=func_name
|
||||
)
|
||||
# Non fatale, usa solo il subject se disponibile
|
||||
|
||||
# Se siamo arrivati qui, l'operazione ha avuto successo
|
||||
status = "success"
|
||||
message = f"Details retrieved successfully for commit '{commit_hash}'."
|
||||
log_handler.log_info(f"[Worker] {message}", func_name=func_name)
|
||||
|
||||
elif "unknown revision or path not in the working tree" in (show_result.stderr or "").lower():
|
||||
# Caso specifico: hash commit non valido o non trovato
|
||||
status = "error"
|
||||
message = f"Commit hash '{commit_hash}' not found or invalid."
|
||||
log_handler.log_error(f"[Worker] {message}", func_name=func_name)
|
||||
exception_obj = ValueError(message) # Usa ValueError per input errato
|
||||
else:
|
||||
# Altro errore da 'git show'
|
||||
status = "error"
|
||||
stderr_msg = show_result.stderr.strip() if show_result.stderr else "Unknown git show error"
|
||||
message = f"Failed to get commit details (RC={show_result.returncode}): {stderr_msg}"
|
||||
log_handler.log_error(f"[Worker] {message}", func_name=func_name)
|
||||
exception_obj = GitCommandError(message, stderr=show_result.stderr)
|
||||
|
||||
except GitCommandError as git_err:
|
||||
# Errore durante l'esecuzione di un comando git (es. permessi, repo corrotto)
|
||||
log_handler.log_exception(
|
||||
f"[Worker] GitCommandError getting commit details: {git_err}",
|
||||
func_name=func_name
|
||||
)
|
||||
status = "error"
|
||||
message = f"Git error retrieving commit details: {git_err}"
|
||||
exception_obj = git_err
|
||||
except Exception as e:
|
||||
# Errore imprevisto nel worker
|
||||
log_handler.log_exception(
|
||||
f"[Worker] UNEXPECTED EXCEPTION getting commit details: {e}",
|
||||
func_name=func_name
|
||||
)
|
||||
status = "error"
|
||||
message = f"Unexpected error retrieving commit details: {type(e).__name__}"
|
||||
exception_obj = e
|
||||
|
||||
# --- Metti il risultato nella coda ---
|
||||
results_queue.put({
|
||||
"status": status,
|
||||
"message": message,
|
||||
"result": commit_details, # Passa il dizionario con i dettagli
|
||||
"exception": exception_obj
|
||||
})
|
||||
|
||||
log_handler.log_debug(
|
||||
f"[Worker] Finished: Get details for commit '{commit_hash}'",
|
||||
func_name=func_name
|
||||
)
|
||||
|
||||
365
commit_detail_window.py
Normal file
365
commit_detail_window.py
Normal file
@ -0,0 +1,365 @@
|
||||
# --- 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 ---
|
||||
33
gui.py
33
gui.py
@ -458,7 +458,7 @@ class MainFrame(ttk.Frame):
|
||||
delete_local_branch_cb: Callable[[str, bool], None], # Azione da menu contestuale locale
|
||||
merge_local_branch_cb: Callable[[str], None], # Azione da menu contestuale locale
|
||||
compare_branch_with_current_cb: Callable[[str], None],
|
||||
# Aggiungere qui futuri callback (es. delete remote branch, compare branches, etc.)
|
||||
view_commit_details_cb: Callable[[str], None]
|
||||
):
|
||||
"""Initializes the MainFrame."""
|
||||
super().__init__(master)
|
||||
@ -1486,6 +1486,10 @@ class MainFrame(ttk.Frame):
|
||||
relief=tk.SUNKEN,
|
||||
)
|
||||
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.create_tooltip(self.history_text, "Double-click a commit to view details.")
|
||||
|
||||
history_xscroll = ttk.Scrollbar(
|
||||
frame, orient=tk.HORIZONTAL, command=self.history_text.xview
|
||||
)
|
||||
@ -1720,6 +1724,33 @@ class MainFrame(ttk.Frame):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_history_double_click(self, event: tk.Event) -> None:
|
||||
"""
|
||||
Handles the double-click event on the history text area.
|
||||
Extracts the selected line and calls the view_commit_details callback.
|
||||
"""
|
||||
# Ensure the callback exists and is callable
|
||||
if not hasattr(self, "view_commit_details_callback") or \
|
||||
not callable(self.view_commit_details_callback):
|
||||
return
|
||||
|
||||
try:
|
||||
# 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
|
||||
|
||||
# Pass the extracted line content to the callback function
|
||||
if line_content and not line_content.startswith("("): # Avoid acting on "(Error..)" lines
|
||||
self.view_commit_details_callback(line_content)
|
||||
except tk.TclError:
|
||||
# Ignore errors if click is outside text content
|
||||
pass
|
||||
except Exception as e:
|
||||
# Log unexpected errors (consider using log_handler if available)
|
||||
print(f"Error handling history double-click: {e}", file=sys.stderr)
|
||||
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user