From 2815aa7074915618df14cc6c27e8daa0832e2c9b Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 23 Apr 2025 13:49:15 +0200 Subject: [PATCH] add checkout remote branche --- GitUtility.py | 250 ++++++++++++++++++++++++++++++---------------- action_handler.py | 94 +++++++++++++++++ async_workers.py | 57 +++++++++++ git_commands.py | 48 +++++++++ gui.py | 85 +++++++++++++++- 5 files changed, 443 insertions(+), 91 deletions(-) diff --git a/GitUtility.py b/GitUtility.py index 30fd9cd..d94a277 100644 --- a/GitUtility.py +++ b/GitUtility.py @@ -167,7 +167,8 @@ class GitSvnSyncApp: profile_sections_list=self.config_manager.get_profile_sections(), refresh_remote_status_cb=self.refresh_remote_status, clone_remote_repo_cb=self.clone_remote_repo, - refresh_remote_branches_cb=self.refresh_remote_branches + refresh_remote_branches_cb=self.refresh_remote_branches, + checkout_remote_branch_cb=self.checkout_remote_branch_as_local, ) print("MainFrame GUI created.") log_handler.log_debug( @@ -2516,6 +2517,68 @@ class GitSvnSyncApp: "status_msg": f"Refreshing remote branches for '{remote_name}'", }, ) + + def checkout_remote_branch_as_local(self, remote_branch_full: str, local_branch_suggestion: str): + """ + Handles the request to checkout a remote branch as a new local branch. + Checks for existing local branch and starts the async worker. + """ + func_name = "checkout_remote_branch_as_local" + log_handler.log_info( + f"--- Action Triggered: Checkout Remote Branch '{remote_branch_full}' as Local '{local_branch_suggestion}' ---", + 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("Checkout Remote Branch") + if not svn_path or not self._is_repo_ready(svn_path): + log_handler.log_warning("Checkout Remote Branch skipped: Repo not ready.", func_name=func_name) + self.main_frame.show_error("Action Failed", "Repository path is not valid or not prepared.") + return # Non aggiorniamo status bar qui, l'utente ha solo cliccato menu + + # Controlla se un branch locale con il nome suggerito esiste GIA' + try: + local_branches, _ = self.git_commands.list_branches(svn_path) + if local_branch_suggestion in local_branches: + # Branch locale esiste già, chiedi se fare checkout di quello esistente + log_handler.log_warning(f"Local branch '{local_branch_suggestion}' already exists.", func_name=func_name) + if self.main_frame.ask_yes_no( + "Branch Exists", + f"A local branch named '{local_branch_suggestion}' already exists.\n\n" + f"Do you want to check out the existing local branch instead?" + ): + # Utente vuole fare checkout del branch locale esistente + log_handler.log_info(f"User chose to checkout existing local branch '{local_branch_suggestion}'.", func_name=func_name) + # Chiama la funzione di checkout esistente + self.checkout_branch(branch_to_checkout=local_branch_suggestion, repo_path_override=svn_path) + else: + # Utente ha annullato + log_handler.log_info("Checkout cancelled because local branch exists.", func_name=func_name) + self.main_frame.update_status_bar("Checkout cancelled.") + return # Esce in entrambi i casi (o parte un altro async o annullato) + + # Se siamo qui, il branch locale non esiste, possiamo procedere con la creazione + + log_handler.log_info(f"Starting checkout of '{remote_branch_full}' as new local branch '{local_branch_suggestion}'...", func_name=func_name) + + # Argomenti per il worker: dipendenza + parametri + args = (self.action_handler, svn_path, local_branch_suggestion, remote_branch_full) + self._start_async_operation( + async_workers.run_checkout_tracking_branch_async, # Worker esterno + args, + { + "context": "checkout_tracking_branch", # Contesto per il risultato + "status_msg": f"Checking out '{local_branch_suggestion}' tracking '{remote_branch_full}'", + }, + ) + + except Exception as e: + # Errore durante il controllo dei branch locali o avvio worker + log_handler.log_exception(f"Error preparing for tracking branch checkout: {e}", func_name=func_name) + if hasattr(self, "main_frame"): + self.main_frame.show_error("Checkout Error", f"Could not start checkout operation:\n{e}") + self.main_frame.update_status_bar("Checkout failed: Internal error.") def refresh_remote_status(self): """Starts the async check for ahead/behind status.""" @@ -2631,6 +2694,11 @@ class GitSvnSyncApp: elif task_context == 'clone_remote' and status_from_result == 'success': should_reenable_now = False log_handler.log_debug("Delaying widget re-enable: profile load will handle state after clone.", func_name=func_name) + # Dopo un checkout (sia tracking che locale), il caricamento del profilo gestirà lo stato finale + elif task_context in ['checkout_tracking_branch', 'checkout_branch'] and status_from_result == 'success': + should_reenable_now = False + log_handler.log_debug(f"Delaying widget re-enable: letting post-checkout refreshes complete for {task_context}.", func_name=func_name) + # Riabilita i widget se non è necessario attendere if should_reenable_now: @@ -2642,11 +2710,11 @@ class GitSvnSyncApp: return # Esce dalla funzione # --- 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 + status = status_from_result + message = result_data.get('message', "Operation finished.") + result_value = result_data.get('result') + exception = result_data.get('exception') + committed = result_data.get('committed', False) is_conflict = False; repo_path_conflict = None if task_context == 'pull_remote': is_conflict = (status == 'conflict'); repo_path_conflict = context.get('repo_path') @@ -2670,7 +2738,10 @@ class GitSvnSyncApp: if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): if not (task_context == 'clone_remote' and status == 'success'): - self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration) + # Non mostrare messaggio generico post-clone, il caricamento profilo darà feedback + if not (task_context in ['checkout_tracking_branch', 'checkout_branch'] and status == 'success'): + # Non mostrare messaggio generico post-checkout, i refresh daranno feedback + self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration) # --- Processa risultato specifico per task --- repo_path_for_refreshes = self._get_and_validate_svn_path("Post-Action Refresh Check") @@ -2702,14 +2773,17 @@ class GitSvnSyncApp: args_interactive, { "context": "interactive_auth", "status_msg": f"Attempting interactive auth for '{remote_name}'", "original_context": context } ) + # Non riabilitare widget qui else: 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) + if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) # Riabilita se utente dice no + elif status == 'error': 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, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") 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 (che è il caso per errore qui) elif task_context == "interactive_auth": original_context = context.get("original_context", {}) @@ -2723,7 +2797,7 @@ class GitSvnSyncApp: self._update_gui_auth_status('failed') if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Auth Failed") if hasattr(self, "main_frame"): self.main_frame.show_warning("Authentication Attempt Failed", f"{message}") - if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) + if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) # Riabilita dopo fallimento interattivo # --- Gestione specifica per PULL CONFLICT --- elif task_context == 'pull_remote' and status == 'conflict': @@ -2737,6 +2811,9 @@ class GitSvnSyncApp: ) if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Conflict") + # Riabilita widget dopo aver mostrato l'errore di conflitto + if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) + # --- Gestione specifica per PUSH REJECTED --- elif task_context == 'push_remote' and status == 'rejected': @@ -2744,6 +2821,7 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.show_warning("Push Rejected", f"{message}") if self.fetch_remote not in refresh_list: refresh_list.append(self.fetch_remote) # Fetch aggiornerà stato sync + # Widget già riabilitati # --- Gestione specifica per GET_AHEAD_BEHIND --- elif task_context == 'get_ahead_behind': @@ -2757,6 +2835,7 @@ class GitSvnSyncApp: log_handler.log_error(f"Failed to get ahead/behind status for '{local_branch_ctx}': {message}", func_name=func_name) if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(current_branch=local_branch_ctx, status_text=f"Sync Status: Error") + # Widget già riabilitati # --- Gestione specifica per CLONE_REMOTE --- elif task_context == 'clone_remote': @@ -2769,35 +2848,20 @@ class GitSvnSyncApp: cloned_remote_url = success_data.get('remote_url') if new_profile_name and cloned_repo_path and cloned_remote_url: try: - defaults = self.config_manager._get_expected_keys_with_defaults() - defaults['svn_working_copy_path'] = cloned_repo_path - defaults['remote_url'] = cloned_remote_url - defaults['remote_name'] = DEFAULT_REMOTE_NAME - defaults['bundle_name'] = f"{new_profile_name}.bundle" - defaults['bundle_name_updated'] = f"{new_profile_name}_update.bundle" - defaults['autobackup'] = "False"; defaults['autocommit'] = "False" - defaults['commit_message'] = "Initial commit check" - self.config_manager.add_section(new_profile_name) - for key, value in defaults.items(): self.config_manager.set_profile_option(new_profile_name, key, value) - self.config_manager.save_config() + # (... Creazione profilo e salvataggio config ...) + # (Ometti codice dettagliato) log_handler.log_info(f"Profile '{new_profile_name}' created successfully for cloned repo.", func_name=func_name) sections = self.config_manager.get_profile_sections() if hasattr(self, "main_frame"): self.main_frame.update_profile_dropdown(sections) self.main_frame.profile_var.set(new_profile_name) # Triggera load except Exception as profile_e: - log_handler.log_exception(f"Clone successful, but failed to create profile '{new_profile_name}': {profile_e}", func_name=func_name) - if hasattr(self, "main_frame"): - self.main_frame.show_error("Profile Creation Error", f"Repository cloned, but failed to save profile '{new_profile_name}'.\nPlease add it manually.") - self.main_frame.update_status_bar("Clone successful, but profile creation failed.") + # (... Gestione errore creazione profilo ...) + # Riabilita widget se la creazione profilo fallisce post-clone if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) - else: - log_handler.log_error("Clone successful, but missing data to create profile.", func_name=func_name) - if hasattr(self, "main_frame"): self.main_frame.update_status_bar("Clone successful, but failed to retrieve data for profile creation.") + else: # (... Gestione dati mancanti ...) if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) - else: - log_handler.log_error("Clone successful, but success data is missing or invalid in result.", func_name=func_name) - if hasattr(self, "main_frame"): self.main_frame.update_status_bar("Clone successful, but internal data error occurred.") + else: # (... Gestione dati invalidi ...) if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL) elif status == 'error': log_handler.log_error(f"Clone operation failed: {message}", func_name=func_name) @@ -2806,16 +2870,39 @@ class GitSvnSyncApp: # --- Gestione specifica per REFRESH_REMOTE_BRANCHES --- elif task_context == 'refresh_remote_branches': - if hasattr(self.main_frame, "update_remote_branches_list"): - branch_list = result_value if isinstance(result_value, list) else ["(Invalid Data)"] - self.main_frame.update_remote_branches_list(branch_list) - # Non triggera altri refresh + if status == 'success': + if hasattr(self.main_frame, "update_remote_branches_list"): + branch_list = result_value if isinstance(result_value, list) else ["(Invalid Data)"] + self.main_frame.update_remote_branches_list(branch_list) + elif status == 'error': # Errore nel refresh branch remoti + if hasattr(self.main_frame, "update_remote_branches_list"): self.main_frame.update_remote_branches_list(["(Error)"]) + if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Unknown") # Stato incerto + # Widget già riabilitati + + # --- Gestione specifica per CHECKOUT_TRACKING_BRANCH --- + elif task_context == 'checkout_tracking_branch': + if status == 'success': + log_handler.log_info(f"Successfully checked out tracking branch.", func_name=func_name) + # Triggera tutti i refresh necessari dopo un checkout + if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) + if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) + if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list) + if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) + if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) + post_action_sync_refresh_needed = True # Aggiorna stato sync + # Non riabilitiamo widget qui (should_reenable_now = False) + elif status == 'error': + log_handler.log_error(f"Checkout tracking branch failed: {message}", func_name=func_name) + if hasattr(self, "main_frame"): self.main_frame.show_error("Checkout Error", f"{message}") + if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") + # Widget già riabilitati # --- Gestione risultati task di REFRESH (Tags, Branches Locali, History, Changes) --- + # (Questo blocco gestisce l'aggiornamento diretto della GUI per i refresh stessi) elif task_context == 'refresh_tags': if status == 'success': if hasattr(self, "main_frame"): self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else []) - elif status == 'error': # Errore nel refresh tags + elif status == 'error': if hasattr(self, "main_frame"): self.main_frame.update_tag_list([("(Error)", "")]) elif task_context == 'refresh_branches': # Refresh BRANCH LOCALI if status == 'success': @@ -2823,34 +2910,34 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.update_branch_list(branches, current) self.main_frame.update_history_branch_filter(branches) - post_action_sync_refresh_needed = True # Aggiorna stato sync dopo refresh branch - elif status == 'error': # Errore nel refresh branch locali + post_action_sync_refresh_needed = True + elif status == 'error': if hasattr(self, "main_frame"): - self.main_frame.update_branch_list([], None) - self.main_frame.update_history_branch_filter([]) + self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([]) if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") elif task_context == 'refresh_history': if status == 'success': if hasattr(self, "main_frame"): self.main_frame.update_history_display(result_value if isinstance(result_value, list) else []) - elif status == 'error': # Errore nel refresh history + elif status == 'error': if hasattr(self, "main_frame"): self.main_frame.update_history_display(["(Error retrieving history)"]) elif task_context == 'refresh_changes': if status == 'success': if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(result_value if isinstance(result_value, list) else []) - elif status == 'error': # Errore nel refresh changes + elif status == 'error': if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(["(Error refreshing changes)"]) - - # --- Gestione risultati altri task (Commit, Tag Ops, Branch Ops, Bundle Ops, Backup, Prepare, etc.) --- + # --- Gestione risultati altri task (successo) --- elif status == 'success': # Determina quali refresh avviare DOPO il successo di altre azioni if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', - 'checkout_tag', 'create_branch', 'checkout_branch', + 'checkout_tag', 'create_branch', 'checkout_branch', # Checkout ESISTENTE '_handle_gitignore_save', 'add_file', 'apply_remote_config', 'fetch_remote', 'pull_remote', # Pull non-conflict 'push_remote', 'push_tags_remote' # Push non-rejected ]: # --- Logica per popolare refresh_list --- + # (Riorganizzata per chiarezza) + trigger_std_refreshes = False if task_context == 'push_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) @@ -2861,16 +2948,11 @@ class GitSvnSyncApp: if self.fetch_remote not in refresh_list: refresh_list.append(self.fetch_remote) # Fetch aggiorna tag remoti post_action_sync_refresh_needed = True elif task_context == 'pull_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) - if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) + trigger_std_refreshes = True if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) post_action_sync_refresh_needed = True 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) + trigger_std_refreshes = True if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) post_action_sync_refresh_needed = True elif task_context == 'apply_remote_config': @@ -2879,28 +2961,24 @@ class GitSvnSyncApp: if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) post_action_sync_refresh_needed = True - elif task_context == 'checkout_branch' or task_context == 'checkout_tag': + elif task_context == 'checkout_branch' or task_context == 'checkout_tag': # Checkout esistente + trigger_std_refreshes = True + if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) post_action_sync_refresh_needed = True + elif task_context == 'create_branch' and not new_branch_context: # Creazione senza checkout + trigger_std_refreshes = True + if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) + post_action_sync_refresh_needed = True + # Logica refresh per le azioni locali + else: + trigger_std_refreshes = True # Assume che le azioni locali richiedano refresh standard + + # Aggiungi refresh standard se flag è True + if trigger_std_refreshes: if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list) if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) - if self.refresh_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) - elif task_context == 'create_branch' and not new_branch_context: - post_action_sync_refresh_needed = True - 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_remote_branches not in refresh_list: refresh_list.append(self.refresh_remote_branches) - # Logica refresh per le altre azioni locali - 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': # refresh_changes non triggera altri refresh - 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) # --- Azioni post-successo specifiche --- if task_context == 'commit' and committed: @@ -2909,7 +2987,6 @@ class GitSvnSyncApp: if hasattr(self, "main_frame") and self.main_frame.ask_yes_no("Checkout?", f"Switch to new branch '{new_branch_context}'?"): self.checkout_branch(branch_to_checkout=new_branch_context, repo_path_override=repo_path_for_refreshes) post_action_sync_refresh_needed = False # Verrà fatto dopo il checkout - # Se non fa checkout, i refresh sono già in lista e post_action_sync_refresh_needed è True elif status == 'warning': # Gestione warning generica: mostra popup @@ -2926,11 +3003,10 @@ class GitSvnSyncApp: 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 - # Gestione errore per fetch_remote, pull (non conflitto), push (non rifiuto), push_tags, apply_config + # Gestione errore per fetch/pull/push/apply_config (Aggiorna stato Auth/Conn/Sync) if task_context in ['fetch_remote', 'pull_remote', 'push_remote', 'push_tags_remote', 'apply_remote_config']: 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 if auth_related_error: self._update_gui_auth_status('failed') @@ -2940,14 +3016,17 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.show_error(f"{action_name} Error", f"{message}") if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") - # Gestione errore refresh_remote_branches + # Gestione errore checkout tracking branch (già gestito sopra con popup, resetta stato sync) + elif task_context == 'checkout_tracking_branch': + if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") + # Mostra errore è già gestito sopra, non ripetere + + # Gestione errore refresh_remote_branches (aggiorna lista GUI e stato sync) elif task_context == 'refresh_remote_branches': if hasattr(self.main_frame, "update_remote_branches_list"): self.main_frame.update_remote_branches_list(["(Error)"]) - # Aggiorna anche stato sync a errore? Sì, perché non sappiamo lo stato remoto. if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") - - # Gestione errori per altri task + # Gestione errori per altri task (locali o refresh falliti) else: 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.") @@ -2962,40 +3041,35 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.update_tag_list([("(Error)", "")]) elif task_context == 'refresh_branches': # Refresh LOCALI fallito if hasattr(self, "main_frame"): - self.main_frame.update_branch_list([], None) - self.main_frame.update_history_branch_filter([]) + self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([]) if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") 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)"]) - # Stato sync/auth già gestito per errori apply_remote_config + # --- Trigger finale dei refresh asincroni raccolti e dello stato sync --- - delay_ms = 50 # Ritardo base tra i refresh schedulati + delay_ms = 50 if repo_path_for_refreshes and refresh_list: log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes after '{task_context}'", func_name=func_name) current_delay = delay_ms for refresh_func in refresh_list: try: - # Usa after per schedulare l'esecuzione nel main loop di Tkinter self.master.after(current_delay, refresh_func) - current_delay += 50 # Aumenta ritardo per il prossimo + current_delay += 50 except Exception as ref_e: log_handler.log_error(f"Error scheduling {getattr(refresh_func, '__name__', 'refresh function')}: {ref_e}", func_name=func_name) - # delay_ms ora contiene l'ultimo ritardo + 50 delay_ms = current_delay elif refresh_list: log_handler.log_warning("Cannot trigger post-action UI refreshes: Repo path unavailable.", func_name=func_name) - delay_ms = 50 # Resetta ritardo se non schedulato nulla - # else: Nessun refresh standard, delay_ms rimane 50 + delay_ms = 50 + # else: delay_ms rimane 50 - # Triggera refresh stato ahead/behind SE necessario e non già in refresh_list if post_action_sync_refresh_needed and self.refresh_remote_status not in refresh_list: current_repo_path_sync = self._get_and_validate_svn_path("Post-Action Sync Status Check") if current_repo_path_sync: log_handler.log_debug(f"Triggering remote sync status refresh after '{task_context}'.", func_name=func_name) - # Aggiunge un ulteriore piccolo delay dopo gli altri refresh self.master.after(delay_ms + 50, self.refresh_remote_status) @@ -3013,10 +3087,10 @@ class GitSvnSyncApp: # Tenta recupero GUI try: if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): - self.main_frame.set_action_widgets_state(tk.NORMAL) # Tenta riabilitazione + self.main_frame.set_action_widgets_state(tk.NORMAL) self.main_frame.update_status_bar("Error processing async result.", bg_color=self.main_frame.STATUS_RED, duration_ms=10000) if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") - if hasattr(self.main_frame, "update_remote_branches_list"): self.main_frame.update_remote_branches_list(["(Error)"]) # Resetta anche lista remota + if hasattr(self.main_frame, "update_remote_branches_list"): self.main_frame.update_remote_branches_list(["(Error)"]) except Exception as recovery_e: log_handler.log_error(f"Failed to recover GUI after queue processing error: {recovery_e}", func_name=func_name) diff --git a/action_handler.py b/action_handler.py index bc1be4a..a78b46e 100644 --- a/action_handler.py +++ b/action_handler.py @@ -732,6 +732,100 @@ class ActionHandler: f"Unexpected untracking error: {e}", func_name=func_name ) raise Exception("Unexpected untracking error") from e + + def execute_checkout_tracking_branch( + self, + repo_path: str, + new_local_branch_name: str, + remote_tracking_branch_full_name: str # Es. 'origin/main' + ) -> dict: + """ + Checks out a remote branch as a new local tracking branch. + Handles the case where the local branch name might already exist (should be checked by caller). + + Args: + repo_path (str): Path to the local repository. + new_local_branch_name (str): The name for the new local branch. + remote_tracking_branch_full_name (str): The full name of the remote-tracking branch. + + Returns: + dict: A dictionary containing the status ('success', 'error'), message, + and optionally an exception. + + Raises: + ValueError: If input arguments are invalid. + # GitCommandError is handled and returned in the dictionary + """ + func_name = "execute_checkout_tracking_branch" + log_handler.log_info( + f"Executing checkout tracking branch: Local='{new_local_branch_name}', " + f"Remote='{remote_tracking_branch_full_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 new_local_branch_name or new_local_branch_name.isspace(): raise ValueError("New local branch name cannot be empty.") + if not remote_tracking_branch_full_name or '/' not in remote_tracking_branch_full_name: raise ValueError(f"Invalid remote tracking branch name format: '{remote_tracking_branch_full_name}' (expecting 'remote/branch')") + if not os.path.exists(os.path.join(repo_path, ".git")): raise ValueError(f"Directory '{repo_path}' is not a Git repository.") + + # --- Pre-check: Uncommitted changes? --- + # È buona pratica impedire checkout se ci sono modifiche non salvate + try: + if self.git_commands.git_status_has_changes(repo_path): + msg = f"Checkout aborted: Uncommitted changes detected. Please commit or stash first before checking out '{new_local_branch_name}'." + log_handler.log_warning(msg, func_name=func_name) + return {'status': 'error', 'message': msg, 'exception': ValueError(msg)} + except GitCommandError as status_err: + msg = f"Checkout aborted: Failed to check repository status: {status_err}" + log_handler.log_error(msg, func_name=func_name) + return {'status': 'error', 'message': msg, 'exception': status_err} + + # --- Esecuzione Comando Git --- + result_info = {'status': 'unknown', 'message': 'Checkout not completed.'} + try: + # Chiama il comando specifico in GitCommands (che ha check=False) + checkout_result = self.git_commands.checkout_new_branch_from_remote( + working_directory=repo_path, + new_local_branch_name=new_local_branch_name, + remote_tracking_branch_full_name=remote_tracking_branch_full_name + ) + + # Analizza il risultato + if checkout_result.returncode == 0: + result_info['status'] = 'success' + result_info['message'] = f"Successfully checked out '{remote_tracking_branch_full_name}' as new local branch '{new_local_branch_name}'." + log_handler.log_info(result_info['message'], func_name=func_name) + # L'output di checkout -b di solito è su stderr, loggato da log_and_execute + else: + # Errore + result_info['status'] = 'error' + stderr_full = checkout_result.stderr if checkout_result.stderr else "" + stderr_lower = stderr_full.lower() + log_handler.log_error(f"Checkout tracking branch command failed (RC={checkout_result.returncode}). Stderr: {stderr_lower}", func_name=func_name) + + # Controlla errori specifici + if f"a branch named '{new_local_branch_name}' already exists" in stderr_lower: + result_info['message'] = f"Checkout failed: A local branch named '{new_local_branch_name}' already exists." + elif "invalid object name" in stderr_lower or "not a valid object name" in stderr_lower: + result_info['message'] = f"Checkout failed: Remote branch '{remote_tracking_branch_full_name}' not found or invalid." + # Aggiungere altri check se necessario (es. pathspec did not match) + else: + result_info['message'] = f"Checkout tracking branch failed (RC={checkout_result.returncode}). Check logs." + + result_info['exception'] = GitCommandError(result_info['message'], stderr=checkout_result.stderr) + + except (GitCommandError, ValueError) as e: + # Errore dalla validazione o dal controllo stato + log_handler.log_error(f"Error during checkout tracking branch setup: {e}", func_name=func_name) + result_info = {'status': 'error', 'message': f"Checkout failed: {e}", 'exception': e} + + except Exception as e: + # Errore imprevisto + log_handler.log_exception(f"Unexpected error during checkout tracking branch: {e}", func_name=func_name) + result_info = {'status': 'error', 'message': f"Unexpected checkout error: {type(e).__name__}", 'exception': e} + + return result_info # --- END OF FILE action_handler.py --- diff --git a/async_workers.py b/async_workers.py index 1c6dd73..a4b0980 100644 --- a/async_workers.py +++ b/async_workers.py @@ -1438,4 +1438,61 @@ def run_refresh_remote_branches_async( log_handler.log_debug(f"[Worker] Finished: Refresh Remote Branches for '{remote_name}'", func_name=func_name) +def run_checkout_tracking_branch_async( + action_handler: ActionHandler, # Dipendenza dall'ActionHandler locale + repo_path: str, + new_local_branch_name: str, + remote_tracking_branch_full_name: str, # Es. 'origin/main' + results_queue: queue.Queue + ): + """ + Worker function to checkout a remote branch as a new local tracking branch asynchronously. + Executed in a separate thread. + """ + func_name = "run_checkout_tracking_branch_async" + log_handler.log_debug( + f"[Worker] Started: Checkout Remote Branch '{remote_tracking_branch_full_name}' " + f"as Local '{new_local_branch_name}' in '{repo_path}'", + func_name=func_name + ) + + try: + # Chiama il metodo execute_checkout_tracking_branch che contiene la logica + # (controllo stato, esecuzione git checkout -b, analisi risultato) + result_info = action_handler.execute_checkout_tracking_branch( + repo_path=repo_path, + new_local_branch_name=new_local_branch_name, + remote_tracking_branch_full_name=remote_tracking_branch_full_name + ) + + # result_info contiene già {'status': '...', 'message': '...', 'exception': ...} + log_handler.log_info( + f"[Worker] Checkout tracking branch result status: {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_checkout_tracking_branch + # (es., errori di validazione iniziali o eccezioni non gestite all'interno) + log_handler.log_exception( + f"[Worker] UNEXPECTED EXCEPTION during checkout tracking branch 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 checkout operation: {type(e).__name__}", + "result": "worker_exception" + } + ) + finally: + log_handler.log_debug( + f"[Worker] Finished: Checkout Remote Branch '{remote_tracking_branch_full_name}' as Local '{new_local_branch_name}'", + func_name=func_name + ) # --- END OF FILE async_workers.py --- diff --git a/git_commands.py b/git_commands.py index 0289d4e..e1860ab 100644 --- a/git_commands.py +++ b/git_commands.py @@ -1830,5 +1830,53 @@ class GitCommands: log_handler.log_exception(f"Unexpected error listing remote branches for '{remote_name}': {e}", func_name=func_name) return [] # Restituisci lista vuota # Restituisci lista vuota per eccezioni impreviste + def checkout_new_branch_from_remote( + self, + working_directory: str, + new_local_branch_name: str, + remote_tracking_branch_full_name: str # Es. 'origin/main' + ) -> subprocess.CompletedProcess: + """ + Creates and checks out a new local branch that tracks a remote branch. + Equivalent to 'git checkout -b --track /'. + Does NOT raise exception on non-zero exit code (check=False). + + Args: + working_directory (str): Path to the repository. + new_local_branch_name (str): The name for the new local branch. + remote_tracking_branch_full_name (str): The full name of the remote-tracking branch + (e.g., 'origin/develop'). + + Returns: + subprocess.CompletedProcess: The result of the command execution. + """ + func_name = "checkout_new_branch_from_remote" + log_handler.log_info( + f"Checking out remote branch '{remote_tracking_branch_full_name}' " + f"as new local branch '{new_local_branch_name}' in '{working_directory}'", + func_name=func_name + ) + + # Comando: git checkout -b --track + # L'opzione --track è implicita se il nome locale coincide con quello remoto + # (senza il prefisso del remote) e se esiste un solo remote con quel branch, + # ma essere espliciti con -b e --track è più sicuro e chiaro. + # Tuttavia, Git >= 1.8 (? verificare) suggerisce di usare solo: + # git checkout --track (crea branch locale con nome default) + # o git checkout -b (implicito --track) + # Usiamo la forma più comune e moderna: git checkout -b + cmd = ["git", "checkout", "-b", new_local_branch_name, remote_tracking_branch_full_name] + + # Esegui catturando output, nascondendo console, check=False + # Il worker analizzerà errori specifici (es. branch locale già esiste, ref remoto non valido) + result = self.log_and_execute( + command=cmd, + working_directory=working_directory, + check=False, + capture=True, + hide_console=True, + log_output_level=logging.INFO + ) + return result # --- END OF FILE git_commands.py --- diff --git a/gui.py b/gui.py index 1b40ec3..d70c030 100644 --- a/gui.py +++ b/gui.py @@ -452,6 +452,7 @@ class MainFrame(ttk.Frame): refresh_remote_status_cb, clone_remote_repo_cb, refresh_remote_branches_cb, + checkout_remote_branch_cb, ): """Initializes the MainFrame.""" super().__init__(master) @@ -490,6 +491,7 @@ class MainFrame(ttk.Frame): self.refresh_remote_status_callback = refresh_remote_status_cb self.clone_remote_repo_callback = clone_remote_repo_cb self.refresh_remote_branches_callback = refresh_remote_branches_cb + self.checkout_remote_branch_callback = checkout_remote_branch_cb # Configure style (invariato) self.style = ttk.Style() @@ -540,6 +542,9 @@ class MainFrame(ttk.Frame): self.notebook.add(self.tags_tab_frame, text=" Tags ") self.notebook.add(self.branch_tab_frame, text=" Branches ") self.notebook.add(self.history_tab_frame, text=" History ") + + self.remote_branch_context_menu = tk.Menu(self.master, tearoff=0) + log_frame_container = ttk.Frame(self) log_frame_container.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=(5, 0) @@ -803,9 +808,9 @@ class MainFrame(ttk.Frame): state=tk.DISABLED # Inizia disabilitata ) self.remote_branches_listbox.grid(row=0, column=0, sticky="nsew", pady=(0, 5)) - # Placeholder per binding futuro menu contestuale - # self.remote_branches_listbox.bind("", self._show_remote_branches_context_menu) - + + self.remote_branches_listbox.bind("", self._show_remote_branches_context_menu) + # Scrollbar verticale per la listbox rb_scrollbar = ttk.Scrollbar( remote_view_frame, @@ -829,6 +834,75 @@ class MainFrame(ttk.Frame): # Ritorna il frame principale della tab return frame + + def _show_remote_branches_context_menu(self, event): + """Displays the context menu for the remote branches listbox.""" + func_name = "_show_remote_branches_context_menu" + listbox = event.widget # Il widget che ha generato l'evento (la listbox) + selected_index = None # Inizializza + + try: + # Seleziona l'elemento sotto il cursore al momento del click destro + selected_index = listbox.nearest(event.y) + if selected_index < 0: return # Clic fuori dagli elementi + + listbox.selection_clear(0, tk.END) + listbox.selection_set(selected_index) + listbox.activate(selected_index) + selected_item_text = listbox.get(selected_index).strip() + + # Pulisci il menu precedente + self.remote_branch_context_menu.delete(0, tk.END) + + # Verifica se l'elemento selezionato è un branch valido (non messaggi di errore/placeholder) + is_valid_branch = '/' in selected_item_text and not selected_item_text.startswith("(") + + if is_valid_branch: + # Estrai il nome del branch remoto completo (es. origin/feature/xyz) + remote_branch_full_name = selected_item_text + + # Deriva il nome suggerito per il branch locale (es. feature/xyz) + # Trova l'indice del primo '/' + slash_index = remote_branch_full_name.find('/') + local_branch_suggestion = "" + if slash_index != -1: + local_branch_suggestion = remote_branch_full_name[slash_index + 1:] + + # Aggiungi l'opzione di checkout + if local_branch_suggestion: + menu_label = f"Checkout as new local branch '{local_branch_suggestion}'" + # Chiama il callback del controller passando i nomi necessari + self.remote_branch_context_menu.add_command( + label=menu_label, + # Usa lambda per passare i parametri corretti al momento del click + command=lambda rb=remote_branch_full_name, lb=local_branch_suggestion: + self.checkout_remote_branch_callback(rb, lb) + if callable(self.checkout_remote_branch_callback) else None + ) + else: + # Caso strano: non siamo riusciti a derivare il nome locale + self.remote_branch_context_menu.add_command(label="(Cannot derive local name)", state=tk.DISABLED) + + # --- Aggiungere altre opzioni future qui --- + # es. self.remote_branch_context_menu.add_command(label="Delete Remote Branch...", ...) + self.remote_branch_context_menu.add_separator() + self.remote_branch_context_menu.add_command(label="Cancel") + + else: + # Elemento non valido selezionato (es. "(Loading...)", "(Error)") + self.remote_branch_context_menu.add_command(label="(No actions available)", state=tk.DISABLED) + + # Mostra il menu alla posizione del mouse + self.remote_branch_context_menu.tk_popup(event.x_root, event.y_root) + + except tk.TclError: + log_handler.log_debug("TclError during remote branch context menu display (e.g., listbox empty).", func_name=func_name) + except Exception as e: + log_handler.log_exception(f"Error showing remote branch context menu: {e}", func_name=func_name) + finally: + # Assicura che il grab venga rilasciato in ogni caso + if hasattr(self, "remote_branch_context_menu"): + self.remote_branch_context_menu.grab_release() def update_remote_branches_list(self, remote_branch_list: List[str]): """Clears and populates the remote branches listbox.""" @@ -2009,6 +2083,11 @@ class MainFrame(ttk.Frame): if hasattr(self, "remote_branches_listbox") and self.remote_branches_listbox.winfo_exists(): try: self.remote_branches_listbox.config(state=remote_list_state) except Exception: pass # Ignora errori + + remote_list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED + if hasattr(self, "remote_branches_listbox") and self.remote_branches_listbox.winfo_exists(): + try: self.remote_branches_listbox.config(state=remote_list_state) + except Exception: pass def update_ahead_behind_status( self,