diff --git a/doc/Manual.md b/doc/English-Manual.md similarity index 100% rename from doc/Manual.md rename to doc/English-Manual.md diff --git a/doc/Manuale.md b/doc/Italian-Manual.md similarity index 100% rename from doc/Manuale.md rename to doc/Italian-Manual.md diff --git a/gitutility/app.py b/gitutility/app.py index fe235f8..32eb325 100644 --- a/gitutility/app.py +++ b/gitutility/app.py @@ -25,6 +25,7 @@ from gitutility.config.config_manager import ( from gitutility.core.action_handler import ActionHandler from gitutility.core.backup_handler import BackupHandler from gitutility.core.remote_actions import RemoteActionHandler +from gitutility.core.wiki_updater import WikiUpdater # Command execution wrapper from gitutility.commands.git_commands import GitCommands, GitCommandError @@ -97,6 +98,8 @@ class GitSvnSyncApp: self.config_manager: ConfigManager = ConfigManager() self.git_commands: GitCommands = GitCommands() self.backup_handler: BackupHandler = BackupHandler() + self.wiki_updater: WikiUpdater = WikiUpdater(self.git_commands) + # Action Handlers depend on GitCommands and BackupHandler self.action_handler: ActionHandler = ActionHandler( self.git_commands, self.backup_handler @@ -192,6 +195,7 @@ class GitSvnSyncApp: # Dependencies config_manager_instance=self.config_manager, # Pass instance if needed by GUI profile_sections_list=self.config_manager.get_profile_sections(), # Pass initial profiles + update_gitea_wiki_cb=self.update_gitea_wiki, ) print("MainFrame GUI created.") log_handler.log_debug( @@ -854,6 +858,10 @@ class GitSvnSyncApp: fetch_button_state: str = self._calculate_fetch_button_state( mf, svn_path, is_repo_ready ) + + remote_url_present = bool(hasattr(self, 'remote_url_var') and self.remote_url_var.get().strip()) + wiki_button_state = tk.NORMAL if is_repo_ready and remote_url_present else tk.DISABLED + try: # Apply states if hasattr(mf, "prepare_svn_button"): mf.prepare_svn_button.config(state=prepare_state) @@ -913,6 +921,8 @@ class GitSvnSyncApp: mf.refresh_remote_branches_button.config(state=repo_ready_state) if hasattr(mf, "remote_branches_listbox"): mf.remote_branches_listbox.config(state=repo_ready_state) + if hasattr(self, "update_wiki_button"): + self.update_wiki_button.config(state=wiki_button_state) if hasattr(mf, "local_branches_listbox_remote_tab"): mf.local_branches_listbox_remote_tab.config(state=repo_ready_state) if hasattr(mf, "refresh_local_branches_button_remote_tab"): @@ -4035,6 +4045,58 @@ class GitSvnSyncApp: self.main_frame.show_error( "Diff Viewer Error", f"Could not display file changes:\n{e_diff}" ) + + def update_gitea_wiki(self): + """Starts the asynchronous task to update the Gitea wiki.""" + func_name = "update_gitea_wiki" + log_handler.log_info(f"--- Action Triggered: Update Gitea Wiki ---", func_name=func_name) + + if not hasattr(self, "main_frame") or not self.main_frame.winfo_exists(): return + svn_path = self._get_and_validate_svn_path("Update Gitea Wiki") + if not svn_path or not self._is_repo_ready(svn_path): + log_handler.log_warning("Update Wiki skipped: Repo not ready.", func_name=func_name) + self.main_frame.show_error("Action Failed", "Repository path is invalid or not prepared.") + self.main_frame.update_status_bar("Update Wiki failed: Repo not ready.") + return + + # Recupera l'URL remoto dal profilo corrente + remote_url = self.main_frame.remote_url_var.get().strip() + if not remote_url: + log_handler.log_warning("Update Wiki skipped: Remote URL not configured.", func_name=func_name) + self.main_frame.show_error("Action Failed", "Remote URL is not configured in the current profile.") + self.main_frame.update_status_bar("Update Wiki failed: Remote URL missing.") + return + + # Opzionale: recupera nomi file da config (o usa default) + # wiki_en_target = self.config_manager.get_profile_option(profile, "wiki_en_filename", fallback=None) + # wiki_it_target = self.config_manager.get_profile_option(profile, "wiki_it_filename", fallback=None) + + # Chiedi conferma all'utente + if not self.main_frame.ask_yes_no("Confirm Wiki Update", + "Update Gitea Wiki pages from local 'doc/' folder?\n" + "(This will clone the wiki, commit, and push changes.)"): + self.main_frame.update_status_bar("Wiki update cancelled.") + return + + # Prepara argomenti per il worker + args = ( + self.wiki_updater, + svn_path, + remote_url, + # Passa altri argomenti se hai reso i nomi file configurabili + # wiki_en_target, + # wiki_it_target, + ) + + # Avvia l'operazione asincrona + self._start_async_operation( + worker_func=async_workers.run_update_wiki_async, # Devi creare questo worker + args_tuple=args, + context_dict={ + "context": "update_wiki", + "status_msg": "Updating Gitea Wiki...", + } + ) # --- Application Entry Point --- diff --git a/gitutility/async_tasks/async_result_handler.py b/gitutility/async_tasks/async_result_handler.py index fef3eda..454d7a9 100644 --- a/gitutility/async_tasks/async_result_handler.py +++ b/gitutility/async_tasks/async_result_handler.py @@ -136,6 +136,7 @@ class AsyncResultHandler: "clone_remote": self._handle_clone_remote_result, "checkout_tracking_branch": self._handle_checkout_tracking_branch_result, "get_commit_details": self._handle_get_commit_details_result, + "update_wiki": self._handle_generic_result, } # Get the handler method from the map diff --git a/gitutility/async_tasks/async_workers.py b/gitutility/async_tasks/async_workers.py index 2a62f76..83e6381 100644 --- a/gitutility/async_tasks/async_workers.py +++ b/gitutility/async_tasks/async_workers.py @@ -16,6 +16,7 @@ from ..commands.git_commands import GitCommands, GitCommandError from ..core.action_handler import ActionHandler from ..core.backup_handler import BackupHandler from ..core.remote_actions import RemoteActionHandler +from ..core.wiki_updater import WikiUpdater # ---<<< FINE MODIFICA IMPORT >>>--- @@ -2151,4 +2152,42 @@ def run_get_commit_details_async( ) -# --- END OF FILE gitsync_tool/async_tasks/async_workers.py --- +def run_update_wiki_async( + wiki_updater: WikiUpdater, + main_repo_path: str, + main_repo_remote_url: str, + # Aggiungi altri parametri se necessari (es. nomi file wiki) + results_queue: queue.Queue[Dict[str, Any]], +) -> None: + """Worker to update the Gitea Wiki asynchronously.""" + func_name = "run_update_wiki_async" + log_handler.log_debug("[Worker] Started: Update Gitea Wiki", func_name=func_name) + result_payload = { + "status": "error", + "message": "Wiki update failed.", + "exception": None, + "result": False, + } + try: + # Chiama il metodo principale di WikiUpdater + success, message = wiki_updater.update_wiki_from_docs( + main_repo_path=main_repo_path, + main_repo_remote_url=main_repo_remote_url, + # Passa altri argomenti se necessari + ) + result_payload["status"] = "success" if success else "error" + result_payload["message"] = message + result_payload["result"] = success + log_handler.log_info(f"[Worker] Wiki update result: {message}", func_name=func_name) + + except Exception as e: + log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during wiki update: {e}", func_name=func_name) + result_payload["status"] = "error" + result_payload["message"] = f"Unexpected error during wiki update: {type(e).__name__}" + result_payload["exception"] = e + finally: + try: + results_queue.put(result_payload) + except Exception as qe: + log_handler.log_error(f"[Worker] Failed to put result in queue for {func_name}: {qe}", func_name=func_name) + log_handler.log_debug("[Worker] Finished: Update Gitea Wiki", func_name=func_name) diff --git a/gitutility/core/wiki_updater.py b/gitutility/core/wiki_updater.py new file mode 100644 index 0000000..57a2d9d --- /dev/null +++ b/gitutility/core/wiki_updater.py @@ -0,0 +1,237 @@ +# --- FILE: gitsync_tool/core/wiki_updater.py --- + +import os +import shutil +import tempfile +import re +import time +from typing import Optional, Tuple + +from ..commands.git_commands import GitCommands, GitCommandError +from ..logging_setup import log_handler + +class WikiUpdater: + """Handles updating the Gitea Wiki repository from local Markdown files.""" + + # Nomi file di default nella Wiki (potrebbero diventare configurabili) + DEFAULT_WIKI_EN_FILENAME = "English-Manual.md" + DEFAULT_WIKI_IT_FILENAME = "Italian-Manual.md" + + def __init__(self, git_commands: GitCommands): + if not isinstance(git_commands, GitCommands): + raise TypeError("WikiUpdater requires a GitCommands instance.") + self.git_commands: GitCommands = git_commands + log_handler.log_debug("WikiUpdater initialized.", func_name="__init__") + + def _get_wiki_repo_url(self, main_repo_url: str) -> Optional[str]: + """ + Attempts to derive the .wiki.git URL from the main repository URL. + + Args: + main_repo_url (str): The URL of the main Gitea repository. + + Returns: + Optional[str]: The derived Wiki repository URL (ending in .wiki.git) + or None if derivation fails. + """ + if not main_repo_url: + return None + # Rimuovi eventuale / finale + url = main_repo_url.rstrip('/') + # Gestisci common suffixes .git + if url.lower().endswith(".git"): + base_url = url[:-4] + else: + base_url = url + # Aggiungi .wiki.git + if base_url: + return f"{base_url}.wiki.git" + return None # Fallback + + def update_wiki_from_docs( + self, + main_repo_path: str, # Path del repo principale per trovare /doc + main_repo_remote_url: str, # URL del repo principale per derivare wiki URL + doc_dir_name: str = "doc", # Nome cartella documenti + en_manual_filename: str = "English-Manual.md", + it_manual_filename: str = "Italian-Manual.md", + wiki_en_target_filename: Optional[str] = None, # Override per nome file wiki EN + wiki_it_target_filename: Optional[str] = None, # Override per nome file wiki IT + commit_message: str = "Update Wiki documentation from local files" + ) -> Tuple[bool, str]: # Ritorna (successo, messaggio) + """ + Clones the Gitea wiki repo, updates pages from local doc files, and pushes. + + Returns: + Tuple[bool, str]: (True, success_message) on success, + (False, error_message) on failure. + """ + func_name = "update_wiki_from_docs" + wiki_url = self._get_wiki_repo_url(main_repo_remote_url) + if not wiki_url: + msg = "Could not derive Wiki repository URL from main remote URL." + log_handler.log_error(msg, func_name=func_name) + return False, msg + + doc_path = os.path.join(main_repo_path, doc_dir_name) + en_doc_file = os.path.join(doc_path, en_manual_filename) + it_doc_file = os.path.join(doc_path, it_manual_filename) + + # Determina nomi file di destinazione nella wiki + target_en_wiki_file = wiki_en_target_filename or self.DEFAULT_WIKI_EN_FILENAME + target_it_wiki_file = wiki_it_target_filename or self.DEFAULT_WIKI_IT_FILENAME + + log_handler.log_info(f"Starting Wiki update for Gitea repo: {wiki_url}", func_name=func_name) + log_handler.log_debug(f" Source EN doc: {en_doc_file}", func_name=func_name) + log_handler.log_debug(f" Source IT doc: {it_doc_file}", func_name=func_name) + log_handler.log_debug(f" Target EN wiki file: {target_en_wiki_file}", func_name=func_name) + log_handler.log_debug(f" Target IT wiki file: {target_it_wiki_file}", func_name=func_name) + + # Verifica esistenza file sorgente + en_exists = os.path.isfile(en_doc_file) + it_exists = os.path.isfile(it_doc_file) + if not en_exists and not it_exists: + msg = f"Neither '{en_manual_filename}' nor '{it_manual_filename}' found in '{doc_path}'. Cannot update Wiki." + log_handler.log_warning(msg, func_name=func_name) + return False, msg # Considera questo un "successo" vuoto o errore? Per ora errore. + + temp_dir: Optional[str] = None + try: + # 1. Crea directory temporanea sicura + temp_dir = tempfile.mkdtemp(prefix="gitea_wiki_update_") + log_handler.log_info(f"Cloning Wiki repo '{wiki_url}' into temp dir: {temp_dir}", func_name=func_name) + + # 2. Clona il repository Wiki + # Usa GitCommands.clone (non solleva eccezioni di default, controlla RC) + clone_result = self.git_commands.git_clone(wiki_url, temp_dir) + if clone_result.returncode != 0: + # Gestisci errore clone (es. repo non esiste, auth fallita) + stderr_msg = clone_result.stderr or "Unknown clone error" + # Controlla se il repo non esiste (tipico per wiki non inizializzate) + if "repository not found" in stderr_msg.lower() or \ + "does not appear to be a git repository" in stderr_msg.lower(): + msg = (f"Failed to clone Wiki: Repository at '{wiki_url}' not found or not initialized. " + f"Please create the first page in the Gitea Wiki manually first.") + log_handler.log_error(msg, func_name=func_name) + return False, msg + else: + msg = f"Failed to clone Wiki repository '{wiki_url}'. Error: {stderr_msg.strip()}" + log_handler.log_error(msg, func_name=func_name) + # Potrebbe essere un problema di autenticazione + return False, f"{msg} (Check authentication?)" + + log_handler.log_info("Wiki repository cloned successfully.", func_name=func_name) + + # 3. Leggi i file locali e Scrivi/Aggiorna i file nella Wiki clonata + files_updated = False + if en_exists: + try: + with open(en_doc_file, 'r', encoding='utf-8') as f_en: + en_content = f_en.read() + wiki_en_path = os.path.join(temp_dir, target_en_wiki_file) + # Sovrascrivi o crea il file nella wiki + with open(wiki_en_path, 'w', encoding='utf-8', newline='\n') as f_wiki_en: + f_wiki_en.write(en_content) + log_handler.log_info(f"Updated/Created '{target_en_wiki_file}' in local wiki clone.", func_name=func_name) + files_updated = True + except Exception as e: + msg = f"Error reading local file '{en_doc_file}' or writing to wiki file '{target_en_wiki_file}': {e}" + log_handler.log_exception(msg, func_name=func_name) + return False, msg # Errore critico + + if it_exists: + try: + with open(it_doc_file, 'r', encoding='utf-8') as f_it: + it_content = f_it.read() + wiki_it_path = os.path.join(temp_dir, target_it_wiki_file) + with open(wiki_it_path, 'w', encoding='utf-8', newline='\n') as f_wiki_it: + f_wiki_it.write(it_content) + log_handler.log_info(f"Updated/Created '{target_it_wiki_file}' in local wiki clone.", func_name=func_name) + files_updated = True + except Exception as e: + msg = f"Error reading local file '{it_doc_file}' or writing to wiki file '{target_it_wiki_file}': {e}" + log_handler.log_exception(msg, func_name=func_name) + return False, msg + + if not files_updated: + msg = "Source files found but failed to write to local wiki clone (check logs)." + log_handler.log_error(msg, func_name=func_name) + return False, msg + + # 4. Controlla se ci sono cambiamenti effettivi da committare + if not self.git_commands.git_status_has_changes(temp_dir): + msg = "Wiki content is already up-to-date with local doc files." + log_handler.log_info(msg, func_name=func_name) + return True, msg # Successo, nessuna azione necessaria + + log_handler.log_info("Changes detected in local wiki clone. Proceeding with commit and push.", func_name=func_name) + + # 5. Commit + commit_success = self.git_commands.git_commit(temp_dir, commit_message) + if not commit_success: + # Questo non dovrebbe accadere se status_has_changes era True, ma controlliamo + msg = "Failed to commit changes to local wiki clone (git commit reported no changes unexpectedly)." + log_handler.log_warning(msg, func_name=func_name) + # Consideriamo questo un errore lieve? O un fallimento? + # Proseguiamo con il push per sicurezza, ma segnaliamo + # return False, msg # Potremmo fermarci qui + log_handler.log_info("Wiki changes committed locally.", func_name=func_name) + + # 6. Push + log_handler.log_info("Pushing wiki changes to remote...", func_name=func_name) + # Assumiamo che il push vada al branch di default ('master' o 'main') su 'origin' (il remote della wiki) + # Nota: git_push richiede il nome del branch; otteniamolo dal clone + current_wiki_branch = self.git_commands.get_current_branch_name(temp_dir) + if not current_wiki_branch: + msg = "Could not determine the current branch in the cloned wiki repository." + log_handler.log_error(msg, func_name=func_name) + return False, msg + + push_result = self.git_commands.git_push( + working_directory=temp_dir, + remote_name='origin', # 'origin' è il default per un clone + branch_name=current_wiki_branch, + set_upstream=False, # Non necessario qui + force=False + ) + + if push_result.returncode == 0: + msg = "Wiki update pushed successfully to Gitea." + log_handler.log_info(msg, func_name=func_name) + return True, msg + else: + # Push fallito (conflitto? auth?) + stderr_msg = push_result.stderr or "Unknown push error" + msg = f"Failed to push Wiki updates. Error: {stderr_msg.strip()}" + log_handler.log_error(msg, func_name=func_name) + # Distingui tra rejected e altri errori + if "rejected" in stderr_msg.lower(): + return False, f"{msg} (Remote may have changes, try updating wiki manually first?)" + else: + return False, f"{msg} (Check authentication?)" + + except (GitCommandError, ValueError, IOError, Exception) as e: + # Cattura eccezioni generali dal processo + log_handler.log_exception(f"Error during wiki update process: {e}", func_name=func_name) + return False, f"Wiki update failed: {e}" + + finally: + # 7. Pulizia directory temporanea (SEMPRE) + if temp_dir and os.path.isdir(temp_dir): + try: + shutil.rmtree(temp_dir) + log_handler.log_info(f"Cleaned up temporary wiki directory: {temp_dir}", func_name=func_name) + except Exception as clean_e: + log_handler.log_error(f"Failed to clean up temporary directory '{temp_dir}': {clean_e}", func_name=func_name) + try: + time.sleep(0.5) # Aspetta 500 millisecondi + shutil.rmtree(temp_dir) + log_handler.log_info(f"Successfully cleaned up temporary wiki directory on retry: {temp_dir}", func_name=func_name) + except Exception as clean_e2: + log_handler.log_error( + f"Failed to clean up temporary directory '{temp_dir}' even after retry: {clean_e2}", + func_name=func_name + ) + # Nota: L'operazione principale potrebbe essere comunque riuscita. + # Non cambiamo success_flag qui, ma l'errore è loggato. + diff --git a/gitutility/gui/main_frame.py b/gitutility/gui/main_frame.py index a961dca..bb64a8d 100644 --- a/gitutility/gui/main_frame.py +++ b/gitutility/gui/main_frame.py @@ -101,6 +101,7 @@ class MainFrame(ttk.Frame): # Altre dipendenze config_manager_instance: Any, # Evita import ConfigManager qui profile_sections_list: List[str], + update_gitea_wiki_cb: Callable[[], None], ): # ---<<< FINE MODIFICA >>>--- """Initializes the MainFrame.""" @@ -144,6 +145,7 @@ class MainFrame(ttk.Frame): self.merge_local_branch_callback = merge_local_branch_cb self.compare_branch_with_current_callback = compare_branch_with_current_cb self.view_commit_details_callback = view_commit_details_cb + self.update_gitea_wiki_callback = update_gitea_wiki_cb # Store references needed internally self.config_manager = ( @@ -197,6 +199,8 @@ class MainFrame(ttk.Frame): self.history_tab_frame = ( self._create_history_tab_treeview() ) # Usa la versione TreeView + + self.automation_tab_frame = self._create_automation_tab() # Aggiunta delle tab al Notebook (ordine di visualizzazione) self.notebook.add(self.repo_tab_frame, text=" Repository / Bundle ") @@ -205,6 +209,7 @@ class MainFrame(ttk.Frame): self.notebook.add(self.commit_tab_frame, text=" Commit / Changes ") self.notebook.add(self.tags_tab_frame, text=" Tags ") self.notebook.add(self.branch_tab_frame, text=" Branches (Local Ops) ") + self.notebook.add(self.automation_tab_frame, text=" Automation ") self.notebook.add(self.history_tab_frame, text=" History ") # Creazione area log e status bar @@ -640,6 +645,49 @@ class MainFrame(ttk.Frame): "Stage all current changes (git add .) and commit them with the provided message.", ) return frame + + def _create_automation_tab(self): + """Creates the Automation tab content.""" + frame = ttk.Frame(self.notebook, padding=(10, 10)) + # Puoi usare Grid, Pack o Place all'interno di questo frame + + # Container per le azioni Wiki + wiki_frame = ttk.LabelFrame( + frame, text="Gitea Wiki Synchronization", padding=(10, 5) + ) + # Usa pack per layout verticale semplice, o grid se preferisci + wiki_frame.pack(pady=5, padx=5, fill="x", anchor="n") + + # Descrizione (opzionale) + ttk.Label( + wiki_frame, + text="Update Gitea Wiki pages using local files from the 'doc/' directory.", + wraplength=450, # Per andare a capo + justify=tk.LEFT + ).pack(pady=(0, 10), fill="x") + + # Pulsante per l'aggiornamento Wiki + # Il comando sarà collegato al callback passato da app.py + self.update_wiki_button = ttk.Button( + wiki_frame, + text="Update Gitea Wiki Now", + command=self.update_gitea_wiki_callback, # Assicurati che il nome callback corrisponda + state=tk.DISABLED # Inizialmente disabilitato + ) + self.update_wiki_button.pack(pady=5, anchor="center") # Centra il pulsante + self.create_tooltip( + self.update_wiki_button, + "Clones the associated Gitea Wiki repo, copies 'doc/Manual*.md' files,\n" + "commits the changes, and pushes them to the remote wiki." + ) + + # --- SPAZIO PER FUTURE AUTOMAZIONI --- + # Puoi aggiungere altri LabelFrame o widget qui sotto per altre azioni + # future_action_frame = ttk.LabelFrame(frame, text="Future Automation", ...) + # future_action_frame.pack(...) + # ttk.Button(future_action_frame, text="Do Something Else", ...).pack() + + return frame def _create_tags_tab(self): # ... (Codice da gui.py originale) ... @@ -1824,6 +1872,7 @@ class MainFrame(ttk.Frame): "refresh_sync_status_button", "refresh_remote_branches_button", "refresh_local_branches_button_remote_tab", + "update_wiki_button", ] ] log_handler.log_debug(