Implement Pull functionality with pre-check

This commit is contained in:
VALLONGOL 2025-04-23 09:08:15 +02:00
parent e6142c8f41
commit 91698eac76
6 changed files with 1379 additions and 529 deletions

View File

@ -96,6 +96,9 @@ class GitSvnSyncApp:
self.action_handler = ActionHandler(self.git_commands, self.backup_handler)
# RemoteActionHandler per operazioni remote
self.remote_action_handler = RemoteActionHandler(self.git_commands)
self.remote_auth_status = 'unknown'
print("Core components initialized.")
log_handler.log_debug(
"Core components initialized successfully.", func_name="__init__"
@ -154,8 +157,8 @@ class GitSvnSyncApp:
# --- Remote Actions ---
apply_remote_config_cb=self.apply_remote_config,
check_connection_auth_cb=self.check_connection_auth,
# fetch_remote_cb=self.fetch_remote, # Placeholder
# pull_remote_cb=self.pull_remote, # Placeholder
fetch_remote_cb=self.fetch_remote,
pull_remote_cb=self.pull_remote,
# push_remote_cb=self.push_remote, # Placeholder
# push_tags_remote_cb=self.push_tags_remote, # Placeholder
# === Fine Callbacks ===
@ -2050,15 +2053,24 @@ class GitSvnSyncApp:
def check_connection_auth(self):
"""Callback for 'Check Connection & Auth' button."""
func_name = "check_connection_auth"
log_handler.log_info(f"--- Action Triggered: Check Connection & Auth ---", func_name=func_name)
log_handler.log_info(
f"--- Action Triggered: Check Connection & Auth ---", func_name=func_name
)
# Validazioni
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists():
return
svn_path = self._get_and_validate_svn_path("Check Connection")
if not svn_path or not self._is_repo_ready(svn_path):
log_handler.log_warning("Check Connection skipped: Repo not ready.", func_name=func_name)
self.main_frame.show_error("Action Failed", "Repository path is not valid or not prepared.")
self._update_gui_auth_status('unknown') # Resetta indicatore se repo non pronto
log_handler.log_warning(
"Check Connection skipped: Repo not ready.", func_name=func_name
)
self.main_frame.show_error(
"Action Failed", "Repository path is not valid or not prepared."
)
self._update_gui_auth_status(
"unknown"
) # Resetta indicatore se repo non pronto
return
remote_name = self.main_frame.remote_name_var.get().strip()
@ -2067,8 +2079,11 @@ class GitSvnSyncApp:
remote_name = DEFAULT_REMOTE_NAME
self.main_frame.remote_name_var.set(remote_name)
log_handler.log_info(f"Checking connection/auth for remote '{remote_name}'...", func_name=func_name)
self._update_gui_auth_status('checking') # Stato visivo temporaneo (opzionale)
log_handler.log_info(
f"Checking connection/auth for remote '{remote_name}'...",
func_name=func_name,
)
self._update_gui_auth_status("checking") # Stato visivo temporaneo (opzionale)
# Argomenti per il worker di controllo
args = (self.git_commands, svn_path, remote_name)
@ -2084,7 +2099,112 @@ class GitSvnSyncApp:
},
)
# --- NUOVO METODO HELPER PER AGGIORNARE STATO INTERNO E GUI ---
def fetch_remote(self):
"""Starts the asynchronous 'git fetch' operation."""
func_name = "fetch_remote"
log_handler.log_info(
f"--- Action Triggered: Fetch Remote ---", func_name=func_name
)
# Validazioni
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists():
return
svn_path = self._get_and_validate_svn_path("Fetch Remote")
if not svn_path or not self._is_repo_ready(svn_path):
log_handler.log_warning(
"Fetch Remote skipped: Repo not ready.", func_name=func_name
)
self.main_frame.show_error(
"Action Failed", "Repository path is not valid or not prepared."
)
self.main_frame.update_status_bar("Fetch failed: Repo not ready.")
return
remote_name = self.main_frame.remote_name_var.get().strip()
if not remote_name:
remote_name = DEFAULT_REMOTE_NAME # Usa default se vuoto
self.main_frame.remote_name_var.set(
remote_name
) # Aggiorna GUI per coerenza
# Verifica lo stato dell'autenticazione PRIMA di tentare il fetch
# Se sappiamo già che serve auth, potremmo avvisare l'utente
# if self.remote_auth_status == 'required' or self.remote_auth_status == 'failed':
# if not self.main_frame.ask_yes_no("Authentication May Be Required",
# f"Last check indicated authentication is needed or failed for remote '{remote_name}'.\n"
# f"Attempt fetch anyway? (May open a terminal for credentials)"):
# self.main_frame.update_status_bar("Fetch cancelled by user.")
# return
# Potremmo anche forzare un check prima: self.check_connection_auth() e aspettare il risultato? Complesso.
# Per ora, tentiamo direttamente il fetch. Sarà il worker a gestire errori auth.
log_handler.log_info(
f"Starting fetch for remote '{remote_name}'...", func_name=func_name
)
# Argomenti per il worker: dipendenza + parametri
args = (self.remote_action_handler, svn_path, remote_name)
self._start_async_operation(
async_workers.run_fetch_remote_async, # Worker esterno per fetch
args,
{
"context": "fetch_remote", # Contesto per il risultato
"status_msg": f"Fetching from remote '{remote_name}'",
},
)
def pull_remote(self):
"""Starts the asynchronous 'git pull' operation for the current branch."""
func_name = "pull_remote"
log_handler.log_info(f"--- Action Triggered: Pull Remote ---", func_name=func_name)
# Validazioni
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return
svn_path = self._get_and_validate_svn_path("Pull Remote")
if not svn_path or not self._is_repo_ready(svn_path):
log_handler.log_warning("Pull Remote skipped: Repo not ready.", func_name=func_name)
self.main_frame.show_error("Action Failed", "Repository path is not valid or not prepared.")
self.main_frame.update_status_bar("Pull failed: Repo not ready.")
return
remote_name = self.main_frame.remote_name_var.get().strip()
if not remote_name:
remote_name = DEFAULT_REMOTE_NAME # Usa default
self.main_frame.remote_name_var.set(remote_name)
# Verifica stato autenticazione (opzionale, ma consigliato)
# Se non connesso o auth richiesta, potremmo avvisare o impedire
if self.remote_auth_status != 'ok':
msg = f"Cannot Pull from '{remote_name}':\n"
if self.remote_auth_status == 'required':
msg += "Authentication is required. Use 'Check Connection / Auth' first."
elif self.remote_auth_status == 'failed':
msg += "Authentication previously failed. Use 'Check Connection / Auth' to retry."
elif self.remote_auth_status == 'connection_failed':
msg += "Connection previously failed. Check URL and network."
else: # unknown or unknown_error
msg += "Connection status is unknown or in error. Use 'Check Connection / Auth' first."
log_handler.log_warning(f"Pull Remote skipped: Auth/Connection status is '{self.remote_auth_status}'.", func_name=func_name)
self.main_frame.show_warning("Action Blocked", msg)
self.main_frame.update_status_bar(f"Pull failed: {self.remote_auth_status}")
return
# Il worker `run_pull_remote_async` otterrà il nome del branch corrente internamente
log_handler.log_info(f"Starting pull for remote '{remote_name}'...", func_name=func_name)
# Argomenti per il worker: dipendenze (remote handler + git commands) + parametri repo
args = (self.remote_action_handler, self.git_commands, svn_path, remote_name)
self._start_async_operation(
async_workers.run_pull_remote_async, # Worker esterno per pull
args,
{
"context": "pull_remote", # Contesto per il risultato
"status_msg": f"Pulling from remote '{remote_name}'",
# Passiamo il path nel contesto in caso di conflitto
"repo_path": svn_path,
},
)
def _update_gui_auth_status(self, status: str):
"""Updates internal state and calls GUI update for auth indicator."""
self.remote_auth_status = status # Aggiorna stato interno
@ -2096,131 +2216,195 @@ class GitSvnSyncApp:
def _check_completion_queue(self, results_queue: queue.Queue, context: dict):
"""Checks result queue from async workers, updates GUI accordingly."""
task_context = context.get('context', 'unknown')
# func_name per i log interni a questa funzione
func_name = "_check_completion_queue"
# Ridotto logging verboso all'inizio del check
# log_handler.log_debug(f"Checking completion queue for context: {task_context}", func_name=func_name)
try:
# Tenta di ottenere un risultato dalla coda senza bloccare
result_data = results_queue.get_nowait()
log_handler.log_info(f"Result received for '{task_context}'. Status: {result_data.get('status')}", func_name="_check_completion_queue")
log_handler.log_info(f"Result received for '{task_context}'. Status: {result_data.get('status')}", func_name=func_name)
# --- Riabilita GUI (se non stiamo per avviare un'altra op async) ---
# Determiniamo SE riabilitare subito o aspettare
should_reenable_now = True
# Controlliamo se il risultato corrente richiede un'azione successiva
if task_context == "check_connection" and result_data.get('status') == 'auth_required':
should_reenable_now = False # Aspetta la risposta del messagebox
elif task_context == "interactive_auth" and result_data.get('status') == 'success':
should_reenable_now = False # Avvieremo un altro check
# --- Determina se riabilitare subito i widget ---
should_reenable_now = True # Default: riabilita subito
status_from_result = result_data.get('status') # Ottieni lo stato dal risultato
if should_reenable_now and hasattr(self, "main_frame") and self.main_frame.winfo_exists():
log_handler.log_debug("Re-enabling widgets.", func_name="_check_completion_queue")
if task_context == "check_connection" and status_from_result == 'auth_required':
# Non riabilitare se stiamo per chiedere conferma all'utente
should_reenable_now = False
log_handler.log_debug("Delaying widget re-enable: waiting for auth prompt.", func_name=func_name)
elif task_context == "interactive_auth" and status_from_result == 'success':
# Non riabilitare se stiamo per ri-avviare il check dopo auth interattiva
should_reenable_now = False
log_handler.log_debug("Delaying widget re-enable: re-checking connection after interactive auth.", func_name=func_name)
# Riabilita i widget se non è necessario attendere
if should_reenable_now:
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
log_handler.log_debug("Re-enabling widgets.", func_name=func_name)
self.main_frame.set_action_widgets_state(tk.NORMAL)
else:
# Se la GUI non c'è, non c'è nulla da fare o riabilitare
log_handler.log_warning("Cannot re-enable widgets, MainFrame missing.", func_name=func_name)
return # Esce dalla funzione
# Estrai dettagli
status = result_data.get('status')
message = result_data.get('message', "Operation finished.")
result_value = result_data.get('result') # Rinominato per chiarezza
exception = result_data.get('exception')
# (...) altri flag (...)
committed = result_data.get('committed', False)
is_conflict = result_data.get('conflict', False)
# --- Estrai dettagli dal risultato ---
status = status_from_result # Usa la variabile già ottenuta
message = result_data.get('message', "Operation finished.") # Messaggio di default
result_value = result_data.get('result') # Valore specifico del risultato
exception = result_data.get('exception') # Eventuale eccezione catturata
committed = result_data.get('committed', False) # Flag per operazioni che committano
# Estrai flag conflitto specifico per pull o fetch_bundle
is_conflict = False # Default
repo_path_conflict = None # Default
if task_context == 'pull_remote':
# Per pull, lo stato 'conflict' determina se c'è stato conflitto
is_conflict = (status == 'conflict')
repo_path_conflict = context.get('repo_path') # Prendi path dal contesto originale
elif task_context == 'fetch_bundle': # Mantiene la logica per fetch_bundle
is_conflict = result_data.get('conflict', False) # Qui dipende da flag esplicito
repo_path_conflict = result_data.get('repo_path')
new_branch_context = context.get('new_branch_name')
new_branch_context = context.get('new_branch_name') # Info se si crea branch
# Aggiorna Status Bar
# (...) logica status bar invariata (...)
# --- Aggiorna Status Bar con colore e reset temporizzato ---
status_color = None
reset_duration = 5000
if status == 'success': status_color = self.main_frame.STATUS_GREEN
elif status == 'warning': status_color = self.main_frame.STATUS_YELLOW; reset_duration = 7000
# ---<<< MODIFICA: Colore specifico per auth_required >>>---
elif status == 'auth_required': status_color = self.main_frame.STATUS_YELLOW; reset_duration = 15000 # Giallo, dura di più
elif status == 'error': status_color = self.main_frame.STATUS_RED; reset_duration = 10000
reset_duration = 5000 # Default reset 5 secondi
if status == 'success':
status_color = self.main_frame.STATUS_GREEN
elif status == 'warning':
status_color = self.main_frame.STATUS_YELLOW
reset_duration = 7000
elif status == 'auth_required': # Stato specifico per auth
status_color = self.main_frame.STATUS_YELLOW # Giallo per richiesta auth
reset_duration = 15000 # Dura di più per dare tempo di leggere
elif status == 'conflict': # Stato specifico per conflitti PULL
status_color = self.main_frame.STATUS_RED # Rosso per conflitti
reset_duration = 15000 # Dura di più
elif status == 'error':
status_color = self.main_frame.STATUS_RED
reset_duration = 10000
# Aggiorna la status bar (usa la funzione helper della GUI)
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration)
# --- Processa risultato ---
repo_path_for_updates = self._get_and_validate_svn_path("Post-Action Update")
# --- Processa risultato specifico per task ---
# Ottieni path corrente per eventuali refresh
repo_path_for_updates = self._get_and_validate_svn_path("Post-Action Update Check")
# Lista per raccogliere funzioni di refresh da chiamare alla fine
refresh_list = []
# ---<<< NUOVA GESTIONE PER CHECK_CONNECTION E INTERACTIVE_AUTH >>>---
# --- Gestione specifica per check_connection e interactive_auth ---
if task_context == "check_connection":
remote_name = context.get("remote_name_checked", "unknown remote")
if status == 'success':
log_handler.log_info(f"Connection check successful for '{remote_name}'.", func_name="_check_completion_queue")
self._update_gui_auth_status('ok') # Aggiorna indicatore a verde
auth_status = 'ok' # Stato interno e per l'indicatore GUI
if result_value == 'connected_empty':
log_handler.log_info(f"Connection check successful for '{remote_name}' (remote empty/unborn).", func_name=func_name)
else:
log_handler.log_info(f"Connection check successful for '{remote_name}'.", func_name=func_name)
self._update_gui_auth_status(auth_status) # Aggiorna stato interno e indicatore
elif status == 'auth_required':
log_handler.log_warning(f"Authentication required for remote '{remote_name}'.", func_name="_check_completion_queue")
self._update_gui_auth_status('required') # Aggiorna indicatore (giallo/rosso)
# Chiedi all'utente se vuole tentare l'autenticazione interattiva
log_handler.log_warning(f"Authentication required for remote '{remote_name}'.", func_name=func_name)
self._update_gui_auth_status('required') # Aggiorna stato interno e indicatore
repo_path_checked = context.get("repo_path_checked")
if repo_path_checked and self.main_frame.ask_yes_no(
# Chiedi conferma all'utente
if repo_path_checked and hasattr(self,"main_frame") and self.main_frame.ask_yes_no(
"Authentication Required",
f"Authentication is required to connect to remote '{remote_name}'.\n\n"
f"Do you want to attempt authentication now?\n"
f"(This may open a separate terminal window for credential input.)"
):
log_handler.log_info("User requested interactive authentication attempt.", func_name="_check_completion_queue")
log_handler.log_info("User requested interactive authentication attempt.", func_name=func_name)
# Avvia il worker interattivo
args_interactive = (self.git_commands, repo_path_checked, remote_name)
self._start_async_operation(
async_workers.run_interactive_auth_attempt_async,
args_interactive,
{
"context": "interactive_auth", # Nuovo contesto
"context": "interactive_auth", # Contesto per il risultato
"status_msg": f"Attempting interactive auth for '{remote_name}'",
"original_context": context # Passa contesto originale se serve
"original_context": context # Passa contesto originale
}
)
# Non riabilitare i widget qui, lo farà il risultato del tentativo interattivo
# Non riabilitare widget qui (should_reenable_now è False)
else:
# L'utente ha detto no, riabilita i widget
log_handler.log_info("User declined interactive authentication attempt.", func_name="_check_completion_queue")
# Utente ha detto No, riabilita i widget ora
log_handler.log_info("User declined interactive authentication attempt.", func_name=func_name)
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
self.main_frame.set_action_widgets_state(tk.NORMAL)
elif status == 'error':
# Gestisci diversi tipi di errore di connessione
if result_value == 'connection_failed':
log_handler.log_error(f"Connection failed for remote '{remote_name}'.", func_name="_check_completion_queue")
self._update_gui_auth_status('connection_failed')
else: # unknown_error o worker_exception
log_handler.log_error(f"Unknown error checking remote '{remote_name}'.", func_name="_check_completion_queue")
self._update_gui_auth_status('unknown_error')
self.main_frame.show_error("Connection Error", f"{message}")
# Errore durante il check: aggiorna indicatore e mostra messaggio
error_type = result_value if result_value in ['connection_failed', 'unknown_error', 'worker_exception'] else 'unknown_error'
self._update_gui_auth_status(error_type)
if hasattr(self, "main_frame"): self.main_frame.show_error("Connection Error", f"{message}")
# Widget già riabilitati all'inizio se should_reenable_now era True
elif task_context == "interactive_auth":
original_context = context.get("original_context", {})
remote_name = original_context.get("remote_name_checked", "unknown remote")
if status == 'success' and result_value == 'auth_attempt_success':
# Il tentativo interattivo sembra riuscito, ri-verifica silenziosamente
log_handler.log_info(f"Interactive auth attempt for '{remote_name}' successful. Re-checking connection...", func_name="_check_completion_queue")
self.main_frame.update_status_bar(f"Authentication successful. Checking status...")
# Ri-avvia il check originale
self.check_connection_auth() # Richiama il metodo che avvia il check silenzioso
# Non riabilitare i widget qui, lo farà il risultato del re-check
elif status == 'error' and result_value == 'auth_attempt_failed':
# Il tentativo interattivo è fallito
log_handler.log_warning(f"Interactive auth attempt for '{remote_name}' failed.", func_name="_check_completion_queue")
self._update_gui_auth_status('failed') # Imposta indicatore su fallito
self.main_frame.show_warning("Authentication Failed", f"{message}")
# Tentativo interattivo riuscito, ri-verifica silenziosamente
log_handler.log_info(f"Interactive auth attempt for '{remote_name}' successful. Re-checking connection...", func_name=func_name)
if hasattr(self, "main_frame"): self.main_frame.update_status_bar(f"Authentication successful. Checking status...")
# Richiama il metodo che avvia il check standard (che è asincrono)
self.check_connection_auth()
# Non riabilitare widget qui (should_reenable_now è False)
elif status == 'error': # Include auth_attempt_failed e worker_exception
log_handler.log_warning(f"Interactive auth attempt for '{remote_name}' failed or error occurred: {message}", func_name=func_name)
# Imposta indicatore su stato fallito
self._update_gui_auth_status('failed')
if hasattr(self, "main_frame"): self.main_frame.show_warning("Authentication Attempt Failed", f"{message}")
# Ora riabilita i widget
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
self.main_frame.set_action_widgets_state(tk.NORMAL)
else: # Errore nel worker interattivo stesso
log_handler.log_error(f"Error during interactive auth worker for '{remote_name}': {message}", func_name="_check_completion_queue")
self._update_gui_auth_status('unknown_error')
self.main_frame.show_error("Internal Error", f"An unexpected error occurred during the authentication attempt:\n{message}")
# Riabilita widget
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
self.main_frame.set_action_widgets_state(tk.NORMAL)
# --- FINE NUOVA GESTIONE ---
# --- Gestione specifica per PULL CONFLICT ---
elif task_context == 'pull_remote' and status == 'conflict':
log_handler.log_error(f"Merge conflict occurred during pull. User needs to resolve manually in '{repo_path_conflict}'.", func_name=func_name)
# Mostra errore specifico per conflitto
if hasattr(self, "main_frame"):
self.main_frame.show_error(
"Merge Conflict",
f"Merge conflict occurred during pull from '{context.get('remote_name', 'remote')}'.\n\n"
f"Please resolve the conflicts manually in:\n{repo_path_conflict}\n\n"
f"After resolving, stage the changes and commit them."
)
# Dopo un conflitto, aggiorna la lista dei file modificati
if self.refresh_changed_files_list not in refresh_list:
refresh_list.append(self.refresh_changed_files_list)
# Triggera SOLO il refresh dei file modificati qui, altri potrebbero non avere senso
if repo_path_for_updates and self.refresh_changed_files_list:
log_handler.log_debug(f"Triggering changes refresh after pull conflict.", func_name=func_name)
try: self.refresh_changed_files_list()
except Exception as ref_e: log_handler.log_error(f"Error triggering changes refresh after conflict: {ref_e}", func_name=func_name)
# --- GESTIONE RISULTATI PER ALTRI TASK (Esistente) ---
# --- Gestione risultati altri task (successo) ---
elif status == 'success':
# (...Logica esistente per determinare refresh_list...)
if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', 'checkout_tag', 'create_branch', 'checkout_branch', '_handle_gitignore_save', 'add_file', 'apply_remote_config']: # Aggiunto apply_remote_config
# (...logica invariata per popolare refresh_list...)
if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save', 'apply_remote_config']: # Apply config non committa ma può giustificare refresh history
# Determina quali refresh avviare in base al task completato
if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag',
'checkout_tag', 'create_branch', 'checkout_branch',
'_handle_gitignore_save', 'add_file', 'apply_remote_config',
'fetch_remote', 'pull_remote']: # 'pull_remote' successo (non conflitto)
# Logica per popolare refresh_list
if task_context == 'pull_remote': # Pull successo
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
elif task_context == 'fetch_remote':
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
elif task_context == 'apply_remote_config':
refresh_list.append(self.check_connection_auth)
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
# Logica refresh per le altre azioni
else:
if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save']:
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
if task_context != 'refresh_changes':
if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
@ -2228,76 +2412,136 @@ class GitSvnSyncApp:
if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
if task_context not in ['refresh_branches', 'checkout_branch']:
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
# Dopo aver applicato la config remota, è utile verificare la connessione
if task_context == 'apply_remote_config':
refresh_list.append(self.check_connection_auth)
# (...Logica esistente per aggiornamenti diretti GUI post-refresh...)
if task_context == 'refresh_tags': self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else [])
# --- Aggiornamenti diretti GUI (per i task di refresh stessi) ---
if task_context == 'refresh_tags':
if hasattr(self, "main_frame"): self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_branches':
branches, current = result_value if isinstance(result_value, tuple) and len(result_value) == 2 else ([], None)
if hasattr(self, "main_frame"):
self.main_frame.update_branch_list(branches, current)
self.main_frame.update_history_branch_filter(branches)
elif task_context == 'refresh_history': self.main_frame.update_history_display(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_changes': self.main_frame.update_changed_files_list(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_history':
if hasattr(self, "main_frame"): self.main_frame.update_history_display(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_changes':
if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(result_value if isinstance(result_value, list) else [])
# (...Logica esistente per azioni post-successo specifiche: commit, create branch...)
if task_context == 'commit' and committed: self.main_frame.clear_commit_message()
# --- Azioni post-successo specifiche ---
if task_context == 'commit' and committed:
if hasattr(self, "main_frame"): self.main_frame.clear_commit_message()
if task_context == 'create_branch' and new_branch_context:
if self.main_frame.ask_yes_no("Checkout?", f"Switch to new branch '{new_branch_context}'?"):
if hasattr(self, "main_frame") and self.main_frame.ask_yes_no("Checkout?", f"Switch to new branch '{new_branch_context}'?"):
# Avvia checkout asincrono del nuovo branch
self.checkout_branch(branch_to_checkout=new_branch_context, repo_path_override=repo_path_for_updates)
elif self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
elif self.refresh_commit_history not in refresh_list:
# Se non fa checkout, assicurati che la history venga aggiornata
refresh_list.append(self.refresh_commit_history)
# Trigger refresh asincroni raccolti
# --- Trigger finale dei refresh asincroni raccolti ---
if repo_path_for_updates and refresh_list:
log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes for '{task_context}'", func_name="_check_completion_queue")
log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes after '{task_context}'", func_name=func_name)
for refresh_func in refresh_list:
try: refresh_func()
except Exception as ref_e: log_handler.log_error(f"Error triggering {refresh_func.__name__}: {ref_e}", func_name="_check_completion_queue")
try:
# Chiama la funzione di refresh (es. self.refresh_tag_list)
refresh_func()
except Exception as ref_e:
log_handler.log_error(f"Error triggering {getattr(refresh_func, '__name__', 'refresh function')}: {ref_e}", func_name=func_name)
elif refresh_list:
log_handler.log_warning("Cannot trigger UI refreshes: Repo path unavailable.", func_name="_check_completion_queue")
# Se la lista refresh non è vuota ma manca il path, logga warning
log_handler.log_warning("Cannot trigger post-action UI refreshes: Repo path unavailable.", func_name=func_name)
elif status == 'warning':
# (...Gestione warning esistente...)
self.main_frame.show_warning("Operation Info", message)
# Gestione warning generica: mostra popup
if hasattr(self, "main_frame"): self.main_frame.show_warning("Operation Info", message)
# Logica specifica per warning "already prepared"
if "already prepared" in message:
# Trigger refresh dopo warning "already prepared"
if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
# Avvia refresh dopo warning
if repo_path_for_updates and refresh_list:
log_handler.log_debug("Triggering refreshes after 'already prepared' warning.", func_name=func_name)
for rf in refresh_list: rf()
elif status == 'error':
# (...Gestione errori esistente: popup, aggiorna liste GUI con errore...)
# Gestione errori generica (esclusi check_connection, interactive_auth, pull_conflict gestiti sopra)
log_handler.log_error(f"Error reported for task '{task_context}': {message}", func_name=func_name)
error_details = f"{message}\n({type(exception).__name__}: {exception})" if exception else message
if is_conflict and repo_path_conflict: self.main_frame.show_error("Merge Conflict", f"Conflict occurred.\nResolve in:\n{repo_path_conflict}\nThen commit.")
elif exception and "Uncommitted changes" in str(exception): self.main_frame.show_warning("Action Blocked", f"{exception}\nCommit or stash first.")
else: self.main_frame.show_error("Error: Operation Failed", error_details)
if task_context == 'refresh_tags': self.main_frame.update_tag_list([("(Error)", "")])
elif task_context == 'refresh_branches': self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([])
elif task_context == 'refresh_history': self.main_frame.update_history_display(["(Error retrieving history)"])
elif task_context == 'refresh_changes': self.main_frame.update_changed_files_list(["(Error refreshing changes)"])
# Aggiorna indicatore auth in caso di errore specifico
elif task_context == 'apply_remote_config': self._update_gui_auth_status('unknown_error') # Errore durante l'applicazione della config
# Gestione errore per fetch_remote
if task_context == 'fetch_remote':
auth_related_error = False
conn_related_error = False
if isinstance(exception, GitCommandError) and exception.stderr:
stderr_low = exception.stderr.lower()
if any(e in stderr_low for e in ["authentication failed", "permission denied", "could not read"]): auth_related_error = True
if any(e in stderr_low for e in ["repository not found", "could not resolve host"]): conn_related_error = True
# Aggiorna indicatore auth
if auth_related_error: self._update_gui_auth_status('failed')
elif conn_related_error: self._update_gui_auth_status('connection_failed')
else: self._update_gui_auth_status('unknown_error')
# Mostra popup
if hasattr(self, "main_frame"): self.main_frame.show_error("Fetch Error", f"{message}")
# Gestione errore per PULL (non conflitto)
elif task_context == 'pull_remote':
# Errore durante il pull (es. auth, connessione, upstream)
auth_related_error = False
conn_related_error = False
upst_related_error = False
if isinstance(exception, GitCommandError) and exception.stderr:
stderr_low = exception.stderr.lower()
if any(e in stderr_low for e in ["authentication failed", "permission denied", "could not read"]): auth_related_error = True
if any(e in stderr_low for e in ["repository not found", "could not resolve host"]): conn_related_error = True
if any(e in stderr_low for e in ["no tracking information", "unrelated histories"]): upst_related_error = True
# Aggiorna indicatore auth/conn
if auth_related_error: self._update_gui_auth_status('failed')
elif conn_related_error: self._update_gui_auth_status('connection_failed')
else: self._update_gui_auth_status('unknown_error') # Errore generico pull
# Mostra popup
if hasattr(self, "main_frame"): self.main_frame.show_error("Pull Error", f"{message}")
# Gestione errori per altri task
else:
# Mostra popup specifici o generico
if is_conflict and repo_path_conflict and task_context == 'fetch_bundle':
if hasattr(self, "main_frame"): self.main_frame.show_error("Merge Conflict", f"Conflict occurred during bundle fetch.\nResolve in:\n{repo_path_conflict}\nThen commit.")
elif exception and "Uncommitted changes" in str(exception):
if hasattr(self, "main_frame"): self.main_frame.show_warning("Action Blocked", f"{exception}\nCommit or stash first.")
else:
if hasattr(self, "main_frame"): self.main_frame.show_error("Error: Operation Failed", error_details)
# Aggiorna liste GUI con stato errore (se applicabile al task)
if task_context == 'refresh_tags':
if hasattr(self, "main_frame"): self.main_frame.update_tag_list([("(Error)", "")])
elif task_context == 'refresh_branches':
if hasattr(self, "main_frame"):
self.main_frame.update_branch_list([], None)
self.main_frame.update_history_branch_filter([])
elif task_context == 'refresh_history':
if hasattr(self, "main_frame"): self.main_frame.update_history_display(["(Error retrieving history)"])
elif task_context == 'refresh_changes':
if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(["(Error refreshing changes)"])
elif task_context == 'apply_remote_config':
self._update_gui_auth_status('unknown_error') # Errore durante apply config
# Log finale solo se non è stata gestita una ricorsione/nuovo avvio
if should_reenable_now:
log_handler.log_debug(f"Finished processing result for context '{task_context}'.", func_name="_check_completion_queue")
log_handler.log_debug(f"Finished processing result for context '{task_context}'.", func_name=func_name)
except queue.Empty:
# Coda vuota, riprogramma
if self.master.winfo_exists():
# Coda vuota, riprogramma check se la finestra esiste ancora
if hasattr(self, "master") and self.master.winfo_exists():
self.master.after(self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context)
except Exception as e:
# Errore nel processare la coda
log_handler.log_exception(f"Critical error processing completion queue for {task_context}: {e}", func_name="_check_completion_queue")
# Errore critico nel processare la coda stessa
log_handler.log_exception(f"Critical error processing completion queue for {task_context}: {e}", func_name=func_name)
# Tenta recupero GUI
try:
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
self.main_frame.set_action_widgets_state(tk.NORMAL)
self.main_frame.set_action_widgets_state(tk.NORMAL) # Tenta riabilitazione
self.main_frame.update_status_bar("Error processing async result.", bg_color=self.main_frame.STATUS_RED, duration_ms=10000)
except: pass
except Exception as recovery_e:
log_handler.log_error(f"Failed to recover GUI after queue processing error: {recovery_e}", func_name=func_name)
# --- Helper Methods (interni alla classe) ---
def _generate_next_tag_suggestion(self, svn_path: str) -> str:

270
ToDo.md
View File

@ -1,203 +1,95 @@
Ottima domanda! È sempre utile fare un passo indietro e pensare a possibili miglioramenti o funzionalità aggiuntive. Discutiamone:
Ottima scelta di priorità! Queste coprono i casi d'uso fondamentali per l'interazione con un repository remoto. Analizziamole e vediamo come si traducono in operazioni Git e funzionalità della nostra applicazione, aggiungendo qualche altra proposta utile.
**Potenziali Miglioramenti e Funzionalità Mancanti:**
**Analisi delle Priorità e Operazioni Git Corrispondenti:**
1. **Gestione Conflitti (GUI):**
* **Situazione Attuale:** Quando "Fetch from Bundle" causa un conflitto, lo script lo rileva, logga l'errore e mostra un messaggio all'utente dicendo di risolvere manualmente.
* **Possibile Miglioramento:** Potrebbe essere *molto* utile avere un'indicazione visiva più chiara nella GUI quando si verifica un conflitto (magari un'icona di stato diversa, un messaggio persistente in una status bar). Ancora più avanzato (e complesso) sarebbe integrare un semplice strumento di visualizzazione/risoluzione dei conflitti direttamente nell'applicazione, anche se questo aumenterebbe significativamente la complessità. Un primo passo potrebbe essere semplicemente elencare i file in conflitto nell'area log o in un popup dedicato.
1. **Download Completo (Clone):**
* **Caso d'uso:** Hai l'URL di un repository esistente su Gitea e vuoi crearne una copia locale completa in una nuova directory vuota.
* **Operazione Git:** `git clone <remote_url> <local_directory_path>`
* **Implementazione App:**
* **GUI:** Potrebbe essere un pulsante dedicato "Clone Remote Repository" (magari in una sezione separata o nella tab "Repository/Bundle" dato che è un'operazione di creazione locale). Richiederebbe campi per inserire l'URL remoto e scegliere la directory locale di destinazione (che deve essere vuota o non esistente).
* **Backend:**
* `GitCommands`: Aggiungere `git_clone(remote_url, local_dir)`. Deve gestire l'output (progresso) e gli errori (URL non valido, dir non vuota, autenticazione fallita).
* `RemoteActionHandler` (o `ActionHandler`): `execute_clone_remote(...)`.
* `async_workers`: `run_clone_remote_async(...)`.
* `GitUtility`: Callback, avvio worker, gestione risultato.
2. **Bundle Incrementali:**
* **Situazione Attuale:** `git bundle create --all` crea un bundle completo ogni volta. Questo è sicuro ma può essere inefficiente se le modifiche tra le sincronizzazioni sono piccole e il repository è grande.
* **Possibile Miglioramento:** Esplorare l'uso dei bundle incrementali. `git bundle create mio_update.bundle <ultimo_tag_o_commit_noto_all_altra_parte>..HEAD`. Questo richiede di tenere traccia dell'ultimo stato sincronizzato (magari salvando l'hash dell'ultimo commit importato/esportato nel profilo?). Questo renderebbe i trasferimenti molto più veloci per grandi repository.
* **Complessità:** Richiede una gestione dello stato più sofisticata.
2. **Upload Completo Iniziale (Primo Push):**
* **Caso d'uso:** Hai un repository locale esistente (con la sua history) e un repository remoto appena creato e vuoto su Gitea. Vuoi inviare tutta la history e i branch locali al remoto.
* **Operazione Git:**
* Assicurarsi che il remote sia configurato (`git remote add origin <url>` - già fatto con "Apply Config").
* Eseguire il push del branch principale (es. `main` o `master`) impostando l'upstream: `git push -u origin main` (o `master`). L'opzione `-u` crea il branch remoto se non esiste e imposta il collegamento per futuri push/pull.
* (Opzionale ma raccomandato) Eseguire il push di tutti i tag: `git push origin --tags`.
* **Implementazione App:**
* Questa funzionalità si integra bene con il pulsante **"Push (Current Branch)"** che avevamo già previsto come essenziale. Dobbiamo solo assicurarci che la logica di backend gestisca il caso del "primo push" usando l'opzione `-u` (o `--set-upstream`).
* Il pulsante **"Push Tags"** copre l'invio dei tag.
* **GUI:** I pulsanti "Push (Current Branch)" e "Push Tags" nella tab "Remote Repository".
* **Backend:**
* `GitCommands`: Implementare `git_push(remote, branch, set_upstream=False)` e `git_push_tags(remote)`.
* `RemoteActionHandler`: `execute_remote_push` (che rileva se l'upstream non è impostato e passa `set_upstream=True` a `git_push`) e `execute_push_tags`.
* `async_workers`: `run_push_remote_async`, `run_push_tags_async`.
* `GitUtility`: Callback, avvio worker, gestione risultato (incluso l'errore "push rifiutato").
3. **Gestione Errori di `git rm --cached` nell'Untrack Automatico:**
* **Situazione Attuale:** Se un batch di `git rm --cached` fallisce, l'intero processo si interrompe.
* **Possibile Miglioramento:** Si potrebbe decidere di rendere l'operazione più resiliente: se un batch fallisce, loggare l'errore, *continuare* con i batch successivi e alla fine riportare un successo parziale o un avviso, magari elencando i file che non è riuscito a untraccare. Il commit automatico verrebbe comunque creato per i file untracciati con successo. Questo eviterebbe che un singolo file problematico blocchi l'untracking di tutti gli altri.
3. **Aggiornamento Remoto (Push Modifiche):**
* **Caso d'uso:** Hai fatto commit locali su un branch che è già collegato a un branch remoto (upstream). Vuoi inviare i nuovi commit locali al remoto.
* **Operazione Git:** `git push origin <nome-branch-locale>` (o semplicemente `git push` se l'upstream è correttamente configurato).
* **Implementazione App:**
* Questa è coperta esattamente dallo stesso pulsante **"Push (Current Branch)"** del punto 2. La logica in `RemoteActionHandler.execute_remote_push` non passerà `set_upstream=True` se rileva che l'upstream è già configurato.
* Idem per i tag con il pulsante **"Push Tags"**.
4. **Visualizzazione Modifiche (Diff):**
* **Situazione Attuale:** L'utente non ha modo di vedere quali file sono stati modificati direttamente dall'interno del tool prima di fare un commit (manuale o pre-tag).
* **Possibile Miglioramento:** Aggiungere una sezione (magari una nuova scheda o un pannello nella scheda "Commit") che esegua `git status --short` o `git diff --name-status` e mostri l'elenco dei file modificati, aggiunti, cancellati. Ancora più avanzato sarebbe mostrare il diff effettivo per il file selezionato. Questo aiuterebbe l'utente a scrivere messaggi di commit più accurati.
4. **Confronto Locale vs Remoto:**
* **Caso d'uso:** Vuoi sapere cosa è cambiato localmente rispetto al remoto e viceversa, prima di fare pull o push.
* **Operazioni Git:** Questo richiede più comandi:
* **Vedere commit locali non presenti sul remoto:** `git log origin/<branch>..HEAD` (mostra i commit fatti localmente dopo l'ultimo stato conosciuto del branch remoto).
* **Vedere commit remoti non presenti localmente:** `git fetch origin` (per aggiornare la conoscenza del remoto) seguito da `git log HEAD..origin/<branch>` (mostra i commit sul remoto che non hai ancora localmente).
* **Vedere differenze nei file (tra working dir e remoto, o tra branch locali e remoti):** Questo è più complesso. `git diff origin/<branch>` mostra le differenze tra il tuo branch locale *attuale* e il branch remoto. `git diff --stat origin/<branch>` dà solo un riepilogo. Per differenze tra working dir e remoto servirebbe un fetch e poi un diff con `origin/<branch>`.
* **Implementazione App:**
* **GUI:** Potrebbe essere una sezione dedicata nella tab "Remote Repository" o una funzionalità integrata con la history e la lista dei changed files. Potremmo avere:
* Un'indicazione "X commits ahead, Y commits behind" per il branch corrente rispetto al suo upstream (richiede `git status` o `git rev-list`).
* Un pulsante "Compare with Remote..." che esegue un `git fetch` seguito da un `git log origin/<branch>..HEAD` e `git log HEAD..origin/<branch>`, mostrando i risultati magari in un popup o nell'area log.
* Integrare la possibilità di fare `diff` con `origin/<branch>` nel Diff Viewer esistente (ma richiede prima un fetch).
* **Backend:**
* `GitCommands`: Aggiungere metodi per `git log <range>`, `git fetch`, `git rev-list --count --left-right <locale>...<remoto>`.
* `RemoteActionHandler`/`async_workers`: Logica per combinare fetch e log/rev-list.
* **Priorità:** Questa funzionalità è utile ma più complessa da visualizzare bene. Potrebbe essere implementata *dopo* le operazioni base di push/pull/fetch/clone.
5. **Gestione Branch Remoti (se Applicabile):**
* **Situazione Attuale:** Il tool si concentra sui branch locali e sui bundle. Non gestisce direttamente interazioni con repository remoti (tipo GitHub/GitLab).
* **Considerazione:** Sebbene lo scopo principale sia la sincronizzazione offline, potrebbe esserci uno scenario in cui la macchina "origine" interagisce anche con un remote. Aggiungere funzionalità `push`/`pull`/`fetch` standard potrebbe essere utile per alcuni, ma forse snaturerebbe l'obiettivo primario del tool focalizzato sui bundle.
**Altre Funzionalità Utili (Proposte):**
6. **Opzioni di Pulizia:**
* **Situazione Attuale:** Non ci sono comandi di pulizia integrati.
* **Possibile Miglioramento:** Aggiungere pulsanti per eseguire comandi Git utili come `git clean -fdx` (per rimuovere file non tracciati e ignorati - **pericoloso, da usare con cautela!**) o `git gc --prune=now --aggressive` (per ottimizzare il repository). Questi dovrebbero avere conferme molto chiare sui rischi.
5. **Fetch:**
* **Caso d'uso:** Aggiornare la conoscenza locale dello stato del repository remoto *senza* modificare i propri file o il branch corrente. Utile prima di fare `pull` o per vedere se ci sono novità.
* **Operazione Git:** `git fetch <remote_name>` (spesso `git fetch origin`). Potrebbe includere l'opzione `--prune` per rimuovere i riferimenti locali a branch cancellati sul remoto.
* **Implementazione App:** (Già prevista come essenziale)
* **GUI:** Pulsante "Fetch" nella tab "Remote Repository".
* **Backend:** `GitCommands.git_fetch`, `RemoteActionHandler.execute_remote_fetch`, `async_workers.run_fetch_remote_async`.
* **Effetti:** Dopo un fetch, la history (se filtrata per "All") e le liste di branch/tag remoti (se implementate) mostrerebbero le novità. L'indicatore "ahead/behind" (se implementato) si aggiornerebbe.
7. **Interfaccia Utente (Piccoli Ritocchi):**
* **Progresso Operazioni Lunghe:** Per operazioni come la creazione di bundle grandi o il backup, una progress bar (anche indeterminata) darebbe un feedback migliore rispetto al solo log testuale. (Richiederebbe però di reintrodurre un po' di threading/async per non bloccare la GUI).
* **Stato Pulsanti:** Rivedere attentamente quando i pulsanti dovrebbero essere abilitati/disabilitati per guidare l'utente nel flusso corretto (ad esempio, il pulsante "Commit" potrebbe essere disabilitato se `git status` non riporta modifiche).
* **Status Bar:** Una piccola area in basso (sotto l'area log) per messaggi di stato rapidi ("Ready", "Operation in progress...", "Conflict detected", "Last backup: ...") potrebbe essere utile.
6. **Pull (Aggiornamento Locale):**
* **Caso d'uso:** Scaricare le modifiche dal branch remoto corrispondente al branch locale corrente e integrarle (merge o rebase) nel branch locale.
* **Operazione Git:** `git pull origin <branch>` (o `git pull`).
* **Implementazione App:** (Già prevista come essenziale)
* **GUI:** Pulsante "Pull (Current Branch)".
* **Backend:** `GitCommands.git_pull`, `RemoteActionHandler.execute_remote_pull`, `async_workers.run_pull_remote_async`. La gestione dei **conflitti** qui è cruciale: l'applicazione deve rilevare un fallimento dovuto a conflitto e informare l'utente che deve risolverlo manualmente (non tenteremo la risoluzione automatica dei conflitti dall'app).
8. **Configurazione Avanzata Bundle:**
* **Situazione Attuale:** Usa sempre `--all` per la creazione.
* **Possibile Miglioramento:** Permettere all'utente di specificare riferimenti specifici (branch/tag) da includere nel bundle, invece di usare sempre `--all`, tramite un'interfaccia o opzioni nel profilo.
7. **Visualizzazione Branch/Tag Remoti:**
* **Caso d'uso:** Vedere quali branch e tag esistono sul server remoto.
* **Operazione Git:** `git fetch` seguito da `git branch -r` per i branch e `git tag -l` (i tag sono condivisi, ma `ls-remote` è più preciso per solo i remoti). `git ls-remote --heads <remote>` e `git ls-remote --tags <remote>` sono alternative che non richiedono un fetch completo ma solo una connessione.
* **Implementazione App:** (Prevista come "Importante/Utile")
* **GUI:** Liste separate nella tab "Remote Repository" con pulsanti "Refresh".
* **Backend:** Nuovi metodi in `GitCommands` (`git_list_remote_branches`, `git_list_remote_tags` usando `ls-remote`), nuovi worker asincroni, aggiornamento GUI.
9. **Internazionalizzazione (i18n):**
* **Situazione Attuale:** L'interfaccia è solo in inglese (testi nei widget). I messaggi di log e i commenti sono in inglese, ma le interazioni con te sono in italiano.
* **Possibile Miglioramento:** Se dovesse essere usato da altri, separare le stringhe dell'interfaccia in file di traduzione per supportare più lingue.
**Piano di Implementazione Proposto:**
**Discussione:**
Basandomi sulle tue priorità e aggiungendo Fetch/Pull che sono complementari e fondamentali, propongo questo ordine:
* **Priorità Alta (Secondo Me):**
* Migliore gestione/visualizzazione dei conflitti (almeno elencare i file).
* Raffinamento dello stato dei pulsanti GUI.
* Resilienza opzionale per `git rm --cached` in batch.
* **Priorità Media:**
* Visualizzazione delle modifiche (`git status`/`diff`).
* Bundle incrementali (grande vantaggio per repo grandi, ma più complesso).
* Status bar.
* **Priorità Bassa/Opzionale:**
* Opzioni di pulizia (`git clean`, `gc`).
* Gestione remoti standard.
* Configurazione avanzata bundle.
* Internazionalizzazione.
* Progress bar (richiede più lavoro strutturale).
1. **(Già Fatto)** Configurazione Remote nel Profilo e Pulsante "Apply Config".
2. **(Già Fatto)** Funzionalità "Check Connection & Auth" con opzione interattiva.
3. **Implementare Fetch:** Pulsante + logica backend. Questo ci permette di aggiornare la conoscenza del remoto.
4. **Implementare Pull (Current Branch):** Pulsante + logica backend, con **gestione base dei conflitti** (rilevamento e messaggio all'utente).
5. **Implementare Push (Current Branch):** Pulsante + logica backend, con gestione del **primo push (`-u`)** e degli errori comuni (push rifiutato).
6. **Implementare Push Tags:** Pulsante + logica backend.
7. **Implementare Clone:** Pulsante + logica backend. Questo è separato perché agisce *prima* che un repository locale sia "attivo" nell'interfaccia principale per un profilo. Potrebbe richiedere una finestra di dialogo dedicata o una modalità specifica dell'applicazione.
8. **(Successivo)** Implementare Confronto Locale/Remoto (indicatori ahead/behind, log specifici).
9. **(Successivo)** Implementare Visualizzazione Branch/Tag Remoti.
Cosa ne pensi? Quali di questi punti ti sembrano più interessanti o utili per i tuoi casi d'uso?
Questo piano costruisce le funzionalità in modo logico: prima ci si connette e si configura (`Apply`, `Check`), poi si sincronizza (`Fetch`, `Pull`, `Push`), poi si gestiscono casi speciali (`Clone`, `Push Tags`), e infine si aggiungono funzionalità di visualizzazione/confronto avanzate.
ALTRE INDICAZIONI
Certamente! Ho analizzato attentamente il codice che hai fornito per l'applicazione `GitSvnSyncApp`. Ecco un riassunto della mia comprensione del suo funzionamento attuale e poi possiamo discutere come integrare la gestione dei repository remoti.
**Comprensione dell'Applicazione Attuale (`GitSvnSyncApp`)**
L'applicazione, scritta in Python con interfaccia grafica Tkinter, è progettata per facilitare la gestione di repository Git locali, con un focus particolare sulla sincronizzazione tra ambienti potenzialmente disconnessi (offline) tramite l'uso di file bundle Git.
**Componenti Chiave e Flusso di Lavoro:**
1. **Struttura Modulare:** Il codice è ben organizzato in moduli distinti, ognuno con una responsabilità specifica:
* `GitUtility.py`: Classe principale dell'applicazione (`GitSvnSyncApp`), punto di ingresso (`main`), orchestra l'interazione tra GUI e logica backend.
* `gui.py`: Definisce tutti gli elementi dell'interfaccia utente (`MainFrame` con `ttk.Notebook` per le schede, dialoghi modali, `Tooltip`, `GitignoreEditorWindow`, `DiffViewerWindow`, etc.).
* `config_manager.py`: Gestisce la lettura e scrittura del file di configurazione `git_svn_sync.ini`, che contiene i profili e le impostazioni associate (percorsi, nomi bundle, flag).
* `action_handler.py`: Contiene la logica di esecuzione per le azioni principali (Prepare, Create Bundle, Fetch Bundle, Commit, Tag, Branch, Backup, Untrack .gitignore). Fa da ponte tra la GUI e i comandi Git/Backup.
* `git_commands.py`: Classe wrapper che esegue i comandi `git` effettivi tramite `subprocess`. Gestisce l'esecuzione, il parsing dell'output, la registrazione (logging) e la gestione degli errori specifici di Git (`GitCommandError`). **Attualmente, si concentra su operazioni locali e gestione dei bundle.**
* `backup_handler.py`: Implementa la logica per creare backup ZIP della directory del repository, gestendo esclusioni di file e directory.
* `logger_config.py`: Imposta il sistema di logging per scrivere sia su file (`git_svn_sync.log`) sia su un widget di testo nella GUI.
* `diff_viewer.py`: Una finestra dedicata per visualizzare le differenze (`git diff`) tra la versione HEAD e quella della working directory per un file specifico.
* `profile_handler.py`: Sembra essere un modulo meno utilizzato o forse una versione precedente della gestione profili; l'interazione principale avviene tramite `ConfigManager` direttamente in `GitUtility`.
2. **Gestione Profili:** L'utente può definire e selezionare diversi profili. Ogni profilo memorizza:
* Percorso della copia di lavoro locale (`svn_working_copy_path` - il nome suggerisce un'origine SVN, ma l'app opera su un repository Git).
* Percorso di destinazione per i bundle (es. una chiavetta USB).
* Nomi dei file bundle (per creazione e fetch).
* Flag per autobackup e autocommit.
* Percorso per i backup.
* Regole di esclusione per i backup (estensioni e nomi di directory).
3. **Workflow Principale (Offline/Bundle):**
* **Preparazione:** `prepare_svn_for_git` inizializza un repository Git (`git init`) se non esiste e si assicura che `.svn` sia ignorato in `.gitignore`.
* **Creazione Bundle:** L'azione `create_git_bundle`:
* Opzionalmente esegue un backup ZIP.
* Opzionalmente esegue un `git add .` e `git commit`.
* Esegue `git bundle create --all <percorso_bundle>` per impacchettare l'intera storia del repository in un singolo file.
* **Fetch da Bundle:** L'azione `fetch_from_git_bundle`:
* Se la directory di destinazione non è un repo Git, esegue `git clone <percorso_bundle> <destinazione>` per creare il repository dal bundle.
* Se è già un repo Git:
* Opzionalmente esegue un backup ZIP.
* Esegue `git fetch <percorso_bundle>`.
* Esegue `git merge FETCH_HEAD --no-ff` per integrare le modifiche, gestendo potenziali conflitti (che vengono segnalati all'utente).
4. **Altre Funzionalità Git Locali:** L'applicazione fornisce un'interfaccia grafica per:
* Eseguire commit manuali (`git add .`, `git commit -m`).
* Gestire i tag (listare, creare tag annotati, fare checkout di un tag - stato detached HEAD).
* Gestire i branch locali (listare, creare, fare checkout).
* Visualizzare la cronologia dei commit (`git log`).
* Visualizzare i file modificati (`git status --short`).
* Aprire un visualizzatore di diff per un file modificato.
* Modificare `.gitignore` e untrackare automaticamente i file che diventano ignorati.
5. **Interfaccia Utente:** Utilizza Tkinter con `ttk` per un look più moderno, organizzato in schede (`Notebook`). L'interazione avviene tramite callback che collegano i widget (pulsanti, dropdown, etc.) ai metodi in `GitSvnSyncApp`, i quali poi delegano ad `ActionHandler` o `GitCommands`.
**Limitazione Attuale:**
Come hai giustamente intuito, l'applicazione **attualmente non ha alcuna funzionalità per interagire direttamente con repository Git remoti** (es. su GitHub, GitLab, Bitbucket, o server Git interni). Il flusso di lavoro si basa esclusivamente su operazioni locali e sulla sincronizzazione tramite file bundle.
---
**Proposta per l'Integrazione della Gestione Remota**
L'obiettivo è estendere l'applicazione per permettere operazioni standard come `clone`, `fetch`, `pull`, `push` verso repository remoti, affiancandole all'attuale sistema di bundle.
**Strategia di Integrazione:**
1. **Mantenere i Bundle:** Il workflow basato sui bundle è utile per scenari offline e va mantenuto. Le nuove funzionalità remote saranno aggiuntive.
2. **Configurazione (`config_manager.py`):**
* Dobbiamo aggiungere nuovi campi *opzionali* alla sezione di ciascun profilo nel file `.ini`:
* `remote_url`: L'URL del repository remoto (es. `https://github.com/tuo_utente/tuo_repo.git` o `git@github.com:tuo_utente/tuo_repo.git`).
* `remote_name`: Il nome locale per quel remote (convenzionalmente `origin`, ma potrebbe essere configurabile).
* Inizialmente, potremmo supportare un solo remote per profilo per semplicità.
3. **Comandi Git (`git_commands.py`):**
* È necessario implementare nuove funzioni che eseguano i comandi Git remoti:
* `git_clone_remote(url, destination_directory)`: Per clonare da un URL.
* `add_remote(working_directory, name, url)`: Per eseguire `git remote add <name> <url>`.
* `remove_remote(working_directory, name)`: Per eseguire `git remote remove <name>`.
* `list_remotes(working_directory)`: Per eseguire `git remote -v` e parsare i risultati.
* `git_fetch_remote(working_directory, remote_name)`: Per eseguire `git fetch <remote_name>`.
* `git_pull(working_directory, remote_name, branch_name)`: Per eseguire `git pull <remote_name> <branch_name>`. Richiede gestione attenta dei conflitti.
* `git_push(working_directory, remote_name, branch_name, force=False)`: Per eseguire `git push <remote_name> <branch_name>`. Richiede gestione dei rifiuti (es. non fast-forward) e opzione `--force` (da usare con cautela).
* Queste funzioni useranno `log_and_execute` e dovranno gestire nuovi tipi di errori (rete, autenticazione, conflitti, rifiuti).
4. **Logica delle Azioni (`action_handler.py`):**
* Creare metodi `execute_...` corrispondenti per orchestrare le nuove operazioni remote:
* `execute_clone_remote`: Prende URL e directory di destinazione.
* `execute_remote_add_or_update`: Prende nome e URL, verifica se esiste già, poi aggiunge o aggiorna (usando `git remote set-url`).
* `execute_remote_remove`: Prende il nome del remote da rimuovere.
* `execute_fetch_remote`: Prende il nome del remote.
* `execute_pull`: Prende remote e branch (potrebbe usare il branch corrente come default).
* `execute_push`: Prende remote e branch (potrebbe usare il branch corrente).
* Questi metodi recupereranno i dati necessari (URL, nomi) dal profilo o dalla GUI e chiameranno le funzioni appropriate in `git_commands.py`.
5. **Interfaccia Utente (`gui.py` e `GitUtility.py`):**
* **Configurazione:**
* Nella scheda "Repository / Bundle", aggiungere campi per "Remote Name" (es. `origin`) e "Remote URL".
* Aggiungere pulsanti accanto a questi campi: "Add/Update Remote", "Remove Remote".
* **Azioni:**
* **Opzione A (Scheda Esistente):** Aggiungere pulsanti "Fetch Remote", "Pull", "Push" nella scheda "Repository / Bundle" o forse nella scheda "Branches".
* **Opzione B (Nuova Scheda):** Creare una nuova scheda dedicata "Remote". Questa conterrebbe:
* Visualizzazione dei remote configurati (lista risultato di `git remote -v`).
* Pulsanti "Fetch", "Pull", "Push". Potrebbe avere dropdown per selezionare remote/branch specifici o operare sul remote/branch configurato/corrente. Questa opzione sembra più pulita.
* **Clonazione:** Un pulsante "Clone from URL" potrebbe essere aggiunto all'inizio, forse vicino alla gestione profili, come alternativa a "Prepare Repository" quando si inizia da un remote.
* **Feedback Utente:** La GUI deve:
* Indicare chiaramente l'inizio e la fine delle operazioni remote (che possono richiedere tempo).
* Mostrare messaggi di errore specifici per fallimenti di rete, autenticazione, conflitti di merge (da `pull`), rifiuti di push.
* Per i conflitti, istruire l'utente a risolverli manualmente (il diff viewer esistente può aiutare) e poi fare commit.
* Per i rifiuti di push, suggerire di fare `pull` prima.
* **Callbacks:** `GitUtility.py` avrà bisogno di nuovi metodi callback per gestire i click sui nuovi pulsanti remoti.
6. **Autenticazione (La Parte Difficile):**
* **Approccio Iniziale (Consigliato):** **Non gestire le credenziali direttamente nell'applicazione.** Fare affidamento sui meccanismi standard di Git:
* **SSH:** L'utente configura le chiavi SSH (`~/.ssh/id_rsa`, `~/.ssh/config`). Il comando `git` le userà automaticamente per URL `git@...`.
* **HTTPS:** L'utente configura un *credential helper* di Git (come Git Credential Manager su Windows/macOS/Linux, o `store`, `cache`). Quando `git` ha bisogno di username/password o token (PAT - Personal Access Token), interroga l'helper. L'applicazione Python, eseguendo `git` via `subprocess`, beneficerà implicitamente di questo helper.
* **Feedback:** Se un comando fallisce per autenticazione (spesso riconoscibile da messaggi specifici in `stderr`), la GUI deve mostrare un errore chiaro, suggerendo all'utente di controllare la configurazione delle chiavi SSH o del credential helper del proprio sistema.
* **Futuro:** Gestire l'autenticazione all'interno dell'app (es. prompt per token, memorizzazione sicura) è molto più complesso e richiede attenzione alla sicurezza. Meglio iniziare senza.
**Prossimi Passi (Discussione):**
* Cosa ne pensi di questa strategia generale? Ti sembra ragionevole aggiungere le funzionalità remote in questo modo, mantenendo il sistema a bundle?
* Preferiresti i pulsanti per le azioni remote nelle schede esistenti o in una nuova scheda dedicata "Remote"?
* Sei d'accordo con l'approccio iniziale di non gestire l'autenticazione direttamente nell'app e affidarsi agli helper esterni?
* Quale operazione remota ritieni più prioritaria da implementare (es. `clone`, `pull`/`push`, gestione `remote`)?
---
**Altre Idee Potenziali (Oltre alla Gestione Remota):**
* **Operazioni Asincrone:** Comandi Git lunghi (clone, fetch, push, bundle) bloccano la GUI. Si potrebbe refattorizzare l'esecuzione usando `threading` o `asyncio` per mantenere l'interfaccia reattiva, mostrando un indicatore di "lavori in corso". Questo è un cambiamento architetturale significativo.
* **Gestione Stash:** Aggiungere pulsanti per `git stash`, `git stash pop`, `git stash list`. Utile per salvare temporaneamente modifiche non committate.
* **Annulla Modifiche:** Aggiungere un'opzione (magari nel menu contestuale della lista file) per annullare le modifiche a un file (`git checkout -- <file>`) o a tutti i file (`git reset --hard HEAD` - MOLTO PERICOLOSO, richiede conferma forte).
* **Visualizzazione Grafica dei Branch:** Mostrare `git log --graph` invece di una lista testuale può essere più intuitivo, ma complesso da realizzare in Tkinter.
* **Integrazione SVN Reale:** Se l'obiettivo è anche interagire con SVN, servirebbero comandi `git svn` (un mondo a parte).
* **Miglioramenti UI/UX:** Piccoli affinamenti come indicatori di progresso, messaggi di stato più dettagliati, forse personalizzazione del tema.
Fammi sapere cosa ne pensi della proposta per i remoti e se qualcuna di queste altre idee ti interessa particolarmente!
Sei d'accordo con questo piano? Iniziamo con **Fetch**?

View File

@ -675,37 +675,66 @@ def run_apply_remote_config_async(
repo_path: str,
remote_name: str,
remote_url: str,
results_queue: queue.Queue
):
results_queue: queue.Queue,
):
# (Implementazione precedente invariata)
func_name = "run_apply_remote_config_async"
log_handler.log_debug(f"[Worker] Started: Apply Remote Config for '{remote_name}' in '{repo_path}'", func_name=func_name)
log_handler.log_debug(
f"[Worker] Started: Apply Remote Config for '{remote_name}' in '{repo_path}'",
func_name=func_name,
)
try:
success = remote_action_handler.apply_remote_config(repo_path, remote_name, remote_url)
success = remote_action_handler.apply_remote_config(
repo_path, remote_name, remote_url
)
message = f"Remote '{remote_name}' configuration applied successfully."
log_handler.log_info(f"[Worker] {message}", func_name=func_name)
results_queue.put({"status": "success", "result": success, "message": message})
except (GitCommandError, ValueError) as e:
log_handler.log_error(f"[Worker] EXCEPTION applying remote config: {e}", func_name=func_name)
results_queue.put({"status": "error", "exception": e, "message": f"Error applying remote config: {e}"})
log_handler.log_error(
f"[Worker] EXCEPTION applying remote config: {e}", func_name=func_name
)
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Error applying remote config: {e}",
}
)
except Exception as e:
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION applying remote config: {e}", func_name=func_name)
results_queue.put({"status": "error", "exception": e, "message": f"Unexpected error applying remote config: {type(e).__name__}"})
log_handler.log_exception(
f"[Worker] UNEXPECTED EXCEPTION applying remote config: {e}",
func_name=func_name,
)
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Unexpected error applying remote config: {type(e).__name__}",
}
)
finally:
log_handler.log_debug(f"[Worker] Finished: Apply Remote Config for '{remote_name}'", func_name=func_name)
log_handler.log_debug(
f"[Worker] Finished: Apply Remote Config for '{remote_name}'",
func_name=func_name,
)
def run_check_connection_async(
git_commands: GitCommands,
repo_path: str,
remote_name: str,
results_queue: queue.Queue
):
results_queue: queue.Queue,
):
"""
Worker to check remote connection and potential auth issues using 'git ls-remote'.
Does NOT prompt for credentials.
"""
func_name = "run_check_connection_async"
log_handler.log_debug(f"[Worker] Started: Check Connection/Auth for '{remote_name}' in '{repo_path}'", func_name=func_name)
log_handler.log_debug(
f"[Worker] Started: Check Connection/Auth for '{remote_name}' in '{repo_path}'",
func_name=func_name,
)
try:
# Esegui ls-remote catturando output, senza check=True
result = git_commands.git_ls_remote(repo_path, remote_name)
@ -730,36 +759,74 @@ def run_check_connection_async(
else:
# Errore: analizza stderr per capire la causa
stderr_lower = result.stderr.lower() if result.stderr else ""
log_handler.log_warning(f"[Worker] ls-remote failed (RC={result.returncode}). Stderr: {stderr_lower}", func_name=func_name)
log_handler.log_warning(
f"[Worker] ls-remote failed (RC={result.returncode}). Stderr: {stderr_lower}",
func_name=func_name,
)
# Controlla errori comuni di autenticazione/permessi
auth_errors = ["authentication failed", "permission denied", "could not read username", "fatal: could not read password"]
connection_errors = ["repository not found", "could not resolve host", "name or service not known", "network is unreachable"]
auth_errors = [
"authentication failed",
"permission denied",
"could not read username",
"fatal: could not read password",
]
connection_errors = [
"repository not found",
"could not resolve host",
"name or service not known",
"network is unreachable",
]
if any(err in stderr_lower for err in auth_errors):
message = f"Authentication required or failed for remote '{remote_name}'."
message = (
f"Authentication required or failed for remote '{remote_name}'."
)
log_handler.log_warning(f"[Worker] {message}", func_name=func_name)
results_queue.put(
# Stato speciale per triggerare il prompt interattivo
{"status": "auth_required", "result": "authentication needed", "message": message, "exception": GitCommandError(message, stderr=result.stderr)}
{
"status": "auth_required",
"result": "authentication needed",
"message": message,
"exception": GitCommandError(message, stderr=result.stderr),
}
)
elif any(err in stderr_lower for err in connection_errors):
message = f"Failed to connect to remote '{remote_name}': Repository or host not found/reachable."
log_handler.log_error(f"[Worker] {message}", func_name=func_name)
results_queue.put(
{"status": "error", "result": "connection_failed", "message": message, "exception": GitCommandError(message, stderr=result.stderr)}
{
"status": "error",
"result": "connection_failed",
"message": message,
"exception": GitCommandError(message, stderr=result.stderr),
}
)
else:
# Errore generico di Git
message = f"Failed to check remote '{remote_name}'. Check logs for details."
log_handler.log_error(f"[Worker] Unknown error checking remote. Stderr: {result.stderr}", func_name=func_name)
message = (
f"Failed to check remote '{remote_name}'. Check logs for details."
)
log_handler.log_error(
f"[Worker] Unknown error checking remote. Stderr: {result.stderr}",
func_name=func_name,
)
results_queue.put(
{"status": "error", "result": "unknown_error", "message": message, "exception": GitCommandError(message, stderr=result.stderr)}
{
"status": "error",
"result": "unknown_error",
"message": message,
"exception": GitCommandError(message, stderr=result.stderr),
}
)
except Exception as e:
# Errore imprevisto nell'esecuzione del worker stesso
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION checking connection: {e}", func_name=func_name)
log_handler.log_exception(
f"[Worker] UNEXPECTED EXCEPTION checking connection: {e}",
func_name=func_name,
)
results_queue.put(
{
"status": "error",
@ -769,21 +836,27 @@ def run_check_connection_async(
}
)
finally:
log_handler.log_debug(f"[Worker] Finished: Check Connection/Auth for '{remote_name}'", func_name=func_name)
log_handler.log_debug(
f"[Worker] Finished: Check Connection/Auth for '{remote_name}'",
func_name=func_name,
)
def run_interactive_auth_attempt_async(
git_commands: GitCommands,
repo_path: str,
remote_name: str,
results_queue: queue.Queue
):
results_queue: queue.Queue,
):
"""
Worker to attempt an interactive Git operation (fetch) to trigger credential prompts.
This worker intentionally does NOT capture output or hide the console.
"""
func_name = "run_interactive_auth_attempt_async"
log_handler.log_info(f"[Worker] Started: Interactive Auth Attempt for '{remote_name}' via Fetch in '{repo_path}'", func_name=func_name)
log_handler.log_info(
f"[Worker] Started: Interactive Auth Attempt for '{remote_name}' via Fetch in '{repo_path}'",
func_name=func_name,
)
try:
# Esegui git fetch in modalità interattiva (no capture, no hide)
result = git_commands.git_fetch_interactive(repo_path, remote_name)
@ -794,19 +867,31 @@ def run_interactive_auth_attempt_async(
message = f"Interactive authentication attempt for '{remote_name}' seems successful."
log_handler.log_info(f"[Worker] {message}", func_name=func_name)
results_queue.put(
{"status": "success", "result": "auth_attempt_success", "message": message}
{
"status": "success",
"result": "auth_attempt_success",
"message": message,
}
)
else:
# Fallimento (utente ha annullato, credenziali errate, altro errore)
message = f"Interactive authentication attempt for '{remote_name}' failed or was cancelled (RC={result.returncode})."
log_handler.log_warning(f"[Worker] {message}", func_name=func_name)
results_queue.put(
{"status": "error", "result": "auth_attempt_failed", "message": message, "exception": GitCommandError(message, stderr=None)} # Non abbiamo stderr qui
{
"status": "error",
"result": "auth_attempt_failed",
"message": message,
"exception": GitCommandError(message, stderr=None),
} # Non abbiamo stderr qui
)
except Exception as e:
# Errore imprevisto nell'esecuzione del worker
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during interactive auth attempt: {e}", func_name=func_name)
log_handler.log_exception(
f"[Worker] UNEXPECTED EXCEPTION during interactive auth attempt: {e}",
func_name=func_name,
)
results_queue.put(
{
"status": "error",
@ -816,12 +901,131 @@ def run_interactive_auth_attempt_async(
}
)
finally:
log_handler.log_debug(f"[Worker] Finished: Interactive Auth Attempt for '{remote_name}'", func_name=func_name)
log_handler.log_debug(
f"[Worker] Finished: Interactive Auth Attempt for '{remote_name}'",
func_name=func_name,
)
# --- Placeholder per future funzioni worker remote ---
# def run_fetch_remote_async(remote_action_handler, repo_path, remote_name, results_queue): pass
# def run_pull_remote_async(remote_action_handler, repo_path, remote_name, branch_name, results_queue): pass
def run_fetch_remote_async(
remote_action_handler: RemoteActionHandler, # Dipendenza dall'handler delle azioni remote
repo_path: str,
remote_name: str,
results_queue: queue.Queue,
):
"""
Worker function to execute 'git fetch' asynchronously.
Executed in a separate thread.
"""
func_name = "run_fetch_remote_async"
log_handler.log_debug(
f"[Worker] Started: Fetch Remote '{remote_name}' for '{repo_path}'",
func_name=func_name,
)
try:
# Chiama il metodo execute_remote_fetch che contiene la logica e l'analisi dell'errore
result_info = remote_action_handler.execute_remote_fetch(repo_path, remote_name)
# result_info contiene già {'status': '...', 'message': '...', 'exception': ...}
log_handler.log_info(
f"[Worker] Fetch result status for '{remote_name}': {result_info.get('status')}",
func_name=func_name,
)
# Metti il dizionario del risultato direttamente nella coda
results_queue.put(result_info)
except Exception as e:
# Cattura eccezioni impreviste sollevate da execute_remote_fetch stesso
# (es., errori di validazione o eccezioni non gestite all'interno)
log_handler.log_exception(
f"[Worker] UNEXPECTED EXCEPTION during fetch execution: {e}",
func_name=func_name,
)
# Metti un risultato di errore generico nella coda
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Unexpected error during fetch operation: {type(e).__name__}",
}
)
finally:
log_handler.log_debug(
f"[Worker] Finished: Fetch Remote '{remote_name}'", func_name=func_name
)
def run_pull_remote_async(
remote_action_handler: RemoteActionHandler, # Dipendenza per eseguire il pull
git_commands: GitCommands, # Necessario per ottenere il branch corrente
repo_path: str,
remote_name: str,
# branch_name non è più necessario passarlo qui, lo otteniamo nel worker
results_queue: queue.Queue
):
"""
Worker function to execute 'git pull' asynchronously.
Executed in a separate thread.
"""
func_name = "run_pull_remote_async"
log_handler.log_debug(f"[Worker] Started: Pull Remote '{remote_name}' for '{repo_path}'", func_name=func_name)
try:
# --- Ottieni il branch corrente ---
# È necessario conoscere il branch corrente per passarlo a execute_remote_pull
# (anche se git pull di base non lo richiede, la nostra logica di action lo usa)
# e per log/messaggi più chiari.
current_branch_name = git_commands.get_current_branch_name(repo_path) # Assumendo che questo metodo esista/venga aggiunto a GitCommands
if not current_branch_name:
# Se non riusciamo a determinare il branch (es. detached HEAD), non possiamo fare pull standard
raise ValueError("Cannot perform pull: Unable to determine current branch (possibly detached HEAD).")
log_handler.log_debug(f"[Worker] Current branch identified as: '{current_branch_name}'", func_name=func_name)
# --- Chiama l'Action Handler ---
# execute_remote_pull contiene la logica per:
# 1. Controllare modifiche non committate
# 2. Eseguire git pull
# 3. Analizzare l'esito (successo, conflitto, errore auth/conn/altro)
result_info = remote_action_handler.execute_remote_pull(
repo_path, remote_name, current_branch_name
)
# result_info è il dizionario restituito da execute_remote_pull
log_handler.log_info(f"[Worker] Pull result status for '{remote_name}': {result_info.get('status')}", func_name=func_name)
# --- Aggiungi informazioni extra per la gestione dei conflitti ---
if result_info.get('status') == 'conflict':
result_info['repo_path'] = repo_path # Assicura che il path sia nel risultato per il messaggio GUI
# Metti il dizionario del risultato direttamente nella coda
results_queue.put(result_info)
except (GitCommandError, ValueError) as e:
# Cattura errori dalla determinazione del branch o altri errori noti
log_handler.log_error(f"[Worker] Handled EXCEPTION during pull setup/execution: {e}", func_name=func_name)
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Pull failed: {e}", # Usa messaggio eccezione
}
)
except Exception as e:
# Cattura eccezioni impreviste
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during pull operation: {e}", func_name=func_name)
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Unexpected error during pull operation: {type(e).__name__}",
}
)
finally:
log_handler.log_debug(f"[Worker] Finished: Pull Remote '{remote_name}'", func_name=func_name)
# def run_push_remote_async(remote_action_handler, repo_path, remote_name, branch_name, results_queue): pass
# def run_push_tags_async(remote_action_handler, repo_path, remote_name, results_queue): pass

View File

@ -63,7 +63,7 @@ class GitCommands:
log_output_level: int = logging.INFO,
# ---<<< NUOVI PARAMETRI >>>---
capture: bool = True, # Cattura stdout/stderr?
hide_console: bool = True # Nascondi finestra console (Windows)?
hide_console: bool = True, # Nascondi finestra console (Windows)?
# ---<<< FINE NUOVI PARAMETRI >>>---
) -> subprocess.CompletedProcess:
"""
@ -92,7 +92,7 @@ class GitCommands:
log_handler.log_debug(
f"Executing in '{working_directory}': {command_str} "
f"(Capture={capture}, HideConsole={hide_console})", # Log nuovi parametri
func_name=func_name
func_name=func_name,
)
# --- Validazione Working Directory (invariato) ---
@ -105,7 +105,9 @@ class GitCommands:
else:
effective_cwd = os.path.abspath(working_directory)
if not os.path.isdir(effective_cwd):
msg = f"Working directory does not exist or is not a dir: {effective_cwd}"
msg = (
f"Working directory does not exist or is not a dir: {effective_cwd}"
)
log_handler.log_error(msg, func_name=func_name)
raise GitCommandError(msg, command=safe_command_parts)
# log_handler.log_debug(f"Effective CWD: {effective_cwd}", func_name=func_name) # Log meno verboso
@ -116,13 +118,13 @@ class GitCommands:
startupinfo = None
creationflags = 0
# Applica solo se richiesto E siamo su Windows
if hide_console and os.name == 'nt':
if hide_console and os.name == "nt":
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# CREATE_NO_WINDOW potrebbe essere troppo aggressivo e impedire prompt
# creationflags = subprocess.CREATE_NO_WINDOW
elif not hide_console and os.name == 'nt':
elif not hide_console and os.name == "nt":
# Se NON vogliamo nascondere, potremmo esplicitamente chiedere una nuova console
# per isolare l'input/output del comando interattivo
creationflags = subprocess.CREATE_NEW_CONSOLE
@ -145,17 +147,21 @@ class GitCommands:
errors="replace", # Gestione errori decodifica
timeout=timeout_seconds,
startupinfo=startupinfo, # Passa la configurazione (o None)
creationflags=creationflags # Passa i flag (o 0)
creationflags=creationflags, # Passa i flag (o 0)
)
log_handler.log_debug(
f"Command '{command_str}' finished. RC={result.returncode}",
func_name=func_name
func_name=func_name,
)
# --- 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>"
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
log_handler.log_debug(
f"Command successful (RC={result.returncode}). Output:\n"
@ -177,17 +183,32 @@ 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_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"Command timed out after {timeout_seconds}s: {command_str}",
func_name=func_name,
)
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)
raise GitCommandError(f"Timeout after {timeout_seconds}s.", command=safe_command_parts, stderr=e.stderr if capture else None) from e
raise GitCommandError(
f"Timeout after {timeout_seconds}s.",
command=safe_command_parts,
stderr=e.stderr if capture else None,
) from e
except subprocess.CalledProcessError as e:
# (Gestione errore comando fallito)
# Se l'output non è stato catturato, e.stdout/e.stderr saranno None
stderr_err = e.stderr.strip() if capture and e.stderr else "<stderr not captured>"
stdout_err = e.stdout.strip() if capture and e.stdout else "<stdout not captured>"
stderr_err = (
e.stderr.strip() if capture and e.stderr else "<stderr not captured>"
)
stdout_err = (
e.stdout.strip() 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}\nSTDERR: {stderr_err}\nSTDOUT: {stdout_err}"
@ -202,18 +223,31 @@ class GitCommands:
# (Gestione FileNotFoundError, PermissionError, Exception generica invariata)
except FileNotFoundError as e:
log_handler.log_error(f"FileNotFoundError for command: {safe_command_parts[0]}", func_name=func_name)
log_handler.log_error(
f"FileNotFoundError for command: {safe_command_parts[0]}",
func_name=func_name,
)
error_msg = f"Command not found: '{safe_command_parts[0]}'. Is Git installed/in PATH?"
log_handler.log_error(error_msg, func_name=func_name)
raise GitCommandError(error_msg, command=safe_command_parts) from e
except PermissionError as e:
log_handler.log_error(f"PermissionError executing in '{effective_cwd}': {e}", func_name=func_name)
log_handler.log_error(
f"PermissionError executing in '{effective_cwd}': {e}",
func_name=func_name,
)
error_msg = f"Permission denied executing command in '{effective_cwd}'."
log_handler.log_error(error_msg, func_name=func_name)
raise GitCommandError(error_msg, command=safe_command_parts, stderr=str(e)) from e
raise GitCommandError(
error_msg, command=safe_command_parts, stderr=str(e)
) from e
except Exception as e:
log_handler.log_exception(f"Unexpected Exception executing command {command_str}: {e}", func_name=func_name)
raise GitCommandError(f"Unexpected execution error: {e}", command=safe_command_parts) from e
log_handler.log_exception(
f"Unexpected Exception executing command {command_str}: {e}",
func_name=func_name,
)
raise GitCommandError(
f"Unexpected execution error: {e}", command=safe_command_parts
) from e
# --- Core Repo Operations (usano log_handler) ---
def prepare_svn_for_git(self, working_directory: str):
@ -928,14 +962,24 @@ class GitCommands:
log_handler.log_info("All untracking batches completed.", func_name=func_name)
return succeeded
def git_ls_remote(self, working_directory: str, remote_name: str) -> subprocess.CompletedProcess:
def git_ls_remote(
self, working_directory: str, remote_name: str
) -> subprocess.CompletedProcess:
"""
Executes 'git ls-remote <remote_name>' to check connection and list refs.
Captures output and hides console by default. Raises GitCommandError on failure.
"""
func_name = "git_ls_remote"
log_handler.log_debug(f"Running ls-remote for '{remote_name}' in '{working_directory}'", func_name=func_name)
cmd = ["git", "ls-remote", "--exit-code", remote_name] # --exit-code fa fallire se remote non raggiungibile
log_handler.log_debug(
f"Running ls-remote for '{remote_name}' in '{working_directory}'",
func_name=func_name,
)
cmd = [
"git",
"ls-remote",
"--exit-code",
remote_name,
] # --exit-code fa fallire se remote non raggiungibile
# Esegui catturando output e nascondendo console, solleva eccezione su errore
# Non impostiamo check=True qui, analizzeremo il CompletedProcess nel chiamante
result = self.log_and_execute(
@ -944,12 +988,13 @@ class GitCommands:
check=False, # Analizziamo noi il codice di ritorno e stderr
capture=True,
hide_console=True,
log_output_level=logging.DEBUG # Logga output solo a DEBUG
log_output_level=logging.DEBUG, # Logga output solo a DEBUG
)
return result
def git_fetch_interactive(self, working_directory: str, remote_name: str) -> subprocess.CompletedProcess:
def git_fetch_interactive(
self, working_directory: str, remote_name: str
) -> subprocess.CompletedProcess:
"""
Executes 'git fetch <remote_name>' allowing user interaction.
Does NOT capture output and tries to show a console window for prompts.
@ -957,7 +1002,7 @@ class GitCommands:
func_name = "git_fetch_interactive"
log_handler.log_info(
f"Running interactive fetch for '{remote_name}' in '{working_directory}'. User may see a terminal.",
func_name=func_name
func_name=func_name,
)
cmd = ["git", "fetch", remote_name]
# Esegui SENZA catturare output e SENZA nascondere la console
@ -1147,12 +1192,16 @@ class GitCommands:
"""Gets a dictionary of remote names and their fetch URLs."""
# (Implementazione precedente invariata)
func_name = "get_remotes"
log_handler.log_debug(f"Getting remotes for '{working_directory}'", func_name=func_name)
log_handler.log_debug(
f"Getting remotes for '{working_directory}'", func_name=func_name
)
cmd = ["git", "remote", "-v"]
remotes = {}
try:
# Usa le opzioni di default (capture=True, hide_console=True)
result = self.log_and_execute(cmd, working_directory, check=True, log_output_level=logging.DEBUG)
result = self.log_and_execute(
cmd, working_directory, check=True, log_output_level=logging.DEBUG
)
lines = result.stdout.strip().splitlines()
for line in lines:
parts = line.split()
@ -1160,78 +1209,200 @@ class GitCommands:
name = parts[0]
url = parts[1]
remotes[name] = url
log_handler.log_info(f"Found {len(remotes)} remotes: {list(remotes.keys())}", func_name=func_name)
log_handler.log_info(
f"Found {len(remotes)} remotes: {list(remotes.keys())}",
func_name=func_name,
)
return remotes
except GitCommandError as e:
# Gestione caso nessun remote trovato (non è un errore)
# Se check=True fallisce, è un errore; altrimenti controlla output
is_error = e.stderr and "fatal:" in e.stderr.lower() # Un vero errore git di solito è fatal
is_error = (
e.stderr and "fatal:" in e.stderr.lower()
) # Un vero errore git di solito è fatal
if not is_error and not remotes:
log_handler.log_info("No remotes found.", func_name=func_name)
return {}
else:
log_handler.log_error(f"Failed to get remotes: {e}", func_name=func_name)
log_handler.log_error(
f"Failed to get remotes: {e}", func_name=func_name
)
raise # Rilancia veri errori
except Exception as e:
log_handler.log_exception(f"Unexpected error getting remotes: {e}", func_name=func_name)
log_handler.log_exception(
f"Unexpected error getting remotes: {e}", func_name=func_name
)
raise GitCommandError(f"Unexpected error getting remotes: {e}") from e
def add_remote(self, working_directory: str, remote_name: str, remote_url: str) -> bool:
def add_remote(
self, working_directory: str, remote_name: str, remote_url: str
) -> bool:
"""Adds a new remote repository reference."""
# (Implementazione precedente invariata)
func_name = "add_remote"
log_handler.log_info(f"Adding remote '{remote_name}' -> '{remote_url}'", func_name=func_name)
log_handler.log_info(
f"Adding remote '{remote_name}' -> '{remote_url}'", func_name=func_name
)
cmd = ["git", "remote", "add", remote_name, remote_url]
try:
# Usa le opzioni di default (capture=True, hide_console=True, check=True)
self.log_and_execute(cmd, working_directory, check=True)
log_handler.log_info(f"Remote '{remote_name}' added successfully.", func_name=func_name)
log_handler.log_info(
f"Remote '{remote_name}' added successfully.", func_name=func_name
)
return True
except GitCommandError as e:
# Gestisce errore specifico "already exists"
stderr_low = e.stderr.lower() if e.stderr else ""
if "already exists" in stderr_low:
# Rilancia per farlo gestire al chiamante (RemoteActionHandler)
raise GitCommandError(f"Remote '{remote_name}' already exists.", command=cmd, stderr=e.stderr) from e
raise GitCommandError(
f"Remote '{remote_name}' already exists.",
command=cmd,
stderr=e.stderr,
) from e
else:
log_handler.log_error(f"Failed to add remote '{remote_name}': {e}", func_name=func_name)
log_handler.log_error(
f"Failed to add remote '{remote_name}': {e}", func_name=func_name
)
raise # Rilancia altri errori Git
except Exception as e:
log_handler.log_exception(f"Unexpected error adding remote '{remote_name}': {e}", func_name=func_name)
raise GitCommandError(f"Unexpected error adding remote: {e}", command=cmd) from e
log_handler.log_exception(
f"Unexpected error adding remote '{remote_name}': {e}",
func_name=func_name,
)
raise GitCommandError(
f"Unexpected error adding remote: {e}", command=cmd
) from e
def set_remote_url(self, working_directory: str, remote_name: str, remote_url: str) -> bool:
def set_remote_url(
self, working_directory: str, remote_name: str, remote_url: str
) -> bool:
"""Changes the URL of an existing remote."""
# (Implementazione precedente invariata)
func_name = "set_remote_url"
log_handler.log_info(f"Setting URL for remote '{remote_name}' to '{remote_url}'", func_name=func_name)
log_handler.log_info(
f"Setting URL for remote '{remote_name}' to '{remote_url}'",
func_name=func_name,
)
cmd = ["git", "remote", "set-url", remote_name, remote_url]
try:
# Usa le opzioni di default (capture=True, hide_console=True, check=True)
self.log_and_execute(cmd, working_directory, check=True)
log_handler.log_info(f"URL for remote '{remote_name}' set successfully.", func_name=func_name)
log_handler.log_info(
f"URL for remote '{remote_name}' set successfully.", func_name=func_name
)
return True
except GitCommandError as e:
# Gestisce errore specifico "no such remote"
stderr_low = e.stderr.lower() if e.stderr else ""
if "no such remote" in stderr_low:
raise GitCommandError(f"Remote '{remote_name}' does not exist, cannot set URL.", command=cmd, stderr=e.stderr) from e
raise GitCommandError(
f"Remote '{remote_name}' does not exist, cannot set URL.",
command=cmd,
stderr=e.stderr,
) from e
else:
log_handler.log_error(f"Failed to set URL for remote '{remote_name}': {e}", func_name=func_name)
log_handler.log_error(
f"Failed to set URL for remote '{remote_name}': {e}",
func_name=func_name,
)
raise # Rilancia altri errori Git
except Exception as e:
log_handler.log_exception(f"Unexpected error setting remote URL for '{remote_name}': {e}", func_name=func_name)
raise GitCommandError(f"Unexpected error setting remote URL: {e}", command=cmd) from e
log_handler.log_exception(
f"Unexpected error setting remote URL for '{remote_name}': {e}",
func_name=func_name,
)
raise GitCommandError(
f"Unexpected error setting remote URL: {e}", command=cmd
) from e
# --- Placeholder for future remote commands ---
def git_fetch(self, working_directory: str, remote_name: str):
# To be implemented: git fetch <remote_name> [--prune?]
pass
def git_fetch(
self, working_directory: str, remote_name: str, prune: bool = True
) -> subprocess.CompletedProcess:
"""
Executes 'git fetch <remote_name>' possibly with --prune.
Captures output and hides console by default.
Does NOT raise exception on non-zero exit code by default (check=False),
allowing the caller to analyze the result.
def git_pull(self, working_directory: str, remote_name: str, branch_name: str):
# To be implemented: git pull <remote_name> <branch_name>
pass
Args:
working_directory (str): Path to the repository.
remote_name (str): The name of the remote to fetch from.
prune (bool): If True, add '--prune' to remove stale remote-tracking branches.
Returns:
subprocess.CompletedProcess: The result of the command execution.
"""
func_name = "git_fetch"
log_handler.log_info(
f"Fetching from remote '{remote_name}' in '{working_directory}' (Prune={prune})",
func_name=func_name,
)
cmd = ["git", "fetch", remote_name]
if prune:
cmd.append(
"--prune"
) # Aggiunge opzione per pulire branch remoti non più esistenti
# Esegui catturando output, nascondendo console, ma NON sollevare eccezione su errore (check=False)
# Il worker analizzerà il codice di ritorno e stderr per capire l'esito.
result = self.log_and_execute(
command=cmd,
working_directory=working_directory,
check=False, # Importante: non sollevare eccezioni qui
capture=True,
hide_console=True,
log_output_level=logging.INFO, # Logga output di fetch (es. aggiornamenti branch) a INFO
)
return result
def git_pull(self, working_directory: str, remote_name: str, branch_name: str) -> subprocess.CompletedProcess:
"""
Executes 'git pull <remote_name> <branch_name>'.
This performs a fetch and then merges the fetched branch into the current local branch.
Captures output and hides console by default.
Does NOT raise exception on non-zero exit code by default (check=False),
allowing the caller to analyze the result for success, conflicts, or errors.
Args:
working_directory (str): Path to the repository.
remote_name (str): The name of the remote to pull from.
branch_name (str): The name of the local branch currently checked out,
which corresponds to the remote branch to merge.
Returns:
subprocess.CompletedProcess: The result of the command execution.
"""
func_name = "git_pull"
# Nota: Git pull implicitamente opera sul branch corrente se non specificato diversamente,
# ma specificare remote e branch rende il comando più esplicito e meno dipendente
# dalla configurazione upstream (anche se idealmente quella è impostata).
# Il branch_name qui è più per riferimento nel log e potenziali future opzioni.
# Il comando di base `git pull <remote_name>` di solito basta se l'upstream è settato.
# Per ora, manteniamo il comando semplice `git pull <remote_name>`.
# Se l'upstream non è settato, il comando fallirà e l'utente dovrà impostarlo
# (potremmo aggiungere una feature per questo in futuro).
log_handler.log_info(
f"Pulling from remote '{remote_name}' into current branch ('{branch_name}') "
f"in '{working_directory}'",
func_name=func_name
)
cmd = ["git", "pull", remote_name]
# Esegui catturando output, nascondendo console, ma NON sollevare eccezione (check=False)
# Il worker analizzerà il risultato per conflitti o altri errori.
result = self.log_and_execute(
command=cmd,
working_directory=working_directory,
check=False, # Fondamentale per rilevare conflitti (RC=1)
capture=True,
hide_console=True,
log_output_level=logging.INFO # Logga output (aggiornamenti, merge) a INFO
)
return result
def git_push(
self,
@ -1247,5 +1418,35 @@ class GitCommands:
# To be implemented: git push <remote_name> --tags
pass
def get_current_branch_name(self, working_directory: str) -> str | None:
"""
Gets the name of the currently checked-out branch.
Returns None if in detached HEAD state or on error.
"""
func_name = "get_current_branch_name"
log_handler.log_debug(f"Getting current branch name in '{working_directory}'", func_name=func_name)
# Usa 'git branch --show-current' (disponibile da Git 2.22+)
# In alternativa 'git rev-parse --abbrev-ref HEAD', che funziona anche prima
# ma può restituire 'HEAD' in detached state. 'symbolic-ref' è più robusto.
cmd = ["git", "symbolic-ref", "--short", "-q", "HEAD"] # -q sopprime errori se non è un branch (detached)
try:
# check=False perché può fallire legittimamente in detached HEAD (RC=1)
result = self.log_and_execute(cmd, working_directory, check=False, capture=True, hide_console=True, log_output_level=logging.DEBUG)
if result.returncode == 0 and result.stdout:
branch_name = result.stdout.strip()
log_handler.log_info(f"Current branch is '{branch_name}'", func_name=func_name)
return branch_name
elif result.returncode == 1: # Codice atteso per detached HEAD con symbolic-ref -q
log_handler.log_warning("Currently in detached HEAD state.", func_name=func_name)
return None
else: # Altro errore
log_handler.log_error(f"Failed to get current branch (RC={result.returncode}). Stderr: {result.stderr.strip() if result.stderr else 'N/A'}", func_name=func_name)
return None
except Exception as e:
log_handler.log_exception(f"Unexpected error getting current branch: {e}", func_name=func_name)
return None # Ritorna None anche per eccezioni impreviste
# --- END OF FILE git_commands.py ---

143
gui.py
View File

@ -442,10 +442,10 @@ class MainFrame(ttk.Frame):
refresh_changed_files_cb,
open_diff_viewer_cb,
add_selected_file_cb,
apply_remote_config_cb, # Callback per applicare la configurazione remota
apply_remote_config_cb,
check_connection_auth_cb,
# fetch_remote_cb, # Placeholder per futuro
# pull_remote_cb, # Placeholder per futuro
fetch_remote_cb,
pull_remote_cb,
# push_remote_cb, # Placeholder per futuro
# push_tags_remote_cb, # Placeholder per futuro
):
@ -479,8 +479,8 @@ class MainFrame(ttk.Frame):
self.initial_profile_sections = profile_sections_list
self.apply_remote_config_callback = apply_remote_config_cb
self.check_connection_auth_callback = check_connection_auth_cb
# self.fetch_remote_callback = fetch_remote_cb
# self.pull_remote_callback = pull_remote_cb
self.fetch_remote_callback = fetch_remote_cb
self.pull_remote_callback = pull_remote_cb
# self.push_remote_callback = push_remote_cb
# self.push_tags_remote_callback = push_tags_remote_cb
@ -623,69 +623,115 @@ class MainFrame(ttk.Frame):
frame.columnconfigure(1, weight=1)
# --- Sezione Configurazione ---
config_frame = ttk.LabelFrame(frame, text="Remote Configuration (Saved in Profile)", padding=(10, 5))
config_frame = ttk.LabelFrame(
frame, text="Remote Configuration (Saved in Profile)", padding=(10, 5)
)
config_frame.pack(pady=5, fill="x", expand=False)
config_frame.columnconfigure(1, weight=1)
# Remote URL
ttk.Label(config_frame, text="Remote URL:").grid(row=0, column=0, sticky=tk.W, padx=5, pady=3)
self.remote_url_entry = ttk.Entry(config_frame, textvariable=self.remote_url_var, width=70)
ttk.Label(config_frame, text="Remote URL:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=3
)
self.remote_url_entry = ttk.Entry(
config_frame, textvariable=self.remote_url_var, width=70
)
self.remote_url_entry.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=3)
self.create_tooltip(self.remote_url_entry, "URL of the remote repository (e.g., https://... or ssh://...).")
self.create_tooltip(
self.remote_url_entry,
"URL of the remote repository (e.g., https://... or ssh://...).",
)
# Remote Name
ttk.Label(config_frame, text="Local Name:").grid(row=1, column=0, sticky=tk.W, padx=5, pady=3)
self.remote_name_entry = ttk.Entry(config_frame, textvariable=self.remote_name_var, width=20)
ttk.Label(config_frame, text="Local Name:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=3
)
self.remote_name_entry = ttk.Entry(
config_frame, textvariable=self.remote_name_var, width=20
)
self.remote_name_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=3)
self.create_tooltip(self.remote_name_entry, f"Local alias for the remote (Default: '{DEFAULT_REMOTE_NAME}').")
self.create_tooltip(
self.remote_name_entry,
f"Local alias for the remote (Default: '{DEFAULT_REMOTE_NAME}').",
)
# ---<<< MODIFICA: Frame per bottoni a destra >>>---
# Mettiamo i bottoni di azione sulla configurazione in un frame separato a destra
config_action_frame = ttk.Frame(config_frame)
config_action_frame.grid(row=0, column=2, rowspan=2, sticky="ne", padx=(10, 5)) # Allineato Nord-Est
config_action_frame.grid(
row=0, column=2, rowspan=2, sticky="ne", padx=(10, 5)
) # Allineato Nord-Est
# Pulsante Applica Configurazione
self.apply_remote_config_button = ttk.Button(
config_action_frame,
text="Apply Config to Local Repo",
command=self.apply_remote_config_callback,
state=tk.DISABLED
state=tk.DISABLED,
)
self.apply_remote_config_button.pack(side=tk.TOP, pady=2, fill=tk.X) # Sopra
self.create_tooltip(self.apply_remote_config_button, "Add or update this remote configuration in the local .git/config file.")
self.create_tooltip(
self.apply_remote_config_button,
"Add or update this remote configuration in the local .git/config file.",
)
# ---<<< NUOVO: Pulsante Check Connection & Auth >>>---
self.check_auth_button = ttk.Button(
config_action_frame,
text="Check Connection / Auth",
command=self.check_connection_auth_callback, # Nuovo callback
state=tk.DISABLED # Abilitato quando repo pronto
state=tk.DISABLED, # Abilitato quando repo pronto
)
self.check_auth_button.pack(side=tk.TOP, pady=(5, 2), fill=tk.X) # Sotto Apply
self.create_tooltip(self.check_auth_button, "Verify connection and authentication status for the configured remote.")
self.create_tooltip(
self.check_auth_button,
"Verify connection and authentication status for the configured remote.",
)
# ---<<< NUOVO: Indicatore Stato Auth >>>---
# Usiamo un Label che cambierà colore e testo
self.auth_status_indicator_label = ttk.Label(
config_action_frame,
textvariable=self.remote_auth_status_var, # Collegato alla variabile
anchor=tk.CENTER,
relief=tk.SUNKEN,
padding=(5,2),
width=25 # Larghezza fissa per consistenza
padding=(5, 2),
width=25, # Larghezza fissa per consistenza
)
self.auth_status_indicator_label.pack(side=tk.TOP, pady=(2, 2), fill=tk.X)
# Tooltip iniziale (verrà aggiornato)
self.create_tooltip(self.auth_status_indicator_label, "Connection and authentication status.")
self.create_tooltip(
self.auth_status_indicator_label, "Connection and authentication status."
)
# Imposta colore iniziale (es. grigio)
self._update_auth_status_indicator('unknown')
# ---<<< FINE NUOVI ELEMENTI >>>---
self._update_auth_status_indicator("unknown")
# --- Sezione Azioni Remote ---
actions_frame = ttk.LabelFrame(frame, text="Remote Actions", padding=(10, 5))
actions_frame.pack(pady=10, fill="x", expand=False)
# (Placeholder per Fetch, Pull, Push...)
self.fetch_button = ttk.Button(
actions_frame,
text="Fetch",
command=self.fetch_remote_callback, # Usa il nuovo callback
state=tk.DISABLED, # Abilitato quando repo pronto e remote configurato? (O solo repo pronto?)
)
self.fetch_button.pack(side=tk.LEFT, padx=5, pady=5)
self.create_tooltip(
self.fetch_button,
"Download objects and references from the configured remote repository.",
)
self.pull_button = ttk.Button(
actions_frame,
text="Pull (Current Branch)",
command=self.pull_remote_callback, # Usa il nuovo callback
state=tk.DISABLED # Abilitato quando repo pronto e connesso?
)
self.pull_button.pack(side=tk.LEFT, padx=5, pady=5)
self.create_tooltip(self.pull_button, "Fetch from and integrate with the remote branch corresponding to the current local branch (merge or rebase).")
# self.create_tooltip(self.pull_button, ...)
# self.push_button = ttk.Button(actions_frame, text="Push (Current Branch)", command=self.push_remote_callback, state=tk.DISABLED)
# self.push_button.pack(side=tk.LEFT, padx=5, pady=5)
# self.create_tooltip(self.push_button, ...)
# self.push_tags_button = ttk.Button(actions_frame, text="Push Tags", command=self.push_tags_remote_callback, state=tk.DISABLED)
# self.push_tags_button.pack(side=tk.LEFT, padx=5, pady=5)
# self.create_tooltip(self.push_tags_button, ...)
return frame
@ -699,23 +745,23 @@ class MainFrame(ttk.Frame):
color = self.STATUS_DEFAULT_BG # Grigio/Default
tooltip = "Connection and authentication status."
if status == 'ok':
if status == "ok":
text = "Status: Connected"
color = self.STATUS_GREEN
tooltip = "Successfully connected and authenticated to the remote."
elif status == 'required':
elif status == "required":
text = "Status: Auth Required"
color = self.STATUS_YELLOW
tooltip = "Authentication needed. Use 'Check Connection' to attempt interactive login."
elif status == 'failed':
elif status == "failed":
text = "Status: Auth Failed"
color = self.STATUS_RED
tooltip = "Authentication failed. Check credentials or use 'Check Connection' to retry."
elif status == 'connection_failed':
elif status == "connection_failed":
text = "Status: Connection Failed"
color = self.STATUS_RED
tooltip = "Could not connect to the remote. Check URL and network."
elif status == 'unknown_error':
elif status == "unknown_error":
text = "Status: Error"
color = self.STATUS_RED
tooltip = "An unknown error occurred while checking the remote."
@ -726,7 +772,10 @@ class MainFrame(ttk.Frame):
label.config(background=color)
self.update_tooltip(label, tooltip) # Aggiorna anche il tooltip
except Exception as e:
log_handler.log_error(f"Failed to update auth status indicator GUI: {e}", func_name="_update_auth_status_indicator")
log_handler.log_error(
f"Failed to update auth status indicator GUI: {e}",
func_name="_update_auth_status_indicator",
)
def _create_repo_tab(self):
frame = ttk.Frame(self.notebook, padding=(10, 10))
@ -1614,16 +1663,23 @@ class MainFrame(ttk.Frame):
# ---<<< FINE MODIFICA >>>---
except tk.TclError:
# Errore nel trovare/selezionare l'elemento (es. listbox vuota o click strano)
log_handler.log_debug(f"TclError getting selected line for context menu.", func_name=func_name)
log_handler.log_debug(
f"TclError getting selected line for context menu.", func_name=func_name
)
return # Esce se non si può selezionare nulla
except Exception as e:
# Altri errori imprevisti
log_handler.log_error(f"Error getting selected line for context menu: {e}", func_name=func_name)
log_handler.log_error(
f"Error getting selected line for context menu: {e}",
func_name=func_name,
)
return
# Ora controlla se 'line' è stata ottenuta con successo
if line is None:
log_handler.log_debug(f"Could not retrieve line content at index {idx}.", func_name=func_name)
log_handler.log_debug(
f"Could not retrieve line content at index {idx}.", func_name=func_name
)
return # Esce se non siamo riusciti a ottenere il testo
# Ora 'line' è sicuramente definita se siamo arrivati qui
@ -1656,14 +1712,21 @@ class MainFrame(ttk.Frame):
)
# Disabilita Diff per Untracked (??), Ignored (!!), Deleted ( D)
diff_state = tk.DISABLED
if not is_untracked and not cleaned.startswith("!!") and not cleaned.startswith(" D") and can_diff:
if (
not is_untracked
and not cleaned.startswith("!!")
and not cleaned.startswith(" D")
and can_diff
):
diff_state = tk.NORMAL
self.changed_files_context_menu.add_command(
label="View Changes (Diff)",
state=diff_state,
command=lambda current_line=line: (
self.open_diff_viewer_callback(current_line) if diff_state == tk.NORMAL else None
self.open_diff_viewer_callback(current_line)
if diff_state == tk.NORMAL
else None
),
)
@ -1773,8 +1836,8 @@ class MainFrame(ttk.Frame):
self.autocommit_checkbox,
self.apply_remote_config_button,
self.check_auth_button,
# self.fetch_button, # Da aggiungere quando implementati
# self.pull_button,
self.fetch_button,
self.pull_button,
# self.push_button,
# self.push_tags_button,
]

View File

@ -144,13 +144,259 @@ class RemoteActionHandler:
# --- Placeholder for future remote methods ---
def execute_remote_fetch(self, repo_path: str, remote_name: str):
# To be implemented: Calls git_commands.git_fetch
pass
def execute_remote_fetch(self, repo_path: str, remote_name: str) -> dict:
"""
Executes 'git fetch' for the specified remote.
def execute_remote_pull(self, repo_path: str, remote_name: str, branch_name: str):
# To be implemented: Calls git_commands.git_pull, handles conflicts
pass
Args:
repo_path (str): Path to the local repository.
remote_name (str): The name of the remote to fetch (e.g., 'origin').
Returns:
dict: A dictionary containing the status and potential error details.
Example success: {'status': 'success', 'message': 'Fetch successful.'}
Example error: {'status': 'error', 'message': 'Auth failed', 'exception': GitCommandError}
Raises:
ValueError: If input arguments are invalid.
GitCommandError: Propagated from git_commands if fetch fails critically
(though git_fetch is called with check=False).
"""
func_name = "execute_remote_fetch"
log_handler.log_info(
f"Executing fetch for remote '{remote_name}' in '{repo_path}'",
func_name=func_name,
)
# --- Input Validation ---
if not repo_path or not os.path.isdir(repo_path):
raise ValueError(f"Invalid repository path: '{repo_path}'")
if not remote_name or remote_name.isspace():
raise ValueError("Remote name cannot be empty.")
if not os.path.exists(os.path.join(repo_path, ".git")):
raise ValueError(f"Directory '{repo_path}' is not a Git repository.")
result_info = {
"status": "unknown",
"message": "Fetch not completed.",
} # Default result
try:
# Chiama il metodo git_fetch (che ha check=False)
fetch_result = self.git_commands.git_fetch(
repo_path, remote_name, prune=True
)
# Analizza il risultato del comando fetch
if fetch_result.returncode == 0:
# Successo
result_info["status"] = "success"
result_info["message"] = (
f"Fetch from '{remote_name}' completed successfully."
)
# Potremmo analizzare stdout per dettagli, ma per ora basta il successo
log_handler.log_info(
f"Fetch successful for '{remote_name}'.", func_name=func_name
)
else:
# Errore durante il fetch: analizza stderr
result_info["status"] = "error"
stderr_lower = (
fetch_result.stderr.lower() if fetch_result.stderr else ""
)
log_handler.log_error(
f"Fetch command failed for '{remote_name}' (RC={fetch_result.returncode}). Stderr: {stderr_lower}",
func_name=func_name,
)
# Controlla errori specifici noti
auth_errors = [
"authentication failed",
"permission denied",
"could not read username",
"fatal: could not read password",
]
connection_errors = [
"repository not found",
"could not resolve host",
"name or service not known",
"network is unreachable",
]
if any(err in stderr_lower for err in auth_errors):
result_info["message"] = (
f"Authentication required or failed for remote '{remote_name}'."
)
# Crea un'eccezione fittizia o riusa quella originale se possibile
result_info["exception"] = GitCommandError(
result_info["message"], stderr=fetch_result.stderr
)
elif any(err in stderr_lower for err in connection_errors):
result_info["message"] = (
f"Failed to connect to remote '{remote_name}': Repository or host not found/reachable."
)
result_info["exception"] = GitCommandError(
result_info["message"], stderr=fetch_result.stderr
)
else:
# Errore generico di Git
result_info["message"] = (
f"Fetch from '{remote_name}' failed (RC={fetch_result.returncode}). Check logs."
)
result_info["exception"] = GitCommandError(
result_info["message"], stderr=fetch_result.stderr
)
except (GitCommandError, ValueError) as e:
# Cattura errori sollevati dalla validazione o da git_commands (se check=True fosse usato)
log_handler.log_error(
f"Error during fetch execution for '{remote_name}': {e}",
func_name=func_name,
)
result_info = {
"status": "error",
"message": f"Fetch failed: {e}",
"exception": e,
}
# Non rilanciamo qui, il worker gestirà l'errore tramite il dizionario restituito
except Exception as e:
# Cattura errori imprevisti
log_handler.log_exception(
f"Unexpected error during fetch for '{remote_name}': {e}",
func_name=func_name,
)
result_info = {
"status": "error",
"message": f"Unexpected fetch error: {type(e).__name__}",
"exception": e,
}
return result_info
def execute_remote_pull(self, repo_path: str, remote_name: str, current_branch_name: str) -> dict:
"""
Executes 'git pull' for the specified remote and current branch.
Detects success, merge conflicts, and other errors.
Args:
repo_path (str): Path to the local repository.
remote_name (str): The name of the remote to pull from (e.g., 'origin').
current_branch_name (str): The name of the currently checked-out local branch.
Returns:
dict: A dictionary containing the status ('success', 'conflict', 'error'),
a message, and optionally an exception.
Example success: {'status': 'success', 'message': 'Pull successful.'}
Example conflict: {'status': 'conflict', 'message': 'Merge conflict occurred.'}
Example error: {'status': 'error', 'message': 'Auth failed', 'exception': GitCommandError}
Raises:
ValueError: If input arguments are invalid.
"""
func_name = "execute_remote_pull"
log_handler.log_info(
f"Executing pull from remote '{remote_name}' into branch '{current_branch_name}' in '{repo_path}'",
func_name=func_name
)
# --- Input Validation ---
if not repo_path or not os.path.isdir(repo_path):
raise ValueError(f"Invalid repository path: '{repo_path}'")
if not remote_name or remote_name.isspace():
raise ValueError("Remote name cannot be empty.")
if not current_branch_name or current_branch_name.isspace():
# Potremmo provare a ottenerlo se non fornito, ma è meglio che il chiamante lo sappia
raise ValueError("Current branch name cannot be empty.")
if not os.path.exists(os.path.join(repo_path, ".git")):
raise ValueError(f"Directory '{repo_path}' is not a Git repository.")
# --- Controllo Modifiche Locali (Pre-Pull Check) ---
# È buona pratica non fare pull se ci sono modifiche non committate
try:
if self.git_commands.git_status_has_changes(repo_path):
msg = "Pull aborted: Uncommitted changes detected in the working directory. Please commit or stash first."
log_handler.log_warning(msg, func_name=func_name)
# Restituisce un errore specifico per questo caso
return {'status': 'error', 'message': msg, 'exception': ValueError(msg)}
except GitCommandError as status_err:
# Se il controllo dello stato fallisce, non procedere
msg = f"Pull aborted: Failed to check repository status before pull: {status_err}"
log_handler.log_error(msg, func_name=func_name)
return {'status': 'error', 'message': msg, 'exception': status_err}
# --- Esecuzione Git Pull ---
result_info = {'status': 'unknown', 'message': 'Pull not completed.'} # Default
try:
# Chiama il metodo git_pull (che ha check=False)
pull_result = self.git_commands.git_pull(repo_path, remote_name, current_branch_name)
# Analizza il risultato del comando pull
stdout_full = pull_result.stdout if pull_result.stdout else ""
stderr_full = pull_result.stderr if pull_result.stderr else ""
combined_output_lower = (stdout_full + stderr_full).lower()
if pull_result.returncode == 0:
# Successo
result_info['status'] = 'success'
if "already up to date" in combined_output_lower:
result_info['message'] = f"Pull from '{remote_name}': Repository already up-to-date."
log_handler.log_info(f"Pull successful (already up-to-date) for '{remote_name}'.", func_name=func_name)
else:
result_info['message'] = f"Pull from '{remote_name}' completed successfully."
log_handler.log_info(f"Pull successful for '{remote_name}'. Output logged.", func_name=func_name)
# L'output dettagliato è già loggato da log_and_execute a livello INFO
# --- Rilevamento Conflitti (RC=1 e messaggi specifici) ---
elif pull_result.returncode == 1 and (
"conflict" in combined_output_lower or
"automatic merge failed" in combined_output_lower or
"fix conflicts and then commit the result" in combined_output_lower
):
result_info['status'] = 'conflict' # Stato specifico per conflitti
result_info['message'] = f"Pull from '{remote_name}' resulted in merge conflicts. Please resolve them manually in '{repo_path}' and commit."
log_handler.log_error(f"Merge conflict detected during pull from '{remote_name}'.", func_name=func_name)
# Non impostiamo 'exception' qui, lo stato 'conflict' è l'informazione chiave
else:
# Altro Errore (RC != 0 e non è conflitto)
result_info['status'] = 'error'
stderr_lower = stderr_full.lower() # Usa solo stderr per errori specifici
log_handler.log_error(f"Pull command failed for '{remote_name}' (RC={pull_result.returncode}). Stderr: {stderr_lower}", func_name=func_name)
# Controlla errori specifici noti (simili a fetch)
auth_errors = ["authentication failed", "permission denied", "could not read username", "fatal: could not read password"]
connection_errors = ["repository not found", "could not resolve host", "name or service not known", "network is unreachable"]
upstream_errors = ["no tracking information", "no upstream branch", "refusing to merge unrelated histories"] # Errori legati a config/history
if any(err in stderr_lower for err in auth_errors):
result_info['message'] = f"Authentication required or failed for remote '{remote_name}' during pull."
result_info['exception'] = GitCommandError(result_info['message'], stderr=pull_result.stderr)
elif any(err in stderr_lower for err in connection_errors):
result_info['message'] = f"Failed to connect to remote '{remote_name}' during pull: Repository or host not found/reachable."
result_info['exception'] = GitCommandError(result_info['message'], stderr=pull_result.stderr)
elif any(err in stderr_lower for err in upstream_errors):
result_info['message'] = f"Pull failed for '{remote_name}': Check branch upstream configuration or related history. Error: {pull_result.stderr.strip()}"
result_info['exception'] = GitCommandError(result_info['message'], stderr=pull_result.stderr)
else:
# Errore generico di Git
result_info['message'] = f"Pull from '{remote_name}' failed (RC={pull_result.returncode}). Check logs."
result_info['exception'] = GitCommandError(result_info['message'], stderr=pull_result.stderr)
except (GitCommandError, ValueError) as e:
# Cattura errori dalla validazione iniziale o dal check dello stato
log_handler.log_error(f"Error during pull execution for '{remote_name}': {e}", func_name=func_name)
result_info = {'status': 'error', 'message': f"Pull failed: {e}", 'exception': e}
except Exception as e:
# Cattura errori imprevisti
log_handler.log_exception(f"Unexpected error during pull for '{remote_name}': {e}", func_name=func_name)
result_info = {'status': 'error', 'message': f"Unexpected pull error: {type(e).__name__}", 'exception': e}
return result_info
def execute_remote_push(self, repo_path: str, remote_name: str, branch_name: str):
# To be implemented: Calls git_commands.git_push, handles upstream/errors