441 lines
16 KiB
Python
441 lines
16 KiB
Python
# --- FILE: gitsync_tool/gui/dialogs.py ---
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog, filedialog
|
|
import os
|
|
import re # Per validazione nomi branch/tag
|
|
from typing import Optional, Tuple, List
|
|
|
|
# --- Tooltip (non necessario qui, i dialoghi sono semplici) ---
|
|
|
|
# --- Costanti (non necessarie qui) ---
|
|
|
|
|
|
class CreateTagDialog(simpledialog.Dialog):
|
|
"""
|
|
Dialog to get tag name and message from the user for creating
|
|
an annotated Git tag.
|
|
"""
|
|
|
|
def __init__(
|
|
self, parent, title: str = "Create New Tag", suggested_tag_name: str = ""
|
|
):
|
|
"""
|
|
Initialize the dialog.
|
|
|
|
Args:
|
|
parent: The parent window.
|
|
title (str): The title for the dialog window.
|
|
suggested_tag_name (str): A pre-filled suggestion for the tag name.
|
|
"""
|
|
self.tag_name_var = tk.StringVar()
|
|
self.tag_message_var = tk.StringVar()
|
|
# self.result will store the tuple (tag_name, tag_message) on success
|
|
self.result: Optional[Tuple[str, str]] = None
|
|
self.suggested_tag_name: str = suggested_tag_name
|
|
# Call Dialog's __init__ AFTER setting up variables needed by body()
|
|
super().__init__(parent, title=title)
|
|
|
|
# Override
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
"""Creates the dialog body with entry fields."""
|
|
# Use a ttk.Frame for better padding and layout control
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="x", expand=True)
|
|
frame.columnconfigure(1, weight=1) # Make the entry column expandable
|
|
|
|
# Tag Name Row
|
|
ttk.Label(frame, text="Tag Name:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.name_entry = ttk.Entry(frame, textvariable=self.tag_name_var, width=40)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
# Pre-fill suggested name if provided
|
|
if self.suggested_tag_name:
|
|
self.tag_name_var.set(self.suggested_tag_name)
|
|
|
|
# Tag Message Row
|
|
ttk.Label(frame, text="Tag Message:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.message_entry = ttk.Entry(
|
|
frame, textvariable=self.tag_message_var, width=40
|
|
)
|
|
self.message_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
# Return the widget that should have initial focus
|
|
return self.name_entry
|
|
|
|
# Override
|
|
def validate(self) -> bool:
|
|
"""Validates the input before closing the dialog."""
|
|
name = self.tag_name_var.get().strip()
|
|
msg = self.tag_message_var.get().strip()
|
|
|
|
if not name:
|
|
messagebox.showwarning(
|
|
"Input Error", "Tag name cannot be empty.", parent=self
|
|
)
|
|
self.name_entry.focus_set() # Focus back to name entry
|
|
return False # Validation failed
|
|
|
|
# Basic Git tag name validation (adjust regex if needed)
|
|
# Avoids common invalid characters and patterns like '..', '.git', ending ., /
|
|
# Ref: https://git-scm.com/docs/git-check-ref-format
|
|
pattern = r"^(?!\.|/|.*@\{|.*\\)(?!.*\.lock$)(?!.*\.\.$)[^ \t\n\r\f\v~^:?*\[]+(?<!\.)$"
|
|
if not re.match(pattern, name):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
f"Invalid tag name format: '{name}'.\n"
|
|
f"Tags cannot contain spaces, control chars, or standard invalid path chars (~^:?*[]\\), "
|
|
f"and cannot start/end with '.' or '/', or contain '..', '.lock', '@{{'.",
|
|
parent=self,
|
|
)
|
|
self.name_entry.focus_set()
|
|
return False
|
|
|
|
if not msg:
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
"Tag message cannot be empty (for annotated tag).",
|
|
parent=self,
|
|
)
|
|
self.message_entry.focus_set() # Focus back to message entry
|
|
return False # Validation failed
|
|
|
|
return True # Validation successful
|
|
|
|
# Override
|
|
def apply(self) -> None:
|
|
"""Processes the validated data and stores it in self.result."""
|
|
# Store the cleaned data as a tuple
|
|
self.result = (
|
|
self.tag_name_var.get().strip(),
|
|
self.tag_message_var.get().strip(),
|
|
)
|
|
|
|
|
|
class CreateBranchDialog(simpledialog.Dialog):
|
|
"""Dialog to get a new branch name from the user."""
|
|
|
|
def __init__(self, parent, title: str = "Create New Branch"):
|
|
"""Initialize the dialog."""
|
|
self.branch_name_var = tk.StringVar()
|
|
self.result: Optional[str] = None
|
|
super().__init__(parent, title=title)
|
|
|
|
# Override
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
"""Creates the dialog body."""
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="x", expand=True)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
ttk.Label(frame, text="New Branch Name:").grid(
|
|
row=0, column=0, padx=5, pady=10, sticky="w"
|
|
)
|
|
self.name_entry = ttk.Entry(frame, textvariable=self.branch_name_var, width=40)
|
|
self.name_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
|
|
return self.name_entry # Initial focus
|
|
|
|
# Override
|
|
def validate(self) -> bool:
|
|
"""Validates the branch name input."""
|
|
name = self.branch_name_var.get().strip()
|
|
if not name:
|
|
messagebox.showwarning(
|
|
"Input Error", "Branch name cannot be empty.", parent=self
|
|
)
|
|
self.name_entry.focus_set()
|
|
return False
|
|
|
|
# Git branch name validation (similar to tags but allows '/')
|
|
# Ref: https://git-scm.com/docs/git-check-ref-format
|
|
# Cannot start/end with '/', contain '..', spaces, control chars, ~^:?*[\ ]
|
|
# Cannot be exactly 'HEAD'.
|
|
pattern = r"^(?!\.|/)(?!.*@\{|.*\\)(?!.*\.lock$)(?!.*\.\.)(?!.*/$)[^ \t\n\r\f\v~^:?*\[\\]+$"
|
|
if name.lower() == "head" or not re.match(pattern, name):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
f"Invalid branch name: '{name}'.\n"
|
|
f"Check for spaces, invalid characters (~^:?*[]\\), consecutive dots, "
|
|
f"starting/ending slashes or dots, '.lock' suffix, or using 'HEAD'.",
|
|
parent=self,
|
|
)
|
|
self.name_entry.focus_set()
|
|
return False
|
|
return True
|
|
|
|
# Override
|
|
def apply(self) -> None:
|
|
"""Stores the validated branch name."""
|
|
self.result = self.branch_name_var.get().strip()
|
|
|
|
|
|
class CloneFromRemoteDialog(simpledialog.Dialog):
|
|
"""Dialog to get Remote URL and Local Parent Directory for cloning."""
|
|
|
|
def __init__(self, parent, title: str = "Clone Remote Repository"):
|
|
"""Initialize the dialog."""
|
|
self.remote_url_var = tk.StringVar()
|
|
self.local_parent_dir_var = tk.StringVar()
|
|
self.profile_name_var = tk.StringVar() # Optional profile name
|
|
# self.result will store (url, parent_dir, profile_name_input)
|
|
self.result: Optional[Tuple[str, str, str]] = None
|
|
|
|
# Set suggested initial directory for the local folder
|
|
# Default to user's home directory
|
|
self.local_parent_dir_var.set(os.path.expanduser("~"))
|
|
super().__init__(parent, title=title)
|
|
|
|
# Override
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
"""Creates the dialog body."""
|
|
main_frame = ttk.Frame(master, padding="10")
|
|
main_frame.pack(fill="both", expand=True)
|
|
main_frame.columnconfigure(1, weight=1) # Entry column expands
|
|
|
|
row_idx: int = 0
|
|
|
|
# Remote URL Row
|
|
ttk.Label(main_frame, text="Remote Repository URL:").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.url_entry = ttk.Entry(
|
|
main_frame, textvariable=self.remote_url_var, width=60
|
|
)
|
|
self.url_entry.grid(
|
|
row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew"
|
|
)
|
|
# Consider adding Tooltip here if Tooltip class is imported
|
|
row_idx += 1
|
|
|
|
# Local Parent Directory Row
|
|
ttk.Label(main_frame, text="Clone into Directory:").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.dir_entry = ttk.Entry(
|
|
main_frame, textvariable=self.local_parent_dir_var, width=60
|
|
)
|
|
self.dir_entry.grid(row=row_idx, column=1, padx=5, pady=5, sticky="ew")
|
|
self.browse_button = ttk.Button(
|
|
main_frame, text="Browse...", width=9, command=self._browse_local_dir
|
|
)
|
|
self.browse_button.grid(row=row_idx, column=2, padx=(0, 5), pady=5, sticky="w")
|
|
row_idx += 1
|
|
|
|
# Explanatory Label Row
|
|
ttk.Label(
|
|
main_frame,
|
|
text="(A new sub-folder named after the repository will be created inside this directory)",
|
|
font=("Segoe UI", 8), # Smaller font
|
|
foreground="grey",
|
|
).grid(row=row_idx, column=1, columnspan=2, padx=5, pady=(0, 5), sticky="w")
|
|
row_idx += 1
|
|
|
|
# New Profile Name Row (Optional)
|
|
ttk.Label(main_frame, text="New Profile Name (Optional):").grid(
|
|
row=row_idx, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.profile_entry = ttk.Entry(
|
|
main_frame, textvariable=self.profile_name_var, width=60
|
|
)
|
|
self.profile_entry.grid(
|
|
row=row_idx, column=1, columnspan=2, padx=5, pady=5, sticky="ew"
|
|
)
|
|
# Consider adding Tooltip here
|
|
row_idx += 1
|
|
|
|
return self.url_entry # Initial focus
|
|
|
|
def _browse_local_dir(self) -> None:
|
|
"""Callback for the local directory browse button."""
|
|
current_path: str = self.local_parent_dir_var.get()
|
|
initial_dir: str = (
|
|
current_path if os.path.isdir(current_path) else os.path.expanduser("~")
|
|
)
|
|
directory: Optional[str] = filedialog.askdirectory(
|
|
initialdir=initial_dir,
|
|
title="Select Parent Directory for Clone",
|
|
parent=self, # Modal to this dialog
|
|
)
|
|
if directory:
|
|
self.local_parent_dir_var.set(directory)
|
|
|
|
# Override
|
|
def validate(self) -> bool:
|
|
"""Validates the input fields before closing."""
|
|
url: str = self.remote_url_var.get().strip()
|
|
parent_dir: str = self.local_parent_dir_var.get().strip()
|
|
profile_name: str = self.profile_name_var.get().strip()
|
|
|
|
if not url:
|
|
messagebox.showwarning(
|
|
"Input Error", "Remote Repository URL cannot be empty.", parent=self
|
|
)
|
|
self.url_entry.focus_set()
|
|
return False
|
|
|
|
# Basic URL check (not exhaustive)
|
|
if not (
|
|
url.startswith("http://")
|
|
or url.startswith("https://")
|
|
or url.startswith("ssh://")
|
|
or ("@" in url and ":" in url) # Heuristic for git@server:path
|
|
):
|
|
if not messagebox.askokcancel(
|
|
"URL Format Warning",
|
|
f"The URL '{url}' does not look like a standard HTTPS, HTTP, or SSH URL.\n\nProceed anyway?",
|
|
icon=messagebox.WARNING, # Use constant
|
|
parent=self,
|
|
):
|
|
self.url_entry.focus_set()
|
|
return False
|
|
|
|
if not parent_dir:
|
|
messagebox.showwarning(
|
|
"Input Error", "Parent Local Directory cannot be empty.", parent=self
|
|
)
|
|
self.dir_entry.focus_set()
|
|
return False
|
|
|
|
if not os.path.isdir(parent_dir):
|
|
messagebox.showwarning(
|
|
"Input Error",
|
|
f"The selected parent directory does not exist:\n{parent_dir}",
|
|
parent=self,
|
|
)
|
|
self.dir_entry.focus_set()
|
|
return False
|
|
|
|
# Optional profile name validation could be added here if needed
|
|
# e.g., check for invalid characters for profile names
|
|
|
|
return True # Validation successful
|
|
|
|
# Override
|
|
def apply(self) -> None:
|
|
"""Stores the validated result."""
|
|
# Return a tuple with the cleaned values
|
|
self.result = (
|
|
self.remote_url_var.get().strip(),
|
|
self.local_parent_dir_var.get().strip(),
|
|
self.profile_name_var.get().strip(), # Return empty string if not provided
|
|
)
|
|
|
|
|
|
class SelectSubmoduleDialog(simpledialog.Dialog):
|
|
"""Dialog to select a submodule from a list for cleaning."""
|
|
|
|
def __init__(self, parent, title: str = "Select Submodule to Clean", submodule_paths: List[str] = None):
|
|
self.submodule_paths = submodule_paths if submodule_paths else []
|
|
self.result: Optional[str] = None
|
|
self.selected_path_var = tk.StringVar()
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="both", expand=True)
|
|
|
|
ttk.Label(
|
|
frame,
|
|
text="Select the submodule with an inconsistent state to clean from the repository:"
|
|
).pack(pady=(0, 10))
|
|
|
|
if not self.submodule_paths:
|
|
ttk.Label(frame, text="No registered submodules found in .gitmodules.", foreground="red").pack()
|
|
return None # No items to select
|
|
|
|
self.combobox = ttk.Combobox(
|
|
frame,
|
|
textvariable=self.selected_path_var,
|
|
values=self.submodule_paths,
|
|
state="readonly",
|
|
width=50
|
|
)
|
|
self.combobox.pack(pady=5)
|
|
if self.submodule_paths:
|
|
self.combobox.current(0) # Pre-select the first item
|
|
|
|
return self.combobox
|
|
|
|
def validate(self) -> bool:
|
|
if not self.selected_path_var.get():
|
|
messagebox.showwarning("Selection Required", "You must select a submodule path from the list.", parent=self)
|
|
return False
|
|
return True
|
|
|
|
def apply(self) -> None:
|
|
self.result = self.selected_path_var.get()
|
|
|
|
class AddSubmoduleDialog(simpledialog.Dialog):
|
|
"""Dialog to get a new submodule's URL and local path."""
|
|
|
|
def __init__(self, parent, title: str = "Add New Submodule"):
|
|
self.url_var = tk.StringVar()
|
|
self.path_var = tk.StringVar()
|
|
self.result: Optional[Tuple[str, str]] = None
|
|
super().__init__(parent, title=title)
|
|
|
|
def body(self, master: tk.Frame) -> Optional[tk.Widget]:
|
|
"""Creates the dialog body."""
|
|
frame = ttk.Frame(master, padding="10")
|
|
frame.pack(fill="x", expand=True)
|
|
frame.columnconfigure(1, weight=1)
|
|
|
|
# Repository URL Row
|
|
ttk.Label(frame, text="Repository URL:").grid(
|
|
row=0, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.url_entry = ttk.Entry(frame, textvariable=self.url_var, width=60) #<-- Larghezza aumentata
|
|
self.url_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
# Local Path Row
|
|
ttk.Label(frame, text="Local Path:").grid(
|
|
row=1, column=0, padx=5, pady=5, sticky="w"
|
|
)
|
|
self.path_entry = ttk.Entry(frame, textvariable=self.path_var, width=60) #<-- Larghezza aumentata
|
|
self.path_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
|
|
|
|
ttk.Label(
|
|
frame,
|
|
text="(e.g., libs/my-submodule)",
|
|
font=("Segoe UI", 8),
|
|
foreground="grey"
|
|
).grid(row=2, column=1, padx=5, sticky="w")
|
|
|
|
|
|
return self.url_entry # Initial focus
|
|
|
|
def validate(self) -> bool:
|
|
"""Validates the input."""
|
|
url = self.url_var.get().strip()
|
|
path = self.path_var.get().strip()
|
|
|
|
if not url:
|
|
messagebox.showwarning("Input Error", "Repository URL cannot be empty.", parent=self)
|
|
self.url_entry.focus_set()
|
|
return False
|
|
|
|
if not path:
|
|
messagebox.showwarning("Input Error", "Local Path cannot be empty.", parent=self)
|
|
self.path_entry.focus_set()
|
|
return False
|
|
|
|
# Basic check for invalid path characters
|
|
if any(char in path for char in ['<', '>', ':', '"', '|', '?', '*']):
|
|
messagebox.showwarning("Input Error", f"Local path '{path}' contains invalid characters.", parent=self)
|
|
self.path_entry.focus_set()
|
|
return False
|
|
|
|
return True
|
|
|
|
def apply(self) -> None:
|
|
"""Stores the validated result."""
|
|
self.result = (
|
|
self.url_var.get().strip(),
|
|
self.path_var.get().strip().replace("\\", "/") # Normalize path separators
|
|
)
|
|
|
|
# --- END OF FILE gitsync_tool/gui/dialogs.py ---
|