536 Zeilen
21 KiB
Python
536 Zeilen
21 KiB
Python
"""
|
||
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("<Enter>", on_enter)
|
||
item_frame.bind("<Leave>", 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("<Button-1>", 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 |