add clone function, add remote function

This commit is contained in:
VALLONGOL 2025-04-23 12:03:06 +02:00
parent b5467f0b63
commit 6b6318b191
4 changed files with 455 additions and 40 deletions

View File

@ -165,6 +165,7 @@ class GitSvnSyncApp:
config_manager_instance=self.config_manager, config_manager_instance=self.config_manager,
profile_sections_list=self.config_manager.get_profile_sections(), profile_sections_list=self.config_manager.get_profile_sections(),
refresh_remote_status_cb=self.refresh_remote_status, refresh_remote_status_cb=self.refresh_remote_status,
clone_remote_repo_cb=self.clone_remote_repo,
) )
print("MainFrame GUI created.") print("MainFrame GUI created.")
log_handler.log_debug( log_handler.log_debug(
@ -2376,6 +2377,102 @@ class GitSvnSyncApp:
}, },
) )
def clone_remote_repo(self):
"""Handles the 'Clone from Remote...' action: shows dialog, validates, starts worker."""
func_name = "clone_remote_repo"
log_handler.log_info(f"--- Action Triggered: Clone Remote Repository ---", func_name=func_name)
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists():
log_handler.log_error("Cannot start clone: Main frame not available.", func_name=func_name)
return
# Mostra il dialogo modale per ottenere URL, directory padre e nome profilo
dialog = CloneFromRemoteDialog(self.master)
# dialog.result conterrà None se premuto Cancel, o (url, parent_dir, profile_name) se OK
dialog_result = dialog.result
if not dialog_result:
log_handler.log_info("Clone operation cancelled by user in dialog.", func_name=func_name)
self.main_frame.update_status_bar("Clone cancelled.")
return
# Estrai i dati dal risultato del dialogo
remote_url, local_parent_dir, profile_name_input = dialog_result
# --- Logica per derivare nomi e validare percorso finale ---
final_profile_name = ""
target_clone_dir = ""
try:
# Deriva il nome della directory del repository dall'URL
repo_name_from_url = os.path.basename(remote_url)
if repo_name_from_url.endswith(".git"):
repo_name_from_url = repo_name_from_url[:-4]
if not repo_name_from_url: # Se l'URL termina con / o è strano
raise ValueError("Could not derive repository name from URL.")
# Costruisci il percorso completo dove verrà clonato il repository
target_clone_dir = os.path.join(local_parent_dir, repo_name_from_url)
target_clone_dir = os.path.abspath(target_clone_dir) # Normalizza il percorso
# Determina il nome finale del profilo
if profile_name_input:
final_profile_name = profile_name_input
# Validazione aggiuntiva: assicurati che il nome profilo non esista già
if final_profile_name in self.config_manager.get_profile_sections():
raise ValueError(f"Profile name '{final_profile_name}' already exists. Please choose a different name.")
else:
# Usa il nome derivato dall'URL come nome profilo, verificando non esista
final_profile_name = repo_name_from_url
counter = 1
while final_profile_name in self.config_manager.get_profile_sections():
final_profile_name = f"{repo_name_from_url}_{counter}"
counter += 1
log_handler.log_debug(f"Derived target clone directory: {target_clone_dir}", func_name=func_name)
log_handler.log_debug(f"Determined profile name: {final_profile_name}", func_name=func_name)
# --- CONTROLLO FONDAMENTALE: La directory di destinazione esiste già? ---
if os.path.exists(target_clone_dir):
# Non clonare se la directory esiste (git clone fallirebbe comunque)
error_msg = f"Clone failed: Target directory already exists:\n{target_clone_dir}\nPlease choose a different parent directory or ensure the target is clear."
log_handler.log_error(error_msg, func_name=func_name)
self.main_frame.show_error("Clone Path Error", error_msg)
self.main_frame.update_status_bar("Clone failed: Target directory exists.")
return # Interrompe l'operazione
except ValueError as ve:
# Errore nella derivazione nomi o validazione profilo
log_handler.log_error(f"Clone configuration error: {ve}", func_name=func_name)
self.main_frame.show_error("Configuration Error", str(ve))
self.main_frame.update_status_bar("Clone failed: Configuration error.")
return
except Exception as e:
# Errore imprevisto durante la preparazione
log_handler.log_exception(f"Unexpected error preparing for clone: {e}", func_name=func_name)
self.main_frame.show_error("Internal Error", f"An unexpected error occurred:\n{e}")
self.main_frame.update_status_bar("Clone failed: Internal error.")
return
# --- Avvia Worker Asincrono ---
log_handler.log_info(f"Starting clone for '{remote_url}' into '{target_clone_dir}'...", func_name=func_name)
# Argomenti per il worker: dipendenza + parametri
args = (self.git_commands, remote_url, target_clone_dir, final_profile_name)
self._start_async_operation(
async_workers.run_clone_remote_async, # Worker esterno per clone
args,
{
"context": "clone_remote", # Contesto per il risultato
"status_msg": f"Cloning '{repo_name_from_url}'...", # Usa nome repo per status
# Passiamo i dati necessari per la creazione del profilo nel contesto,
# così _check_completion_queue può accedervi facilmente in caso di successo.
"clone_success_data": {
'profile_name': final_profile_name,
'cloned_path': target_clone_dir,
'remote_url': remote_url
}
},
)
def refresh_remote_status(self): def refresh_remote_status(self):
"""Starts the async check for ahead/behind status.""" """Starts the async check for ahead/behind status."""
func_name = "refresh_remote_status" func_name = "refresh_remote_status"
@ -2487,7 +2584,10 @@ class GitSvnSyncApp:
elif task_context == "interactive_auth" and status_from_result == 'success': elif task_context == "interactive_auth" and status_from_result == 'success':
should_reenable_now = False should_reenable_now = False
log_handler.log_debug("Delaying widget re-enable: re-checking connection after interactive auth.", func_name=func_name) log_handler.log_debug("Delaying widget re-enable: re-checking connection after interactive auth.", func_name=func_name)
# Non ritardare per errore get_ahead_behind, l'utente può riprovare manualmente elif task_context == 'clone_remote' and status_from_result == 'success':
# Non riabilitare dopo clone successo, il caricamento profilo gestirà lo stato
should_reenable_now = False
log_handler.log_debug("Delaying widget re-enable: profile load will handle state after clone.", func_name=func_name)
# Riabilita i widget se non è necessario attendere # Riabilita i widget se non è necessario attendere
if should_reenable_now: if should_reenable_now:
@ -2539,11 +2639,13 @@ class GitSvnSyncApp:
# Aggiorna la status bar (usa la funzione helper della GUI) # Aggiorna la status bar (usa la funzione helper della GUI)
if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): 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) # Non aggiornare la status bar immediatamente dopo un clone successo,
# il caricamento del profilo lo farà.
if not (task_context == 'clone_remote' and status == 'success'):
self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration)
# --- Processa risultato specifico per task --- # --- Processa risultato specifico per task ---
# Ottieni path corrente per eventuali refresh # Ottieni path corrente per eventuali refresh
# Usiamo una variabile separata perché il path per i refresh potrebbe differire da repo_path_conflict
repo_path_for_refreshes = self._get_and_validate_svn_path("Post-Action Refresh Check") repo_path_for_refreshes = self._get_and_validate_svn_path("Post-Action Refresh Check")
# Lista per raccogliere funzioni di refresh da chiamare alla fine # Lista per raccogliere funzioni di refresh da chiamare alla fine
refresh_list = [] refresh_list = []
@ -2557,8 +2659,7 @@ class GitSvnSyncApp:
auth_status = 'ok' auth_status = 'ok'
log_handler.log_info(f"Connection check successful for '{remote_name}'.", func_name=func_name) log_handler.log_info(f"Connection check successful for '{remote_name}'.", func_name=func_name)
self._update_gui_auth_status(auth_status) self._update_gui_auth_status(auth_status)
# Dopo un check OK, aggiorna anche lo stato ahead/behind post_action_sync_refresh_needed = True # Aggiorna stato A/B dopo check OK
post_action_sync_refresh_needed = True
elif status == 'auth_required': elif status == 'auth_required':
log_handler.log_warning(f"Authentication required for remote '{remote_name}'.", func_name=func_name) log_handler.log_warning(f"Authentication required for remote '{remote_name}'.", func_name=func_name)
self._update_gui_auth_status('required') self._update_gui_auth_status('required')
@ -2576,14 +2677,15 @@ class GitSvnSyncApp:
args_interactive, args_interactive,
{ "context": "interactive_auth", "status_msg": f"Attempting interactive auth for '{remote_name}'", "original_context": context } { "context": "interactive_auth", "status_msg": f"Attempting interactive auth for '{remote_name}'", "original_context": context }
) )
# Non riabilitare widget qui
else: else:
log_handler.log_info("User declined interactive authentication attempt.", func_name=func_name) 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)
elif status == 'error': elif status == 'error':
error_type = result_value if result_value in ['connection_failed', 'unknown_error', 'worker_exception'] else 'unknown_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) # Aggiorna stato auth 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") # Aggiorna stato sync 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}") if hasattr(self, "main_frame"): self.main_frame.show_error("Connection Error", f"{message}")
elif task_context == "interactive_auth": elif task_context == "interactive_auth":
@ -2610,9 +2712,7 @@ class GitSvnSyncApp:
f"Please resolve the conflicts manually in:\n{repo_path_conflict}\n\n" f"Please resolve the conflicts manually in:\n{repo_path_conflict}\n\n"
f"After resolving, stage the changes and commit them." f"After resolving, stage the changes and commit them."
) )
# Aggiorna solo la lista dei file modificati
if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list) if self.refresh_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
# Resetta stato sync a unknown/error dopo conflitto
if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Conflict") if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Conflict")
# --- Gestione specifica per PUSH REJECTED --- # --- Gestione specifica per PUSH REJECTED ---
@ -2620,8 +2720,7 @@ class GitSvnSyncApp:
log_handler.log_error(f"Push rejected for branch '{rejected_branch}'. User needs to pull.", func_name=func_name) log_handler.log_error(f"Push rejected for branch '{rejected_branch}'. User needs to pull.", func_name=func_name)
if hasattr(self, "main_frame"): if hasattr(self, "main_frame"):
self.main_frame.show_warning("Push Rejected", f"{message}") self.main_frame.show_warning("Push Rejected", f"{message}")
# Dopo un push rifiutato, suggerisci fetch e aggiorna stato sync if self.fetch_remote not in refresh_list: refresh_list.append(self.fetch_remote) # Fetch aggiornerà stato sync
if self.fetch_remote not in refresh_list: refresh_list.append(self.fetch_remote) # Fetch aggiornerà lo stato sync indirettamente
# --- Gestione specifica per GET_AHEAD_BEHIND --- # --- Gestione specifica per GET_AHEAD_BEHIND ---
elif task_context == 'get_ahead_behind': elif task_context == 'get_ahead_behind':
@ -2629,23 +2728,80 @@ class GitSvnSyncApp:
if status == 'success': if status == 'success':
ahead, behind = result_value if isinstance(result_value, tuple) else (None, None) ahead, behind = result_value if isinstance(result_value, tuple) else (None, None)
log_handler.log_info(f"Ahead/Behind status updated for '{local_branch_ctx}': Ahead={ahead}, Behind={behind}", func_name=func_name) log_handler.log_info(f"Ahead/Behind status updated for '{local_branch_ctx}': Ahead={ahead}, Behind={behind}", func_name=func_name)
log_handler.log_debug(f"Calling update_ahead_behind_status with: branch='{local_branch_ctx}', ahead={ahead}, behind={behind}", func_name=func_name)
if hasattr(self.main_frame, "update_ahead_behind_status"): if hasattr(self.main_frame, "update_ahead_behind_status"):
self.main_frame.update_ahead_behind_status(current_branch=local_branch_ctx, ahead=ahead, behind=behind) self.main_frame.update_ahead_behind_status(current_branch=local_branch_ctx, ahead=ahead, behind=behind)
elif status == 'error': elif status == 'error':
log_handler.log_error(f"Failed to get ahead/behind status for '{local_branch_ctx}': {message}", func_name=func_name) log_handler.log_error(f"Failed to get ahead/behind status for '{local_branch_ctx}': {message}", func_name=func_name)
log_handler.log_debug(f"Calling update_ahead_behind_status with: branch='{local_branch_ctx}', status_text='Sync Status: Error'", func_name=func_name)
if hasattr(self.main_frame, "update_ahead_behind_status"): 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") self.main_frame.update_ahead_behind_status(current_branch=local_branch_ctx, status_text=f"Sync Status: Error")
# --- Gestione specifica per CLONE_REMOTE ---
elif task_context == 'clone_remote':
if status == 'success':
log_handler.log_info(f"Clone successful. Creating profile...", func_name=func_name)
success_data = context.get('clone_success_data') or result_value
if success_data and isinstance(success_data, dict):
new_profile_name = success_data.get('profile_name')
cloned_repo_path = success_data.get('cloned_path')
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()
log_handler.log_info(f"Profile '{new_profile_name}' created successfully for cloned repo.", func_name=func_name)
# Aggiorna GUI e seleziona nuovo profilo (triggera load)
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)
# Non aggiorniamo status bar qui, load_profile_settings lo farà
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.")
# Riabilita widget se la creazione profilo fallisce
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.")
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.")
if hasattr(self, "main_frame") and self.main_frame.winfo_exists(): self.main_frame.set_action_widgets_state(tk.NORMAL)
elif status == 'error':
# Clone fallito
log_handler.log_error(f"Clone operation failed: {message}", func_name=func_name)
if hasattr(self, "main_frame"): self.main_frame.show_error("Clone Error", f"{message}")
# Widget già riabilitati all'inizio
# --- Gestione risultati altri task (successo) --- # --- Gestione risultati altri task (successo) ---
elif status == 'success': elif status == 'success':
# Determina quali refresh avviare e se aggiornare lo stato sync # Determina quali refresh avviare e se aggiornare lo stato sync
if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag',
'checkout_tag', 'create_branch', 'checkout_branch', 'checkout_tag', 'create_branch', 'checkout_branch',
'_handle_gitignore_save', 'add_file', 'apply_remote_config', '_handle_gitignore_save', 'add_file', 'apply_remote_config',
'fetch_remote', 'pull_remote', 'fetch_remote', 'pull_remote', # Pull non-conflict
'push_remote', 'push_tags_remote']: 'push_remote', 'push_tags_remote', # Push non-rejected
'refresh_branches']: # Refresh branches richiede aggiornamento stato sync
# Logica per popolare refresh_list # Logica per popolare refresh_list
if task_context == 'push_remote': if task_context == 'push_remote':
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
@ -2654,34 +2810,34 @@ class GitSvnSyncApp:
elif task_context == 'push_tags_remote': elif task_context == 'push_tags_remote':
if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list) if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
elif task_context == 'pull_remote': # Pull successo (non conflitto) elif task_context == 'pull_remote':
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) 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_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_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_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
elif task_context == 'fetch_remote': # Fetch successo elif task_context == 'fetch_remote':
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) 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_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_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
elif task_context == 'apply_remote_config': # Apply Config successo elif task_context == 'apply_remote_config':
refresh_list.append(self.check_connection_auth) # Controlla connessione dopo apply 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_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_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
elif task_context == 'checkout_branch' or task_context == 'checkout_tag': # Cambio branch/stato elif task_context == 'checkout_branch' or task_context == 'checkout_tag':
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
# Aggiungi refresh standard dopo checkout
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) 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_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_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_changed_files_list not in refresh_list: refresh_list.append(self.refresh_changed_files_list)
elif task_context == 'create_branch' and not new_branch_context: # Creazione senza checkout elif task_context == 'create_branch' and not new_branch_context:
post_action_sync_refresh_needed = True # Aggiorna stato sync (sarà no upstream) 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_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_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
elif task_context == 'refresh_branches': # Caso specifico refresh branches
post_action_sync_refresh_needed = True # Serve aggiornare lo stato sync
# Logica refresh per le altre azioni locali # Logica refresh per le altre azioni locali
else: else:
if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save']: if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save']:
@ -2693,16 +2849,15 @@ class GitSvnSyncApp:
if task_context not in ['refresh_branches', 'checkout_branch']: 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_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
# --- Aggiornamenti diretti GUI (per i task di refresh stessi) --- # --- Aggiornamenti diretti GUI (per i task di refresh stessi) ---
elif task_context == 'refresh_tags': elif task_context == 'refresh_tags':
if hasattr(self, "main_frame"): self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else []) if hasattr(self, "main_frame"): self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_branches': elif task_context == 'refresh_branches':
# Già gestito sopra per triggerare post_action_sync_refresh_needed
branches, current = result_value if isinstance(result_value, tuple) and len(result_value) == 2 else ([], None) branches, current = result_value if isinstance(result_value, tuple) and len(result_value) == 2 else ([], None)
if hasattr(self, "main_frame"): if hasattr(self, "main_frame"):
self.main_frame.update_branch_list(branches, current) self.main_frame.update_branch_list(branches, current)
self.main_frame.update_history_branch_filter(branches) self.main_frame.update_history_branch_filter(branches)
post_action_sync_refresh_needed = True # Aggiorna stato sync dopo refresh branch
elif task_context == 'refresh_history': elif task_context == 'refresh_history':
if hasattr(self, "main_frame"): self.main_frame.update_history_display(result_value if isinstance(result_value, list) else []) if hasattr(self, "main_frame"): self.main_frame.update_history_display(result_value if isinstance(result_value, list) else [])
elif task_context == 'refresh_changes': elif task_context == 'refresh_changes':
@ -2713,7 +2868,6 @@ class GitSvnSyncApp:
if hasattr(self, "main_frame"): self.main_frame.clear_commit_message() if hasattr(self, "main_frame"): self.main_frame.clear_commit_message()
if task_context == 'create_branch' and new_branch_context: if task_context == 'create_branch' and new_branch_context:
if hasattr(self, "main_frame") and 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 (che triggererà i suoi refresh)
self.checkout_branch(branch_to_checkout=new_branch_context, repo_path_override=repo_path_for_refreshes) 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 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 # Se non fa checkout, i refresh sono già in lista e post_action_sync_refresh_needed è True
@ -2728,11 +2882,11 @@ class GitSvnSyncApp:
post_action_sync_refresh_needed = True post_action_sync_refresh_needed = True
elif status == 'error': elif status == 'error':
# Gestione errori generica (esclusi check_connection, interactive_auth, pull_conflict, push_rejected, get_ahead_behind) # Gestione errori generica (esclusi contesti speciali gestiti sopra)
log_handler.log_error(f"Error reported for task '{task_context}': {message}", func_name=func_name) 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 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 # Gestione errore per fetch_remote, pull (non conflitto), push (non rifiuto), push_tags, apply_config
if task_context in ['fetch_remote', 'pull_remote', 'push_remote', 'push_tags_remote', 'apply_remote_config']: if task_context in ['fetch_remote', 'pull_remote', 'push_remote', 'push_tags_remote', 'apply_remote_config']:
auth_related_error = False; conn_related_error = False auth_related_error = False; conn_related_error = False
if isinstance(exception, GitCommandError) and exception.stderr: stderr_low = exception.stderr.lower(); if isinstance(exception, GitCommandError) and exception.stderr: stderr_low = exception.stderr.lower();
@ -2741,10 +2895,8 @@ class GitSvnSyncApp:
if auth_related_error: self._update_gui_auth_status('failed') if auth_related_error: self._update_gui_auth_status('failed')
elif conn_related_error: self._update_gui_auth_status('connection_failed') elif conn_related_error: self._update_gui_auth_status('connection_failed')
else: self._update_gui_auth_status('unknown_error') else: self._update_gui_auth_status('unknown_error')
# Mostra popup specifico del task
action_name = task_context.replace("_remote", "").replace("_", " ").title() action_name = task_context.replace("_remote", "").replace("_", " ").title()
if hasattr(self, "main_frame"): self.main_frame.show_error(f"{action_name} Error", f"{message}") if hasattr(self, "main_frame"): self.main_frame.show_error(f"{action_name} Error", f"{message}")
# Resetta stato sync su errore
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_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
@ -2770,28 +2922,32 @@ class GitSvnSyncApp:
if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(["(Error refreshing changes)"]) if hasattr(self, "main_frame"): self.main_frame.update_changed_files_list(["(Error refreshing changes)"])
# Non serve aggiornare stato auth/sync per errori locali generici # Non serve aggiornare stato auth/sync per errori locali generici
delay_ms = 50 # Inizializza il ritardo base qui
# --- Trigger finale dei refresh asincroni raccolti (spostato dopo il blocco if/elif/else principale) --- # --- Trigger finale dei refresh asincroni raccolti ---
# (Spostato dopo tutta la logica if/elif/else sullo stato)
if repo_path_for_refreshes and refresh_list: 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) log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes after '{task_context}'", func_name=func_name)
# Usa 'after' per separare leggermente l'avvio dei refresh dal ciclo corrente current_delay = 50 # Ritardo base
delay_ms = 50
for refresh_func in refresh_list: for refresh_func in refresh_list:
try: try:
self.master.after(delay_ms, refresh_func) self.master.after(current_delay, refresh_func)
delay_ms += 20 # Scaletta leggermente i refresh current_delay += 50 # Scaletta leggermente i refresh
except Exception as ref_e: except Exception as ref_e:
log_handler.log_error(f"Error scheduling {getattr(refresh_func, '__name__', 'refresh function')}: {ref_e}", func_name=func_name) log_handler.log_error(f"Error scheduling {getattr(refresh_func, '__name__', 'refresh function')}: {ref_e}", func_name=func_name)
# Usa l'ultimo delay per il refresh dello stato sync
delay_ms = current_delay
elif refresh_list: elif refresh_list:
log_handler.log_warning("Cannot trigger post-action UI refreshes: Repo path unavailable.", func_name=func_name) log_handler.log_warning("Cannot trigger post-action UI refreshes: Repo path unavailable.", func_name=func_name)
delay_ms = 50 # Resetta delay se non ci sono refresh standard
else:
delay_ms = 50 # Resetta delay se non ci sono refresh standard
# Triggera refresh stato ahead/behind SE necessario e non già in refresh_list # 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: if post_action_sync_refresh_needed and self.refresh_remote_status not in refresh_list:
current_repo_path = self._get_and_validate_svn_path("Post-Action Sync Status Check") current_repo_path_sync = self._get_and_validate_svn_path("Post-Action Sync Status Check")
if current_repo_path: if current_repo_path_sync:
log_handler.log_debug(f"Triggering remote sync status refresh after '{task_context}'.", func_name=func_name) log_handler.log_debug(f"Triggering remote sync status refresh after '{task_context}'.", func_name=func_name)
self.master.after(delay_ms + 50, self.refresh_remote_status) # Dopo gli altri refresh self.master.after(delay_ms + 50, self.refresh_remote_status) # Aggiunge un ulteriore piccolo delay
# Log finale solo se non è stata gestita una ricorsione/nuovo avvio # Log finale solo se non è stata gestita una ricorsione/nuovo avvio

View File

@ -1293,5 +1293,97 @@ def run_get_ahead_behind_async(
f"[Worker] Finished: Get Ahead/Behind for '{local_branch}'", func_name=func_name f"[Worker] Finished: Get Ahead/Behind for '{local_branch}'", func_name=func_name
) )
def run_clone_remote_async(
git_commands: GitCommands, # Dipendenza per eseguire clone
remote_url: str,
local_clone_path: str, # Path completo dove clonare
profile_name_to_create: str, # Nome del profilo da creare post-clone
results_queue: queue.Queue
):
"""
Worker function to execute 'git clone' asynchronously.
Executed in a separate thread.
Args:
git_commands (GitCommands): Instance to execute git commands.
remote_url (str): URL of the repository to clone.
local_clone_path (str): Full path to the target directory for the clone.
profile_name_to_create (str): The name for the new profile upon success.
results_queue (queue.Queue): Queue to put the result dictionary.
"""
func_name = "run_clone_remote_async"
log_handler.log_debug(f"[Worker] Started: Clone from '{remote_url}' into '{local_clone_path}'", func_name=func_name)
result_info = {'status': 'unknown', 'message': 'Clone not completed.'} # Default
try:
# Il controllo sull'esistenza della directory di destinazione
# è stato fatto PRIMA di avviare questo worker (in GitUtility.py).
# Qui eseguiamo direttamente il comando.
# Chiama il metodo git_clone (che ha check=False)
clone_result = git_commands.git_clone(remote_url, local_clone_path)
# Analizza il risultato del comando clone
if clone_result.returncode == 0:
# Successo
result_info['status'] = 'success'
result_info['message'] = f"Repository cloned successfully into '{os.path.basename(local_clone_path)}'."
# Passa i dati necessari per creare il profilo nel risultato
result_info['result'] = {
'cloned_path': local_clone_path,
'profile_name': profile_name_to_create,
'remote_url': remote_url
}
log_handler.log_info(f"[Worker] Clone successful: {result_info['message']}", func_name=func_name)
else:
# Errore durante il clone
result_info['status'] = 'error'
stderr_full = clone_result.stderr if clone_result.stderr else ""
stderr_lower = stderr_full.lower()
log_handler.log_error(f"Clone command failed (RC={clone_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"]
path_errors = ["already exists and is not an empty directory", "could not create work tree"] # Anche se controllato prima, può succedere
if any(err in stderr_lower for err in auth_errors):
result_info['message'] = f"Authentication required or failed for cloning '{remote_url}'."
result_info['exception'] = GitCommandError(result_info['message'], stderr=clone_result.stderr)
# Potremmo impostare uno stato specifico 'auth_required' se vogliamo distinguerlo
# result_info['result'] = 'authentication needed' # Opzionale
elif any(err in stderr_lower for err in connection_errors):
result_info['message'] = f"Failed to connect while cloning: Repository or host '{remote_url}' not found/reachable."
result_info['exception'] = GitCommandError(result_info['message'], stderr=clone_result.stderr)
# result_info['result'] = 'connection_failed' # Opzionale
elif any(err in stderr_lower for err in path_errors):
result_info['message'] = f"Clone failed: Target directory '{local_clone_path}' already exists and is not empty, or could not be created."
result_info['exception'] = GitCommandError(result_info['message'], stderr=clone_result.stderr)
# result_info['result'] = 'path_error' # Opzionale
else:
# Errore generico di Git
result_info['message'] = f"Clone from '{remote_url}' failed (RC={clone_result.returncode}). Check logs."
result_info['exception'] = GitCommandError(result_info['message'], stderr=clone_result.stderr)
# result_info['result'] = 'unknown_error' # Opzionale
# Metti il risultato in coda
results_queue.put(result_info)
except Exception as e:
# Cattura eccezioni impreviste nel worker stesso
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during clone operation: {e}", func_name=func_name)
results_queue.put(
{
"status": "error",
"exception": e,
"message": f"Unexpected error during clone operation: {type(e).__name__}",
"result": "worker_exception", # Stato specifico per errore worker
}
)
finally:
log_handler.log_debug(f"[Worker] Finished: Clone Remote '{remote_url}'", func_name=func_name)
# --- END OF FILE async_workers.py --- # --- END OF FILE async_workers.py ---

View File

@ -1725,5 +1725,48 @@ class GitCommands:
log_handler.log_exception(f"Unexpected error getting ahead/behind count: {e}", func_name=func_name) log_handler.log_exception(f"Unexpected error getting ahead/behind count: {e}", func_name=func_name)
return None, None # Segnala fallimento generico # Segnala fallimento # Segnala fallimento return None, None # Segnala fallimento generico # Segnala fallimento # Segnala fallimento
def git_clone(self, remote_url: str, local_directory_path: str) -> subprocess.CompletedProcess:
"""
Executes 'git clone --progress <remote_url> <local_directory_path>'.
Captures output (including progress) and hides console by default.
Does NOT raise exception on non-zero exit code (check=False).
Args:
remote_url (str): The URL of the remote repository to clone.
local_directory_path (str): The full path to the new local directory
where the repository will be cloned.
Returns:
subprocess.CompletedProcess: The result of the command execution.
"""
func_name = "git_clone"
log_handler.log_info(f"Cloning repository from '{remote_url}' into '{local_directory_path}'", func_name=func_name)
# Comando: git clone --progress <url> <directory>
# --progress forza l'output dello stato anche se stderr non è un terminale,
# utile per il logging.
cmd = ["git", "clone", "--progress", remote_url, local_directory_path]
# Esegui catturando output, nascondendo console, ma NON sollevare eccezione (check=False)
# Il worker analizzerà il risultato per errori specifici (auth, path, etc.)
# Usiamo un timeout più lungo per clone, che può richiedere tempo
clone_timeout = 300 # 5 minuti, da aggiustare se necessario
# Nota: Eseguiamo il clone nella directory *corrente* del processo principale
# perché la directory di destinazione viene creata dal comando stesso.
# Non passiamo un working_directory specifico a log_and_execute.
result = self.log_and_execute(
command=cmd,
working_directory=".", # Esegui da CWD, Git crea la dir specificata
check=False, # Fondamentale per gestire errori specifici
capture=True,
hide_console=True,
log_output_level=logging.INFO # Logga output (progresso, errori) a INFO
# Timeout aumentato viene gestito internamente da log_and_execute se lo modifichiamo lì,
# altrimenti possiamo passarlo come argomento extra se log_and_execute lo accetta.
# Per ora, assumiamo che il timeout di log_and_execute sia sufficiente o lo aumentiamo lì.
)
return result
# --- END OF FILE git_commands.py --- # --- END OF FILE git_commands.py ---

124
gui.py
View File

@ -449,6 +449,7 @@ class MainFrame(ttk.Frame):
push_remote_cb, push_remote_cb,
push_tags_remote_cb, push_tags_remote_cb,
refresh_remote_status_cb, refresh_remote_status_cb,
clone_remote_repo_cb,
): ):
"""Initializes the MainFrame.""" """Initializes the MainFrame."""
super().__init__(master) super().__init__(master)
@ -485,6 +486,7 @@ class MainFrame(ttk.Frame):
self.push_remote_callback = push_remote_cb self.push_remote_callback = push_remote_cb
self.push_tags_remote_callback = push_tags_remote_cb self.push_tags_remote_callback = push_tags_remote_cb
self.refresh_remote_status_callback = refresh_remote_status_cb self.refresh_remote_status_callback = refresh_remote_status_cb
self.clone_remote_repo_callback = clone_remote_repo_cb
# Configure style (invariato) # Configure style (invariato)
self.style = ttk.Style() self.style = ttk.Style()
@ -611,6 +613,16 @@ class MainFrame(ttk.Frame):
) )
self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5) self.add_profile_button.pack(side=tk.LEFT, padx=(2, 2), pady=5)
self.create_tooltip(self.add_profile_button, "Add new profile.") self.create_tooltip(self.add_profile_button, "Add new profile.")
self.clone_profile_button = ttk.Button(
button_subframe,
text="Clone from Remote", # Testo aggiornato
width=18, # Leggermente più largo
command=self.clone_remote_repo_callback # Chiama il nuovo callback
)
self.clone_profile_button.pack(side=tk.LEFT, padx=5, pady=5)
self.create_tooltip(self.clone_profile_button, "Clone a remote repository into a new local directory and create a profile for it.")
self.remove_profile_button = ttk.Button( self.remove_profile_button = ttk.Button(
button_subframe, button_subframe,
text="Remove", text="Remove",
@ -1981,4 +1993,116 @@ class MainFrame(ttk.Frame):
log_handler.log_error(f"Failed to update sync status variable: {e}", func_name="update_ahead_behind_status") log_handler.log_error(f"Failed to update sync status variable: {e}", func_name="update_ahead_behind_status")
class CloneFromRemoteDialog(simpledialog.Dialog):
"""Dialog to get Remote URL and Local Parent Directory for cloning."""
def __init__(self, parent, title="Clone Remote Repository"):
self.remote_url_var = tk.StringVar()
self.local_parent_dir_var = tk.StringVar()
self.profile_name_var = tk.StringVar() # Opzionale
self.result = None # Conterrà la tupla (url, parent_dir, profile_name)
# Imposta directory iniziale suggerita per la cartella locale
self.local_parent_dir_var.set(os.path.expanduser("~")) # Default alla home
super().__init__(parent, title=title)
def body(self, master):
"""Creates the dialog body."""
main_frame = ttk.Frame(master, padding="10")
main_frame.pack(fill="both", expand=True)
main_frame.columnconfigure(1, weight=1) # Colonna delle entry si espande
row_idx = 0
# Remote URL
ttk.Label(main_frame, text="Remote Repository URL:").grid(
row=row_idx, column=0, padx=5, pady=5, sticky="w")
self.url_entry = ttk.Entry(main_frame, textvariable=self.remote_url_var, width=60)
self.url_entry.grid(row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew")
Tooltip(self.url_entry, "Enter the full URL (HTTPS or SSH) of the repository to clone.")
row_idx += 1
# Local Parent Directory
ttk.Label(main_frame, text="Clone into Directory:").grid(
row=row_idx, column=0, padx=5, pady=5, sticky="w")
self.dir_entry = ttk.Entry(main_frame, textvariable=self.local_parent_dir_var, width=60)
self.dir_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew")
Tooltip(self.dir_entry, "Select the PARENT directory where the new repository folder will be created.")
self.browse_button = ttk.Button(main_frame, text="Browse...", width=9, command=self._browse_local_dir)
self.browse_button.grid(row=row_idx, column=2, padx=(0, 5), pady=5, sticky="w")
row_idx += 1
# Info sulla cartella creata (Label esplicativo)
ttk.Label(main_frame, text="(A new sub-folder named after the repository will be created inside this directory)",
font=("Segoe UI", 8), foreground="grey").grid(
row=row_idx, column=1, columnspan=2, padx=5, pady=(0, 5), sticky="w")
row_idx += 1
# New Profile Name (Opzionale)
ttk.Label(main_frame, text="New Profile Name (Optional):").grid(
row=row_idx, column=0, padx=5, pady=5, sticky="w")
self.profile_entry = ttk.Entry(main_frame, textvariable=self.profile_name_var, width=60)
self.profile_entry.grid(row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew")
Tooltip(self.profile_entry, "Enter a name for the new profile. If left empty, the repository name will be used.")
row_idx += 1
return self.url_entry # initial focus
def _browse_local_dir(self):
"""Callback for the local directory browse button."""
current_path = self.local_parent_dir_var.get()
initial_dir = current_path if os.path.isdir(current_path) else os.path.expanduser("~")
directory = filedialog.askdirectory(
initialdir=initial_dir,
title="Select Parent Directory for Clone",
parent=self # Rendi modale rispetto a questo dialogo
)
if directory:
self.local_parent_dir_var.set(directory)
def validate(self):
"""Validates the input fields before closing."""
url = self.remote_url_var.get().strip()
parent_dir = self.local_parent_dir_var.get().strip()
profile_name = self.profile_name_var.get().strip() # Pulisce anche nome profilo
if not url:
messagebox.showwarning("Input Error", "Remote Repository URL cannot be empty.", parent=self)
return 0 # Fallisce validazione
# Verifica base URL (non una validazione completa, ma meglio di niente)
if not (url.startswith("http://") or url.startswith("https://") or url.startswith("ssh://") or "@" in url):
if not messagebox.askokcancel("URL Format Warning",
f"The URL '{url}' does not look like a standard HTTPS, HTTP, or SSH URL.\n\nProceed anyway?",
icon='warning', parent=self):
return 0
if not parent_dir:
messagebox.showwarning("Input Error", "Parent Local Directory cannot be empty.", parent=self)
return 0
if not os.path.isdir(parent_dir):
messagebox.showwarning("Input Error", f"The selected parent directory does not exist:\n{parent_dir}", parent=self)
return 0
# Non validiamo qui se la sotto-cartella esiste, lo farà il controller principale
# Validazione opzionale nome profilo (se fornito)
if profile_name:
# Applica regole base per nomi di sezione configparser (evita spazi/caratteri speciali?)
# Per semplicità, controlliamo solo che non sia vuoto dopo strip (già fatto)
# Potremmo aggiungere un check regex se necessario.
pass
return 1 # Validazione OK
def apply(self):
"""Stores the validated result."""
# Restituisce una tupla con i valori puliti
self.result = (
self.remote_url_var.get().strip(),
self.local_parent_dir_var.get().strip(),
self.profile_name_var.get().strip() # Restituisce vuoto se non specificato
)
# --- END OF FILE gui.py --- # --- END OF FILE gui.py ---