SXXXXXXX_GitUtility/gitutility/gui/tabs/branch_tab.py

256 lines
9.5 KiB
Python

# --- FILE: gitsync_tool/gui/tabs/branch_tab.py ---
import tkinter as tk
from tkinter import ttk
from typing import Callable, List, Optional
from gitutility.gui.tooltip import Tooltip
from gitutility.logging_setup import log_handler
class BranchTab(ttk.Frame):
"""
The 'Branches (Local Ops)' tab in the main application notebook.
This tab provides widgets for listing, creating, checking out, and managing
local Git branches.
"""
def __init__(self, master: tk.Misc, **kwargs):
"""
Initializes the Branches 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_branches_callback = kwargs.get("refresh_branches_cb")
self.create_branch_callback = kwargs.get("create_branch_cb")
self.checkout_branch_callback = kwargs.get("checkout_branch_cb")
self.compare_branch_with_current_callback = kwargs.get(
"compare_branch_with_current_cb"
)
self.delete_local_branch_callback = kwargs.get("delete_local_branch_cb")
self.merge_local_branch_callback = kwargs.get("merge_local_branch_cb")
self.promote_to_main_callback = kwargs.get("promote_to_main_cb")
# Get a reference to the main frame for shared components
self.main_frame = self.master.master
# Configure layout
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
# Create widgets
self._create_widgets()
def _create_widgets(self) -> None:
"""Creates and arranges all widgets for this tab."""
ttk.Label(self, text="Local Branches (* = Current):").grid(
row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2)
)
# --- Listbox Frame ---
list_frame = ttk.Frame(self)
list_frame.grid(row=1, column=0, sticky="nsew", padx=(5, 0), pady=(0, 5))
list_frame.rowconfigure(0, weight=1)
list_frame.columnconfigure(0, weight=1)
self.branch_listbox = tk.Listbox(
list_frame,
height=10,
exportselection=False,
selectmode=tk.SINGLE,
font=("Segoe UI", 9),
borderwidth=1,
relief=tk.SUNKEN,
)
self.branch_listbox.grid(row=0, column=0, sticky="nsew")
self.branch_listbox.bind("<Button-3>", self._show_local_branches_context_menu)
scrollbar = ttk.Scrollbar(
list_frame, orient=tk.VERTICAL, command=self.branch_listbox.yview
)
scrollbar.grid(row=0, column=1, sticky="ns")
self.branch_listbox.config(yscrollcommand=scrollbar.set)
Tooltip(
self.branch_listbox,
"List of local branches. Right-click for actions (merge, delete, compare).",
)
# --- Button Frame ---
button_frame = ttk.Frame(self)
button_frame.grid(row=1, column=1, sticky="ns", padx=(10, 5), pady=(0, 5))
button_width = 20
self.refresh_branches_button = ttk.Button(
button_frame,
text="Refresh Branches",
width=button_width,
command=self.refresh_branches_callback,
state=tk.DISABLED,
)
self.refresh_branches_button.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
Tooltip(self.refresh_branches_button, "Reload the list of local branches.")
self.create_branch_button = ttk.Button(
button_frame,
text="Create New Branch...",
width=button_width,
command=self.create_branch_callback,
state=tk.DISABLED,
)
self.create_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5)
Tooltip(
self.create_branch_button,
"Create a new local branch starting from the current commit.",
)
self.checkout_branch_button = ttk.Button(
button_frame,
text="Checkout Selected Branch",
width=button_width,
command=lambda: self.checkout_branch_callback(None),
state=tk.DISABLED,
)
self.checkout_branch_button.pack(side=tk.TOP, fill=tk.X, pady=5)
Tooltip(
self.checkout_branch_button,
"Switch the working directory to the selected local branch.",
)
# Promote to main button
self.promote_to_main_button = ttk.Button(
button_frame,
text="Promote Selected to main",
width=button_width,
command=lambda: (
self.promote_to_main_callback(self.get_selected_branch())
if callable(self.promote_to_main_callback)
else None
),
state=tk.DISABLED,
)
self.promote_to_main_button.pack(side=tk.TOP, fill=tk.X, pady=5)
Tooltip(
self.promote_to_main_button,
"Force the selected branch into 'main' (creates backup and can push).",
)
def set_action_widgets_state(self, state: str) -> None:
"""Sets the state of all action widgets in this tab."""
list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED
widgets = [
self.refresh_branches_button,
self.create_branch_button,
self.checkout_branch_button,
self.promote_to_main_button,
self.branch_listbox,
]
for widget in widgets:
if widget and widget.winfo_exists():
widget.config(state=list_state)
def update_branch_list(self, branches: List[str], current_branch: Optional[str]):
"""Populates the listbox with the local branches."""
if not self.branch_listbox.winfo_exists():
return
self.branch_listbox.config(state=tk.NORMAL)
self.branch_listbox.delete(0, tk.END)
selected_index = -1
if branches:
self.branch_listbox.config(fg="black")
for i, branch in enumerate(branches):
prefix = "* " if branch == current_branch else " "
self.branch_listbox.insert(tk.END, f"{prefix}{branch}")
if branch == current_branch:
selected_index = i
else:
self.branch_listbox.insert(tk.END, "(No local branches found)")
self.branch_listbox.config(fg="grey")
if selected_index != -1:
self.branch_listbox.selection_set(selected_index)
self.branch_listbox.see(selected_index)
self.branch_listbox.yview_moveto(0.0)
def get_selected_branch(self) -> Optional[str]:
"""Returns the name of the currently selected local branch, or None."""
selection = self.branch_listbox.curselection()
if not selection:
return None
item_text = self.branch_listbox.get(selection[0])
branch_name = item_text.lstrip("* ").strip()
return branch_name if not branch_name.startswith("(") else None
def _show_local_branches_context_menu(self, event: tk.Event) -> None:
"""Shows a context menu for the selected local branch."""
try:
idx = self.branch_listbox.nearest(event.y)
if idx < 0:
return
self.branch_listbox.selection_clear(0, tk.END)
self.branch_listbox.selection_set(idx)
self.branch_listbox.activate(idx)
selected_item_text = self.branch_listbox.get(idx).strip()
except tk.TclError:
return
menu = self.main_frame.local_branch_context_menu
menu.delete(0, tk.END)
is_current = selected_item_text.startswith("*")
branch_name = selected_item_text.lstrip("* ").strip()
current_branch = self.main_frame.current_local_branch
is_valid = not branch_name.startswith("(")
if is_valid:
menu.add_command(
label=f"Merge '{branch_name}' into '{current_branch}'...",
state=tk.DISABLED if is_current or not current_branch else tk.NORMAL,
command=lambda b=branch_name: self.merge_local_branch_callback(b),
)
menu.add_command(
label=f"Compare with current '{current_branch}'",
state=tk.DISABLED if is_current or not current_branch else tk.NORMAL,
command=lambda b=branch_name: self.compare_branch_with_current_callback(
b
),
)
menu.add_separator()
menu.add_command(
label=f"Delete Branch '{branch_name}'...",
state=tk.DISABLED if is_current else tk.NORMAL,
command=lambda b=branch_name: self.delete_local_branch_callback(
b, False
),
)
menu.add_command(
label=f"Force Delete Branch '{branch_name}'...",
state=tk.DISABLED if is_current else tk.NORMAL,
command=lambda b=branch_name: self.delete_local_branch_callback(
b, True
),
)
# Promote to main (disabled if target is main or if it's the current branch)
menu.add_separator()
menu.add_command(
label=f"Promote '{branch_name}' to 'main'...",
state=tk.DISABLED if is_current or branch_name == "main" else tk.NORMAL,
command=lambda b=branch_name: self.promote_to_main_callback(b),
)
menu.add_separator()
menu.add_command(label="Cancel")
menu.tk_popup(event.x_root, event.y_root)