""" Gitea Repository Explorer for the sidebar """ import customtkinter as ctk from tkinter import ttk, messagebox import threading import logging import os from pathlib import Path from typing import Optional, Callable, List, Dict from gui.styles import COLORS, FONTS from src.gitea.repository_manager import RepositoryManager from src.gitea.gitea_client import GiteaClient logger = logging.getLogger(__name__) class GiteaExplorer(ctk.CTkFrame): def __init__(self, parent, on_repo_select: Optional[Callable] = None, **kwargs): super().__init__(parent, fg_color=COLORS['bg_secondary'], **kwargs) self.on_repo_select = on_repo_select self.repo_manager = RepositoryManager() self.repositories = [] self.selected_repo = None self.view_mode = "organization" # Always organization mode self.organization_name = "IntelSight" # Fixed to IntelSight self.organizations = [] # List of user's organizations self.main_window = None # Will be set by main window self.setup_ui() # Skip load_organizations - we always use IntelSight # Automatisches Laden der Repositories beim Start nach kurzer Verzögerung self.after(500, self.refresh_repositories) def _show_gitea_menu(self, repo: Dict, button: ctk.CTkButton): """Show Gitea operations menu for repository""" import tkinter as tk # Create menu menu = tk.Menu(self, tearoff=0) menu.configure( bg=COLORS['bg_secondary'], fg=COLORS['text_primary'], activebackground=COLORS['bg_tile_hover'], activeforeground=COLORS['text_primary'], borderwidth=0, relief="flat" ) # Check if repository is already cloned local_path = Path.home() / "GiteaRepos" / repo['name'] is_cloned = local_path.exists() if is_cloned: # Already cloned options menu.add_command(label="📂 Im Explorer öffnen", command=lambda: self._open_in_explorer(repo)) menu.add_command(label="📊 Git Status", command=lambda: self._git_status(repo)) menu.add_separator() menu.add_command(label="⬇️ Pull (Aktualisieren)", command=lambda: self._git_pull(repo)) menu.add_command(label="🔄 Fetch", command=lambda: self._git_fetch(repo)) menu.add_separator() menu.add_command(label="🗑️ Lokale Kopie löschen", command=lambda: self._delete_local_copy(repo)) else: # Not cloned options menu.add_command(label="📥 Repository klonen", command=lambda: self._clone_repository(repo)) menu.add_separator() menu.add_command(label="ℹ️ Repository Info", command=lambda: self._show_repo_info(repo)) menu.add_command(label="🌐 In Browser öffnen", command=lambda: self._open_in_browser(repo)) # Show menu at button position try: x = button.winfo_rootx() y = button.winfo_rooty() + button.winfo_height() menu.tk_popup(x, y) finally: menu.grab_release() def setup_ui(self): """Setup the explorer UI""" # Header header_frame = ctk.CTkFrame(self, fg_color="transparent") header_frame.pack(fill="x", padx=10, pady=(10, 5)) # Title self.title_label = ctk.CTkLabel( header_frame, text="🔧 Gitea Repositories", font=FONTS['subtitle'], text_color=COLORS['text_primary'] ) self.title_label.pack(side="left") # Refresh button refresh_btn = ctk.CTkButton( header_frame, text="🔄", command=self.refresh_repositories, width=30, height=30, fg_color=COLORS['accent_primary'], hover_color=COLORS['accent_hover'] ) refresh_btn.pack(side="right") # IntelSight label instead of toggle org_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary']) org_frame.pack(fill="x", padx=10, pady=5) org_label = ctk.CTkLabel( org_frame, text="🏢 IntelSight Organization", font=FONTS['body'], text_color=COLORS['accent_primary'] ) org_label.pack() # Search box self.search_var = ctk.StringVar() self.search_var.trace('w', self._on_search_changed) search_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary']) search_frame.pack(fill="x", padx=10, pady=5) self.search_entry = ctk.CTkEntry( search_frame, placeholder_text="Repository suchen...", textvariable=self.search_var, fg_color=COLORS['bg_primary'], text_color=COLORS['text_primary'] ) self.search_entry.pack(fill="x") # Repository list with scrollbar list_frame = ctk.CTkFrame(self, fg_color=COLORS['bg_secondary']) list_frame.pack(fill="both", expand=True, padx=10, pady=(5, 10)) # Scrollable frame for repositories self.scroll_frame = ctk.CTkScrollableFrame( list_frame, fg_color=COLORS['bg_primary'], corner_radius=6 ) self.scroll_frame.pack(fill="both", expand=True) # Repository items container self.repo_container = ctk.CTkFrame(self.scroll_frame, fg_color=COLORS['bg_primary']) self.repo_container.pack(fill="both", expand=True) # Status label self.status_label = ctk.CTkLabel( self, text="Lade Repositories...", font=FONTS['small'], text_color=COLORS['text_secondary'] ) self.status_label.pack(pady=5) def refresh_repositories(self): """Refresh repository list from Gitea - only IntelSight""" self.status_label.configure(text="Lade IntelSight Repositories...") def fetch_repos(): try: # Always fetch IntelSight organization repositories all_repos = [] page = 1 while True: # Fetch page by page to ensure we get all repositories repos = self.repo_manager.list_organization_repositories("IntelSight", page=page, per_page=50) if not repos: break all_repos.extend(repos) page += 1 # Break if we got less than the requested amount (last page) if len(repos) < 50: break self.repositories = all_repos logger.info(f"Fetched {len(self.repositories)} repositories from IntelSight") self.after(0, self._update_repository_list) self.after(0, lambda: self.status_label.configure( text=f"{len(self.repositories)} IntelSight Repositories" )) except Exception as e: logger.error(f"Error fetching repositories: {str(e)}") self.after(0, lambda: self.status_label.configure( text=f"Fehler: {str(e)}", text_color=COLORS['accent_error'] )) threading.Thread(target=fetch_repos, daemon=True).start() def _update_repository_list(self): """Update the displayed repository list""" # Filter repositories based on search and remove duplicates search_term = self.search_var.get().lower() # Use a dict to track unique repos by name (preferring non-forks) unique_repos = {} for repo in self.repositories: repo_name = repo['name'] # If we haven't seen this repo, or this one is not a fork (prefer originals) if repo_name not in unique_repos or not repo.get('fork', False): unique_repos[repo_name] = repo # Filter based on search filtered_repos = [ repo for repo in unique_repos.values() if search_term in repo['name'].lower() or search_term in repo.get('description', '').lower() ] # Sort by name filtered_repos.sort(key=lambda r: r['name'].lower()) # Get existing repo widgets existing_widgets = {} for widget in self.repo_container.winfo_children(): if hasattr(widget, 'repo_data'): existing_widgets[widget.repo_data['name']] = widget # Remove widgets for repos no longer in filtered list filtered_names = {repo['name'] for repo in filtered_repos} for name, widget in existing_widgets.items(): if name not in filtered_names: widget.destroy() # Update or create repository items for i, repo in enumerate(filtered_repos): if repo['name'] in existing_widgets: # Update existing widget widget = existing_widgets[repo['name']] self._update_repo_item(widget, repo) # Ensure correct order widget.pack_forget() widget.pack(fill="x", pady=2) else: # Create new widget self._create_repo_item(repo) def _create_repo_item(self, repo: Dict): """Create a repository item widget""" # Main frame item_frame = ctk.CTkFrame( self.repo_container, fg_color=COLORS['bg_gitea_tile'], corner_radius=6, height=70 ) item_frame.pack(fill="x", pady=2) item_frame.pack_propagate(False) # Store repo data item_frame.repo_data = repo repo['_widget'] = item_frame # Content frame content_frame = ctk.CTkFrame(item_frame, fg_color="transparent") content_frame.pack(fill="both", expand=True, padx=10, pady=8) # Top row - name and status top_row = ctk.CTkFrame(content_frame, fg_color="transparent") top_row.pack(fill="x") # Repository name name_label = ctk.CTkLabel( top_row, text=repo['name'], font=FONTS['body'], text_color=COLORS['text_primary'], anchor="w" ) name_label.pack(side="left", fill="x", expand=True) # Clone/Status indicator local_path = Path.home() / "GiteaRepos" / repo['name'] if local_path.exists(): status_label = ctk.CTkLabel( top_row, text="✓ Cloned", font=FONTS['small'], text_color=COLORS['accent_success'] ) status_label.pack(side="right", padx=(5, 0)) # Check if it's a CPM project if self.main_window and hasattr(self.main_window, 'project_manager'): for project_data in self.main_window.project_manager.projects: # project_data is a Project object, check if it has gitea_repo attribute if hasattr(project_data, 'gitea_repo') and project_data.gitea_repo == f"{self.organization_name}/{repo['name']}": cpm_label = ctk.CTkLabel( top_row, text="📁 CPM", font=FONTS['small'], text_color=COLORS['accent_primary'] ) cpm_label.pack(side="right", padx=(5, 0)) break # Description if repo.get('description'): desc_label = ctk.CTkLabel( content_frame, text=repo['description'][:60] + '...' if len(repo.get('description', '')) > 60 else repo['description'], font=FONTS['small'], text_color=COLORS['text_secondary'], anchor="w" ) desc_label.pack(fill="x", pady=(2, 0)) # Bottom row - stats stats_row = ctk.CTkFrame(content_frame, fg_color="transparent") stats_row.pack(fill="x", pady=(4, 0)) # Private/Public visibility = "🔒 Private" if repo.get('private', False) else "🌍 Public" vis_label = ctk.CTkLabel( stats_row, text=visibility, font=FONTS['small'], text_color=COLORS['text_dim'] ) vis_label.pack(side="left", padx=(0, 10)) # Stars if available if 'stars_count' in repo: star_label = ctk.CTkLabel( stats_row, text=f"⭐ {repo['stars_count']}", font=FONTS['small'], text_color=COLORS['text_dim'] ) star_label.pack(side="left", padx=(0, 10)) # Menu button menu_btn = ctk.CTkButton( stats_row, text="⋮", width=30, height=24, fg_color=COLORS['bg_tile'], hover_color=COLORS['bg_tile_hover'], font=('Segoe UI', 16) ) menu_btn.configure(command=lambda: self._show_gitea_menu(repo, menu_btn)) menu_btn.pack(side="right") # Click to select def on_click(event): self._select_repository(repo) self._bind_click_recursive(item_frame, on_click) # Hover effect def on_enter(e): if self.selected_repo != repo: item_frame.configure(fg_color=COLORS['bg_gitea_hover']) def on_leave(e): if self.selected_repo != repo: item_frame.configure(fg_color=COLORS['bg_gitea_tile']) item_frame.bind("", on_enter) item_frame.bind("", on_leave) def _update_repo_item(self, item_frame: ctk.CTkFrame, repo: Dict): """Update an existing repository item widget""" # Update stored data item_frame.repo_data = repo repo['_widget'] = item_frame # Update clone status local_path = Path.home() / "GiteaRepos" / repo['name'] # Find status label in widget hierarchy for widget in item_frame.winfo_children(): if isinstance(widget, ctk.CTkFrame): for child in widget.winfo_children(): if isinstance(child, ctk.CTkFrame): for subchild in child.winfo_children(): if isinstance(subchild, ctk.CTkLabel) and "✓ Cloned" in subchild.cget("text"): if not local_path.exists(): subchild.destroy() break def _select_repository(self, repo: Dict): """Select a repository""" # Deselect previous if self.selected_repo and '_widget' in self.selected_repo: self.selected_repo['_widget'].configure( fg_color=COLORS['bg_gitea_tile'] ) # Select new self.selected_repo = repo if '_widget' in repo: repo['_widget'].configure( fg_color=COLORS['bg_selected'] ) # Notify callback if self.on_repo_select: self.on_repo_select(repo) def _on_search_changed(self, *args): """Handle search text change""" self._update_repository_list() def update_theme(self): """Update colors when theme changes""" # Import updated colors from gui.styles import get_colors, FONTS global COLORS COLORS = get_colors() # Update main frame self.configure(fg_color=COLORS['bg_secondary']) # Update labels self.title_label.configure(text_color=COLORS['text_primary']) # Update search entry self.search_entry.configure( fg_color=COLORS['bg_primary'], text_color=COLORS['text_primary'], border_color=COLORS['border_primary'] ) # Update scroll frame and container self.scroll_frame.configure(fg_color=COLORS['bg_primary']) self.repo_container.configure(fg_color=COLORS['bg_primary']) # Update status label self.status_label.configure(text_color=COLORS['text_secondary']) # Refresh the repository list to update item colors self._update_repository_list() # Clear any previous selection when theme changes if self.selected_repo and '_widget' in self.selected_repo: self.selected_repo['_widget'].configure(fg_color=COLORS['bg_gitea_tile']) self.selected_repo = None def _clone_repository(self, repo: Dict): """Clone repository and create CPM project""" if self.main_window and hasattr(self.main_window, 'clone_repository'): self.main_window.clone_repository(repo) def _open_in_explorer(self, repo: Dict): """Open repository folder in file explorer""" import platform import subprocess local_path = Path.home() / "GiteaRepos" / repo['name'] if local_path.exists(): if platform.system() == 'Windows': os.startfile(str(local_path)) elif platform.system() == 'Darwin': subprocess.Popen(['open', str(local_path)]) else: subprocess.Popen(['xdg-open', str(local_path)]) def _git_status(self, repo: Dict): """Show git status for repository""" if self.main_window and hasattr(self.main_window, 'show_git_status'): # Create a pseudo-project object for compatibility from types import SimpleNamespace project = SimpleNamespace() project.name = repo['name'] project.path = str(Path.home() / "GiteaRepos" / repo['name']) self.main_window.show_git_status(project) def _git_pull(self, repo: Dict): """Pull changes from remote""" if self.main_window and hasattr(self.main_window, 'pull_from_gitea'): from types import SimpleNamespace project = SimpleNamespace() project.name = repo['name'] project.path = str(Path.home() / "GiteaRepos" / repo['name']) self.main_window.pull_from_gitea(project) def _git_fetch(self, repo: Dict): """Fetch changes from remote""" if self.main_window and hasattr(self.main_window, 'fetch_from_gitea'): self.main_window.fetch_from_gitea(repo) def _delete_local_copy(self, repo: Dict): """Delete local repository copy""" from tkinter import messagebox local_path = Path.home() / "GiteaRepos" / repo['name'] if messagebox.askyesno("Lokale Kopie löschen", f"Möchten Sie die lokale Kopie von '{repo['name']}' wirklich löschen?\n\n" f"Pfad: {local_path}"): try: import shutil shutil.rmtree(local_path) messagebox.showinfo("Erfolg", f"Lokale Kopie von '{repo['name']}' wurde gelöscht.") self.refresh_repositories() except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Löschen: {str(e)}") def _show_repo_info(self, repo: Dict): """Show repository information""" from tkinter import messagebox info = f"Repository: {repo['name']}\n" info += f"Beschreibung: {repo.get('description', 'Keine Beschreibung')}\n" info += f"Privat: {'Ja' if repo.get('private', False) else 'Nein'}\n" info += f"Erstellt: {repo.get('created_at', 'Unbekannt')}\n" info += f"Aktualisiert: {repo.get('updated_at', 'Unbekannt')}\n" messagebox.showinfo("Repository Info", info) def _open_in_browser(self, repo: Dict): """Open repository in browser""" import webbrowser if 'html_url' in repo: webbrowser.open(repo['html_url']) def _bind_click_recursive(self, widget, callback): """Recursively bind click event to widget and all children""" # Don't bind to buttons to avoid interfering with their functionality if not isinstance(widget, ctk.CTkButton): widget.bind("", callback) for child in widget.winfo_children(): # Skip buttons if not isinstance(child, ctk.CTkButton): self._bind_click_recursive(child, callback) def clear_selection(self): """Clear the current selection""" if self.selected_repo and '_widget' in self.selected_repo: # Remove border from selection self.selected_repo['_widget'].configure(border_width=0) self.selected_repo = None