add clone function, add remote function
This commit is contained in:
parent
b5467f0b63
commit
6b6318b191
234
GitUtility.py
234
GitUtility.py
@ -165,6 +165,7 @@ class GitSvnSyncApp:
|
||||
config_manager_instance=self.config_manager,
|
||||
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,
|
||||
)
|
||||
print("MainFrame GUI created.")
|
||||
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):
|
||||
"""Starts the async check for ahead/behind status."""
|
||||
func_name = "refresh_remote_status"
|
||||
@ -2487,7 +2584,10 @@ class GitSvnSyncApp:
|
||||
elif task_context == "interactive_auth" and status_from_result == 'success':
|
||||
should_reenable_now = False
|
||||
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
|
||||
if should_reenable_now:
|
||||
@ -2539,11 +2639,13 @@ class GitSvnSyncApp:
|
||||
|
||||
# Aggiorna la status bar (usa la funzione helper della GUI)
|
||||
if hasattr(self, "main_frame") and self.main_frame.winfo_exists():
|
||||
# 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 ---
|
||||
# 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")
|
||||
# Lista per raccogliere funzioni di refresh da chiamare alla fine
|
||||
refresh_list = []
|
||||
@ -2557,8 +2659,7 @@ class GitSvnSyncApp:
|
||||
auth_status = 'ok'
|
||||
log_handler.log_info(f"Connection check successful for '{remote_name}'.", func_name=func_name)
|
||||
self._update_gui_auth_status(auth_status)
|
||||
# Dopo un check OK, aggiorna anche lo stato ahead/behind
|
||||
post_action_sync_refresh_needed = True
|
||||
post_action_sync_refresh_needed = True # Aggiorna stato A/B dopo check OK
|
||||
elif status == 'auth_required':
|
||||
log_handler.log_warning(f"Authentication required for remote '{remote_name}'.", func_name=func_name)
|
||||
self._update_gui_auth_status('required')
|
||||
@ -2576,14 +2677,15 @@ 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)
|
||||
|
||||
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) # Aggiorna stato auth
|
||||
if hasattr(self.main_frame, "update_ahead_behind_status"): self.main_frame.update_ahead_behind_status(status_text="Sync Status: Error") # Aggiorna stato sync
|
||||
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}")
|
||||
|
||||
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"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)
|
||||
# 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")
|
||||
|
||||
# --- 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)
|
||||
if hasattr(self, "main_frame"):
|
||||
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à lo stato sync indirettamente
|
||||
if self.fetch_remote not in refresh_list: refresh_list.append(self.fetch_remote) # Fetch aggiornerà stato sync
|
||||
|
||||
# --- Gestione specifica per GET_AHEAD_BEHIND ---
|
||||
elif task_context == 'get_ahead_behind':
|
||||
@ -2629,23 +2728,80 @@ class GitSvnSyncApp:
|
||||
if status == 'success':
|
||||
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_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"):
|
||||
self.main_frame.update_ahead_behind_status(current_branch=local_branch_ctx, ahead=ahead, behind=behind)
|
||||
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_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"):
|
||||
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) ---
|
||||
elif status == 'success':
|
||||
# Determina quali refresh avviare e se aggiornare lo stato sync
|
||||
if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag',
|
||||
'checkout_tag', 'create_branch', 'checkout_branch',
|
||||
'_handle_gitignore_save', 'add_file', 'apply_remote_config',
|
||||
'fetch_remote', 'pull_remote',
|
||||
'push_remote', 'push_tags_remote']:
|
||||
'fetch_remote', 'pull_remote', # Pull non-conflict
|
||||
'push_remote', 'push_tags_remote', # Push non-rejected
|
||||
'refresh_branches']: # Refresh branches richiede aggiornamento stato sync
|
||||
# Logica per popolare refresh_list
|
||||
if task_context == 'push_remote':
|
||||
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':
|
||||
if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list)
|
||||
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_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)
|
||||
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_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)
|
||||
post_action_sync_refresh_needed = True
|
||||
elif task_context == 'apply_remote_config': # Apply Config successo
|
||||
refresh_list.append(self.check_connection_auth) # Controlla connessione dopo apply
|
||||
elif task_context == 'apply_remote_config':
|
||||
refresh_list.append(self.check_connection_auth)
|
||||
if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history)
|
||||
if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
|
||||
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
|
||||
# Aggiungi refresh standard dopo 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)
|
||||
elif task_context == 'create_branch' and not new_branch_context: # Creazione senza checkout
|
||||
post_action_sync_refresh_needed = True # Aggiorna stato sync (sarà no upstream)
|
||||
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 == 'refresh_branches': # Caso specifico refresh branches
|
||||
post_action_sync_refresh_needed = True # Serve aggiornare lo stato sync
|
||||
# Logica refresh per le altre azioni locali
|
||||
else:
|
||||
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 self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list)
|
||||
|
||||
|
||||
# --- Aggiornamenti diretti GUI (per i task di refresh stessi) ---
|
||||
elif task_context == 'refresh_tags':
|
||||
if hasattr(self, "main_frame"): self.main_frame.update_tag_list(result_value if isinstance(result_value, list) else [])
|
||||
elif task_context == 'refresh_branches':
|
||||
# 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)
|
||||
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 task_context == 'refresh_history':
|
||||
if hasattr(self, "main_frame"): self.main_frame.update_history_display(result_value if isinstance(result_value, list) else [])
|
||||
elif task_context == 'refresh_changes':
|
||||
@ -2713,7 +2868,6 @@ class GitSvnSyncApp:
|
||||
if hasattr(self, "main_frame"): self.main_frame.clear_commit_message()
|
||||
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}'?"):
|
||||
# Avvia checkout asincrono (che triggererà i suoi refresh)
|
||||
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
|
||||
@ -2728,11 +2882,11 @@ class GitSvnSyncApp:
|
||||
post_action_sync_refresh_needed = True
|
||||
|
||||
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)
|
||||
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']:
|
||||
auth_related_error = False; conn_related_error = False
|
||||
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')
|
||||
elif conn_related_error: self._update_gui_auth_status('connection_failed')
|
||||
else: self._update_gui_auth_status('unknown_error')
|
||||
# Mostra popup specifico del task
|
||||
action_name = task_context.replace("_remote", "").replace("_", " ").title()
|
||||
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")
|
||||
|
||||
# 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)"])
|
||||
# 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:
|
||||
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
|
||||
delay_ms = 50
|
||||
current_delay = 50 # Ritardo base
|
||||
for refresh_func in refresh_list:
|
||||
try:
|
||||
self.master.after(delay_ms, refresh_func)
|
||||
delay_ms += 20 # Scaletta leggermente i refresh
|
||||
self.master.after(current_delay, refresh_func)
|
||||
current_delay += 50 # Scaletta leggermente i refresh
|
||||
except Exception as ref_e:
|
||||
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:
|
||||
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
|
||||
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")
|
||||
if current_repo_path:
|
||||
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)
|
||||
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
|
||||
|
||||
@ -1293,5 +1293,97 @@ def run_get_ahead_behind_async(
|
||||
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 ---
|
||||
|
||||
@ -1725,5 +1725,48 @@ class GitCommands:
|
||||
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
|
||||
|
||||
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 ---
|
||||
|
||||
124
gui.py
124
gui.py
@ -449,6 +449,7 @@ class MainFrame(ttk.Frame):
|
||||
push_remote_cb,
|
||||
push_tags_remote_cb,
|
||||
refresh_remote_status_cb,
|
||||
clone_remote_repo_cb,
|
||||
):
|
||||
"""Initializes the MainFrame."""
|
||||
super().__init__(master)
|
||||
@ -485,6 +486,7 @@ class MainFrame(ttk.Frame):
|
||||
self.push_remote_callback = push_remote_cb
|
||||
self.push_tags_remote_callback = push_tags_remote_cb
|
||||
self.refresh_remote_status_callback = refresh_remote_status_cb
|
||||
self.clone_remote_repo_callback = clone_remote_repo_cb
|
||||
|
||||
# Configure style (invariato)
|
||||
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.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(
|
||||
button_subframe,
|
||||
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")
|
||||
|
||||
|
||||
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 ---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user