diff --git a/.gitignore b/.gitignore index aeb3fdb..37bad11 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ .log dist build -.ini +.ini \ No newline at end of file diff --git a/GitUtility.py b/GitUtility.py index de51281..07d584a 100644 --- a/GitUtility.py +++ b/GitUtility.py @@ -186,6 +186,40 @@ class GitSvnSyncApp: self.logger.warning("No initial profile set during initial load.") self._clear_and_disable_fields() #self.logger.info("Application started and initial state set.") + + def _handle_gitignore_save(self): + """ + Callback function triggered after .gitignore is saved successfully. + Initiates the process to untrack newly ignored files. + """ + self.logger.info("Callback triggered: .gitignore saved. Checking for files to untrack automatically...") + # Need the svn_path again here + svn_path = self._get_and_validate_svn_path("Automatic Untracking") + if not svn_path: + self.logger.error("Cannot perform automatic untracking: Invalid SVN path after save.") + # Show error? This shouldn't happen if editor opened correctly. + return + + try: + # Call the ActionHandler method + untracked = self.action_handler.execute_untrack_files_from_gitignore(svn_path) + + if untracked: + self.main_frame.show_info( + "Automatic Untrack", + "Successfully untracked newly ignored files and created commit.\nCheck log for details." + ) + # UI might need refreshing after commit - handled after window closes in open_gitignore_editor + else: + # No files needed untracking, or commit failed (ActionHandler logs warnings) + self.logger.info("Automatic untracking check complete. No files untracked or no commit needed.") + + except (GitCommandError, ValueError) as e: + self.logger.error(f"Automatic untracking failed: {e}", exc_info=True) + self.main_frame.show_error("Untrack Error", f"Failed automatic untracking:\n{e}") + except Exception as e: + self.logger.exception(f"Unexpected error during automatic untracking: {e}") + self.main_frame.show_error("Untrack Error", f"Unexpected error during untracking:\n{e}") def on_closing(self): """Handles the window close event.""" @@ -628,22 +662,38 @@ class GitSvnSyncApp: return abs_path def open_gitignore_editor(self): - """Opens the modal editor window for the .gitignore file.""" - # (Mantenere versione precedente robusta) + """Opens the modal editor window for the .gitignore file and + triggers automatic untracking check on successful save.""" self.logger.info("--- Action: Edit .gitignore ---") svn_path = self._get_and_validate_svn_path("Edit .gitignore") if not svn_path: return gitignore_path = os.path.join(svn_path, ".gitignore") - self.logger.debug(f"Target: {gitignore_path}") + self.logger.debug(f"Target .gitignore path: {gitignore_path}") + try: + self.logger.debug("Opening GitignoreEditorWindow...") + # --- MODIFICA: Passa il metodo _handle_gitignore_save come callback --- GitignoreEditorWindow( - self.master, gitignore_path, self.logger - ) # Blocca fino alla chiusura + self.master, + gitignore_path, + self.logger, + on_save_success_callback=self._handle_gitignore_save # Passa il riferimento al metodo + ) + # --- FINE MODIFICA --- + # Execution waits here until the Toplevel window is closed + self.logger.debug("Gitignore editor window closed.") + # Note: The untracking logic is now triggered *by* the callback *before* the window closes. + # We might still want to refresh UI elements *after* it closes. + # Refresh status indicator and potentially history/branches after editor closes + self.update_svn_status_indicator(svn_path) + self.refresh_commit_history() + self.refresh_branch_list() # Commit might affect branch display + except Exception as e: - self.logger.exception(f"Editor error: {e}") - self.main_frame.show_error("Error", f"Editor error:\n{e}") + self.logger.exception(f"Error during .gitignore editing or post-save action: {e}") + self.main_frame.show_error("Editor Error", f"An error occurred:\n{e}") # --- Threading Helpers (REMOVED) --- # Rimuovi _run_action_with_wait diff --git a/action_handler.py b/action_handler.py index 7214b2c..35430a0 100644 --- a/action_handler.py +++ b/action_handler.py @@ -545,3 +545,95 @@ class ActionHandler: except Exception as e: self.logger.exception(f"Failed to delete tag '{tag_name}': {e}") raise Exception("Unexpected tag deletion error") from e + + def execute_untrack_files_from_gitignore(self, svn_path): + """ + Checks tracked files against current .gitignore rules, untracks newly + ignored files, and creates a commit summarizing which rules caused + the untracking. + + Args: + svn_path (str): Path to the repository. + + Returns: + bool: True if files were untracked and committed, False otherwise. + + Raises: + GitCommandError/ValueError/Exception: If any Git operation fails. + """ + self.logger.info(f"Checking for tracked files to untrack based on .gitignore in '{svn_path}'...") + + # --- MODIFICA: Usa un dizionario per raggruppare file per regola --- + # Key: matching pattern (rule), Value: list of file paths matched by that rule + rules_to_files_map = {} + # Lista piatta per il comando git rm + all_files_to_untrack = [] + # --- FINE MODIFICA --- + + try: + # 1. Get all currently tracked files + tracked_files = self.git_commands.get_tracked_files(svn_path) + + # 2. Check each tracked file for matching ignore rules + self.logger.debug(f"Checking {len(tracked_files)} tracked files against ignore rules...") + for file_path in tracked_files: + # Skip checks for the .gitignore file itself + norm_file_path = os.path.normpath(file_path) + if norm_file_path == '.gitignore': + continue + + # --- MODIFICA: Ottieni la regola corrispondente --- + # Check if the file *would* be ignored and get the rule + matching_rule = self.git_commands.get_matching_gitignore_rule(svn_path, file_path) + # --- FINE MODIFICA --- + + if matching_rule is not None: + self.logger.info(f"Tracked file '{file_path}' now matches ignore rule: '{matching_rule}'") + # --- MODIFICA: Popola il dizionario e la lista --- + if matching_rule not in rules_to_files_map: + rules_to_files_map[matching_rule] = [] + rules_to_files_map[matching_rule].append(file_path) + all_files_to_untrack.append(file_path) + # --- FINE MODIFICA --- + + # 3. If files need untracking, perform git rm --cached and commit + # --- MODIFICA: Controlla la lista piatta --- + if all_files_to_untrack: + # --- FINE MODIFICA --- + self.logger.info(f"Found {len(all_files_to_untrack)} tracked files that are now ignored.") + + # 3a. Untrack the files (remove from index) using the flat list + self.git_commands.remove_from_tracking(svn_path, all_files_to_untrack) + + # --- MODIFICA: Crea messaggio di commit riassuntivo basato sulle regole --- + # 3b. Create an automatic commit message summarizing by rule + commit_message = "Chore: Stop tracking files based on .gitignore update.\n\nSummary:\n" + # Ordina le regole per leggibilitĂ  (opzionale) + sorted_rules = sorted(rules_to_files_map.keys()) + for rule in sorted_rules: + file_count = len(rules_to_files_map[rule]) + commit_message += f"- Rule \"{rule}\" untracked {file_count} file(s).\n" + # --- FINE MODIFICA --- + + self.logger.info("Creating automatic commit for untracking changes.") + self.logger.debug(f"Commit message:\n{commit_message}") # Log message multi-line + + # 3c. Perform the commit + commit_made = self.git_commands.git_commit(svn_path, commit_message) + + if commit_made: + self.logger.info("Automatic commit successful.") + return True + else: + self.logger.warning("Untracking performed, but automatic commit reported no changes.") + return False + else: + self.logger.info("No tracked files found matching current .gitignore rules. No action needed.") + return False + + except (GitCommandError, ValueError) as e: + self.logger.error(f"Error during automatic untracking process: {e}", exc_info=True) + raise + except Exception as e: + self.logger.exception(f"Unexpected error during automatic untracking: {e}") + raise Exception("Unexpected untracking error") from e diff --git a/git_commands.py b/git_commands.py index 63c33ff..8c4e175 100644 --- a/git_commands.py +++ b/git_commands.py @@ -70,97 +70,94 @@ class GitCommands: raise ValueError("A valid logging.Logger instance is required.") self.logger = logger - def log_and_execute(self, command, working_directory, check=True): + def log_and_execute(self, command, working_directory, check=True, log_output_level=logging.INFO): """ - Executes a shell command in a specific directory, logs details, - and handles potential errors. + Executes a shell command, logs details, handles errors, and controls output logging level. Args: command (list): Command and arguments as a list of strings. working_directory (str): The directory to execute the command in. - check (bool, optional): If True, raises CalledProcessError (wrapped - in GitCommandError) if the command returns - a non-zero exit code. Defaults to True. + check (bool, optional): If True, raises GitCommandError on non-zero exit. Defaults to True. + log_output_level (int, optional): Logging level for stdout/stderr on success. + Defaults to logging.INFO. Use logging.DEBUG + to hide noisy command output from INFO logs. Returns: - subprocess.CompletedProcess: Result object containing stdout, stderr, etc. + subprocess.CompletedProcess: Result object. Raises: - GitCommandError: For Git-specific errors or execution issues. - ValueError: If working_directory is invalid. - FileNotFoundError: If the command (e.g., 'git') is not found. - PermissionError: If execution permission is denied. + GitCommandError, ValueError, FileNotFoundError, PermissionError. """ - # Ensure all parts of command are strings for reliable execution and logging + # --- FINE MODIFICA --- safe_command = [str(part) for part in command] command_str = " ".join(safe_command) - log_message = f"Executing: {command_str}" - # Log command execution at debug level for less verbosity in standard logs - self.logger.debug(log_message) + # Log command execution always at DEBUG level + self.logger.debug(f"Executing in '{working_directory}': {command_str}") # --- Validate Working Directory --- + # (Logica validazione working_directory invariata) if not working_directory: msg = "Working directory cannot be None or empty." self.logger.error(msg) raise ValueError(msg) - - abs_path = os.path.abspath(working_directory) - if not os.path.isdir(abs_path): - msg = f"Working directory does not exist or is not a directory: {abs_path}" - self.logger.error(msg) - # Raise GitCommandError to signal issue related to command execution context - raise GitCommandError(msg, command=safe_command) - - cwd = abs_path - self.logger.debug(f"Effective Working Directory: {cwd}") + # Use '.' as a special case for current working directory if needed by caller + if working_directory == ".": + cwd = os.getcwd() + else: + abs_path = os.path.abspath(working_directory) + if not os.path.isdir(abs_path): + msg = f"Working directory does not exist or is not a directory: {abs_path}" + self.logger.error(msg) + raise GitCommandError(msg, command=safe_command) + cwd = abs_path + # self.logger.debug(f"Effective Working Directory: {cwd}") # Already logged above # --- Execute Command --- try: - # Platform-specific setup to hide console window on Windows + # (Logica startupinfo/creationflags invariata) startupinfo = None - # creationflags are used to control process creation (e.g., hide window) creationflags = 0 - if os.name == "nt": # Windows specific settings + if os.name == "nt": startupinfo = subprocess.STARTUPINFO() - # Prevent console window from showing startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.wShowWindow = subprocess.SW_HIDE - # Alternative flag to completely detach from console (might affect stdio) - # creationflags = subprocess.CREATE_NO_WINDOW - # Run the command using subprocess.run result = subprocess.run( - safe_command, # Use the command list with string parts - cwd=cwd, # Set working directory - capture_output=True, # Capture stdout and stderr - text=True, # Decode output as text (UTF-8 default) - check=check, # Raise exception on non-zero exit code if True - encoding="utf-8", # Specify encoding explicitly - errors="replace", # Handle potential decoding errors gracefully - startupinfo=startupinfo, # Windows: hide console window - creationflags=creationflags, # Windows: additional process flags + safe_command, + cwd=cwd, + capture_output=True, + text=True, + check=check, + encoding="utf-8", + errors="replace", + startupinfo=startupinfo, + creationflags=creationflags, ) - # Log command output for debugging and info - stdout_log = result.stdout.strip() if result.stdout else "" - stderr_log = result.stderr.strip() if result.stderr else "" - - # Log success differently based on whether check=True was used - # If check=True, CalledProcessError would have been raised on failure - # If check=False, we log success only if return code is 0 + # Log command output based on specified level for success + # Error output is always logged at ERROR level below if check or result.returncode == 0: - self.logger.info( - f"Command successful. Output:\n" - f"--- stdout ---\n{stdout_log}\n" - f"--- stderr ---\n{stderr_log}\n---" - ) - # Note: If check=False and returncode != 0, errors are typically - # handled by the calling method that analyzes the result. + # --- MODIFICA: Usa log_output_level per l'output di successo --- + # Only log stdout/stderr if the requested level is met by the logger config + if self.logger.isEnabledFor(log_output_level): + stdout_log = result.stdout.strip() if result.stdout else "" + stderr_log = result.stderr.strip() if result.stderr else "" + # Use the passed level for logging the output + self.logger.log( + log_output_level, + f"Command successful. Output:\n" + f"--- stdout ---\n{stdout_log}\n" + f"--- stderr ---\n{stderr_log}\n---" + ) + else: + # Log minimal success message if output level is suppressed + self.logger.debug(f"Command successful (output logging suppressed by level).") + # --- FINE MODIFICA --- return result except subprocess.CalledProcessError as e: - # This block runs only if check=True and the command failed + # (Gestione CalledProcessError invariata - logga sempre a ERROR) stderr_err = e.stderr.strip() if e.stderr else "" stdout_err = e.stdout.strip() if e.stdout else "" error_log_msg = ( @@ -170,36 +167,30 @@ class GitCommands: f"--- stdout ---\n{stdout_err}\n---" ) self.logger.error(error_log_msg) - # Wrap the original exception in our custom GitCommandError raise GitCommandError( f"Git command failed in '{cwd}'.", command=safe_command, stderr=e.stderr - ) from e # Preserve original exception context + ) from e except FileNotFoundError as e: - # Handle error if 'git' command (or another part) is not found - error_msg = ( - f"Command not found: '{safe_command[0]}'. Is Git installed " - f"and in system PATH? (Working directory: '{cwd}')" - ) - self.logger.error(error_msg) - self.logger.debug( - f"FileNotFoundError details: {e}" - ) # Log full details only at debug level - raise GitCommandError(error_msg, command=safe_command) from e + # (Gestione FileNotFoundError invariata) + error_msg = f"Command not found: '{safe_command[0]}'. Is Git installed and in system PATH? (Working directory: '{cwd}')" + self.logger.error(error_msg) + self.logger.debug(f"FileNotFoundError details: {e}") + raise GitCommandError(error_msg, command=safe_command) from e except PermissionError as e: - # Handle errors due to lack of execution permissions - error_msg = f"Permission denied executing command in '{cwd}'." - self.logger.error(error_msg) - self.logger.debug(f"PermissionError details: {e}") - raise GitCommandError(error_msg, command=safe_command, stderr=str(e)) from e + # (Gestione PermissionError invariata) + error_msg = f"Permission denied executing command in '{cwd}'." + self.logger.error(error_msg) + self.logger.debug(f"PermissionError details: {e}") + raise GitCommandError(error_msg, command=safe_command, stderr=str(e)) from e except Exception as e: - # Catch any other unexpected errors during subprocess execution - self.logger.exception(f"Unexpected error executing command in '{cwd}': {e}") - raise GitCommandError( - f"Unexpected command execution error: {e}", command=safe_command - ) from e + # (Gestione Exception generica invariata) + self.logger.exception(f"Unexpected error executing command in '{cwd}': {e}") + raise GitCommandError( + f"Unexpected command execution error: {e}", command=safe_command + ) from e # --- Core Repo Operations --- def prepare_svn_for_git(self, working_directory): @@ -908,3 +899,173 @@ class GitCommands: # Catch any other unexpected errors self.logger.exception(f"Unexpected error cloning from bundle: {e}") raise GitCommandError(f"Unexpected clone error: {e}", command=command) from e + + def get_tracked_files(self, working_directory): + """ + Retrieves a list of all files currently tracked by Git in the repository. + Logs command output only at DEBUG level. + """ + self.logger.debug(f"Getting tracked files for '{working_directory}'") + command = ["git", "ls-files", "-z"] + try: + # --- MODIFICA: Passa log_output_level=logging.DEBUG --- + result = self.log_and_execute( + command, + working_directory, + check=True, + log_output_level=logging.DEBUG # Log stdout/stderr only if logger level is DEBUG + ) + # --- FINE MODIFICA --- + tracked_files = [ + f for f in result.stdout.split('\0') if f + ] + # Log the *count* at INFO level, the list itself might only appear in DEBUG log now. + self.logger.info(f"Found {len(tracked_files)} tracked files.") + self.logger.debug(f"Tracked file list: {tracked_files}") # Log list at DEBUG + return tracked_files + except (GitCommandError, ValueError) as e: + self.logger.error(f"Failed to list tracked files: {e}") + raise + except Exception as e: + self.logger.exception(f"Unexpected error listing tracked files: {e}") + raise GitCommandError(f"Unexpected error listing files: {e}", command=command) from e + + def check_if_would_be_ignored(self, working_directory, path_to_check): + """ + Checks if a given path would be ignored by current .gitignore rules, + regardless of whether it's currently tracked. + + Args: + working_directory (str): Path to the Git repository. + path_to_check (str): The relative path within the repo to check. + + Returns: + bool: True if the path matches an ignore rule, False otherwise. + + Raises: + GitCommandError: If the git command fails unexpectedly. + """ + # Use --no-index to check against .gitignore rules directly, + # not the index (tracked status). + # Use --quiet to suppress output, rely only on exit code. + command = ["git", "check-ignore", "--quiet", "--no-index", "--", path_to_check] + self.logger.debug(f"Checking ignore status for path: '{path_to_check}'") + try: + # check=False because exit code 1 (not ignored) is not an error here + result = self.log_and_execute(command, working_directory, check=False) + + if result.returncode == 0: + self.logger.debug(f"Path '{path_to_check}' WOULD be ignored.") + return True # Exit code 0 means it IS ignored + elif result.returncode == 1: + self.logger.debug(f"Path '{path_to_check}' would NOT be ignored.") + return False # Exit code 1 means it is NOT ignored + else: + # Other non-zero exit codes (e.g., 128) indicate an error + error_msg = f"git check-ignore failed with exit code {result.returncode}" + self.logger.error(f"{error_msg}. Stderr: {result.stderr}") + raise GitCommandError(error_msg, command=command, stderr=result.stderr) + + except (GitCommandError, ValueError) as e: + self.logger.error(f"Failed check_if_would_be_ignored for '{path_to_check}': {e}") + raise + except Exception as e: + self.logger.exception(f"Unexpected error in check_if_would_be_ignored: {e}") + raise GitCommandError(f"Unexpected check-ignore error: {e}", command=command) from e + + def remove_from_tracking(self, working_directory, files_to_untrack): + """ + Removes specified files/directories from Git tracking (index) + without deleting them from the working directory. + + Args: + working_directory (str): Path to the Git repository. + files_to_untrack (list): List of relative paths to untrack. + + Returns: + bool: True if the command executed successfully (even if nothing was removed). + + Raises: + GitCommandError: If the git command fails. + ValueError: If the list of files is empty. + """ + if not files_to_untrack: + raise ValueError("File list cannot be empty for remove_from_tracking.") + + self.logger.info(f"Removing {len(files_to_untrack)} items from Git tracking...") + self.logger.debug(f"Items to untrack: {files_to_untrack}") + + # Base command + command = ["git", "rm", "--cached", "--"] # "--" separates options from paths + # Add all file paths to the command + command.extend(files_to_untrack) + + try: + # check=True ensures an error is raised if `git rm` fails + self.log_and_execute(command, working_directory, check=True) + self.logger.info("Successfully removed items from tracking index.") + return True + except (GitCommandError, ValueError) as e: + self.logger.error(f"Failed to remove items from tracking: {e}") + raise # Re-raise original error + except Exception as e: + self.logger.exception(f"Unexpected error removing from tracking: {e}") + raise GitCommandError(f"Unexpected untrack error: {e}", command=command) from e + + def get_matching_gitignore_rule(self, working_directory, path_to_check): + """ + Checks if a given path matches a .gitignore rule and returns the matching pattern. + + Args: + working_directory (str): Path to the Git repository. + path_to_check (str): The relative path within the repo to check. + + Returns: + str or None: The pattern from .gitignore that matched the path, + or None if the path is not ignored. + + Raises: + GitCommandError: If the git command fails unexpectedly. + """ + # Use -v (--verbose) to get the matching rule details. + # Use --no-index to check against .gitignore rules directly. + command = ["git", "check-ignore", "-v", "--no-index", "--", path_to_check] + self.logger.debug(f"Getting matching ignore rule for path: '{path_to_check}'") + try: + # check=False because exit code 1 (not ignored) is not an error here + result = self.log_and_execute(command, working_directory, check=False) + + if result.returncode == 0: + # Output format: ::\t + # Example: .gitignore:5:*.log logs/latest.log + output_line = result.stdout.strip() + if output_line and '\t' in output_line: + rule_part = output_line.split('\t', 1)[0] + # Extract pattern (part after the second colon) + parts = rule_part.split(':', 2) + if len(parts) == 3: + pattern = parts[2] + self.logger.debug(f"Path '{path_to_check}' matched rule: '{pattern}'") + return pattern + else: + self.logger.warning(f"Could not parse pattern from check-ignore output: {output_line}") + return None # Indicate parsing failure rather than no match + else: + self.logger.warning(f"Unexpected output format from check-ignore -v: {output_line}") + return None # Indicate unexpected output + elif result.returncode == 1: + # Exit code 1 means the path is NOT ignored + self.logger.debug(f"Path '{path_to_check}' is not ignored by any rule.") + return None + else: + # Other non-zero exit codes (e.g., 128) indicate an error + error_msg = f"git check-ignore -v failed with exit code {result.returncode}" + self.logger.error(f"{error_msg}. Stderr: {result.stderr}") + raise GitCommandError(error_msg, command=command, stderr=result.stderr) + + except (GitCommandError, ValueError) as e: + self.logger.error(f"Failed get_matching_gitignore_rule for '{path_to_check}': {e}") + raise + except Exception as e: + self.logger.exception(f"Unexpected error in get_matching_gitignore_rule: {e}") + raise GitCommandError(f"Unexpected check-ignore -v error: {e}", command=command) from e \ No newline at end of file diff --git a/gui.py b/gui.py index b3ba3a5..a781ca6 100644 --- a/gui.py +++ b/gui.py @@ -106,7 +106,7 @@ class Tooltip: class GitignoreEditorWindow(tk.Toplevel): """Toplevel window for editing the .gitignore file.""" - def __init__(self, master, gitignore_path, logger): + def __init__(self, master, gitignore_path, logger, on_save_success_callback=None): super().__init__(master) self.gitignore_path = gitignore_path if not isinstance(logger, (logging.Logger, logging.LoggerAdapter)): @@ -115,6 +115,8 @@ class GitignoreEditorWindow(tk.Toplevel): ) self.logger = logger self.original_content = "" + self.on_save_success_callback = on_save_success_callback + self.title(f"Edit {os.path.basename(gitignore_path)}") self.geometry("600x450") self.minsize(400, 300) @@ -191,9 +193,10 @@ class GitignoreEditorWindow(tk.Toplevel): return True # Assume changes on error def _save_file(self): + # (Logica interna di _save_file invariata) if not self._has_changes(): self.logger.info("No changes to save.") - return True + return True # Indicate success even if no changes were made current_content = self.text_editor.get("1.0", "end-1c") self.logger.info(f"Saving changes to: {self.gitignore_path}") try: @@ -202,26 +205,43 @@ class GitignoreEditorWindow(tk.Toplevel): self.logger.info(".gitignore saved.") self.original_content = current_content self.text_editor.edit_modified(False) - return True + return True # Return True on successful save except Exception as e: self.logger.error(f"Error saving .gitignore: {e}", exc_info=True) messagebox.showerror("Save Error", f"Error saving:\n{e}", parent=self) - return False + return False # Return False on error def _save_and_close(self): - if self._save_file(): + # --- MODIFICA: Chiama il callback dopo il salvataggio --- + if self._save_file(): # Check if save succeeded (or no changes) + self.logger.debug("Save successful, attempting to call success callback.") + # Call the callback if it exists + if self.on_save_success_callback: + try: + self.on_save_success_callback() + except Exception as cb_e: + self.logger.error(f"Error executing on_save_success_callback: {cb_e}", exc_info=True) + # Show an error, maybe? Or just log it. + messagebox.showwarning("Callback Error", + "Saved .gitignore, but failed to run post-save action.\nCheck logs.", + parent=self) + # Proceed to destroy the window regardless of callback success/failure self.destroy() + # --- FINE MODIFICA --- + # else: If save failed, the error is already shown, do nothing more. def _on_close(self): + # (Logica _on_close invariata, gestisce solo la chiusura/cancel) if self._has_changes(): res = messagebox.askyesnocancel( "Unsaved Changes", "Save changes?", parent=self ) if res is True: - self._save_and_close() + self._save_and_close() # This will now trigger the callback if save succeeds elif res is False: self.logger.warning("Discarding .gitignore changes.") self.destroy() + # else: Cancel, do nothing else: self.destroy()