From 91698eac76f24b5243230f4e70e545cc959965b5 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 23 Apr 2025 09:08:15 +0200 Subject: [PATCH] Implement Pull functionality with pre-check --- GitUtility.py | 544 +++++++++++++++++++++++++++++++++------------- ToDo.md | 270 +++++++---------------- async_workers.py | 288 ++++++++++++++++++++---- git_commands.py | 361 +++++++++++++++++++++++------- gui.py | 187 ++++++++++------ remote_actions.py | 258 +++++++++++++++++++++- 6 files changed, 1379 insertions(+), 529 deletions(-) diff --git a/GitUtility.py b/GitUtility.py index e0a3363..2fc3df9 100644 --- a/GitUtility.py +++ b/GitUtility.py @@ -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 === @@ -2046,258 +2049,499 @@ class GitSvnSyncApp: "status_msg": f"Applying config for remote '{remote_name}'", }, ) - + 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 - return + 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() if not remote_name: - # Usa default se vuoto (coerente con apply_remote_config) - remote_name = DEFAULT_REMOTE_NAME - self.main_frame.remote_name_var.set(remote_name) + # Usa default se vuoto (coerente con apply_remote_config) + 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) self._start_async_operation( - async_workers.run_check_connection_async, # Worker che esegue ls-remote + async_workers.run_check_connection_async, # Worker che esegue ls-remote args, { - "context": "check_connection", # Contesto per il check iniziale + "context": "check_connection", # Contesto per il check iniziale "status_msg": f"Checking remote '{remote_name}'", # Passiamo il nome del remote nel contesto per usarlo dopo "remote_name_checked": remote_name, - "repo_path_checked": svn_path, # Passiamo anche il path + "repo_path_checked": svn_path, # Passiamo anche il path }, ) - # --- 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 - if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): - # Chiama il metodo della GUI per aggiornare il label - self.main_frame._update_auth_status_indicator(status) + """Updates internal state and calls GUI update for auth indicator.""" + self.remote_auth_status = status # Aggiorna stato interno + if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): + # Chiama il metodo della GUI per aggiornare il label + self.main_frame._update_auth_status_indicator(status) # --- ==== Gestione Coda Risultati ==== --- 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") - self.main_frame.set_action_widgets_state(tk.NORMAL) + 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) - # 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) - repo_path_conflict = result_data.get('repo_path') - new_branch_context = context.get('new_branch_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 - # Aggiorna Status Bar - # (...) logica status bar invariata (...) + # --- 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') # Info se si crea branch + + # --- 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 - self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration) + 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}") - # 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) + 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': + # 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) - # --- 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 task_context != 'refresh_changes': - if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) - if task_context not in ['refresh_tags','checkout_tag'] or committed: - 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': + 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) + if task_context not in ['refresh_tags','checkout_tag'] or committed: + 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) - # (...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) - 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 []) + 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': + 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...) - 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) + # 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 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: diff --git a/ToDo.md b/ToDo.md index 3c4a77a..e68c9d7 100644 --- a/ToDo.md +++ b/ToDo.md @@ -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 ` + * **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 ..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 ` - 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 ` (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/..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/` (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/` mostra le differenze tra il tuo branch locale *attuale* e il branch remoto. `git diff --stat origin/` dà solo un riepilogo. Per differenze tra working dir e remoto servirebbe un fetch e poi un diff con `origin/`. + * **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/..HEAD` e `git log HEAD..origin/`, mostrando i risultati magari in un popup o nell'area log. + * Integrare la possibilità di fare `diff` con `origin/` nel Diff Viewer esistente (ma richiede prima un fetch). + * **Backend:** + * `GitCommands`: Aggiungere metodi per `git log `, `git fetch`, `git rev-list --count --left-right ...`. + * `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 ` (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 ` (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 ` e `git ls-remote --tags ` 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 ` 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 ` per creare il repository dal bundle. - * Se è già un repo Git: - * Opzionalmente esegue un backup ZIP. - * Esegue `git fetch `. - * 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 `. - * `remove_remote(working_directory, name)`: Per eseguire `git remote remove `. - * `list_remotes(working_directory)`: Per eseguire `git remote -v` e parsare i risultati. - * `git_fetch_remote(working_directory, remote_name)`: Per eseguire `git fetch `. - * `git_pull(working_directory, remote_name, branch_name)`: Per eseguire `git pull `. Richiede gestione attenta dei conflitti. - * `git_push(working_directory, remote_name, branch_name, force=False)`: Per eseguire `git push `. 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 -- `) 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! \ No newline at end of file +Sei d'accordo con questo piano? Iniziamo con **Fetch**? \ No newline at end of file diff --git a/async_workers.py b/async_workers.py index eb30d0b..1b58ea7 100644 --- a/async_workers.py +++ b/async_workers.py @@ -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) @@ -719,47 +748,85 @@ def run_check_connection_async( {"status": "success", "result": "connected", "message": message} ) elif result.returncode == 2: - # RC=2: Connesso con successo, ma il remote è vuoto (no refs) o "unborn" - # Lo trattiamo come connessione/auth OK, ma potremmo segnalarlo - message = f"Connected to remote '{remote_name}' (Note: Repository might be empty or unborn)." - log_handler.log_info(f"[Worker] {message}", func_name=func_name) - # Manda 'success' così l'indicatore diventa verde - results_queue.put( - {"status": "success", "result": "connected_empty", "message": message} - ) + # RC=2: Connesso con successo, ma il remote è vuoto (no refs) o "unborn" + # Lo trattiamo come connessione/auth OK, ma potremmo segnalarlo + message = f"Connected to remote '{remote_name}' (Note: Repository might be empty or unborn)." + log_handler.log_info(f"[Worker] {message}", func_name=func_name) + # Manda 'success' così l'indicatore diventa verde + results_queue.put( + {"status": "success", "result": "connected_empty", "message": message} + ) 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)} + # Stato speciale per triggerare il prompt interattivo + { + "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 diff --git a/git_commands.py b/git_commands.py index 94c1815..d82d7b0 100644 --- a/git_commands.py +++ b/git_commands.py @@ -62,8 +62,8 @@ class GitCommands: check: bool = True, log_output_level: int = logging.INFO, # ---<<< NUOVI PARAMETRI >>>--- - capture: bool = True, # Cattura stdout/stderr? - hide_console: bool = True # Nascondi finestra console (Windows)? + capture: bool = True, # Cattura stdout/stderr? + hide_console: bool = True, # Nascondi finestra console (Windows)? # ---<<< FINE NUOVI PARAMETRI >>>--- ) -> subprocess.CompletedProcess: """ @@ -91,8 +91,8 @@ class GitCommands: command_str = " ".join(safe_command_parts) log_handler.log_debug( f"Executing in '{working_directory}': {command_str} " - f"(Capture={capture}, HideConsole={hide_console})", # Log nuovi parametri - func_name=func_name + f"(Capture={capture}, HideConsole={hide_console})", # Log nuovi parametri + 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,21 +118,21 @@ 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': - # Se NON vogliamo nascondere, potremmo esplicitamente chiedere una nuova console - # per isolare l'input/output del comando interattivo - creationflags = subprocess.CREATE_NEW_CONSOLE + 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 # Su Linux/macOS, non impostiamo nulla di speciale per la finestra # ---<<< FINE MODIFICA >>>--- # Timeout diagnostico (mantenuto) - timeout_seconds = 60 # Aumentato leggermente per operazioni remote + timeout_seconds = 60 # Aumentato leggermente per operazioni remote # log_handler.log_debug(f"Setting timeout to {timeout_seconds} seconds.", func_name=func_name) # log_handler.log_debug(f"Attempting subprocess.run for: {command_str}", func_name=func_name) @@ -138,24 +140,28 @@ class GitCommands: result = subprocess.run( safe_command_parts, cwd=effective_cwd, - capture_output=capture, # Usa il nuovo parametro - text=True, # Decodifica output come testo - check=check, # Solleva eccezione su errore se True - encoding="utf-8", # Encoding standard - errors="replace", # Gestione errori decodifica + capture_output=capture, # Usa il nuovo parametro + text=True, # Decodifica output come testo + check=check, # Solleva eccezione su errore se True + encoding="utf-8", # Encoding standard + errors="replace", # Gestione errori decodifica timeout=timeout_seconds, - startupinfo=startupinfo, # Passa la configurazione (o None) - creationflags=creationflags # Passa i flag (o 0) + startupinfo=startupinfo, # Passa la configurazione (o None) + 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 "" - stderr_log_debug = result.stderr.strip() if result.stderr else "" + stdout_log_debug = ( + result.stdout.strip() if result.stdout else "" + ) + stderr_log_debug = ( + result.stderr.strip() if result.stderr else "" + ) # Logga sempre a DEBUG log_handler.log_debug( f"Command successful (RC={result.returncode}). Output:\n" @@ -168,7 +174,7 @@ class GitCommands: if log_output_level > logging.DEBUG: log_handler.log_message( log_output_level, - f"Command successful. Output logged at DEBUG level.", # Messaggio più conciso qui + f"Command successful. Output logged at DEBUG level.", # Messaggio più conciso qui func_name=func_name, ) @@ -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 "" - stdout_out = e.stdout.strip() if capture and e.stdout else "" + 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 "" + ) + stdout_out = ( + e.stdout.strip() if capture and e.stdout else "" + ) 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 "" - stdout_err = e.stdout.strip() if capture and e.stdout else "" + stderr_err = ( + e.stderr.strip() if capture and e.stderr else "" + ) + stdout_err = ( + e.stdout.strip() if capture and e.stdout else "" + ) 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): @@ -927,29 +961,40 @@ class GitCommands: ) from e 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 ' 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( command=cmd, working_directory=working_directory, - check=False, # Analizziamo noi il codice di ritorno e stderr + 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 ' allowing user interaction. Does NOT capture output and tries to show a console window for prompts. @@ -957,8 +1002,8 @@ 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 # check=False perché vogliamo analizzare noi l'esito nel worker @@ -966,8 +1011,8 @@ class GitCommands: command=cmd, working_directory=working_directory, check=False, - capture=False, # Non catturare stdout/stderr - hide_console=False, # Non nascondere la console + capture=False, # Non catturare stdout/stderr + hide_console=False, # Non nascondere la console ) # Nota: result.stdout e result.stderr saranno None qui return result @@ -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 {} + 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) - raise # Rilancia veri errori + 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) - raise # Rilancia altri errori Git + 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 - else: - log_handler.log_error(f"Failed to set URL for remote '{remote_name}': {e}", func_name=func_name) - raise # Rilancia altri errori Git + # 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 + else: + 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 [--prune?] - pass + def git_fetch( + self, working_directory: str, remote_name: str, prune: bool = True + ) -> subprocess.CompletedProcess: + """ + Executes 'git fetch ' 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 - 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 '. + 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 ` di solito basta se l'upstream è settato. + # Per ora, manteniamo il comando semplice `git pull `. + # 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, @@ -1246,6 +1417,36 @@ class GitCommands: def git_push_tags(self, working_directory: str, remote_name: str): # To be implemented: git push --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 --- diff --git a/gui.py b/gui.py index d37a9e2..ff1502c 100644 --- a/gui.py +++ b/gui.py @@ -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,72 +623,118 @@ 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.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.") - # ---<<< 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 + command=self.check_connection_auth_callback, # Nuovo callback + 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.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.") - # ---<<< 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 + 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 ) 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 - + def _update_auth_status_indicator(self, status: str): """Updates the text and background color of the auth status label.""" label = getattr(self, "auth_status_indicator_label", None) @@ -696,37 +742,40 @@ class MainFrame(ttk.Frame): return text = "Status: Unknown" - color = self.STATUS_DEFAULT_BG # Grigio/Default + 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': - text = "Status: Connection Failed" - color = self.STATUS_RED - tooltip = "Could not connect to the remote. Check URL and network." - elif status == 'unknown_error': - text = "Status: Error" - color = self.STATUS_RED - tooltip = "An unknown error occurred while checking the remote." + 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": + text = "Status: Error" + color = self.STATUS_RED + tooltip = "An unknown error occurred while checking the remote." # else: status == 'unknown' -> usa i valori di default try: self.remote_auth_status_var.set(text) label.config(background=color) - self.update_tooltip(label, tooltip) # Aggiorna anche il tooltip + 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)) @@ -1600,7 +1649,7 @@ class MainFrame(ttk.Frame): def _show_changed_files_context_menu(self, event): """Displays the context menu for the changed files listbox.""" func_name = "_show_changed_files_context_menu" - line = None # Inizializza line a None + line = None # Inizializza line a None try: # Trova l'indice dell'elemento più vicino al click idx = self.changed_files_listbox.nearest(event.y) @@ -1614,17 +1663,24 @@ 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) - return # Esce se non si può selezionare nulla + 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) - return + # Altri errori imprevisti + 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) - return # Esce se non siamo riusciti a ottenere il testo + 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 log_handler.log_debug(f"Context menu for line: '{line}'", func_name=func_name) @@ -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: - diff_state = tk.NORMAL + 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, ] diff --git a/remote_actions.py b/remote_actions.py index c4bd4f9..be04c82 100644 --- a/remote_actions.py +++ b/remote_actions.py @@ -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