# --- FILE: gitsync_tool/gui/tabs/remote_tab.py --- import tkinter as tk from tkinter import ttk from typing import Callable, Optional, List, Any from gitutility.gui.tooltip import Tooltip from gitutility.logging_setup import log_handler class RemoteTab(ttk.Frame): """ The 'Remote Repository' tab in the main application notebook. This tab provides widgets for configuring the remote, performing sync actions (fetch, pull, push), and managing remote/local branches. """ def __init__(self, master: tk.Misc, **kwargs): """Initializes the Remote Repository tab.""" super().__init__(master, padding=(10, 10)) # Store callbacks from kwargs self.apply_remote_config_callback = kwargs.get("apply_remote_config_cb") self.check_connection_auth_callback = kwargs.get("check_connection_auth_cb") self.refresh_remote_status_callback = kwargs.get("refresh_remote_status_cb") self.fetch_remote_callback = kwargs.get("fetch_remote_cb") self.pull_remote_callback = kwargs.get("pull_remote_cb") self.push_remote_callback = kwargs.get("push_remote_cb") self.push_tags_remote_callback = kwargs.get("push_tags_remote_cb") self.refresh_remote_branches_callback = kwargs.get("refresh_remote_branches_cb") self.refresh_local_branches_callback = kwargs.get("refresh_branches_cb") self.checkout_remote_branch_callback = kwargs.get("checkout_remote_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.checkout_local_branch_callback = kwargs.get("checkout_branch_cb") self.merge_local_branch_callback = kwargs.get("merge_local_branch_cb") # Get a reference to the main frame for shared components like menus self.main_frame = self.master.master # --- Tkinter Variables specific to this tab --- self.remote_url_var = tk.StringVar() self.remote_name_var = tk.StringVar() self.remote_auth_status_var = tk.StringVar(value="Status: Unknown") self.remote_ahead_behind_var = tk.StringVar(value="Sync Status: Unknown") # Configure layout self.columnconfigure(0, weight=1) self.columnconfigure(1, weight=1) self.rowconfigure(3, weight=1) # Create widgets self._create_widgets() def _create_widgets(self) -> None: """Creates and arranges all widgets for this tab.""" # --- Top Frame: Remote Config & Sync Status --- top_frame = ttk.LabelFrame( self, text="Remote Configuration & Sync Status", padding=(10, 5) ) top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(5, 0)) top_frame.columnconfigure(1, weight=1) ttk.Label(top_frame, text="Remote URL:").grid( row=0, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_url_entry = ttk.Entry( top_frame, textvariable=self.remote_url_var, width=60 ) self.remote_url_entry.grid(row=0, column=1, sticky=tk.EW, padx=5, pady=3) Tooltip( self.remote_url_entry, "URL of the remote repository (e.g., https://... or ssh://...).", ) ttk.Label(top_frame, text="Local Name:").grid( row=1, column=0, sticky=tk.W, padx=5, pady=3 ) self.remote_name_entry = ttk.Entry( top_frame, textvariable=self.remote_name_var, width=20 ) self.remote_name_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=3) Tooltip( self.remote_name_entry, "Local alias for the remote (Default: 'origin')." ) self.sync_status_label = ttk.Label( top_frame, textvariable=self.remote_ahead_behind_var, anchor=tk.W, padding=(5, 2), ) self.sync_status_label.grid(row=2, column=1, sticky="ew", padx=5, pady=(2, 5)) Tooltip( self.sync_status_label, "Shows the current local branch and its sync status (ahead/behind) relative to its upstream.", ) config_action_frame = ttk.Frame(top_frame) config_action_frame.grid( row=0, column=2, rowspan=3, sticky="ne", padx=(15, 5), pady=3 ) self.apply_remote_config_button = ttk.Button( config_action_frame, text="Apply Config", command=self.apply_remote_config_callback, state=tk.DISABLED, width=18, ) self.apply_remote_config_button.pack(side=tk.TOP, fill=tk.X, pady=1) Tooltip( self.apply_remote_config_button, "Add or update this remote configuration in the local .git/config file.", ) self.check_auth_button = ttk.Button( config_action_frame, text="Check Connection", command=self.check_connection_auth_callback, state=tk.DISABLED, width=18, ) self.check_auth_button.pack(side=tk.TOP, fill=tk.X, pady=1) Tooltip( self.check_auth_button, "Verify connection and authentication status for the configured remote.", ) self.auth_status_indicator_label = ttk.Label( config_action_frame, textvariable=self.remote_auth_status_var, anchor=tk.CENTER, relief=tk.SUNKEN, padding=(5, 1), width=18, ) self.auth_status_indicator_label.pack(side=tk.TOP, fill=tk.X, pady=(1, 3)) self.update_auth_status_indicator("unknown") Tooltip( self.auth_status_indicator_label, "Connection and authentication status (Unknown, Checking, Connected, Auth Required, Failed, Error).", ) self.refresh_sync_status_button = ttk.Button( config_action_frame, text="Refresh Sync Status", command=self.refresh_remote_status_callback, state=tk.DISABLED, width=18, ) self.refresh_sync_status_button.pack(side=tk.TOP, fill=tk.X, pady=(3, 1)) Tooltip( self.refresh_sync_status_button, "Check how many commits the current local branch is ahead or behind its upstream remote branch.", ) # --- Middle Frame: Common Remote Actions --- actions_frame = ttk.LabelFrame( self, text="Common Remote Actions", padding=(10, 5) ) actions_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=5) action_buttons_inner_frame = ttk.Frame(actions_frame) action_buttons_inner_frame.pack() self.fetch_button = ttk.Button( action_buttons_inner_frame, text="Fetch", command=self.fetch_remote_callback, state=tk.DISABLED, ) self.fetch_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip( self.fetch_button, "Download objects and references from the remote repository (does not modify local branches).", ) self.pull_button = ttk.Button( action_buttons_inner_frame, text="Pull", command=self.pull_remote_callback, state=tk.DISABLED, ) self.pull_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip( self.pull_button, "Fetch changes from the remote and merge them into the current local branch.", ) self.push_button = ttk.Button( action_buttons_inner_frame, text="Push", command=self.push_remote_callback, state=tk.DISABLED, ) self.push_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip( self.push_button, "Upload local commits from the current branch to the corresponding branch on the remote repository.", ) self.push_tags_button = ttk.Button( action_buttons_inner_frame, text="Push Tags", command=self.push_tags_remote_callback, state=tk.DISABLED, ) self.push_tags_button.pack(side=tk.LEFT, padx=5, pady=5) Tooltip( self.push_tags_button, "Upload all local tags to the remote repository." ) # --- Bottom Frame: Branch Lists --- branch_view_frame = ttk.Frame(self) branch_view_frame.grid( row=3, column=0, columnspan=2, sticky="nsew", padx=0, pady=(5, 5) ) branch_view_frame.rowconfigure(0, weight=1) branch_view_frame.columnconfigure(0, weight=1) branch_view_frame.columnconfigure(1, weight=1) # Remote Branches List remote_list_frame = ttk.Frame(branch_view_frame, padding=(5, 0, 10, 0)) remote_list_frame.grid(row=0, column=0, sticky="nsew") remote_list_frame.rowconfigure(1, weight=1) remote_list_frame.columnconfigure(0, weight=1) ttk.Label(remote_list_frame, text="Remote Branches (from last Fetch):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) self.remote_branches_listbox = tk.Listbox( remote_list_frame, height=8, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), borderwidth=1, relief=tk.SUNKEN, state=tk.DISABLED, ) self.remote_branches_listbox.grid(row=1, column=0, sticky="nsew") self.remote_branches_listbox.bind( "", self._show_remote_branches_context_menu ) rb_scrollbar = ttk.Scrollbar( remote_list_frame, orient=tk.VERTICAL, command=self.remote_branches_listbox.yview, ) rb_scrollbar.grid(row=1, column=1, sticky="ns") self.remote_branches_listbox.config(yscrollcommand=rb_scrollbar.set) Tooltip( self.remote_branches_listbox, "Branches existing on the remote repository. Right-click for actions (Compare, Checkout as local).", ) self.refresh_remote_branches_button = ttk.Button( remote_list_frame, text="Refresh Remote List", command=self.refresh_remote_branches_callback, state=tk.DISABLED, ) self.refresh_remote_branches_button.grid( row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0) ) Tooltip( self.refresh_remote_branches_button, "Update the list of remote branches (requires fetching from the remote).", ) # Local Branches List local_list_frame = ttk.Frame(branch_view_frame, padding=(10, 0, 5, 0)) local_list_frame.grid(row=0, column=1, sticky="nsew") local_list_frame.rowconfigure(1, weight=1) local_list_frame.columnconfigure(0, weight=1) ttk.Label(local_list_frame, text="Local Branches (* = Current):").grid( row=0, column=0, columnspan=2, sticky="w", padx=5, pady=(0, 2) ) self.local_branches_listbox_remote_tab = tk.Listbox( local_list_frame, height=8, exportselection=False, selectmode=tk.SINGLE, font=("Consolas", 9), borderwidth=1, relief=tk.SUNKEN, state=tk.DISABLED, ) self.local_branches_listbox_remote_tab.grid(row=1, column=0, sticky="nsew") self.local_branches_listbox_remote_tab.bind( "", self._show_local_branches_context_menu ) lb_scrollbar_remote_tab = ttk.Scrollbar( local_list_frame, orient=tk.VERTICAL, command=self.local_branches_listbox_remote_tab.yview, ) lb_scrollbar_remote_tab.grid(row=1, column=1, sticky="ns") self.local_branches_listbox_remote_tab.config( yscrollcommand=lb_scrollbar_remote_tab.set ) Tooltip( self.local_branches_listbox_remote_tab, "Your local branches. Right-click for actions (Checkout, Merge, Delete, Compare).", ) self.refresh_local_branches_button_remote_tab = ttk.Button( local_list_frame, text="Refresh Local List", command=self.refresh_local_branches_callback, state=tk.DISABLED, ) self.refresh_local_branches_button_remote_tab.grid( row=2, column=0, columnspan=2, sticky="w", padx=5, pady=(5, 0) ) Tooltip( self.refresh_local_branches_button_remote_tab, "Update the list of local branches.", ) def set_action_widgets_state(self, state: str) -> None: """Sets the state of all action widgets in this tab.""" widgets = [ self.apply_remote_config_button, self.check_auth_button, self.refresh_sync_status_button, self.fetch_button, self.pull_button, self.push_button, self.push_tags_button, self.refresh_remote_branches_button, self.refresh_local_branches_button_remote_tab, ] list_state = tk.NORMAL if state == tk.NORMAL else tk.DISABLED listboxes = [ self.remote_branches_listbox, self.local_branches_listbox_remote_tab, ] for widget in widgets: if widget and widget.winfo_exists(): widget.config(state=state) for lb in listboxes: if lb and lb.winfo_exists(): lb.config(state=list_state) def update_auth_status_indicator(self, status: str): label = self.auth_status_indicator_label if not label or not label.winfo_exists(): return text, color, tooltip_text = ( "Status: Unknown", self.main_frame.STATUS_DEFAULT_BG, "Connection status.", ) if status == "ok": text, color, tooltip_text = ( "Status: Connected", self.main_frame.STATUS_GREEN, "Successfully connected to the remote.", ) elif status == "required": text, color, tooltip_text = ( "Status: Auth Required", self.main_frame.STATUS_YELLOW, "Authentication needed. Use 'Check Connection'.", ) elif status == "failed": text, color, tooltip_text = ( "Status: Auth Failed", self.main_frame.STATUS_RED, "Authentication failed. Check credentials.", ) elif status == "connection_failed": text, color, tooltip_text = ( "Status: Connection Failed", self.main_frame.STATUS_RED, "Could not connect to the remote.", ) elif status == "checking": text, color, tooltip_text = ( "Status: Checking...", self.main_frame.STATUS_YELLOW, "Attempting to contact the remote...", ) elif status == "unknown_error": text, color, tooltip_text = ( "Status: Error", self.main_frame.STATUS_RED, "An unknown error occurred.", ) self.remote_auth_status_var.set(text) label.config(background=color) Tooltip(label, tooltip_text) def update_ahead_behind_status( self, current_branch: Optional[str] = None, status_text: Optional[str] = None, ahead: Optional[int] = None, behind: Optional[int] = None, ): if not hasattr(self, "remote_ahead_behind_var"): return branch_part = ( f"Branch '{current_branch}': " if current_branch else "Current Branch: " ) if status_text is not None: text_to_display = status_text elif ahead is not None and behind is not None: if ahead == 0 and behind == 0: status_part = "Up to date" else: parts = [] if ahead > 0: parts.append(f"{ahead} ahead (Push needed)") if behind > 0: parts.append(f"{behind} behind (Pull needed)") status_part = ", ".join(parts) text_to_display = branch_part + status_part else: text_to_display = branch_part + "Unknown Status" self.remote_ahead_behind_var.set(text_to_display) def update_remote_branches_list(self, remote_branch_list: List[str]): listbox = self.remote_branches_listbox if not listbox or not listbox.winfo_exists(): return listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) if remote_branch_list: if remote_branch_list == ["(Error)"]: listbox.insert(tk.END, "(Error retrieving list)") listbox.config(fg="red") else: listbox.config(fg="black") for branch in remote_branch_list: listbox.insert(tk.END, f" {branch}") else: listbox.insert(tk.END, "(No remote branches found)") listbox.config(fg="grey") listbox.yview_moveto(0.0) def update_local_branch_list( self, branches: List[str], current_branch: Optional[str] ): listbox = self.local_branches_listbox_remote_tab if not listbox or not listbox.winfo_exists(): return listbox.config(state=tk.NORMAL) listbox.delete(0, tk.END) selected_index = -1 if branches: listbox.config(fg="black") for i, branch in enumerate(branches): prefix = "* " if branch == current_branch else " " listbox.insert(tk.END, f"{prefix}{branch}") if branch == current_branch: selected_index = i else: listbox.insert(tk.END, "(No local branches)") listbox.config(fg="grey") if selected_index != -1: listbox.selection_set(selected_index) listbox.see(selected_index) listbox.yview_moveto(0.0) def get_selected_local_branch(self) -> Optional[str]: listbox = self.local_branches_listbox_remote_tab selection = listbox.curselection() if not selection: return None item_text = listbox.get(selection[0]).lstrip("* ").strip() return item_text if not item_text.startswith("(") else None def _show_remote_branches_context_menu(self, event: tk.Event) -> None: func_name = "_show_remote_branches_context_menu" listbox = event.widget try: idx = listbox.nearest(event.y) if idx < 0: return listbox.selection_clear(0, tk.END) listbox.selection_set(idx) listbox.activate(idx) selected_item_text = listbox.get(idx).strip() except tk.TclError: return menu = self.main_frame.remote_branch_context_menu menu.delete(0, tk.END) is_valid_branch = ( "/" in selected_item_text and not selected_item_text.startswith("(") ) if is_valid_branch: remote_branch_full_name = selected_item_text local_branch_suggestion = remote_branch_full_name.split("/", 1)[1] current_local_branch = self.main_frame.current_local_branch compare_label = ( f"Compare with current '{current_local_branch}'" if current_local_branch else "Compare..." ) menu.add_command( label=compare_label, state=tk.NORMAL if current_local_branch else tk.DISABLED, command=lambda b=remote_branch_full_name: self.compare_branch_with_current_callback( b ), ) menu.add_command( label=f"Checkout as new local branch '{local_branch_suggestion}'", command=lambda rb=remote_branch_full_name, lb=local_branch_suggestion: self.checkout_remote_branch_callback( rb, lb ), ) menu.add_separator() menu.add_command(label="Cancel") menu.tk_popup(event.x_root, event.y_root) def _show_local_branches_context_menu(self, event: tk.Event) -> None: func_name = "_show_local_branches_context_menu" listbox = event.widget try: idx = listbox.nearest(event.y) if idx < 0: return listbox.selection_clear(0, tk.END) listbox.selection_set(idx) listbox.activate(idx) selected_item_text = 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"Checkout Branch '{branch_name}'", state=tk.DISABLED if is_current else tk.NORMAL, command=lambda b=branch_name: self.checkout_local_branch_callback(b), ) 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 ), ) menu.add_separator() menu.add_command(label="Cancel") menu.tk_popup(event.x_root, event.y_root)