add checkout remote branche

This commit is contained in:
VALLONGOL 2025-04-23 13:49:15 +02:00
parent 411e5cc00f
commit 2815aa7074
5 changed files with 443 additions and 91 deletions

View File

@ -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(
@ -2517,6 +2518,68 @@ class GitSvnSyncApp:
},
)
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."""
func_name = "refresh_remote_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,6 +2738,9 @@ class GitSvnSyncApp:
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
if not (task_context == 'clone_remote' and status == 'success'):
# 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 ---
@ -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 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)
# Non triggera altri refresh
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':
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_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 == '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)
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)
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)
# Logica refresh per le altre azioni locali
post_action_sync_refresh_needed = True
# Logica refresh per le azioni locali
else:
if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save']:
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 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)
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)
# --- 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)

View File

@ -733,5 +733,99 @@ class ActionHandler:
)
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 ---

View File

@ -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 ---

View File

@ -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 <local_name> --track <remote_name>/<branch_name>'.
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 <local_name> --track <remote_tracking_branch>
# 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 <remote_tracking_branch> (crea branch locale con nome default)
# o git checkout -b <local_name> <remote_tracking_branch> (implicito --track)
# Usiamo la forma più comune e moderna: git checkout -b <local> <remote_tracking>
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 ---

83
gui.py
View File

@ -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,8 +808,8 @@ 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("<Button-3>", self._show_remote_branches_context_menu)
self.remote_branches_listbox.bind("<Button-3>", self._show_remote_branches_context_menu)
# Scrollbar verticale per la listbox
rb_scrollbar = ttk.Scrollbar(
@ -830,6 +835,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."""
listbox = getattr(self, "remote_branches_listbox", None)
@ -2010,6 +2084,11 @@ class MainFrame(ttk.Frame):
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,
current_branch: str | None = None, # Aggiunto parametro branch