add async operation

This commit is contained in:
VALLONGOL 2025-04-22 14:42:02 +02:00
parent db2878034f
commit 788334d969
3 changed files with 309 additions and 351 deletions

View File

@ -144,7 +144,7 @@ class GitSvnSyncApp:
def _setup_logging_processing(self): def _setup_logging_processing(self):
"""Configures file logging and starts the log queue processing loop.""" """Configures file logging and starts the log queue processing loop."""
# 1. Configure file logging only. Level determines what goes to file. # 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 # 2. Start the log queue polling loop if GUI widget exists
if hasattr(self, "main_frame") and hasattr(self.main_frame, "log_text"): 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") log_handler.log_debug("Folder browse cancelled.", func_name="browse_folder")
def update_svn_status_indicator(self, svn_path): 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_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")) is_repo_ready = is_valid_dir and os.path.exists(os.path.join(svn_path, ".git"))
log_handler.log_debug( 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")
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
)
if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists():
return
mf = self.main_frame 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 repo_ready_state = tk.NORMAL if is_repo_ready else tk.DISABLED
valid_path_state = tk.NORMAL if is_valid_dir 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 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) # ... (logica fetch_button_state invariata) ...
try: fetch_button_state = tk.DISABLED
svn_path_str = mf.svn_path_entry.get().strip() try: # Logic per fetch button
usb_path_str = mf.usb_path_entry.get().strip() 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()
bundle_fetch_name = mf.bundle_updated_name_entry.get().strip() can_use_svn_dir=False
can_use_svn_dir = False
if os.path.isdir(svn_path_str): if os.path.isdir(svn_path_str):
if not os.listdir(svn_path_str): if not os.listdir(svn_path_str): can_use_svn_dir=True
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)
elif svn_path_str: is_valid_usb_dir=os.path.isdir(usb_path_str); has_bundle_name=bool(bundle_fetch_name); bundle_file_exists=False
parent_dir = os.path.dirname(svn_path_str) 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)
can_use_svn_dir = (parent_dir and os.path.isdir(parent_dir)) or ( if is_repo_ready or (can_use_svn_dir and bundle_file_exists): fetch_button_state = tk.NORMAL
not parent_dir 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
)
is_valid_usb_dir = os.path.isdir(usb_path_str) # --- Update Widget States ---
has_bundle_name = bool(bundle_fetch_name) try:
bundle_file_exists = False # ... (Aggiorna tutti gli altri widget come prima) ...
if is_valid_usb_dir and has_bundle_name: if hasattr(mf,"prepare_svn_button"): mf.prepare_svn_button.config(state=prepare_state)
bundle_full_path = os.path.join(usb_path_str, bundle_fetch_name) if hasattr(mf,"create_bundle_button"): mf.create_bundle_button.config(state=repo_ready_state)
bundle_file_exists = os.path.isfile(bundle_full_path) if hasattr(mf,"fetch_bundle_button"): mf.fetch_bundle_button.config(state=fetch_button_state)
if is_repo_ready or (can_use_svn_dir and bundle_file_exists): # ... etc per tutti gli altri widget ...
fetch_button_state = tk.NORMAL
except Exception as e: # <<< MODIFICA: Non cancellare la lista changes qui se repo è pronto >>>
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 hasattr(mf, "changed_files_listbox"): if hasattr(mf, "changed_files_listbox"):
if repo_ready_state == tk.DISABLED: # Cancella la lista SOLO se il repo NON è pronto.
mf.update_changed_files_list(["(Repo not ready)"]) # Se è pronto, lascia che sia refresh_changed_files_list a popolarla.
widgets_require_ready = [ if repo_ready_state == tk.DISABLED:
mf.refresh_tags_button, log_handler.log_debug("Repo not ready, clearing changes list via status update.", func_name="update_svn_status_indicator")
mf.create_tag_button, mf.update_changed_files_list(["(Repository not ready)"])
mf.checkout_tag_button, # else: Non fare nulla qui se repo è pronto
mf.refresh_branches_button, # <<< FINE MODIFICA >>>
mf.create_branch_button,
mf.checkout_branch_button, # ... (Aggiorna altri widget come prima) ...
mf.refresh_history_button, 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
mf.history_branch_filter_combo,
mf.history_text,
mf.tag_listbox,
mf.branch_listbox,
]
for widget in widgets_require_ready: for widget in widgets_require_ready:
name_attr = getattr(widget, "winfo_name", None) name_attr=getattr(widget,'winfo_name',None)
if name_attr and hasattr(mf, name_attr()): if name_attr and hasattr(mf,name_attr()):
target = getattr(mf, name_attr()) target=getattr(mf,name_attr())
if target and target.winfo_exists(): if target and target.winfo_exists():
state = repo_ready_state state=repo_ready_state;
if isinstance(target, ttk.Combobox): try:
target.config( if isinstance(target,ttk.Combobox): target.config(state="readonly" if state==tk.NORMAL else tk.DISABLED)
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)
elif isinstance(target, (tk.Text, scrolledtext.ScrolledText)): else: target.config(state=state)
target.config(state=state) except tk.TclError: pass # Ignora errori Tcl rari
elif isinstance(target, tk.Listbox):
target.config(state=state)
else:
target.config(state=state)
except Exception as e: except Exception as e:
log_handler.log_error( log_handler.log_error(f"Error updating widget states: {e}", func_name="update_svn_status_indicator")
f"Error updating widget states: {e}",
func_name="update_svn_status_indicator",
)
def _is_repo_ready(self, repo_path): def _is_repo_ready(self, repo_path):
return bool( return bool(
@ -807,54 +774,36 @@ class GitSvnSyncApp:
# --- ==== ASYNCHRONOUS ACTION IMPLEMENTATIONS ==== --- # --- ==== ASYNCHRONOUS ACTION IMPLEMENTATIONS ==== ---
def _start_async_operation(self, worker_func, args_tuple, context_dict): 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(): if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists():
log_handler.log_error( log_handler.log_error("Cannot start async op: Main frame missing.", func_name="_start_async_operation")
"Cannot start async operation: Main frame not available.",
func_name="_start_async_operation",
)
return return
context_name = context_dict.get("context", "unknown_op") context_name = context_dict.get('context', 'unknown_op')
log_handler.log_info( status_msg = context_dict.get('status_msg', context_name)
f"--- Action Triggered: {context_name} (Async Queue) ---", log_handler.log_info(f"--- Action Triggered: {context_name} (Async Queue) ---", func_name=context_name)
func_name=context_name,
) # Usa contesto per log
# Disable UI # --- Update UI: Disable widgets and set PROCESSING status ---
self.main_frame.set_action_widgets_state(tk.DISABLED) self.main_frame.set_action_widgets_state(tk.DISABLED)
self.main_frame.update_status_bar( # <<< MODIFICA: Imposta colore giallo per "in corso" >>>
f"Processing: {context_dict.get('status_msg', context_name)}..." self.main_frame.update_status_bar(f"Processing: {status_msg}...", bg_color=self.main_frame.STATUS_YELLOW)
)
results_queue = queue.Queue(maxsize=1) results_queue = queue.Queue(maxsize=1)
# Prepend results_queue to args for the worker # --- Start Worker Thread ---
full_args = args_tuple + (results_queue,) full_args = args_tuple + (results_queue,)
log_handler.log_debug(f"Creating worker thread for {context_name}.", func_name="_start_async_operation")
# Create and start thread worker_thread = threading.Thread(target=worker_func, args=full_args, daemon=True)
log_handler.log_debug( log_handler.log_debug(f"Starting worker thread for {context_name}.", func_name="_start_async_operation")
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() worker_thread.start()
# Schedule completion check # --- Schedule Completion Check ---
log_handler.log_debug( log_handler.log_debug(f"Scheduling completion check for {context_name}.", func_name="_start_async_operation")
f"Scheduling completion check for {context_name}.",
func_name="_start_async_operation",
)
self.master.after( self.master.after(
self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self.ASYNC_QUEUE_CHECK_INTERVAL_MS,
self._check_completion_queue, self._check_completion_queue,
results_queue, results_queue,
context_dict, # Pass the whole context dict context_dict
) )
# --- Specific Action Wrappers --- # --- Specific Action Wrappers ---
@ -1468,20 +1417,25 @@ class GitSvnSyncApp:
) )
def _run_refresh_changes_async(self, svn_path, results_queue): def _run_refresh_changes_async(self, svn_path, results_queue):
log_handler.log_debug( func_name = "_run_refresh_changes_async"
"[Worker] Started: Refresh Changes", 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: 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) 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) count = len(files_status_list)
message = ( log_handler.log_info(f"[Worker] Found {count} changes.", func_name=func_name)
f"Ready ({count} changes detected)." message = f"Ready ({count} changes detected)." if count > 0 else "Ready (No changes detected)."
if count > 0 log_handler.log_debug(f"[Worker] Preparing to put result in queue. Data: status='success', count={count}", func_name=func_name)
else "Ready (No changes detected)." 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( results_queue.put(result_dict)
{"status": "success", "result": files_status_list, "message": message} log_handler.log_debug("[Worker] Successfully PUT result in queue.", func_name=func_name)
)
except (GitCommandError, ValueError) as e: except (GitCommandError, ValueError) as e:
log_handler.log_exception( log_handler.log_exception(
f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_changes_async" f"[Worker] EXCEPTION: {e}", func_name="_run_refresh_changes_async"
@ -1507,9 +1461,7 @@ class GitSvnSyncApp:
"message": "Unexpected error refreshing changes.", "message": "Unexpected error refreshing changes.",
} }
) )
log_handler.log_debug( log_handler.log_debug(f"[Worker] Reached end of function.", func_name=func_name)
"[Worker] Finished: Refresh Changes", func_name="_run_refresh_changes_async"
)
def _run_prepare_async(self, svn_path, results_queue): def _run_prepare_async(self, svn_path, results_queue):
log_handler.log_debug( log_handler.log_debug(
@ -1880,211 +1832,124 @@ class GitSvnSyncApp:
# --- ==== Gestione Coda Risultati ==== --- # --- ==== Gestione Coda Risultati ==== ---
def _check_completion_queue(self, results_queue, context): def _check_completion_queue(self, results_queue, context):
"""Checks operation result queue and updates GUI. Runs in main thread.""" """Checks result queue, updates GUI (incl. status bar color)."""
task_context = context.get("context", "unknown") task_context = context.get('context', 'unknown')
log_handler.log_debug( # log_handler.log_debug(f"Checking completion queue for context: {task_context}", func_name="_check_completion_queue") # Mantenuto commentato per ora
f"Checking completion queue for context: {task_context}",
func_name="_check_completion_queue",
)
try: try:
result_data = results_queue.get_nowait() result_data = results_queue.get_nowait()
log_handler.log_info( log_handler.log_info(f"Result received for '{task_context}'. Status: {result_data.get('status')}", func_name="_check_completion_queue")
f"Result received for '{task_context}'. Status: {result_data.get('status')}",
func_name="_check_completion_queue",
)
# --- 1. Re-enable GUI --- # --- 1. Re-enable GUI Widgets ---
log_handler.log_debug( log_handler.log_debug("Re-enabling widgets.", func_name="_check_completion_queue")
"Re-enabling widgets.", func_name="_check_completion_queue"
)
self.main_frame.set_action_widgets_state(tk.NORMAL) self.main_frame.set_action_widgets_state(tk.NORMAL)
# --- 2. Extract Details --- # --- 2. Extract Details ---
status = result_data.get("status") status = result_data.get('status')
message = result_data.get("message") message = result_data.get('message')
result = result_data.get("result") result = result_data.get('result')
exception = result_data.get("exception") exception = result_data.get('exception')
committed = result_data.get("committed", False) committed = result_data.get('committed', False)
is_conflict = result_data.get("conflict", False) is_conflict = result_data.get('conflict', False)
repo_path_conflict = result_data.get("repo_path") repo_path_conflict = result_data.get('repo_path')
new_branch_context = context.get("new_branch_name") # From original context 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 --- # --- 4. Process Result & Trigger Updates ---
repo_path_for_updates = self._get_and_validate_svn_path( repo_path_for_updates = self._get_and_validate_svn_path("Post-Action Update")
"Post-Action Update"
) # Get current path
if status == "success": 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
refresh_list = [] refresh_list = []
if task_context in [ # (Logica per popolare refresh_list invariata)
"prepare_repo", if task_context in ['prepare_repo', 'fetch_bundle', 'commit', 'create_tag', 'checkout_tag', 'create_branch', 'checkout_branch', '_handle_gitignore_save_async', 'add_file']:
"fetch_bundle", if committed or task_context in ['fetch_bundle','prepare_repo','create_tag','_handle_gitignore_save_async']: refresh_list.append(self.refresh_commit_history)
"commit", if task_context != 'refresh_changes': refresh_list.append(self.refresh_changed_files_list)
"create_tag", if task_context not in ['refresh_tags','checkout_tag'] or committed: refresh_list.append(self.refresh_tag_list)
"checkout_tag", if task_context != 'refresh_branches': refresh_list.append(self.refresh_branch_list)
"create_branch",
"checkout_branch",
"_handle_gitignore_save_async", # Gestione aggiornamenti diretti post-refresh
"add_file", if task_context == 'refresh_tags':
]:
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":
self.main_frame.update_tag_list(result if result else []) 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) branches, current = result if result else ([], None)
self.main_frame.update_branch_list(branches, current) self.main_frame.update_branch_list(branches, current)
if hasattr(self.main_frame, "update_history_branch_filter"): self.main_frame.update_history_branch_filter(branches)
self.main_frame.update_history_branch_filter( elif task_context == 'refresh_history':
[b for b in branches if not b.startswith("(")] or [],
current,
)
elif task_context == "refresh_history":
self.main_frame.update_history_display(result if result else []) 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 []) self.main_frame.update_changed_files_list(result if result else [])
# Clear commit message if appropriate # (Altre gestioni di successo invariate: commit, create_branch checkout, etc.)
if task_context == "commit" and committed: if task_context == 'commit' and committed: self.main_frame.clear_commit_message()
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) # Trigger collected async refreshes
if repo_path_for_updates: # Need path to refresh if repo_path_for_updates and refresh_list:
log_handler.log_debug( log_handler.log_debug(f"Triggering {len(refresh_list)} async refreshes for '{task_context}'", func_name="_check_completion_queue")
f"Triggering {len(refresh_list)} async refreshes for context '{task_context}'",
func_name="_check_completion_queue",
)
for refresh_func in refresh_list: for refresh_func in refresh_list:
try: try: refresh_func()
refresh_func() except Exception as ref_e: log_handler.log_error(f"Error triggering {refresh_func.__name__}: {ref_e}", func_name="_check_completion_queue")
except Exception as ref_e:
log_handler.log_error(
f"Error triggering refresh {refresh_func.__name__}: {ref_e}",
func_name="_check_completion_queue",
)
elif refresh_list: elif refresh_list:
log_handler.log_warning( log_handler.log_warning("Cannot trigger UI refreshes: Repo path unavailable.", func_name="_check_completion_queue")
"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 error_details = f"{message}\n({exception})" if exception else message
if is_conflict and repo_path_conflict: 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.")
self.main_frame.show_error( elif "Uncommitted changes" in message: self.main_frame.show_warning("Action Blocked", f"{exception}\nCommit or stash first.")
"Merge Conflict", else: self.main_frame.show_error("Error: Operation Failed", error_details)
f"Conflict occurred.\nResolve in:\n{repo_path_conflict}\nThen commit.", # Aggiornamento liste con errore (opzionale, dipende dal task)
) if task_context == 'refresh_tags': self.main_frame.update_tag_list([("(Error)", "")])
elif "Uncommitted changes" in message: elif task_context == 'refresh_branches': self.main_frame.update_branch_list([], None); self.main_frame.update_history_branch_filter([])
self.main_frame.show_warning( elif task_context == 'refresh_history': self.main_frame.update_history_display(["(Error retrieving history)"])
"Action Blocked", f"{exception}\nCommit or stash first." elif task_context == 'refresh_changes': self.main_frame.update_changed_files_list(["(Error refreshing changes)"])
)
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
log_handler.log_debug(
f"Finished processing result for context '{task_context}'.", log_handler.log_debug(f"Finished processing result for context '{task_context}'.", func_name="_check_completion_queue")
func_name="_check_completion_queue",
)
except queue.Empty: except queue.Empty:
# Reschedule check # Reschedule check
self.master.after( self.master.after(self.ASYNC_QUEUE_CHECK_INTERVAL_MS, self._check_completion_queue, results_queue, context)
self.ASYNC_QUEUE_CHECK_INTERVAL_MS,
self._check_completion_queue,
results_queue,
context,
)
except Exception as e: except Exception as e:
log_handler.log_exception( log_handler.log_exception(f"Critical error processing completion queue for {task_context}: {e}", func_name="_check_completion_queue")
f"Critical error processing completion queue for {task_context}: {e}", try: self.main_frame.set_action_widgets_state(tk.NORMAL) # Attempt recovery
func_name="_check_completion_queue", except: pass
) self.main_frame.update_status_bar("Error processing async result.", bg_color=self.main_frame.STATUS_RED, duration_ms=10000)
try:
self.main_frame.set_action_widgets_state(tk.NORMAL) # Attempt recovery
except:
pass
self.main_frame.update_status_bar("Error processing async result.")
# --- Punto di Ingresso (invariato) --- # --- Punto di Ingresso (invariato) ---

View File

@ -997,19 +997,26 @@ class GitCommands:
def get_status_short(self, working_directory: str): def get_status_short(self, working_directory: str):
func_name = "get_status_short" func_name = "get_status_short"
log_handler.log_debug( log_handler.log_debug(f"Getting short status for '{working_directory}' (-z)", func_name=func_name)
f"Getting short status for '{working_directory}'", func_name=func_name
)
cmd = ["git", "status", "--short", "-z", "--ignored=no"] cmd = ["git", "status", "--short", "-z", "--ignored=no"]
try: try:
result = self.log_and_execute( result = self.log_and_execute(cmd, working_directory, check=True, log_output_level=logging.DEBUG)
cmd, working_directory, check=True, log_output_level=logging.DEBUG
) # <<< MODIFICA/VERIFICA >>>
lines = [line for line in result.stdout.split("\0") if line] raw_output = result.stdout
log_handler.log_info( log_handler.log_debug(f"Raw stdout length: {len(raw_output)}", func_name=func_name)
f"Status check returned {len(lines)} items.", 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)
return lines
# 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: except GitCommandError as e:
log_handler.log_error(f"Failed get status: {e}", func_name=func_name) log_handler.log_error(f"Failed get status: {e}", func_name=func_name)
return [] return []

142
gui.py
View File

@ -408,6 +408,11 @@ class MainFrame(ttk.Frame):
GREEN = "#90EE90" GREEN = "#90EE90"
RED = "#F08080" # Color constants 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__( def __init__(
self, self,
@ -524,6 +529,17 @@ class MainFrame(ttk.Frame):
) )
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(2, 0), padx=0) 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 --- # --- Initial State ---
self._initialize_profile_selection() self._initialize_profile_selection()
self.toggle_backup_dir() self.toggle_backup_dir()
@ -1282,37 +1298,65 @@ class MainFrame(ttk.Frame):
) )
def update_changed_files_list(self, files_status_list): def update_changed_files_list(self, files_status_list):
if ( """Clears and populates the changed files listbox, sanitizing input."""
not hasattr(self, "changed_files_listbox") # Usa la costante Listbox invece di hasattr ogni volta
or not self.changed_files_listbox.winfo_exists() 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 return
try: try:
self.changed_files_listbox.config(state=tk.NORMAL) listbox.config(state=tk.NORMAL)
self.changed_files_listbox.delete(0, tk.END) listbox.delete(0, tk.END)
if files_status_list: if files_status_list:
# Reset color
try: try:
if self.changed_files_listbox.cget("fg") == "grey": if listbox.cget("fg") == "grey":
self.changed_files_listbox.config( listbox.config(fg=self.style.lookup("TListbox", "foreground"))
fg=self.style.lookup("TListbox", "foreground") except tk.TclError: pass
)
except tk.TclError: # <<< MODIFICA: Sanifica ogni riga prima di inserirla >>>
pass processed_lines = 0
for line in files_status_list: for status_line in files_status_list:
self.changed_files_listbox.insert(tk.END, line) 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: else:
self.changed_files_listbox.insert(tk.END, "(No changes detected)") listbox.insert(tk.END, "(No changes detected)")
self.changed_files_listbox.config(fg="grey") listbox.config(fg="grey")
self.changed_files_listbox.config(state=tk.NORMAL)
self.changed_files_listbox.yview_moveto(0.0) listbox.config(state=tk.NORMAL) # Mantieni selezionabile
listbox.yview_moveto(0.0)
except Exception as e: except Exception as e:
print(f"ERROR updating changes list GUI: {e}", file=sys.stderr) print(f"ERROR updating changed files list GUI: {e}", file=sys.stderr)
try: try:
self.changed_files_listbox.delete(0, tk.END) listbox.delete(0, tk.END)
self.changed_files_listbox.insert(tk.END, "(Error)") listbox.insert(tk.END, "(Error updating list)")
self.changed_files_listbox.config(fg="red") listbox.config(fg="red")
except: except: pass # Ignora errori durante la visualizzazione dell'errore
pass
def _on_changed_file_double_click(self, event): def _on_changed_file_double_click(self, event):
widget = event.widget widget = event.widget
@ -1372,13 +1416,53 @@ class MainFrame(ttk.Frame):
finally: finally:
self.changed_files_context_menu.grab_release() self.changed_files_context_menu.grab_release()
def update_status_bar(self, message): def update_status_bar(self, message, bg_color=None, duration_ms=None):
if hasattr(self, "status_bar_var"): """
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: 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: except Exception as e:
print(f"ERROR updating status bar: {e}", file=sys.stderr) 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): def ask_new_profile_name(self):
return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master) return simpledialog.askstring("Add Profile", "Enter name:", parent=self.master)
@ -1422,6 +1506,8 @@ class MainFrame(ttk.Frame):
self.checkout_branch_button, self.checkout_branch_button,
self.refresh_history_button, self.refresh_history_button,
self.history_branch_filter_combo, 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 # log_handler.log_debug(f"Setting action widgets state to: {state}", func_name="set_action_widgets_state") # Usa log_handler
for widget in widgets: for widget in widgets: