diff --git a/GitUtility.py b/GitUtility.py index 7851d29..3aeacc7 100644 --- a/GitUtility.py +++ b/GitUtility.py @@ -164,6 +164,7 @@ class GitSvnSyncApp: # === Dati / Istanze per la GUI === config_manager_instance=self.config_manager, profile_sections_list=self.config_manager.get_profile_sections(), + refresh_remote_status_cb=self.refresh_remote_status, ) print("MainFrame GUI created.") log_handler.log_debug( @@ -2371,6 +2372,104 @@ class GitSvnSyncApp: }, ) + def refresh_remote_status(self): + """Starts the async check for ahead/behind status.""" + func_name = "refresh_remote_status" + log_handler.log_info( + f"--- Action Triggered: Refresh Remote Sync Status ---", func_name=func_name + ) + + # Validazioni + if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): + return + svn_path = self._get_and_validate_svn_path("Refresh Sync Status") + if not svn_path or not self._is_repo_ready(svn_path): + log_handler.log_warning( + "Refresh Status skipped: Repo not ready.", func_name=func_name + ) + # Aggiorna label a stato neutro/sconosciuto se repo non pronto + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: (Repo not ready)" + ) + return + + # --- Ottieni branch corrente e upstream --- + current_branch = None + upstream_branch = None + try: + current_branch = self.git_commands.get_current_branch_name(svn_path) + if current_branch: + upstream_branch = self.git_commands.get_branch_upstream( + svn_path, current_branch + ) + else: + log_handler.log_warning( + "Refresh Status: Cannot get status, currently in detached HEAD state.", + func_name=func_name, + ) + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: (Detached HEAD)" + ) + return # Esce se detached + + if not upstream_branch: + log_handler.log_info( + f"Refresh Status: No upstream configured for branch '{current_branch}'.", + func_name=func_name, + ) + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text=f"Sync Status: Upstream not set for '{current_branch}'" + ) + # Abilita/Disabilita pulsanti Push/Pull in base all'upstream? Forse no qui. + # Potremmo disabilitare il pulsante "Refresh Status" stesso? + if hasattr(self.main_frame, "refresh_sync_status_button"): + self.main_frame.refresh_sync_status_button.config( + state=tk.DISABLED + ) # Disabilita refresh se manca upstream + return # Esce se manca upstream + + # Se siamo qui, abbiamo branch e upstream, abilita il pulsante refresh (se era disabilitato) + if hasattr(self.main_frame, "refresh_sync_status_button"): + self.main_frame.refresh_sync_status_button.config(state=tk.NORMAL) + + except Exception as e: + log_handler.log_exception( + f"Error getting branch/upstream before status check: {e}", + func_name=func_name, + ) + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Error getting info" + ) + return + + # --- Avvia worker asincrono --- + log_handler.log_info( + f"Checking ahead/behind status for '{current_branch}' vs '{upstream_branch}'...", + func_name=func_name, + ) + # Aggiorna label GUI a "checking..." + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Checking..." + ) + + args = (self.git_commands, svn_path, current_branch, upstream_branch) + self._start_async_operation( + async_workers.run_get_ahead_behind_async, # Worker esterno + args, + { + "context": "get_ahead_behind", # Contesto per il risultato + "status_msg": f"Checking sync status for '{current_branch}'", + # Passa nomi branch nel contesto per riferimento nel risultato + "local_branch": current_branch, + "upstream_branch": upstream_branch, + }, + ) + def _update_gui_auth_status(self, status: str): """Updates internal state and calls GUI update for auth indicator.""" self.remote_auth_status = status # Aggiorna stato interno @@ -2416,6 +2515,8 @@ class GitSvnSyncApp: "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 == "get_ahead_behind" and status_from_result == 'error': should_reenable_now = False # Riabilita i widget se non è necessario attendere if should_reenable_now: @@ -2423,11 +2524,12 @@ class GitSvnSyncApp: log_handler.log_debug("Re-enabling widgets.", func_name=func_name) self.main_frame.set_action_widgets_state(tk.NORMAL) else: + # Se la GUI non c'è, non c'è nulla da fare o riabilitare log_handler.log_warning( "Cannot re-enable widgets, MainFrame missing.", func_name=func_name, ) - return # Esce dalla funzione se la GUI non c'è più + return # Esce dalla funzione # --- Estrai dettagli dal risultato --- status = status_from_result # Usa la variabile già ottenuta @@ -2513,10 +2615,18 @@ class GitSvnSyncApp: ) if status == "success": auth_status = "ok" - # (...) log info (...) + if result_value == "connected_empty": + log_handler.log_info( + f"Connection check successful for '{remote_name}' (remote empty/unborn).", + func_name=func_name, + ) + else: + log_handler.log_info( + f"Connection check successful for '{remote_name}'.", + func_name=func_name, + ) self._update_gui_auth_status(auth_status) elif status == "auth_required": - # (...) log warning, update gui status, askyesno (...) log_handler.log_warning( f"Authentication required for remote '{remote_name}'.", func_name=func_name, @@ -2526,8 +2636,13 @@ class GitSvnSyncApp: if ( repo_path_checked and hasattr(self, "main_frame") - and self.main_frame.ask_yes_no(...) - ): # Messaggio invariato + and self.main_frame.ask_yes_no( + "Authentication Required", + f"Authentication is required to connect to remote '{remote_name}'.\n\n" + f"Do you want to attempt authentication now?\n" + f"(This may open a separate terminal window for credential input.)", + ) + ): log_handler.log_info( "User requested interactive authentication attempt.", func_name=func_name, @@ -2558,7 +2673,6 @@ class GitSvnSyncApp: self.main_frame.set_action_widgets_state(tk.NORMAL) elif status == "error": - # (... gestione errori connessione/sconosciuto e aggiornamento indicatore ...) error_type = ( result_value if result_value @@ -2570,7 +2684,6 @@ class GitSvnSyncApp: self.main_frame.show_error("Connection Error", f"{message}") elif task_context == "interactive_auth": - # (... gestione risultato interactive_auth e riavvio check ...) original_context = context.get("original_context", {}) remote_name = original_context.get( "remote_name_checked", remote_name_context or "unknown remote" @@ -2584,13 +2697,15 @@ class GitSvnSyncApp: self.main_frame.update_status_bar( f"Authentication successful. Checking status..." ) - self.check_connection_auth() + self.check_connection_auth() # Ri-avvia check silenzioso elif status == "error": log_handler.log_warning( f"Interactive auth attempt for '{remote_name}' failed or error occurred: {message}", func_name=func_name, ) - self._update_gui_auth_status("failed") + self._update_gui_auth_status( + "failed" + ) # Imposta indicatore su fallito if hasattr(self, "main_frame"): self.main_frame.show_warning( "Authentication Attempt Failed", f"{message}" @@ -2607,7 +2722,7 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.show_error( "Merge Conflict", - f"Merge conflict occurred during pull from '{remote_name_context or 'remote'}'.\n\n" # Usa nome dal contesto se disponibile + f"Merge conflict occurred during pull from '{remote_name_context or 'remote'}'.\n\n" f"Please resolve the conflicts manually in:\n{repo_path_conflict}\n\n" f"After resolving, stage the changes and commit them.", ) @@ -2653,9 +2768,38 @@ class GitSvnSyncApp: func_name=func_name, ) + # --- Gestione specifica per GET_AHEAD_BEHIND --- + elif task_context == "get_ahead_behind": + if status == "success": + ahead, behind = ( + result_value + if isinstance(result_value, tuple) + else (None, None) + ) + log_handler.log_info( + f"Ahead/Behind status updated: Ahead={ahead}, Behind={behind}", + func_name=func_name, + ) + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + ahead=ahead, behind=behind + ) + elif status == "error": + log_handler.log_error( + f"Failed to get ahead/behind status: {message}", + func_name=func_name, + ) + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text=f"Sync Status: Error" + ) + # --- Gestione risultati altri task (successo) --- elif status == "success": # Determina quali refresh avviare in base al task completato + post_action_refresh_needed = ( + False # Flag per refresh stato ahead/behind + ) if task_context in [ "prepare_repo", "fetch_bundle", @@ -2673,15 +2817,19 @@ class GitSvnSyncApp: "push_tags_remote", ]: # Logica per popolare refresh_list - if task_context == "push_remote": # Push Branch successo + if task_context == "push_remote": if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) - elif task_context == "push_tags_remote": # Push Tags successo + post_action_refresh_needed = True + elif task_context == "push_tags_remote": if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list) - elif task_context == "pull_remote": # Pull successo (non conflitto) + post_action_refresh_needed = ( + True # Anche push tags può cambiare stato relativo + ) + 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: @@ -2690,22 +2838,23 @@ class GitSvnSyncApp: 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 == "fetch_remote": # Fetch successo + post_action_refresh_needed = True + elif task_context == "fetch_remote": if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) if self.refresh_tag_list not in refresh_list: refresh_list.append(self.refresh_tag_list) - elif task_context == "apply_remote_config": # Apply Config successo - refresh_list.append( - self.check_connection_auth - ) # Controlla connessione dopo apply + post_action_refresh_needed = True + 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) - # Logica refresh per le altre azioni + post_action_refresh_needed = True # Controlla stato dopo apply + # Logica refresh per le altre azioni (non remote) else: if committed or task_context in [ "fetch_bundle", @@ -2743,6 +2892,8 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.update_branch_list(branches, current) self.main_frame.update_history_branch_filter(branches) + # Dopo refresh branch, aggiorna anche lo stato sync se possibile + post_action_refresh_needed = True elif task_context == "refresh_history": if hasattr(self, "main_frame"): self.main_frame.update_history_display( @@ -2766,8 +2917,11 @@ class GitSvnSyncApp: branch_to_checkout=new_branch_context, repo_path_override=repo_path_for_updates, ) + # Se non fa checkout, assicurati che la history venga aggiornata + # e anche lo stato sync (perché siamo su un nuovo branch senza upstream) elif self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) + post_action_refresh_needed = True # Aggiorna stato sync dopo creazione branch (sarà 'no upstream') # --- Trigger finale dei refresh asincroni raccolti --- if repo_path_for_updates and refresh_list: @@ -2789,6 +2943,22 @@ class GitSvnSyncApp: func_name=func_name, ) + # --- Triggera refresh stato ahead/behind se necessario --- + if post_action_refresh_needed: + # Solo se il repo è ancora valido (potrebbe essere cambiato da un checkout) + current_repo_path = self._get_and_validate_svn_path( + "Post-Action Sync Status Check" + ) + if current_repo_path: # Usa path corrente, non quello vecchio + log_handler.log_debug( + f"Triggering remote sync status refresh after '{task_context}'.", + func_name=func_name, + ) + # Usa after per separarlo leggermente dal ciclo corrente e permettere GUI update + self.master.after( + 100, self.refresh_remote_status + ) # Leggero ritardo + elif status == "warning": # Gestione warning generica: mostra popup if hasattr(self, "main_frame"): @@ -2799,6 +2969,7 @@ class GitSvnSyncApp: refresh_list.append(self.refresh_changed_files_list) if self.refresh_branch_list not in refresh_list: refresh_list.append(self.refresh_branch_list) + post_action_refresh_needed = True # Aggiorna stato sync anche qui if repo_path_for_updates and refresh_list: log_handler.log_debug( "Triggering refreshes after 'already prepared' warning.", @@ -2806,9 +2977,11 @@ class GitSvnSyncApp: ) for rf in refresh_list: rf() + if post_action_refresh_needed: + self.master.after(100, self.refresh_remote_status) elif status == "error": - # Gestione errori generica (esclusi check_connection, interactive_auth, pull_conflict, push_rejected) + # Gestione errori generica (esclusi check_connection, interactive_auth, pull_conflict, push_rejected, get_ahead_behind) log_handler.log_error( f"Error reported for task '{task_context}': {message}", func_name=func_name, @@ -2819,7 +2992,7 @@ class GitSvnSyncApp: else message ) - # Gestione errore per fetch_remote + # --- Gestione errore per fetch_remote --- if task_context == "fetch_remote": auth_related_error = False conn_related_error = False @@ -2847,13 +3020,17 @@ class GitSvnSyncApp: self._update_gui_auth_status("unknown_error") if hasattr(self, "main_frame"): self.main_frame.show_error("Fetch Error", f"{message}") + # Resetta stato ahead/behind su errore fetch + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Error" + ) - # Gestione errore per PULL (non conflitto) + # --- Gestione errore per PULL (non conflitto) --- elif task_context == "pull_remote": auth_related_error = False conn_related_error = False upst_related_error = False - stderr_low = "" if isinstance(exception, GitCommandError) and exception.stderr: stderr_low = exception.stderr.lower() if any( @@ -2865,7 +3042,6 @@ class GitSvnSyncApp: ] ): auth_related_error = True - if any( e in stderr_low for e in ["repository not found", "could not resolve host"] @@ -2884,8 +3060,12 @@ class GitSvnSyncApp: self._update_gui_auth_status("unknown_error") if hasattr(self, "main_frame"): self.main_frame.show_error("Pull Error", f"{message}") + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Error" + ) - # Gestione errore per PUSH (non rifiuto) e PUSH TAGS + # --- Gestione errore per PUSH (non rifiuto) e PUSH TAGS --- elif ( task_context == "push_remote" or task_context == "push_tags_remote" ): @@ -2918,8 +3098,12 @@ class GitSvnSyncApp: self._update_gui_auth_status("unknown_error") if hasattr(self, "main_frame"): self.main_frame.show_error(f"{action_name} Error", f"{message}") + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Error" + ) - # Gestione errori per altri task + # --- Gestione errori per altri task --- else: # Mostra popup specifici o generico if ( @@ -2951,6 +3135,11 @@ class GitSvnSyncApp: if hasattr(self, "main_frame"): self.main_frame.update_branch_list([], None) self.main_frame.update_history_branch_filter([]) + # Se fallisce refresh branch, stato sync è sconosciuto + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Unknown" + ) elif task_context == "refresh_history": if hasattr(self, "main_frame"): self.main_frame.update_history_display( @@ -2965,6 +3154,10 @@ class GitSvnSyncApp: self._update_gui_auth_status( "unknown_error" ) # Errore durante apply config + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Unknown" + ) # Log finale solo se non è stata gestita una ricorsione/nuovo avvio if should_reenable_now: @@ -2999,6 +3192,11 @@ class GitSvnSyncApp: bg_color=self.main_frame.STATUS_RED, duration_ms=10000, ) + # Resetta anche stato sync su errore grave + if hasattr(self.main_frame, "update_ahead_behind_status"): + self.main_frame.update_ahead_behind_status( + status_text="Sync Status: Error" + ) except Exception as recovery_e: log_handler.log_error( f"Failed to recover GUI after queue processing error: {recovery_e}", diff --git a/async_workers.py b/async_workers.py index d0d676e..2901713 100644 --- a/async_workers.py +++ b/async_workers.py @@ -1205,4 +1205,93 @@ def run_push_tags_async( ) +def run_get_ahead_behind_async( + git_commands: GitCommands, # Dipendenza + repo_path: str, + local_branch: str, + upstream_branch: str, + results_queue: queue.Queue, +): + """ + Worker function to get ahead/behind commit counts asynchronously. + Executed in a separate thread. + """ + func_name = "run_get_ahead_behind_async" + log_handler.log_debug( + f"[Worker] Started: Get Ahead/Behind for '{local_branch}' vs '{upstream_branch}' in '{repo_path}'", + func_name=func_name, + ) + + ahead_count = None + behind_count = None + status = "error" # Default a errore + message = f"Could not determine ahead/behind status for branch '{local_branch}'." + exception_obj = None + + try: + # Chiama il metodo in GitCommands per ottenere i conteggi + ahead_count, behind_count = git_commands.get_ahead_behind_count( + working_directory=repo_path, + local_branch=local_branch, + upstream_branch=upstream_branch, + ) + + # Verifica se il comando ha restituito valori validi (non None) + if ahead_count is not None and behind_count is not None: + status = "success" + # Costruisci un messaggio descrittivo + if ahead_count == 0 and behind_count == 0: + message = ( + f"Branch '{local_branch}' is up to date with '{upstream_branch}'." + ) + else: + parts = [] + if ahead_count > 0: + plural_a = "s" if ahead_count > 1 else "" + parts.append(f"{ahead_count} commit{plural_a} ahead") + if behind_count > 0: + plural_b = "s" if behind_count > 1 else "" + parts.append(f"{behind_count} commit{plural_b} behind") + message = ( + f"Branch '{local_branch}' is " + + " and ".join(parts) + + f" of '{upstream_branch}'." + ) + log_handler.log_info(f"[Worker] {message}", func_name=func_name) + + else: + # Se get_ahead_behind_count ha restituito None, significa che c'è stato un errore + # nel comando git o nell'analisi. Il messaggio di default va bene. + log_handler.log_warning( + f"[Worker] Failed to get valid ahead/behind counts for '{local_branch}'.", + func_name=func_name, + ) + status = "error" # Mantiene lo stato di errore + # Potremmo cercare di recuperare un'eccezione se disponibile, ma è complesso qui + + except Exception as e: + # Cattura eccezioni impreviste nel worker stesso + log_handler.log_exception( + f"[Worker] UNEXPECTED EXCEPTION getting ahead/behind: {e}", + func_name=func_name, + ) + status = "error" + message = f"Unexpected error getting ahead/behind status: {type(e).__name__}" + exception_obj = e + + # Metti il risultato nella coda + results_queue.put( + { + "status": status, + "result": (ahead_count, behind_count), # La tupla con i conteggi (o None) + "message": message, + "exception": exception_obj, # Passa l'eccezione se c'è stata + } + ) + + log_handler.log_debug( + f"[Worker] Finished: Get Ahead/Behind for '{local_branch}'", func_name=func_name + ) + + # --- END OF FILE async_workers.py --- diff --git a/git_commands.py b/git_commands.py index e9519a1..14b88e1 100644 --- a/git_commands.py +++ b/git_commands.py @@ -10,6 +10,7 @@ import re # Importa il nuovo gestore della coda log import log_handler +from typing import Tuple, Dict, List # --- Custom Exception Definition (invariata) --- @@ -1656,5 +1657,90 @@ class GitCommands: ) return None + def get_ahead_behind_count( + self, working_directory: str, local_branch: str, upstream_branch: str + ) -> Tuple[int | None, int | None]: + """ + Gets the number of commits the local branch is ahead and behind its upstream counterpart. + + Args: + working_directory (str): Path to the repository. + local_branch (str): The name of the local branch. + upstream_branch (str): The full name of the upstream branch (e.g., 'origin/main'). + + Returns: + Tuple[int | None, int | None]: A tuple containing (ahead_count, behind_count). + Returns (None, None) if the command fails or + branches are invalid/unrelated. + """ + func_name = "get_ahead_behind_count" + log_handler.log_debug( + f"Getting ahead/behind count for '{local_branch}'...'{upstream_branch}' in '{working_directory}'", + func_name=func_name, + ) + + # Comando: git rev-list --count --left-right ... + # L'output è nel formato: \t + # Usiamo '...' per la differenza simmetrica. + cmd = [ + "git", + "rev-list", + "--count", + "--left-right", + f"{local_branch}...{upstream_branch}", + ] + + try: + # Esegui catturando output, nascondendo console. check=False perché potrebbe fallire + # se i branch non esistono o non hanno un antenato comune. + result = self.log_and_execute( + cmd, + working_directory, + check=False, + capture=True, + hide_console=True, + log_output_level=logging.DEBUG, + ) + + if result.returncode == 0 and result.stdout: + output = result.stdout.strip() + parts = output.split() # Divide per spazio o tab + if len(parts) == 2: + try: + behind_count = int( + parts[0] + ) # Il primo numero è 'behind' (--left-right A...B -> A è left) + ahead_count = int(parts[1]) # Il secondo numero è 'ahead' + log_handler.log_info( + f"Ahead/Behind for '{local_branch}': Ahead={ahead_count}, Behind={behind_count}", + func_name=func_name, + ) + return ahead_count, behind_count + except ValueError: + log_handler.log_error( + f"Failed to parse rev-list count output: '{output}'", + func_name=func_name, + ) + return None, None + else: + log_handler.log_error( + f"Unexpected output format from rev-list count: '{output}'", + func_name=func_name, + ) + return None, None + else: + # Il comando potrebbe fallire se i branch non sono validi, non hanno antenato comune, ecc. + log_handler.log_warning( + f"Failed to get ahead/behind count (RC={result.returncode}). Maybe invalid branches or no common history? Stderr: {result.stderr.strip() if result.stderr else 'N/A'}", + func_name=func_name, + ) + return None, None # Segnala fallimento + + except Exception as e: + log_handler.log_exception( + f"Unexpected error getting ahead/behind count: {e}", func_name=func_name + ) + return None, None # Segnala fallimento + # --- END OF FILE git_commands.py --- diff --git a/gui.py b/gui.py index d39523a..30d4e11 100644 --- a/gui.py +++ b/gui.py @@ -448,6 +448,7 @@ class MainFrame(ttk.Frame): pull_remote_cb, push_remote_cb, push_tags_remote_cb, + refresh_remote_status_cb, ): """Initializes the MainFrame.""" super().__init__(master) @@ -483,6 +484,7 @@ class MainFrame(ttk.Frame): self.pull_remote_callback = pull_remote_cb 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 # Configure style (invariato) self.style = ttk.Style() @@ -513,6 +515,7 @@ class MainFrame(ttk.Frame): self.remote_url_var = tk.StringVar() self.remote_name_var = tk.StringVar() self.remote_auth_status_var = tk.StringVar(value="Status: Unknown") + self.remote_ahead_behind_var = tk.StringVar(value="Sync Status: Unknown") # --- Create UI Elements --- self._create_profile_frame() @@ -703,6 +706,42 @@ class MainFrame(ttk.Frame): # Imposta colore iniziale (es. grigio) self._update_auth_status_indicator("unknown") + # --- Sync Status --- + status_frame = ttk.LabelFrame( + frame, text="Synchronization Status", padding=(10, 5) + ) + status_frame.pack(pady=(5, 5), fill="x", expand=False) + status_frame.columnconfigure(0, weight=1) # Label si espande + + # Label per Ahead/Behind + self.ahead_behind_label = ttk.Label( + status_frame, + textvariable=self.remote_ahead_behind_var, # Collegato alla variabile + anchor=tk.W, # Allinea testo a sinistra + # relief=tk.SUNKEN, # Forse senza bordo è meglio? + padding=(5, 2), + ) + self.ahead_behind_label.grid(row=0, column=0, sticky=tk.EW, padx=(5, 10)) + self.create_tooltip( + self.ahead_behind_label, + "Indicates if the local branch is ahead (needs push) or behind (needs pull) the remote branch.", + ) + + # Pulsante Refresh Status + self.refresh_sync_status_button = ttk.Button( + status_frame, + text="Refresh Status", + command=self.refresh_remote_status_callback, # Nuovo callback + state=tk.DISABLED, # Abilitato quando repo pronto e upstream configurato? + ) + self.refresh_sync_status_button.grid( + row=0, column=1, sticky=tk.E, padx=(0, 5), pady=2 + ) + self.create_tooltip( + self.refresh_sync_status_button, + "Check how many commits the local branch is ahead or behind the remote branch.", + ) + # --- Sezione Azioni Remote --- actions_frame = ttk.LabelFrame(frame, text="Remote Actions", padding=(10, 5)) actions_frame.pack(pady=10, fill="x", expand=False) @@ -1862,6 +1901,7 @@ class MainFrame(ttk.Frame): self.pull_button, self.push_button, self.push_tags_button, + self.refresh_sync_status_button, ] # log_handler.log_debug(f"Setting action widgets state to: {state}", func_name="set_action_widgets_state") # Usa log_handler for widget in widgets: @@ -1893,5 +1933,45 @@ class MainFrame(ttk.Frame): except Exception as e: pass # log_handler.log_error(f"Error setting state for profile dropdown: {e}", func_name="set_action_widgets_state") + def update_ahead_behind_status( + self, + status_text: str | None = None, + ahead: int | None = None, + behind: int | None = None, + ): + """Updates the ahead/behind status label.""" + label = getattr(self, "ahead_behind_label", None) + var = getattr(self, "remote_ahead_behind_var", None) + if not label or not var or not label.winfo_exists(): + return + + text_to_display = "Sync Status: Unknown" # Default + + if status_text is not None: + # Se viene passato un testo specifico (es. errore, no upstream), usa quello + text_to_display = status_text + elif ahead is not None and behind is not None: + # Se abbiamo i conteggi, costruisci il messaggio + if ahead == 0 and behind == 0: + text_to_display = "Sync Status: Up to date" + else: + parts = [] + if ahead > 0: + plural_a = "s" if ahead > 1 else "" + parts.append(f"{ahead} commit{plural_a} ahead (Push needed)") + if behind > 0: + plural_b = "s" if behind > 1 else "" + parts.append(f"{behind} commit{plural_b} behind (Pull needed)") + text_to_display = "Sync Status: " + ", ".join(parts) + # else: Se non viene passato testo e i conteggi sono None, rimane "Unknown" + + try: + var.set(text_to_display) + except Exception as e: + log_handler.log_error( + f"Failed to update ahead/behind status variable: {e}", + func_name="update_ahead_behind_status", + ) + # --- END OF FILE gui.py ---