add reset to commit from history tab

This commit is contained in:
VALLONGOL 2025-09-24 15:39:00 +02:00
parent 8d1d712785
commit ad4fe4cf44
7 changed files with 223 additions and 37 deletions

View File

@ -137,6 +137,7 @@ class GitSvnSyncApp:
merge_local_branch_cb=self.merge_local_branch, merge_local_branch_cb=self.merge_local_branch,
compare_branch_with_current_cb=self.compare_branch_with_current, compare_branch_with_current_cb=self.compare_branch_with_current,
view_commit_details_cb=self.view_commit_details, view_commit_details_cb=self.view_commit_details,
reset_to_commit_cb=self.repository_handler.handle_reset_to_commit if self.repository_handler else None,
config_manager_instance=self.config_manager, config_manager_instance=self.config_manager,
profile_sections_list=self.config_manager.get_profile_sections(), profile_sections_list=self.config_manager.get_profile_sections(),
) )
@ -179,6 +180,8 @@ class GitSvnSyncApp:
self.main_frame.tags_tab.checkout_tag_callback = self.repository_handler.checkout_tag self.main_frame.tags_tab.checkout_tag_callback = self.repository_handler.checkout_tag
self.main_frame.tags_tab.revert_to_tag_callback = self.repository_handler.revert_to_tag self.main_frame.tags_tab.revert_to_tag_callback = self.repository_handler.revert_to_tag
self.main_frame.history_tab.reset_to_commit_callback = self.repository_handler.handle_reset_to_commit
self.main_frame.branch_tab.create_branch_callback = self.repository_handler.create_branch self.main_frame.branch_tab.create_branch_callback = self.repository_handler.create_branch
self.main_frame.branch_tab.checkout_branch_callback = self.repository_handler.checkout_branch self.main_frame.branch_tab.checkout_branch_callback = self.repository_handler.checkout_branch
self.main_frame.branch_tab.merge_local_branch_callback = self.repository_handler.handle_merge_local_branch self.main_frame.branch_tab.merge_local_branch_callback = self.repository_handler.handle_merge_local_branch

View File

@ -20,7 +20,7 @@ import re
from gitutility.logging_setup import log_handler from gitutility.logging_setup import log_handler
from gitutility.commands.git_commands import GitCommandError from gitutility.commands.git_commands import GitCommandError
from gitutility.async_tasks import async_workers from gitutility.async_tasks import async_workers
from gitutility.config.config_manager import DEFAULT_REMOTE_NAME from ..core.wiki_updater import WikiUpdateStatus
if TYPE_CHECKING: if TYPE_CHECKING:
from ..app import GitSvnSyncApp from ..app import GitSvnSyncApp
@ -80,7 +80,7 @@ class AsyncResultHandler:
"clone_remote": self._handle_clone_remote_result, "clone_remote": self._handle_clone_remote_result,
"checkout_tracking_branch": self._handle_checkout_tracking_branch_result, "checkout_tracking_branch": self._handle_checkout_tracking_branch_result,
"get_commit_details": self._handle_get_commit_details_result, "get_commit_details": self._handle_get_commit_details_result,
"update_wiki": self._handle_generic_result, "update_wiki": self._handle_update_wiki_result,
"revert_to_tag": self._handle_revert_to_tag_result, "revert_to_tag": self._handle_revert_to_tag_result,
"analyze_history": self._handle_analyze_history_result, "analyze_history": self._handle_analyze_history_result,
"purge_history": self._handle_purge_history_result, "purge_history": self._handle_purge_history_result,
@ -92,6 +92,7 @@ class AsyncResultHandler:
"init_submodules": self._handle_init_submodules_result, "init_submodules": self._handle_init_submodules_result,
"force_clean_submodule": self._handle_force_clean_submodule_result, "force_clean_submodule": self._handle_force_clean_submodule_result,
"clean_submodule": self._handle_clean_submodule_result, "clean_submodule": self._handle_clean_submodule_result,
"reset_to_commit": self._handle_reset_to_commit_result,
} }
handler_method = handler_map.get(task_context) handler_method = handler_map.get(task_context)
@ -107,6 +108,14 @@ class AsyncResultHandler:
if should_trigger_refreshes: if should_trigger_refreshes:
self.app._trigger_post_action_refreshes(post_action_sync_refresh_needed) self.app._trigger_post_action_refreshes(post_action_sync_refresh_needed)
def _handle_reset_to_commit_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
if result_data.get("status") == "success":
self.main_frame.show_info("Operation Successful", result_data.get("message"))
return True, True
else:
self.main_frame.show_error("Reset to Commit Error", f"Failed:\n{result_data.get('message')}")
return False, False
def _handle_refresh_tags_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]: def _handle_refresh_tags_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
if result_data.get("status") == "success": if result_data.get("status") == "success":
self.main_frame.tags_tab.update_tag_list(result_data.get("result", [])) self.main_frame.tags_tab.update_tag_list(result_data.get("result", []))
@ -600,3 +609,23 @@ class AsyncResultHandler:
self.main_frame.submodules_tab.update_remote_statuses(statuses) self.main_frame.submodules_tab.update_remote_statuses(statuses)
return False, False return False, False
def _handle_update_wiki_result(self, result_data: Dict[str, Any], context: Dict[str, Any]) -> Tuple[bool, bool]:
"""Handles the result of the update_wiki_from_docs async task."""
status = result_data.get("status")
message = result_data.get("message", "Wiki operation finished.")
if status == "success":
# In our new implementation, 'success' is for both actual updates and no-changes scenarios
self.main_frame.show_info("Wiki Update", message)
# We only need to trigger a full refresh if something actually changed.
# The result object from the worker now contains the detailed status.
update_result = result_data.get("result")
if update_result and update_result.status == WikiUpdateStatus.SUCCESS:
return True, True
elif status == "warning":
self.main_frame.show_warning("Wiki Update Info", message)
elif status == "error":
self.main_frame.show_error("Wiki Update Error", f"Failed:\n{message}")
return False, False

View File

@ -13,7 +13,7 @@ from ..commands.git_commands import GitCommands, GitCommandError
from ..core.action_handler import ActionHandler from ..core.action_handler import ActionHandler
from ..core.backup_handler import BackupHandler from ..core.backup_handler import BackupHandler
from ..core.remote_actions import RemoteActionHandler from ..core.remote_actions import RemoteActionHandler
from ..core.wiki_updater import WikiUpdater from ..core.wiki_updater import WikiUpdater, WikiUpdateStatus
from ..core.history_cleaner import HistoryCleaner from ..core.history_cleaner import HistoryCleaner
from ..core.submodule_handler import SubmoduleHandler from ..core.submodule_handler import SubmoduleHandler
@ -1040,17 +1040,27 @@ def run_update_wiki_async(
"status": "error", "status": "error",
"message": "Wiki update failed.", "message": "Wiki update failed.",
"exception": None, "exception": None,
"result": False, "result": None,
} }
try: try:
success, message = wiki_updater.update_wiki_from_docs( update_result = wiki_updater.update_wiki_from_docs(
main_repo_path=main_repo_path, main_repo_path=main_repo_path,
main_repo_remote_url=main_repo_remote_url, main_repo_remote_url=main_repo_remote_url,
) )
result_payload["status"] = "success" if success else "error"
result_payload["message"] = message # The status for the queue can be 'success', 'warning', or 'error'
result_payload["result"] = success # We map our detailed status to one of these
log_handler.log_info(f"[Worker] Wiki update result: {message}", func_name=func_name) if update_result.status in [WikiUpdateStatus.SUCCESS, WikiUpdateStatus.NO_CHANGES]:
queue_status = "success"
elif update_result.status in [WikiUpdateStatus.DOC_NOT_FOUND]:
queue_status = "warning"
else:
queue_status = "error"
result_payload["status"] = queue_status
result_payload["message"] = update_result.message
result_payload["result"] = update_result # Pass the whole result object
log_handler.log_info(f"[Worker] Wiki update result: {update_result.message}", func_name=func_name)
except Exception as e: except Exception as e:
log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during wiki update: {e}", func_name=func_name) log_handler.log_exception(f"[Worker] UNEXPECTED EXCEPTION during wiki update: {e}", func_name=func_name)
@ -1093,6 +1103,36 @@ def run_revert_to_tag_async(
log_handler.log_error(f"[Worker] Failed to put result in queue for {func_name}: {qe}", func_name=func_name) log_handler.log_error(f"[Worker] Failed to put result in queue for {func_name}: {qe}", func_name=func_name)
log_handler.log_debug(f"[Worker] Finished: Revert to Tag '{tag_name}'", func_name=func_name) log_handler.log_debug(f"[Worker] Finished: Revert to Tag '{tag_name}'", func_name=func_name)
def run_reset_to_commit_async(
action_handler: ActionHandler,
repo_path: str,
commit_hash: str,
results_queue: queue.Queue[Dict[str, Any]],
) -> None:
"""Worker to perform a hard reset to a specific commit asynchronously."""
func_name = "run_reset_to_commit_async"
log_handler.log_debug(f"[Worker] Started: Reset to Commit '{commit_hash}' in '{repo_path}'", func_name=func_name)
result_payload: Dict[str, Any] = {
"status": "error", "result": False, "message": f"Failed to reset to commit '{commit_hash[:7]}'.", "exception": None,
}
try:
# Directly call the git_reset_hard from action_handler's git_commands
action_handler.git_commands.git_reset_hard(repo_path, commit_hash)
result_payload["status"] = "success"
result_payload["result"] = True
result_payload["message"] = f"Repository successfully reset to commit '{commit_hash[:7]}'."
log_handler.log_info(f"[Worker] {result_payload['message']}", func_name=func_name)
except (GitCommandError, ValueError, Exception) as e:
log_handler.log_exception(f"[Worker] EXCEPTION resetting to commit: {e}", func_name=func_name)
result_payload["exception"] = e
result_payload["message"] = f"Error resetting to commit '{commit_hash[:7]}': {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(f"[Worker] Finished: Reset to Commit '{commit_hash}'", func_name=func_name)
def run_analyze_repo_for_purge_async( def run_analyze_repo_for_purge_async(
history_cleaner: HistoryCleaner, history_cleaner: HistoryCleaner,
repo_path: str, repo_path: str,

View File

@ -905,6 +905,32 @@ class GitCommands:
raise e raise e
# --- History / Log --- # --- History / Log ---
def reset_hard(self, working_directory: str, commit_hash: str) -> None:
"""
Performs a git reset --hard to the specified commit hash.
Args:
working_directory (str): Path to the repository.
commit_hash (str): The commit hash to reset to.
"""
func_name = "reset_hard"
log_handler.log_warning(
f"Performing a destructive 'git reset --hard' to '{commit_hash}' in '{working_directory}'",
func_name=func_name
)
if not commit_hash or commit_hash.isspace():
raise ValueError("Commit hash cannot be empty.")
cmd = ["git", "reset", "--hard", commit_hash]
try:
self.log_and_execute(cmd, working_directory, check=True)
log_handler.log_info(
f"Successfully reset to commit '{commit_hash}'.", func_name=func_name
)
except GitCommandError as e:
log_handler.log_error(f"Failed to 'git reset --hard': {e}", func_name=func_name)
raise
def get_commit_log( def get_commit_log(
self, working_directory: str, max_count: int = 200, branch: Optional[str] = None self, working_directory: str, max_count: int = 200, branch: Optional[str] = None
) -> List[str]: ) -> List[str]:
@ -2048,11 +2074,11 @@ class GitCommands:
raise e raise e
log_handler.log_warning( log_handler.log_warning(
"Performing DESTRUCTIVE operation: git clean -fdx", func_name=func_name "Performing DESTRUCTIVE operation: git clean -fd", func_name=func_name
) )
# 2. Esegui git clean -fdx per rimuovere file non tracciati # 2. Esegui git clean -fd per rimuovere file non tracciati
clean_cmd: List[str] = ["git", "clean", "-fdx"] clean_cmd: List[str] = ["git", "clean", "-fd"]
try: try:
self.log_and_execute( self.log_and_execute(
clean_cmd, working_directory, check=True, log_output_level=logging.INFO clean_cmd, working_directory, check=True, log_output_level=logging.INFO

View File

@ -5,14 +5,33 @@ import shutil
import tempfile import tempfile
import re import re
import time import time
from typing import Optional, Tuple from typing import Optional
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
from enum import Enum, auto
from dataclasses import dataclass
from ..commands.git_commands import GitCommands, GitCommandError from ..commands.git_commands import GitCommands, GitCommandError
from ..logging_setup import log_handler from ..logging_setup import log_handler
class WikiUpdateStatus(Enum):
"""Represents the result of the wiki update operation."""
SUCCESS = auto()
NO_CHANGES = auto()
URL_DERIVATION_FAILED = auto()
CLONE_FAILED = auto()
PUSH_FAILED = auto()
COMMIT_FAILED = auto()
DOC_NOT_FOUND = auto()
BRANCH_NOT_FOUND = auto()
GENERIC_ERROR = auto()
@dataclass
class WikiUpdateResult:
"""Holds the result of the wiki update operation."""
status: WikiUpdateStatus
message: str
class WikiUpdater: class WikiUpdater:
# ... (il resto della classe è invariato fino a update_wiki_from_docs)
DEFAULT_WIKI_EN_FILENAME = "English-Manual.md" DEFAULT_WIKI_EN_FILENAME = "English-Manual.md"
DEFAULT_WIKI_IT_FILENAME = "Italian-Manual.md" DEFAULT_WIKI_IT_FILENAME = "Italian-Manual.md"
@ -23,7 +42,6 @@ class WikiUpdater:
log_handler.log_debug("WikiUpdater initialized.", func_name="__init__") log_handler.log_debug("WikiUpdater initialized.", func_name="__init__")
def _get_wiki_repo_url(self, main_repo_url: str) -> Optional[str]: def _get_wiki_repo_url(self, main_repo_url: str) -> Optional[str]:
# ... (codice invariato)
if not main_repo_url: if not main_repo_url:
return None return None
try: try:
@ -52,7 +70,7 @@ class WikiUpdater:
wiki_en_target_filename: Optional[str] = None, wiki_en_target_filename: Optional[str] = None,
wiki_it_target_filename: Optional[str] = None, wiki_it_target_filename: Optional[str] = None,
commit_message: str = "Update Wiki documentation from local files" commit_message: str = "Update Wiki documentation from local files"
) -> Tuple[bool, str]: ) -> WikiUpdateResult:
""" """
Clones the Gitea wiki repo, updates pages from local doc files, and pushes. Clones the Gitea wiki repo, updates pages from local doc files, and pushes.
""" """
@ -63,7 +81,7 @@ class WikiUpdater:
if not wiki_url: if not wiki_url:
msg = "Could not derive Wiki repository URL from main remote URL." msg = "Could not derive Wiki repository URL from main remote URL."
log_handler.log_error(msg, func_name=func_name) log_handler.log_error(msg, func_name=func_name)
return False, msg return WikiUpdateResult(status=WikiUpdateStatus.URL_DERIVATION_FAILED, message=msg)
log_handler.log_debug(f"Derived wiki URL: {wiki_url}", func_name=func_name) log_handler.log_debug(f"Derived wiki URL: {wiki_url}", func_name=func_name)
@ -78,7 +96,7 @@ class WikiUpdater:
if not en_exists and not it_exists: 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." 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) log_handler.log_warning(msg, func_name=func_name)
return False, msg return WikiUpdateResult(status=WikiUpdateStatus.DOC_NOT_FOUND, message=msg)
temp_dir: Optional[str] = None temp_dir: Optional[str] = None
try: try:
@ -93,7 +111,7 @@ class WikiUpdater:
else: else:
msg = f"Failed to clone Wiki repository '{wiki_url}'. Error: {stderr_msg.strip()}" msg = f"Failed to clone Wiki repository '{wiki_url}'. Error: {stderr_msg.strip()}"
log_handler.log_error(msg, func_name=func_name) log_handler.log_error(msg, func_name=func_name)
return False, f"{msg} (Check authentication?)" return WikiUpdateResult(status=WikiUpdateStatus.CLONE_FAILED, message=f"{msg} (Check authentication?)")
log_handler.log_info("Wiki repository cloned successfully.", func_name=func_name) log_handler.log_info("Wiki repository cloned successfully.", func_name=func_name)
@ -108,26 +126,24 @@ class WikiUpdater:
if not self.git_commands.git_status_has_changes(temp_dir): if not self.git_commands.git_status_has_changes(temp_dir):
msg = "Wiki content is already up-to-date with local doc files." msg = "Wiki content is already up-to-date with local doc files."
log_handler.log_info(msg, func_name=func_name) log_handler.log_info(msg, func_name=func_name)
return True, msg return WikiUpdateResult(status=WikiUpdateStatus.NO_CHANGES, message=msg)
# --- MODIFICA PER PROBLEMA COMMIT ---
log_handler.log_info("Changes detected. Staging with --renormalize to handle line endings.", func_name=func_name) log_handler.log_info("Changes detected. Staging with --renormalize to handle line endings.", func_name=func_name)
# Questo comando forza Git a ri-processare i file, risolvendo problemi di CRLF/LF
self.git_commands.add_file(temp_dir, ".", renormalize=True) self.git_commands.add_file(temp_dir, ".", renormalize=True)
# Ora eseguiamo il commit, ma senza fare un altro 'add'
commit_success = self.git_commands.git_commit(temp_dir, commit_message, stage_all_first=False)
# --- FINE MODIFICA ---
commit_success = self.git_commands.git_commit(temp_dir, commit_message, stage_all_first=False)
if not commit_success: if not commit_success:
msg = "Staged changes, but commit reported no changes to commit. Push will be skipped." msg = "Staged changes, but commit reported no changes to commit. Push will be skipped."
log_handler.log_warning(msg, func_name=func_name) log_handler.log_warning(msg, func_name=func_name)
return True, msg # Consideriamo un "successo" perché non c'era nulla di sostanziale da pushare return WikiUpdateResult(status=WikiUpdateStatus.NO_CHANGES, message=msg)
log_handler.log_info("Wiki changes committed locally.", func_name=func_name) log_handler.log_info("Wiki changes committed locally.", func_name=func_name)
current_wiki_branch = self.git_commands.get_current_branch_name(temp_dir) current_wiki_branch = self.git_commands.get_current_branch_name(temp_dir)
if not current_wiki_branch: if not current_wiki_branch:
return False, "Could not determine the current branch in the cloned wiki repository." msg = "Could not determine the current branch in the cloned wiki repository."
log_handler.log_error(msg, func_name=func_name)
return WikiUpdateResult(status=WikiUpdateStatus.BRANCH_NOT_FOUND, message=msg)
log_handler.log_info("Pushing wiki changes to remote...", func_name=func_name) log_handler.log_info("Pushing wiki changes to remote...", func_name=func_name)
push_result = self.git_commands.git_push( push_result = self.git_commands.git_push(
@ -140,30 +156,28 @@ class WikiUpdater:
if push_result.returncode == 0: if push_result.returncode == 0:
msg = "Wiki update pushed successfully to Gitea." msg = "Wiki update pushed successfully to Gitea."
log_handler.log_info(msg, func_name=func_name) log_handler.log_info(msg, func_name=func_name)
return True, msg return WikiUpdateResult(status=WikiUpdateStatus.SUCCESS, message=msg)
else: else:
stderr_msg = push_result.stderr or "Unknown push error" stderr_msg = push_result.stderr or "Unknown push error"
msg = f"Failed to push Wiki updates. Error: {stderr_msg.strip()}" msg = f"Failed to push Wiki updates. Error: {stderr_msg.strip()}"
log_handler.log_error(msg, func_name=func_name) log_handler.log_error(msg, func_name=func_name)
return False, msg return WikiUpdateResult(status=WikiUpdateStatus.PUSH_FAILED, message=msg)
except (GitCommandError, ValueError, IOError, Exception) as e: except (GitCommandError, ValueError, IOError, Exception) as e:
log_handler.log_exception(f"Error during wiki update process: {e}", func_name=func_name) log_handler.log_exception(f"Error during wiki update process: {e}", func_name=func_name)
return False, f"Wiki update failed: {e}" return WikiUpdateResult(status=WikiUpdateStatus.GENERIC_ERROR, message=f"Wiki update failed: {e}")
finally: finally:
# --- MODIFICA PER PROBLEMA PULIZIA ---
if temp_dir and os.path.isdir(temp_dir): if temp_dir and os.path.isdir(temp_dir):
log_handler.log_debug(f"Attempting to clean up temporary directory: {temp_dir}", func_name=func_name) log_handler.log_debug(f"Attempting to clean up temporary directory: {temp_dir}", func_name=func_name)
for attempt in range(3): # Prova fino a 3 volte for attempt in range(3):
try: try:
shutil.rmtree(temp_dir) shutil.rmtree(temp_dir)
log_handler.log_info(f"Cleanup of temporary directory successful.", func_name=func_name) log_handler.log_info(f"Cleanup of temporary directory successful.", func_name=func_name)
break # Esci dal ciclo se la pulizia ha successo break
except Exception as clean_e: except Exception as clean_e:
log_handler.log_warning(f"Cleanup attempt {attempt + 1} failed for '{temp_dir}': {clean_e}", func_name=func_name) log_handler.log_warning(f"Cleanup attempt {attempt + 1} failed for '{temp_dir}': {clean_e}", func_name=func_name)
if attempt < 2: if attempt < 2:
time.sleep(0.5) # Aspetta 500ms prima di riprovare time.sleep(0.5)
else: else:
log_handler.log_error(f"Final cleanup attempt failed for '{temp_dir}'.", func_name=func_name) log_handler.log_error(f"Final cleanup attempt failed for '{temp_dir}'.", func_name=func_name)
# --- FINE MODIFICA ---

View File

@ -27,6 +27,7 @@ class HistoryTab(ttk.Frame):
# Store callbacks # Store callbacks
self.refresh_history_callback = kwargs.get('refresh_history_cb') self.refresh_history_callback = kwargs.get('refresh_history_cb')
self.view_commit_details_callback = kwargs.get('view_commit_details_cb') self.view_commit_details_callback = kwargs.get('view_commit_details_cb')
self.reset_to_commit_callback = kwargs.get('reset_to_commit_cb')
# --- Get a reference to the main frame for shared components --- # --- Get a reference to the main frame for shared components ---
self.main_frame = self.master.master self.main_frame = self.master.master
@ -97,7 +98,8 @@ class HistoryTab(ttk.Frame):
tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew") tree_scrollbar_x.grid(row=1, column=0, columnspan=2, sticky="ew")
self.history_tree.bind("<Double-Button-1>", self._on_history_double_click) self.history_tree.bind("<Double-Button-1>", self._on_history_double_click)
Tooltip(self.history_tree, "Double-click a commit line to view details.") self.history_tree.bind("<Button-3>", self._show_context_menu)
Tooltip(self.history_tree, "Double-click a commit line to view details.\nRight-click for more options.")
def set_action_widgets_state(self, state: str) -> None: def set_action_widgets_state(self, state: str) -> None:
"""Sets the state of all action widgets in this tab.""" """Sets the state of all action widgets in this tab."""
@ -115,8 +117,10 @@ class HistoryTab(ttk.Frame):
if self.history_tree and self.history_tree.winfo_exists(): if self.history_tree and self.history_tree.winfo_exists():
if state == tk.DISABLED: if state == tk.DISABLED:
self.history_tree.unbind("<Double-Button-1>") self.history_tree.unbind("<Double-Button-1>")
self.history_tree.unbind("<Button-3>")
else: else:
self.history_tree.bind("<Double-Button-1>", self._on_history_double_click) self.history_tree.bind("<Double-Button-1>", self._on_history_double_click)
self.history_tree.bind("<Button-3>", self._show_context_menu)
def update_history_display(self, log_lines: List[str]) -> None: def update_history_display(self, log_lines: List[str]) -> None:
"""Populates the history treeview with parsed log data.""" """Populates the history treeview with parsed log data."""
@ -186,3 +190,58 @@ class HistoryTab(ttk.Frame):
except Exception as e: except Exception as e:
log_handler.log_exception(f"Error handling history double-click: {e}", func_name=func_name) log_handler.log_exception(f"Error handling history double-click: {e}", func_name=func_name)
messagebox.showerror("Error", f"Could not process history selection:\n{e}", parent=self) messagebox.showerror("Error", f"Could not process history selection:\n{e}", parent=self)
def _show_context_menu(self, event: tk.Event) -> None:
"""Displays a context menu on right-click."""
# Select the item under the cursor
iid = self.history_tree.identify_row(event.y)
if not iid: # Clicked outside of any item
return
self.history_tree.selection_set(iid)
self.history_tree.focus(iid)
item_data = self.history_tree.item(iid)
item_values = item_data.get("values")
if not item_values or not str(item_values[0]).strip():
return
commit_hash = str(item_values[0]).strip()
context_menu = tk.Menu(self, tearoff=0)
context_menu.add_command(
label=f"View Details for {commit_hash[:7]}...",
command=lambda: self.view_commit_details_callback(commit_hash)
)
context_menu.add_separator()
context_menu.add_command(
label=f"Reset branch to this commit ({commit_hash[:7]}) ...",
command=lambda: self._on_reset_to_commit(commit_hash)
)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
def _on_reset_to_commit(self, commit_hash: str) -> None:
"""Handles the 'Reset to commit' action from the context menu."""
func_name = "_on_reset_to_commit"
if not callable(self.reset_to_commit_callback):
log_handler.log_warning("reset_to_commit_callback is not available.", func_name)
return
title = "Confirm Destructive Action"
message = (
f"Are you sure you want to reset your current branch to commit {commit_hash[:7]}?"
f"WARNING: This is a destructive operation!"
f"- All commits made after this one will be permanently lost."
f"- All uncommitted changes in your working directory will be permanently lost."
f"This action cannot be undone. Proceed with caution."
)
if messagebox.askokcancel(title, message, icon=messagebox.WARNING, parent=self):
log_handler.log_warning(f"User confirmed reset to commit: '{commit_hash}'", func_name)
self.reset_to_commit_callback(commit_hash)
else:
log_handler.log_info("User cancelled reset operation.", func_name)

View File

@ -408,3 +408,18 @@ class RepositoryHandler:
args_tuple=args, args_tuple=args,
context_dict={"context": "get_commit_details", "status_msg": f"Loading details for commit {commit_hash_short}"} context_dict={"context": "get_commit_details", "status_msg": f"Loading details for commit {commit_hash_short}"}
) )
def handle_reset_to_commit(self, commit_hash: str):
"""Handles the destructive 'Reset to Commit' action."""
func_name = "handle_reset_to_commit"
svn_path = self.app._get_and_validate_svn_path("Reset to Commit")
if not svn_path or not self.app._is_repo_ready(svn_path):
self.main_frame.show_error("Action Failed", "Repository is not ready.")
return
args = (self.action_handler, svn_path, commit_hash)
self.app._start_async_operation(
worker_func=async_workers.run_reset_to_commit_async,
args_tuple=args,
context_dict={"context": "reset_to_commit", "status_msg": f"Resetting to commit '{commit_hash[:7]}'"},
)