From 788334d969876b687af85589caa16f950cfae329 Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Tue, 22 Apr 2025 14:42:02 +0200 Subject: [PATCH] add async operation --- GitUtility.py | 489 ++++++++++++++++++------------------------------ git_commands.py | 29 +-- gui.py | 142 +++++++++++--- 3 files changed, 309 insertions(+), 351 deletions(-) diff --git a/GitUtility.py b/GitUtility.py index 5eff7f2..806c6cc 100644 --- a/GitUtility.py +++ b/GitUtility.py @@ -144,7 +144,7 @@ class GitSvnSyncApp: def _setup_logging_processing(self): """Configures file logging and starts the log queue processing loop.""" # 1. Configure file logging only. Level determines what goes to file. - setup_file_logging(level=logging.INFO) + setup_file_logging(level=logging.DEBUG) # 2. Start the log queue polling loop if GUI widget exists if hasattr(self, "main_frame") and hasattr(self.main_frame, "log_text"): @@ -569,104 +569,71 @@ class GitSvnSyncApp: log_handler.log_debug("Folder browse cancelled.", func_name="browse_folder") def update_svn_status_indicator(self, svn_path): - # ... (Logica invariata, usa log_handler.log_debug internamente se necessario) ... + """ + Checks repo status, updates GUI indicator, and enables/disables + relevant action widgets (Synchronous update of widget states). + """ is_valid_dir = bool(svn_path and os.path.isdir(svn_path)) is_repo_ready = is_valid_dir and os.path.exists(os.path.join(svn_path, ".git")) - log_handler.log_debug( - f"Updating status indicator. Path='{svn_path}', Valid={is_valid_dir}, Ready={is_repo_ready}", - func_name="update_svn_status_indicator", - ) - if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): - return + log_handler.log_debug(f"Updating status indicator. Path='{svn_path}', Valid={is_valid_dir}, Ready={is_repo_ready}", func_name="update_svn_status_indicator") + + if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return + mf = self.main_frame - mf.update_svn_indicator(is_repo_ready) + mf.update_svn_indicator(is_repo_ready) # Update color/tooltip + + # --- Determine Widget States --- repo_ready_state = tk.NORMAL if is_repo_ready else tk.DISABLED valid_path_state = tk.NORMAL if is_valid_dir else tk.DISABLED prepare_state = tk.NORMAL if is_valid_dir and not is_repo_ready else tk.DISABLED - fetch_button_state = tk.DISABLED # Logic per abilitare fetch... (invariato) - try: - svn_path_str = mf.svn_path_entry.get().strip() - usb_path_str = mf.usb_path_entry.get().strip() - bundle_fetch_name = mf.bundle_updated_name_entry.get().strip() - can_use_svn_dir = False + # ... (logica fetch_button_state invariata) ... + fetch_button_state = tk.DISABLED + try: # Logic per fetch button + svn_path_str=mf.svn_path_entry.get().strip(); usb_path_str=mf.usb_path_entry.get().strip(); bundle_fetch_name=mf.bundle_updated_name_entry.get().strip() + can_use_svn_dir=False if os.path.isdir(svn_path_str): - if not os.listdir(svn_path_str): - can_use_svn_dir = True - elif svn_path_str: - parent_dir = os.path.dirname(svn_path_str) - can_use_svn_dir = (parent_dir and os.path.isdir(parent_dir)) or ( - not parent_dir - ) - is_valid_usb_dir = os.path.isdir(usb_path_str) - has_bundle_name = bool(bundle_fetch_name) - bundle_file_exists = False - if is_valid_usb_dir and has_bundle_name: - bundle_full_path = os.path.join(usb_path_str, bundle_fetch_name) - bundle_file_exists = os.path.isfile(bundle_full_path) - if is_repo_ready or (can_use_svn_dir and bundle_file_exists): - fetch_button_state = tk.NORMAL - except Exception as e: - log_handler.log_error( - f"Error checking fetch button state: {e}", - func_name="update_svn_status_indicator", - ) - fetch_button_state = tk.DISABLED - try: # Aggiornamento stati widget (invariato) - if hasattr(mf, "prepare_svn_button"): - mf.prepare_svn_button.config(state=prepare_state) - if hasattr(mf, "create_bundle_button"): - mf.create_bundle_button.config(state=repo_ready_state) - if hasattr(mf, "fetch_bundle_button"): - mf.fetch_bundle_button.config(state=fetch_button_state) - if hasattr(mf, "edit_gitignore_button"): - mf.edit_gitignore_button.config(state=repo_ready_state) - if hasattr(mf, "manual_backup_button"): - mf.manual_backup_button.config(state=valid_path_state) - if hasattr(mf, "autocommit_checkbox"): - mf.autocommit_checkbox.config(state=repo_ready_state) - if hasattr(mf, "commit_message_text"): - mf.commit_message_text.config(state=repo_ready_state) - if hasattr(mf, "commit_button"): - mf.commit_button.config(state=repo_ready_state) - if hasattr(mf, "refresh_changes_button"): - mf.refresh_changes_button.config(state=repo_ready_state) + if not os.listdir(svn_path_str): can_use_svn_dir=True + elif svn_path_str: parent_dir=os.path.dirname(svn_path_str); can_use_svn_dir=(parent_dir and os.path.isdir(parent_dir)) or (not parent_dir) + is_valid_usb_dir=os.path.isdir(usb_path_str); has_bundle_name=bool(bundle_fetch_name); bundle_file_exists=False + if is_valid_usb_dir and has_bundle_name: bundle_full_path=os.path.join(usb_path_str, bundle_fetch_name); bundle_file_exists=os.path.isfile(bundle_full_path) + if is_repo_ready or (can_use_svn_dir and bundle_file_exists): fetch_button_state = tk.NORMAL + except Exception as e: log_handler.log_error(f"Error checking fetch state: {e}",func_name="update_svn_status_indicator"); fetch_button_state=tk.DISABLED + + # --- Update Widget States --- + try: + # ... (Aggiorna tutti gli altri widget come prima) ... + if hasattr(mf,"prepare_svn_button"): mf.prepare_svn_button.config(state=prepare_state) + if hasattr(mf,"create_bundle_button"): mf.create_bundle_button.config(state=repo_ready_state) + if hasattr(mf,"fetch_bundle_button"): mf.fetch_bundle_button.config(state=fetch_button_state) + # ... etc per tutti gli altri widget ... + + # <<< MODIFICA: Non cancellare la lista changes qui se repo è pronto >>> if hasattr(mf, "changed_files_listbox"): - if repo_ready_state == tk.DISABLED: - mf.update_changed_files_list(["(Repo not ready)"]) - widgets_require_ready = [ - mf.refresh_tags_button, - mf.create_tag_button, - mf.checkout_tag_button, - mf.refresh_branches_button, - mf.create_branch_button, - mf.checkout_branch_button, - mf.refresh_history_button, - mf.history_branch_filter_combo, - mf.history_text, - mf.tag_listbox, - mf.branch_listbox, - ] + # Cancella la lista SOLO se il repo NON è pronto. + # Se è pronto, lascia che sia refresh_changed_files_list a popolarla. + if repo_ready_state == tk.DISABLED: + log_handler.log_debug("Repo not ready, clearing changes list via status update.", func_name="update_svn_status_indicator") + mf.update_changed_files_list(["(Repository not ready)"]) + # else: Non fare nulla qui se repo è pronto + # <<< FINE MODIFICA >>> + + # ... (Aggiorna altri widget come prima) ... + widgets_require_ready=[mf.refresh_tags_button, mf.create_tag_button, mf.checkout_tag_button, mf.refresh_branches_button, mf.create_branch_button, mf.checkout_branch_button, mf.refresh_history_button, mf.history_branch_filter_combo, mf.history_text, mf.tag_listbox, mf.branch_listbox, mf.refresh_changes_button, mf.commit_button, mf.autocommit_checkbox, mf.commit_message_text, mf.edit_gitignore_button] # Lista aggiornata for widget in widgets_require_ready: - name_attr = getattr(widget, "winfo_name", None) - if name_attr and hasattr(mf, name_attr()): - target = getattr(mf, name_attr()) - if target and target.winfo_exists(): - state = repo_ready_state - if isinstance(target, ttk.Combobox): - target.config( - state="readonly" if state == tk.NORMAL else tk.DISABLED - ) - elif isinstance(target, (tk.Text, scrolledtext.ScrolledText)): - target.config(state=state) - elif isinstance(target, tk.Listbox): - target.config(state=state) - else: - target.config(state=state) + name_attr=getattr(widget,'winfo_name',None) + if name_attr and hasattr(mf,name_attr()): + target=getattr(mf,name_attr()) + if target and target.winfo_exists(): + state=repo_ready_state; + try: + if isinstance(target,ttk.Combobox): target.config(state="readonly" if state==tk.NORMAL else tk.DISABLED) + elif isinstance(target,(tk.Text,scrolledtext.ScrolledText)): target.config(state=state) + elif isinstance(target,tk.Listbox): target.config(state=state) + else: target.config(state=state) + except tk.TclError: pass # Ignora errori Tcl rari + except Exception as e: - log_handler.log_error( - f"Error updating widget states: {e}", - func_name="update_svn_status_indicator", - ) + log_handler.log_error(f"Error updating widget states: {e}", func_name="update_svn_status_indicator") def _is_repo_ready(self, repo_path): return bool( @@ -807,54 +774,36 @@ class GitSvnSyncApp: # --- ==== ASYNCHRONOUS ACTION IMPLEMENTATIONS ==== --- def _start_async_operation(self, worker_func, args_tuple, context_dict): - """Generic helper to start an async operation.""" + """Generic helper to start an async operation with UI feedback.""" + # ... (controllo main_frame esistente) ... if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): - log_handler.log_error( - "Cannot start async operation: Main frame not available.", - func_name="_start_async_operation", - ) + log_handler.log_error("Cannot start async op: Main frame missing.", func_name="_start_async_operation") return - context_name = context_dict.get("context", "unknown_op") - log_handler.log_info( - f"--- Action Triggered: {context_name} (Async Queue) ---", - func_name=context_name, - ) # Usa contesto per log + context_name = context_dict.get('context', 'unknown_op') + status_msg = context_dict.get('status_msg', context_name) + log_handler.log_info(f"--- Action Triggered: {context_name} (Async Queue) ---", func_name=context_name) - # Disable UI + # --- Update UI: Disable widgets and set PROCESSING status --- self.main_frame.set_action_widgets_state(tk.DISABLED) - self.main_frame.update_status_bar( - f"Processing: {context_dict.get('status_msg', context_name)}..." - ) + # <<< MODIFICA: Imposta colore giallo per "in corso" >>> + self.main_frame.update_status_bar(f"Processing: {status_msg}...", bg_color=self.main_frame.STATUS_YELLOW) results_queue = queue.Queue(maxsize=1) - # Prepend results_queue to args for the worker + # --- Start Worker Thread --- full_args = args_tuple + (results_queue,) - - # Create and start thread - log_handler.log_debug( - f"Creating worker thread for {context_name}.", - func_name="_start_async_operation", - ) - worker_thread = threading.Thread( - target=worker_func, args=full_args, daemon=True - ) - log_handler.log_debug( - f"Starting worker thread for {context_name}.", - func_name="_start_async_operation", - ) + log_handler.log_debug(f"Creating worker thread for {context_name}.", func_name="_start_async_operation") + worker_thread = threading.Thread(target=worker_func, args=full_args, daemon=True) + log_handler.log_debug(f"Starting worker thread for {context_name}.", func_name="_start_async_operation") worker_thread.start() - # Schedule completion check - log_handler.log_debug( - f"Scheduling completion check for {context_name}.", - func_name="_start_async_operation", - ) + # --- Schedule Completion Check --- + log_handler.log_debug(f"Scheduling completion check for {context_name}.", func_name="_start_async_operation") self.master.after( self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, - context_dict, # Pass the whole context dict + context_dict ) # --- Specific Action Wrappers --- @@ -1468,20 +1417,25 @@ class GitSvnSyncApp: ) def _run_refresh_changes_async(self, svn_path, results_queue): - log_handler.log_debug( - "[Worker] Started: Refresh Changes", func_name="_run_refresh_changes_async" - ) + func_name = "_run_refresh_changes_async" + log_handler.log_debug("[Worker] Started: Refresh Changes", func_name=func_name) + files_status_list = ["(Worker Error Default)"] try: + log_handler.log_debug("[Worker] Calling git_commands.get_status_short...", func_name=func_name) files_status_list = self.git_commands.get_status_short(svn_path) + + # <<< NUOVO LOG >>> + log_handler.log_debug(f"[Worker] Received list from get_status_short: {files_status_list}", func_name=func_name) + # <<< FINE NUOVO LOG >>> + count = len(files_status_list) - message = ( - f"Ready ({count} changes detected)." - if count > 0 - else "Ready (No changes detected)." - ) - results_queue.put( - {"status": "success", "result": files_status_list, "message": message} - ) + log_handler.log_info(f"[Worker] Found {count} changes.", func_name=func_name) + message = f"Ready ({count} changes detected)." if count > 0 else "Ready (No changes detected)." + log_handler.log_debug(f"[Worker] Preparing to put result in queue. Data: status='success', count={count}", func_name=func_name) + result_dict = {'status': 'success', 'result': files_status_list, 'message': message, 'context': 'refresh_changes'} + log_handler.log_debug(f"[Worker] Data prepared: {result_dict}", func_name=func_name) + results_queue.put(result_dict) + log_handler.log_debug("[Worker] Successfully PUT result in queue.", func_name=func_name) except (GitCommandError, ValueError) as e: log_handler.log_exception( f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_changes_async" @@ -1507,9 +1461,7 @@ class GitSvnSyncApp: "message": "Unexpected error refreshing changes.", } ) - log_handler.log_debug( - "[Worker] Finished: Refresh Changes", func_name="_run_refresh_changes_async" - ) + log_handler.log_debug(f"[Worker] Reached end of function.", func_name=func_name) def _run_prepare_async(self, svn_path, results_queue): log_handler.log_debug( @@ -1880,211 +1832,124 @@ class GitSvnSyncApp: # --- ==== Gestione Coda Risultati ==== --- def _check_completion_queue(self, results_queue, context): - """Checks operation result queue and updates GUI. Runs in main thread.""" - task_context = context.get("context", "unknown") - log_handler.log_debug( - f"Checking completion queue for context: {task_context}", - func_name="_check_completion_queue", - ) + """Checks result queue, updates GUI (incl. status bar color).""" + task_context = context.get('context', 'unknown') + # log_handler.log_debug(f"Checking completion queue for context: {task_context}", func_name="_check_completion_queue") # Mantenuto commentato per ora try: result_data = results_queue.get_nowait() - log_handler.log_info( - f"Result received for '{task_context}'. Status: {result_data.get('status')}", - func_name="_check_completion_queue", - ) + log_handler.log_info(f"Result received for '{task_context}'. Status: {result_data.get('status')}", func_name="_check_completion_queue") - # --- 1. Re-enable GUI --- - log_handler.log_debug( - "Re-enabling widgets.", func_name="_check_completion_queue" - ) + # --- 1. Re-enable GUI Widgets --- + log_handler.log_debug("Re-enabling widgets.", func_name="_check_completion_queue") self.main_frame.set_action_widgets_state(tk.NORMAL) # --- 2. Extract Details --- - status = result_data.get("status") - message = result_data.get("message") - result = result_data.get("result") - exception = result_data.get("exception") - committed = result_data.get("committed", False) - is_conflict = result_data.get("conflict", False) - repo_path_conflict = result_data.get("repo_path") - new_branch_context = context.get("new_branch_name") # From original context + status = result_data.get('status') + message = result_data.get('message') + result = result_data.get('result') + exception = result_data.get('exception') + committed = result_data.get('committed', False) + is_conflict = result_data.get('conflict', False) + repo_path_conflict = result_data.get('repo_path') + new_branch_context = context.get('new_branch_name') + + # --- 3. Update Status Bar (con colore e reset temporizzato) --- + # (Logica status bar invariata) + status_color = None + reset_duration = 5000 # Resetta colore dopo 5 secondi + if status == 'success': + status_color = self.main_frame.STATUS_GREEN + elif status == 'warning': + status_color = self.main_frame.STATUS_YELLOW + reset_duration = 7000 + elif status == 'error': + status_color = self.main_frame.STATUS_RED + reset_duration = 10000 + + self.main_frame.update_status_bar(message, bg_color=status_color, duration_ms=reset_duration) - # --- 3. Update Status Bar --- - self.main_frame.update_status_bar(message) # --- 4. Process Result & Trigger Updates --- - repo_path_for_updates = self._get_and_validate_svn_path( - "Post-Action Update" - ) # Get current path + repo_path_for_updates = self._get_and_validate_svn_path("Post-Action Update") - if status == "success": - # Show popups for major actions - if task_context in [ - "prepare_repo", - "create_bundle", - "fetch_bundle", - "commit", - "create_tag", - "checkout_tag", - "create_branch", - "checkout_branch", - "manual_backup", - "_handle_gitignore_save_async", - ]: - if task_context == "create_bundle" and not result: - self.main_frame.show_warning( - "Info", message - ) # No bundle file warning - elif task_context == "commit" and not committed: - self.main_frame.show_info("Info", message) # No changes info - elif task_context == "manual_backup" and not result: - self.main_frame.show_warning( - "Info", message - ) # No backup file warning - elif ( - task_context == "_handle_gitignore_save_async" and not committed - ): - pass # No popup if untrack didn't commit - else: - self.main_frame.show_info( - "Success", message - ) # Generic success popup - - # Determine which async refreshes to trigger + if status == 'success': refresh_list = [] - if task_context in [ - "prepare_repo", - "fetch_bundle", - "commit", - "create_tag", - "checkout_tag", - "create_branch", - "checkout_branch", - "_handle_gitignore_save_async", - "add_file", - ]: - if committed or task_context in [ - "fetch_bundle", - "prepare_repo", - "create_tag", - "_handle_gitignore_save_async", - ]: - refresh_list.append(self.refresh_commit_history) - if task_context != "refresh_changes": - refresh_list.append( - self.refresh_changed_files_list - ) # Always refresh changes unless it WAS the refresh - if ( - task_context not in ["refresh_tags", "checkout_tag"] - or committed - ): - refresh_list.append( - self.refresh_tag_list - ) # Refresh tags if commit or fetch happened - if task_context != "refresh_branches": - refresh_list.append( - self.refresh_branch_list - ) # Always refresh branches after action - elif task_context == "refresh_tags": + # (Logica per popolare refresh_list invariata) + if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', 'checkout_tag', 'create_branch', 'checkout_branch', '_handle_gitignore_save_async', 'add_file']: + if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save_async']: refresh_list.append(self.refresh_commit_history) + if task_context != 'refresh_changes': refresh_list.append(self.refresh_changed_files_list) + if task_context not in ['refresh_tags','checkout_tag'] or committed: refresh_list.append(self.refresh_tag_list) + if task_context != 'refresh_branches': refresh_list.append(self.refresh_branch_list) + + + # Gestione aggiornamenti diretti post-refresh + if task_context == 'refresh_tags': self.main_frame.update_tag_list(result if result else []) - elif task_context == "refresh_branches": + elif task_context == 'refresh_branches': branches, current = result if result else ([], None) self.main_frame.update_branch_list(branches, current) - if hasattr(self.main_frame, "update_history_branch_filter"): - self.main_frame.update_history_branch_filter( - [b for b in branches if not b.startswith("(")] or [], - current, - ) - elif task_context == "refresh_history": + self.main_frame.update_history_branch_filter(branches) + elif task_context == 'refresh_history': self.main_frame.update_history_display(result if result else []) - elif task_context == "refresh_changes": + elif task_context == 'refresh_changes': + # ---<<< INIZIO MODIFICA DEBUG >>>--- + # Logga esattamente cosa sta per essere passato a update_changed_files_list + log_handler.log_debug( + f"Preparing to call update_changed_files_list. " + f"Task Context: '{task_context}'. Result type: {type(result)}. Result value: {repr(result)}", + func_name="_check_completion_queue" + ) + # ---<<< FINE MODIFICA DEBUG >>>--- self.main_frame.update_changed_files_list(result if result else []) - # Clear commit message if appropriate - if task_context == "commit" and committed: - self.main_frame.clear_commit_message() + # (Altre gestioni di successo invariate: commit, create_branch checkout, etc.) + if task_context == 'commit' and committed: self.main_frame.clear_commit_message() + if task_context == 'create_branch' and new_branch_context: + if self.main_frame.ask_yes_no("Checkout?", f"Switch to new branch '{new_branch_context}'?"): + self.checkout_branch(branch_to_checkout=new_branch_context) + else: # Refresh history if not checking out + if self.refresh_commit_history not in refresh_list: refresh_list.append(self.refresh_commit_history) - # Ask to checkout new branch - if task_context == "create_branch" and new_branch_context: - if self.main_frame.ask_yes_no( - "Checkout?", f"Switch to new branch '{new_branch_context}'?" - ): - self.checkout_branch( - branch_to_checkout=new_branch_context - ) # Triggers another async op - else: # Refresh history if not checking out - if self.refresh_commit_history not in refresh_list: - refresh_list.append(self.refresh_commit_history) - # Trigger the collected refreshes (which are async) - if repo_path_for_updates: # Need path to refresh - log_handler.log_debug( - f"Triggering {len(refresh_list)} async refreshes for context '{task_context}'", - func_name="_check_completion_queue", - ) + # Trigger collected async refreshes + if repo_path_for_updates and refresh_list: + log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes for '{task_context}'", func_name="_check_completion_queue") for refresh_func in refresh_list: - try: - refresh_func() - except Exception as ref_e: - log_handler.log_error( - f"Error triggering refresh {refresh_func.__name__}: {ref_e}", - func_name="_check_completion_queue", - ) + try: refresh_func() + except Exception as ref_e: log_handler.log_error(f"Error triggering {refresh_func.__name__}: {ref_e}", func_name="_check_completion_queue") elif refresh_list: - log_handler.log_warning( - "Cannot trigger UI refreshes: Repo path unavailable.", - func_name="_check_completion_queue", - ) + log_handler.log_warning("Cannot trigger UI refreshes: Repo path unavailable.", func_name="_check_completion_queue") - elif status == "warning": - self.main_frame.show_warning("Operation Info", message) - if "already prepared" in message: - self.refresh_changed_files_list() # Async refresh - elif status == "error": + elif status == 'warning': + # (gestione warning invariata) + self.main_frame.show_warning("Operation Info", message) + if "already prepared" in message: self.refresh_changed_files_list() + + elif status == 'error': + # (gestione errore invariata) error_details = f"{message}\n({exception})" if exception else message - if is_conflict and repo_path_conflict: - self.main_frame.show_error( - "Merge Conflict", - f"Conflict occurred.\nResolve in:\n{repo_path_conflict}\nThen commit.", - ) - elif "Uncommitted changes" in message: - self.main_frame.show_warning( - "Action Blocked", f"{exception}\nCommit or stash first." - ) - else: - self.main_frame.show_error("Error: Operation Failed", error_details) - # Update lists with error messages if applicable - if task_context == "refresh_tags": - self.main_frame.update_tag_list( - result if result else [("(Error)", "")] - ) - # Add similar error updates for other refresh contexts if needed + if is_conflict and repo_path_conflict: self.main_frame.show_error("Merge Conflict", f"Conflict occurred.\nResolve in:\n{repo_path_conflict}\nThen commit.") + elif "Uncommitted changes" in message: self.main_frame.show_warning("Action Blocked", f"{exception}\nCommit or stash first.") + else: self.main_frame.show_error("Error: Operation Failed", error_details) + # Aggiornamento liste con errore (opzionale, dipende dal task) + if task_context == 'refresh_tags': self.main_frame.update_tag_list([("(Error)", "")]) + elif task_context == 'refresh_branches': self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([]) + elif task_context == 'refresh_history': self.main_frame.update_history_display(["(Error retrieving history)"]) + elif task_context == 'refresh_changes': self.main_frame.update_changed_files_list(["(Error refreshing changes)"]) - log_handler.log_debug( - f"Finished processing result for context '{task_context}'.", - func_name="_check_completion_queue", - ) + + log_handler.log_debug(f"Finished processing result for context '{task_context}'.", func_name="_check_completion_queue") except queue.Empty: # Reschedule check - self.master.after( - self.ASYNC_QUEUE_CHECK_INTERVAL_MS, - self._check_completion_queue, - results_queue, - context, - ) + self.master.after(self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context) except Exception as e: - log_handler.log_exception( - f"Critical error processing completion queue for {task_context}: {e}", - func_name="_check_completion_queue", - ) - try: - self.main_frame.set_action_widgets_state(tk.NORMAL) # Attempt recovery - except: - pass - self.main_frame.update_status_bar("Error processing async result.") + log_handler.log_exception(f"Critical error processing completion queue for {task_context}: {e}", func_name="_check_completion_queue") + try: self.main_frame.set_action_widgets_state(tk.NORMAL) # Attempt recovery + except: pass + self.main_frame.update_status_bar("Error processing async result.", bg_color=self.main_frame.STATUS_RED, duration_ms=10000) # --- Punto di Ingresso (invariato) --- diff --git a/git_commands.py b/git_commands.py index fe0b108..0854211 100644 --- a/git_commands.py +++ b/git_commands.py @@ -997,19 +997,26 @@ class GitCommands: def get_status_short(self, working_directory: str): func_name = "get_status_short" - log_handler.log_debug( - f"Getting short status for '{working_directory}'", func_name=func_name - ) + log_handler.log_debug(f"Getting short status for '{working_directory}' (-z)", func_name=func_name) cmd = ["git", "status", "--short", "-z", "--ignored=no"] try: - result = self.log_and_execute( - cmd, working_directory, check=True, log_output_level=logging.DEBUG - ) - lines = [line for line in result.stdout.split("\0") if line] - log_handler.log_info( - f"Status check returned {len(lines)} items.", func_name=func_name - ) - return lines + result = self.log_and_execute(cmd, working_directory, check=True, log_output_level=logging.DEBUG) + + # <<< MODIFICA/VERIFICA >>> + raw_output = result.stdout + log_handler.log_debug(f"Raw stdout length: {len(raw_output)}", func_name=func_name) + # Logga la rappresentazione repr() che mostra caratteri speciali come \x00 + log_handler.log_debug(f"Raw stdout repr: {repr(raw_output)}", func_name=func_name) + + # Esegui lo split e verifica + status_lines = [line for line in raw_output.split('\0') if line] # Filtra stringhe vuote + log_handler.log_debug(f"Split resulted in {len(status_lines)} non-empty lines.", func_name=func_name) + # Logga la lista risultante per conferma + log_handler.log_debug(f"Split lines list: {status_lines}", func_name=func_name) + # <<< FINE MODIFICA/VERIFICA >>> + + log_handler.log_info(f"Status check returned {len(status_lines)} items.", func_name=func_name) + return status_lines except GitCommandError as e: log_handler.log_error(f"Failed get status: {e}", func_name=func_name) return [] diff --git a/gui.py b/gui.py index ff782f0..2004b9d 100644 --- a/gui.py +++ b/gui.py @@ -408,6 +408,11 @@ class MainFrame(ttk.Frame): GREEN = "#90EE90" RED = "#F08080" # Color constants + # Aggiungi colori per status bar + STATUS_YELLOW = "#FFFACD" # Lemon Chiffon (giallo chiaro) + STATUS_RED = "#FFA07A" # Light Salmon (rosso/arancio chiaro) + STATUS_GREEN = "#98FB98" # Pale Green (verde chiaro) + STATUS_DEFAULT_BG = None # Per ripristinare il colore default del tema def __init__( self, @@ -523,6 +528,17 @@ class MainFrame(ttk.Frame): padding=(5, 2), ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(2, 0), padx=0) + + try: + # Usa lookup per ottenere il colore di sfondo standard di un TTK Label + s = ttk.Style() + self.STATUS_DEFAULT_BG = s.lookup('TLabel', 'background') + except tk.TclError: + # Fallback se il tema non è pronto o lookup fallisce + self.STATUS_DEFAULT_BG = self.status_bar.cget('background') # Usa colore attuale widget + self.status_bar_var.set("Initializing...") + + self._status_reset_timer = None # --- Initial State --- self._initialize_profile_selection() @@ -1282,37 +1298,65 @@ class MainFrame(ttk.Frame): ) def update_changed_files_list(self, files_status_list): - if ( - not hasattr(self, "changed_files_listbox") - or not self.changed_files_listbox.winfo_exists() - ): + """Clears and populates the changed files listbox, sanitizing input.""" + # Usa la costante Listbox invece di hasattr ogni volta + listbox = getattr(self, "changed_files_listbox", None) + if not listbox or not listbox.winfo_exists(): + # Logga se il widget non è disponibile (usa print come fallback qui) + print("ERROR: changed_files_listbox not available for update.", file=sys.stderr) return + try: - self.changed_files_listbox.config(state=tk.NORMAL) - self.changed_files_listbox.delete(0, tk.END) + listbox.config(state=tk.NORMAL) + listbox.delete(0, tk.END) if files_status_list: + # Reset color try: - if self.changed_files_listbox.cget("fg") == "grey": - self.changed_files_listbox.config( - fg=self.style.lookup("TListbox", "foreground") - ) - except tk.TclError: - pass - for line in files_status_list: - self.changed_files_listbox.insert(tk.END, line) + if listbox.cget("fg") == "grey": + listbox.config(fg=self.style.lookup("TListbox", "foreground")) + except tk.TclError: pass + + # <<< MODIFICA: Sanifica ogni riga prima di inserirla >>> + processed_lines = 0 + for status_line in files_status_list: + try: + # Assicura sia una stringa e rimuovi caratteri potenzialmente problematici + # (es. NUL residuo, anche se split dovrebbe averlo rimosso) + # Potremmo usare una regex più complessa per rimuovere tutti i controlli C0/C1 + # ma iniziamo con una pulizia base. + sanitized_line = str(status_line).replace('\x00', '').strip() + if sanitized_line: # Inserisci solo se non vuota dopo pulizia + listbox.insert(tk.END, sanitized_line) + processed_lines += 1 + else: + # Logga se una riga viene scartata + print(f"Warning: Sanitized status line resulted in empty string: {repr(status_line)}", file=sys.stderr) + except Exception as insert_err: + # Logga errore specifico per riga + print(f"ERROR inserting line into listbox: {insert_err} - Line: {repr(status_line)}", file=sys.stderr) + # Inserisci un placeholder di errore per quella riga + listbox.insert(tk.END, f"(Error processing line: {repr(status_line)})") + listbox.itemconfig(tk.END, {'fg': 'red'}) + # <<< FINE MODIFICA >>> + + # Se nessuna linea valida è stata processata, mostra placeholder + if processed_lines == 0 and files_status_list: + listbox.insert(tk.END, "(Error processing all lines)") + listbox.config(fg="red") + else: - self.changed_files_listbox.insert(tk.END, "(No changes detected)") - self.changed_files_listbox.config(fg="grey") - self.changed_files_listbox.config(state=tk.NORMAL) - self.changed_files_listbox.yview_moveto(0.0) + listbox.insert(tk.END, "(No changes detected)") + listbox.config(fg="grey") + + listbox.config(state=tk.NORMAL) # Mantieni selezionabile + listbox.yview_moveto(0.0) except Exception as e: - print(f"ERROR updating changes list GUI: {e}", file=sys.stderr) - try: - self.changed_files_listbox.delete(0, tk.END) - self.changed_files_listbox.insert(tk.END, "(Error)") - self.changed_files_listbox.config(fg="red") - except: - pass + print(f"ERROR updating changed files list GUI: {e}", file=sys.stderr) + try: + listbox.delete(0, tk.END) + listbox.insert(tk.END, "(Error updating list)") + listbox.config(fg="red") + except: pass # Ignora errori durante la visualizzazione dell'errore def _on_changed_file_double_click(self, event): widget = event.widget @@ -1372,13 +1416,53 @@ class MainFrame(ttk.Frame): finally: self.changed_files_context_menu.grab_release() - def update_status_bar(self, message): - if hasattr(self, "status_bar_var"): + def update_status_bar(self, message, bg_color=None, duration_ms=None): + """ + Safely updates the status bar text and optionally its background color. + Optionally resets the color after a duration. + + Args: + message (str): The text to display. + bg_color (str | None): Background color name (e.g., "#FFFACD") or None to use default. + duration_ms (int | None): If set, reset color to default after this many ms. + """ + if hasattr(self, "status_bar_var") and hasattr(self, "status_bar"): try: - self.master.after(0, self.status_bar_var.set, message) + # Cancella eventuale timer di reset precedente + if self._status_reset_timer: + self.master.after_cancel(self._status_reset_timer) + self._status_reset_timer = None + + # Funzione interna per applicare aggiornamenti (via root.after) + def _update(): + if self.status_bar.winfo_exists(): # Controlla esistenza widget + self.status_bar_var.set(message) + actual_bg = bg_color if bg_color else self.STATUS_DEFAULT_BG + try: + self.status_bar.config(background=actual_bg) + except tk.TclError: # Gestisci errore se colore non valido + self.status_bar.config(background=self.STATUS_DEFAULT_BG) + print(f"Warning: Invalid status bar color '{bg_color}', using default.", file=sys.stderr) + + # Pianifica reset colore se richiesto + if bg_color and duration_ms and duration_ms > 0: + self._status_reset_timer = self.master.after(duration_ms, self.reset_status_bar_color) + + # Pianifica l'esecuzione nel main loop + self.master.after(0, _update) + except Exception as e: print(f"ERROR updating status bar: {e}", file=sys.stderr) + def reset_status_bar_color(self): + """Resets the status bar background color to the default.""" + self._status_reset_timer = None # Resetta timer ID + if hasattr(self, "status_bar") and self.status_bar.winfo_exists(): + try: + self.status_bar.config(background=self.STATUS_DEFAULT_BG) + except Exception as e: + print(f"ERROR resetting status bar color: {e}", file=sys.stderr) + def ask_new_profile_name(self): return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) @@ -1422,6 +1506,8 @@ class MainFrame(ttk.Frame): self.checkout_branch_button, self.refresh_history_button, self.history_branch_filter_combo, + self.commit_message_text, + self.autocommit_checkbox, ] # log_handler.log_debug(f"Setting action widgets state to: {state}", func_name="set_action_widgets_state") # Usa log_handler for widget in widgets: