add clone function, add remote function
This commit is contained in:
parent
b5467f0b63
commit
6b6318b191
236
GitUtility.py
236
GitUtility.py
@ -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(
|
||||||
@ -2375,6 +2376,102 @@ class GitSvnSyncApp:
|
|||||||
"remote_name": remote_name, # Passa nome remoto per messaggi
|
"remote_name": remote_name, # Passa nome remoto per messaggi
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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."""
|
||||||
@ -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
|
||||||
|
|||||||
@ -1292,6 +1292,98 @@ def run_get_ahead_behind_async(
|
|||||||
log_handler.log_debug(
|
log_handler.log_debug(
|
||||||
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 ---
|
||||||
|
|||||||
@ -1724,6 +1724,49 @@ class GitCommands:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
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
124
gui.py
@ -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 ---
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user