310 lines
12 KiB
Python
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)
|