213 lines
7.3 KiB
Python
213 lines
7.3 KiB
Python
# --- FILE: gitsync_tool/gui/purge_dialog.py ---
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
# Importa usando il percorso assoluto
|
|
from gitutility.logging_setup import log_handler
|
|
from gitutility.gui.tooltip import Tooltip
|
|
|
|
|
|
def _format_size(size_bytes: int) -> str:
|
|
"""Formats a size in bytes to a human-readable string (KB, MB, GB)."""
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} B"
|
|
kb = size_bytes / 1024
|
|
if kb < 1024:
|
|
return f"{kb:.1f} KB"
|
|
mb = kb / 1024
|
|
if mb < 1024:
|
|
return f"{mb:.2f} MB"
|
|
gb = mb / 1024
|
|
return f"{gb:.2f} GB"
|
|
|
|
|
|
class PurgeConfirmationDialog(simpledialog.Dialog):
|
|
"""
|
|
A modal dialog to show files that can be purged from Git history and
|
|
to get explicit user confirmation for this destructive action.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
master: tk.Misc,
|
|
files_to_purge: List[Dict[str, Any]],
|
|
repo_path: str,
|
|
title: str = "Confirm History Purge",
|
|
):
|
|
"""
|
|
Initialize the dialog.
|
|
|
|
Args:
|
|
master: The parent window.
|
|
files_to_purge: A list of dicts, each with 'path' and 'size' keys.
|
|
repo_path: The path of the repository being cleaned.
|
|
title (str): The title for the dialog window.
|
|
"""
|
|
self.files_to_purge = files_to_purge
|
|
self.repo_path = repo_path
|
|
self.confirmation_var = tk.BooleanVar(value=False)
|
|
self.result: bool = False # The final result will be a boolean
|
|
|
|
super().__init__(master, title=title)
|
|
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
"""Creates the dialog body."""
|
|
# Main container frame
|
|
container = ttk.Frame(master, padding="10")
|
|
container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# --- 1. Warning Section ---
|
|
warning_frame = ttk.Frame(container, style="Card.TFrame", padding=10)
|
|
warning_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
# Use a custom style for the warning frame if available
|
|
try:
|
|
s = ttk.Style()
|
|
s.configure(
|
|
"Card.TFrame", background="#FFF5F5", relief="solid", borderwidth=1
|
|
)
|
|
except tk.TclError:
|
|
pass # Fallback to default frame style
|
|
|
|
warning_icon = ttk.Label(warning_frame, text="⚠️", font=("Segoe UI", 16))
|
|
warning_icon.pack(side=tk.LEFT, padx=(0, 10), anchor="n")
|
|
|
|
warning_text_frame = ttk.Frame(warning_frame, style="Card.TFrame")
|
|
warning_text_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
ttk.Label(
|
|
warning_text_frame,
|
|
text="DESTRUCTIVE ACTION",
|
|
font=("Segoe UI", 11, "bold"),
|
|
foreground="#D9534F",
|
|
style="Card.TLabel",
|
|
).pack(anchor="w")
|
|
|
|
warning_message = (
|
|
"This will permanently remove the files listed below from your entire Git history. "
|
|
"This operation rewrites history and requires a force push, which can disrupt collaboration.\n"
|
|
"Ensure all team members have pushed their changes before proceeding."
|
|
)
|
|
ttk.Label(
|
|
warning_text_frame,
|
|
text=warning_message,
|
|
wraplength=500,
|
|
justify=tk.LEFT,
|
|
style="Card.TLabel",
|
|
).pack(anchor="w", pady=(5, 0))
|
|
|
|
try:
|
|
s.configure("Card.TLabel", background="#FFF5F5")
|
|
except tk.TclError:
|
|
pass
|
|
|
|
# --- 2. File List Section ---
|
|
files_frame = ttk.LabelFrame(
|
|
container,
|
|
text=f"Files to Purge ({len(self.files_to_purge)} found)",
|
|
padding=10,
|
|
)
|
|
files_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
|
files_frame.rowconfigure(0, weight=1)
|
|
files_frame.columnconfigure(0, weight=1)
|
|
|
|
columns = ("path", "size")
|
|
self.tree = ttk.Treeview(files_frame, columns=columns, show="headings")
|
|
self.tree.heading("path", text="File Path", anchor="w")
|
|
self.tree.heading("size", text="Size", anchor="e")
|
|
self.tree.column("path", width=400, stretch=tk.YES, anchor="w")
|
|
self.tree.column("size", width=100, stretch=tk.NO, anchor="e")
|
|
|
|
scrollbar = ttk.Scrollbar(
|
|
files_frame, orient=tk.VERTICAL, command=self.tree.yview
|
|
)
|
|
self.tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
self.tree.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
|
|
|
# Populate the treeview
|
|
for file_info in self.files_to_purge:
|
|
path = file_info.get("path", "N/A")
|
|
size_bytes = file_info.get("size", 0)
|
|
formatted_size = _format_size(size_bytes)
|
|
self.tree.insert("", tk.END, values=(path, formatted_size))
|
|
|
|
# --- 3. Confirmation Checkbox Section ---
|
|
confirm_frame = ttk.Frame(container, padding=(0, 10, 0, 0))
|
|
confirm_frame.pack(fill=tk.X)
|
|
|
|
self.confirm_check = ttk.Checkbutton(
|
|
confirm_frame,
|
|
text="I understand the risks and want to permanently delete these files from history.",
|
|
variable=self.confirmation_var,
|
|
command=self._on_confirm_toggle,
|
|
)
|
|
self.confirm_check.pack(anchor="w")
|
|
|
|
return self.tree # Initial focus on the list
|
|
|
|
def _on_confirm_toggle(self):
|
|
"""Enables/disables the 'Confirm' button based on the checkbox state."""
|
|
# Find the OK button in the buttonbox
|
|
ok_button = self.ok_button
|
|
if ok_button:
|
|
if self.confirmation_var.get():
|
|
ok_button.config(state=tk.NORMAL)
|
|
else:
|
|
ok_button.config(state=tk.DISABLED)
|
|
|
|
def buttonbox(self):
|
|
"""Creates the OK and Cancel buttons."""
|
|
box = ttk.Frame(self)
|
|
|
|
self.ok_button = ttk.Button(
|
|
box,
|
|
text="Confirm and Purge",
|
|
width=20,
|
|
command=self.ok,
|
|
state=tk.DISABLED, # Initially disabled
|
|
style="Danger.TButton",
|
|
)
|
|
self.ok_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
try:
|
|
s = ttk.Style()
|
|
s.configure("Danger.TButton", foreground="white", background="#D9534F")
|
|
except tk.TclError:
|
|
pass # Fallback to default button style
|
|
|
|
Tooltip(
|
|
self.ok_button,
|
|
"This button is enabled only after you check the confirmation box.",
|
|
)
|
|
|
|
cancel_button = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
|
|
cancel_button.pack(side=tk.LEFT, padx=5, pady=5)
|
|
|
|
self.bind(
|
|
"<Return>", lambda e: self.ok() if self.confirmation_var.get() else None
|
|
)
|
|
self.bind("<Escape>", lambda e: self.cancel())
|
|
|
|
box.pack()
|
|
|
|
def validate(self) -> bool:
|
|
"""Validation is handled by the confirmation checkbox state."""
|
|
# The OK button is only enabled if the checkbox is checked, so if `ok` is
|
|
# called, we can assume the user has confirmed.
|
|
if not self.confirmation_var.get():
|
|
messagebox.showwarning(
|
|
"Confirmation Required",
|
|
"You must check the box to confirm you understand the risks before proceeding.",
|
|
parent=self,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
def apply(self):
|
|
"""Sets the result to True as the action has been confirmed."""
|
|
self.result = True
|