SXXXXXXX_GitUtility/gitutility/gui/tabs/history_tab.py

310 lines
12 KiB
Python

# --- FILE: gitsync_tool/gui/tabs/history_tab.py ---
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Callable, List, Optional
from gitutility.gui.tooltip import Tooltip
from gitutility.logging_setup import log_handler
class HistoryTab(ttk.Frame):
"""
The 'History' tab in the main application notebook.
This tab provides a treeview for browsing the Git commit history and
widgets for filtering the view.
"""
def __init__(self, master: tk.Misc, **kwargs):
"""
Initializes the History tab.
Args:
master: The parent widget (the ttk.Notebook).
**kwargs: Dictionary of callbacks from the main controller.
"""
super().__init__(master, padding=(10, 10))
# Store callbacks
self.refresh_history_callback = kwargs.get("refresh_history_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 ---
self.main_frame = self.master.master
# --- Tkinter Variables specific to this tab ---
self.history_branch_filter_var = tk.StringVar()
# Configure layout
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
# Create widgets
self._create_widgets()
# ... (il resto del file rimane identico) ...
def _create_widgets(self) -> None:
"""Creates and arranges all widgets for this tab."""
# --- Controls Frame (Filter and Refresh) ---
controls_frame = ttk.Frame(self)
controls_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
controls_frame.columnconfigure(1, weight=1)
ttk.Label(controls_frame, text="Filter History by Branch/Tag:").pack(
side=tk.LEFT, padx=(0, 5)
)
self.history_branch_filter_combo = ttk.Combobox(
controls_frame,
textvariable=self.history_branch_filter_var,
state="readonly",
width=40,
)
self.history_branch_filter_combo.pack(
side=tk.LEFT, expand=True, fill=tk.X, padx=5
)
self.history_branch_filter_combo.bind(
"<<ComboboxSelected>>", lambda e: self.refresh_history_callback()
)
Tooltip(
self.history_branch_filter_combo,
"Select a branch or tag to filter the commit history.",
)
self.refresh_history_button = ttk.Button(
controls_frame,
text="Refresh History",
command=self.refresh_history_callback,
state=tk.DISABLED,
)
self.refresh_history_button.pack(side=tk.LEFT, padx=5)
Tooltip(
self.refresh_history_button,
"Reload commit history based on the selected filter.",
)
# --- Treeview Frame for History ---
content_frame = ttk.Frame(self)
content_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5))
content_frame.rowconfigure(0, weight=1)
content_frame.columnconfigure(0, weight=1)
columns = ("hash", "datetime", "author", "details")
self.history_tree = ttk.Treeview(
content_frame,
columns=columns,
show="headings",
selectmode="browse",
height=15,
)
self.history_tree.heading("hash", text="Hash", anchor="w")
self.history_tree.heading("datetime", text="Date/Time", anchor="w")
self.history_tree.heading("author", text="Author", anchor="w")
self.history_tree.heading("details", text="Subject / Refs", anchor="w")
self.history_tree.column("hash", width=80, stretch=tk.NO, anchor="w")
self.history_tree.column("datetime", width=140, stretch=tk.NO, anchor="w")
self.history_tree.column("author", width=150, stretch=tk.NO, anchor="w")
self.history_tree.column("details", width=450, stretch=tk.YES, anchor="w")
tree_scrollbar_y = ttk.Scrollbar(
content_frame, orient=tk.VERTICAL, command=self.history_tree.yview
)
tree_scrollbar_x = ttk.Scrollbar(
content_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview
)
self.history_tree.configure(
yscrollcommand=tree_scrollbar_y.set, xscrollcommand=tree_scrollbar_x.set
)
self.history_tree.grid(row=0, column=0, sticky="nsew")
tree_scrollbar_y.grid(row=0, column=1, sticky="ns")
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("<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:
"""Sets the state of all action widgets in this tab."""
combo_state = "readonly" if state == tk.NORMAL else tk.DISABLED
widgets = {
self.history_branch_filter_combo: combo_state,
self.refresh_history_button: state,
}
for widget, widget_state in widgets.items():
if widget and widget.winfo_exists():
widget.config(state=widget_state)
if self.history_tree and self.history_tree.winfo_exists():
if state == tk.DISABLED:
self.history_tree.unbind("<Double-Button-1>")
self.history_tree.unbind("<Button-3>")
else:
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:
"""Populates the history treeview with parsed log data."""
func_name = "update_history_display (Tree)"
if not self.history_tree.winfo_exists():
return
for item_id in self.history_tree.get_children():
self.history_tree.delete(item_id)
if not isinstance(log_lines, list):
self.history_tree.insert(
"", "end", values=("", "", "", "(Error: Invalid data received)")
)
return
if not log_lines:
self.history_tree.insert(
"", "end", values=("", "", "", "(No history found)")
)
return
for i, line in enumerate(log_lines):
line_str = str(line).strip()
if not line_str or line_str.startswith("("):
continue
try:
parts = line_str.split("|", 2)
part1 = parts[0].strip()
commit_author = parts[1].strip()
commit_details = parts[2].strip()
first_space = part1.find(" ")
commit_hash = part1[:first_space]
commit_datetime = part1[first_space:].strip()[:16]
except Exception:
commit_hash, commit_datetime, commit_author, commit_details = (
"Error",
"",
"",
line_str,
)
self.history_tree.insert(
"",
"end",
iid=i,
values=(commit_hash, commit_datetime, commit_author, commit_details),
)
self.history_tree.yview_moveto(0.0)
self.history_tree.xview_moveto(0.0)
def update_history_branch_filter(self, branches_tags: List[str]):
"""Updates the options in the branch/tag filter combobox."""
if not self.history_branch_filter_combo.winfo_exists():
return
opts = ["-- All History --"] + sorted(branches_tags if branches_tags else [])
self.history_branch_filter_combo["values"] = opts
current_selection = self.history_branch_filter_var.get()
if current_selection not in opts:
self.history_branch_filter_var.set(opts[0])
def _on_history_double_click(self, event: tk.Event) -> None:
"""Handles double-click on a commit in the history tree."""
func_name = "_on_history_double_click"
try:
selected_iid = self.history_tree.focus()
if not selected_iid:
return
item_data = self.history_tree.item(selected_iid)
item_values = item_data.get("values")
if item_values and len(item_values) > 0:
commit_hash = str(item_values[0]).strip()
if (
commit_hash
and not commit_hash.startswith("(")
and callable(self.view_commit_details_callback)
):
log_handler.log_info(
f"Requesting details for hash: '{commit_hash}'",
func_name=func_name,
)
self.view_commit_details_callback(commit_hash)
except Exception as e:
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
)
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)