""" Gitea Operations Handler Handles all Git and Gitea related operations """ import os import subprocess import time from datetime import datetime from tkinter import messagebox from typing import Optional, TYPE_CHECKING from pathlib import Path from utils.logger import logger from gui.progress_bar import ProgressBar, GitOperationProgress if TYPE_CHECKING: from gui.main_window import MainWindow from project_manager import Project class GiteaOperationsHandler: """Handles all Gitea/Git related operations for MainWindow""" def __init__(self, main_window: 'MainWindow'): """Initialize with reference to main window""" self.main_window = main_window self.root = main_window.root self.repo_manager = main_window.repo_manager self.project_manager = main_window.project_manager self.process_tracker = main_window.process_tracker logger.info("GiteaOperationsHandler initialized") def init_and_push_to_gitea(self, project: 'Project') -> bool: """ Initialize git repository and push to Gitea Refactored into smaller, manageable methods """ from tkinter import simpledialog, messagebox # Step 1: Get repository name repo_name = self._get_repository_name(project) if not repo_name: return False try: # Step 2: Create repository on Gitea repo = self._create_gitea_repository(repo_name) if not repo: return False # Step 3: Verify repository creation if not self._verify_repository_creation(repo, repo_name): return False # Step 4: Initialize local git repository if not self._initialize_local_repository(project): return False # Step 5: Check for large files if not self._handle_large_files(project): return False # Step 6: Commit and push return self._commit_and_push(project, repo, repo_name) except Exception as e: logger.error(f"Failed to init and push to Gitea: {e}") messagebox.showerror("Fehler", f"Repository-Erstellung fehlgeschlagen: {str(e)}") return False def _get_repository_name(self, project: 'Project') -> Optional[str]: """Get repository name from user""" from tkinter import simpledialog repo_name = simpledialog.askstring( "Neues Repository", f"Repository-Name für '{project.name}':", initialvalue=project.name ) return repo_name def _create_gitea_repository(self, repo_name: str) -> Optional[dict]: """Create repository on Gitea""" from tkinter import messagebox # Determine organization if available org_name = None if hasattr(self.main_window, 'gitea_explorer') and self.main_window.gitea_explorer.organization_name: org_name = self.main_window.gitea_explorer.organization_name # Create repository try: if org_name and (not hasattr(self.main_window, 'gitea_explorer') or self.main_window.gitea_explorer.view_mode != "user"): logger.info(f"Creating repository '{repo_name}' in organization '{org_name}'") repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=org_name) else: logger.info(f"Creating repository '{repo_name}' as user repository") repo = self.repo_manager.create_repository(repo_name, auto_init=False, organization=None) logger.info(f"Repository created: {repo}") return repo except Exception as e: logger.error(f"Failed to create repository: {e}") messagebox.showerror("Fehler", f"Repository konnte nicht erstellt werden: {str(e)}") return None def _verify_repository_creation(self, repo: dict, repo_name: str) -> bool: """Verify repository was created correctly""" from tkinter import messagebox repo_owner = repo.get('owner', {}).get('username', 'Unknown') repo_url = repo.get('html_url', 'Unknown') # Check if repo was created in the correct place org_name = None if hasattr(self.main_window, 'gitea_explorer') and self.main_window.gitea_explorer.organization_name: org_name = self.main_window.gitea_explorer.organization_name expected_owner = org_name if org_name else self.repo_manager.client.config.username if repo_owner != expected_owner: messagebox.showwarning("Achtung", f"Repository wurde unter falschem Owner erstellt!\n\n" f"Erwartet: {expected_owner}\n" f"Erstellt unter: {repo_owner}\n\n" f"URL: {repo_url}") else: messagebox.showinfo("Repository erstellt", f"Repository '{repo_name}' wurde erstellt.\n\n" f"Owner: {repo_owner}\n" f"URL: {repo_url}") return True def _initialize_local_repository(self, project: 'Project') -> bool: """Initialize local git repository""" from tkinter import messagebox from pathlib import Path git_ops = self.repo_manager.git_ops success, msg = git_ops.init_repository(Path(project.path)) if not success: messagebox.showerror("Fehler", f"Git-Initialisierung fehlgeschlagen: {msg}") return False return True def _handle_large_files(self, project: 'Project') -> bool: """Check and handle large files""" from tkinter import messagebox from pathlib import Path import os large_files = [] # Scan for large files for root, dirs, files in os.walk(project.path): if '.git' in root: continue for file in files: file_path = os.path.join(root, file) try: file_size = os.path.getsize(file_path) if file_size > 50 * 1024 * 1024: # 50MB limit size_mb = file_size / (1024 * 1024) rel_path = os.path.relpath(file_path, project.path) large_files.append((rel_path, size_mb)) except: pass if large_files: msg = (f"⚠️ WARNUNG: Große Dateien gefunden!\n\n" f"Gitea hat ein Upload-Limit. Folgende Dateien sind zu groß:\n\n") total_size = 0 for i, (file, size_mb) in enumerate(large_files[:5]): msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size_mb if len(large_files) > 5: msg += f" ... und {len(large_files) - 5} weitere\n" msg += f"\nGesamtgröße großer Dateien: {total_size:.1f} MB\n" msg += ("\n❌ DIESE DATEIEN KÖNNEN NICHT ZU GITEA GEPUSHT WERDEN!\n\n" "Optionen:\n" "1. Abbrechen und große Dateien entfernen\n" "2. Große Dateien zur .gitignore hinzufügen\n" "3. Git LFS einrichten (fortgeschritten)\n\n" "Trotzdem fortfahren? (Push wird fehlschlagen!)") if not messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'): # Offer to create .gitignore if messagebox.askyesno("Hilfe", "Soll ich eine .gitignore-Datei mit den großen Dateien erstellen?"): gitignore_path = Path(project.path) / ".gitignore" with open(gitignore_path, 'a', encoding='utf-8') as f: f.write("\n# Große Dateien automatisch hinzugefügt\n") for file, _ in large_files: f.write(f"{file}\n") messagebox.showinfo("Erfolg", ".gitignore wurde aktualisiert. Bitte erneut versuchen.") return False return True def _commit_and_push(self, project: 'Project', repo: dict, repo_name: str) -> bool: """Add files, commit and push to Gitea""" from tkinter import messagebox from pathlib import Path git_ops = self.repo_manager.git_ops project_path = Path(project.path) # Add all files git_ops.add(project_path) # Initial commit git_ops.commit(project_path, "Initial commit") # Determine owner owner = self._determine_repository_owner(repo) logger.info(f"Pushing to repository with owner: {owner}, repo: {repo_name}") # Push to Gitea success, msg = git_ops.push_existing_repo_to_gitea( project_path, owner, repo_name ) if success: self._handle_successful_push(project, repo, repo_name, owner) return True else: messagebox.showerror("Fehler", f"Push fehlgeschlagen: {msg}") return False def _determine_repository_owner(self, repo: dict) -> str: """Determine the repository owner""" owner = None # Try to get owner from created repository if 'owner' in repo and repo['owner']: owner = repo['owner'].get('username', repo['owner'].get('login')) if not owner: # Fallback to determining owner based on mode if hasattr(self.main_window, 'gitea_explorer') and self.main_window.gitea_explorer.view_mode == "organization": if self.main_window.gitea_explorer.organization_name: owner = self.main_window.gitea_explorer.organization_name else: owner = self.repo_manager.client.config.username return owner def _handle_successful_push(self, project: 'Project', repo: dict, repo_name: str, owner: str): """Handle successful push and update UI""" from tkinter import messagebox from pathlib import Path git_ops = self.repo_manager.git_ops # Get current branch success_branch, current_branch = git_ops.branch(Path(project.path)) current_branch = current_branch.strip().replace('* ', '') if success_branch else 'main' push_info = f"Push-Details:\n" push_info += f"Repository: {repo_name}\n" push_info += f"Owner: {owner}\n" push_info += f"Branch: {current_branch} -> main\n\n" # Get remote URL success_remote, remotes = git_ops.remote(Path(project.path), verbose=True) if success_remote: push_info += f"Remote URLs:\n{remotes}\n\n" # Verify repository on Gitea try: verify_repo = self.repo_manager.client.get_repo(owner, repo_name) repo_url = verify_repo.get('html_url', 'Unknown') clone_url = verify_repo.get('clone_url', 'Unknown') has_content = verify_repo.get('size', 0) > 0 or verify_repo.get('default_branch') push_info += f"Repository gefunden auf Gitea:\n" push_info += f"URL: {repo_url}\n" push_info += f"Clone URL: {clone_url}\n" push_info += f"Hat Inhalt: {'Ja' if has_content else 'Nein'}\n" push_info += f"Größe: {verify_repo.get('size', 0)} bytes\n" push_info += f"Default Branch: {verify_repo.get('default_branch', 'Keiner')}\n\n" if not has_content: push_info += "⚠️ WARNUNG: Repository existiert aber scheint leer zu sein!\n\n" push_info += "Bitte prüfen Sie:\n" push_info += f"1. Öffnen Sie: {repo_url}\n" push_info += f"2. Log-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" # Update project with Gitea repo reference project.gitea_repo = f"{owner}/{repo_name}" self.main_window.project_manager.update_project(project.id, gitea_repo=project.gitea_repo) messagebox.showinfo("Push abgeschlossen", push_info) except Exception as e: push_info += f"\n⚠️ Konnte Repository nicht auf Gitea verifizieren:\n{str(e)}\n\n" push_info += "Mögliche Ursachen:\n" push_info += "- Repository wurde unter anderem Namen/Owner erstellt\n" push_info += "- Berechtigungsprobleme\n" push_info += "- Netzwerkprobleme\n\n" push_info += f"Log-Datei prüfen: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" messagebox.showwarning("Push Status unklar", push_info) # Refresh Gitea explorer if exists if hasattr(self.main_window, 'gitea_explorer'): self.main_window.gitea_explorer.refresh_repositories() def push_to_gitea(self, project: 'Project') -> bool: """ Push existing repository to Gitea with progress bar """ import threading from pathlib import Path from tkinter import messagebox logger.info(f"Push to Gitea started for project: {project.name}") project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Step 1: Verify git repository if not self._verify_git_repository(project_path): return False # Step 2: Check remote configuration has_remote, remotes = self._check_remote_configuration(project_path) if not has_remote: return False # Step 3: Check for uncommitted changes if not self._handle_uncommitted_changes(project_path): return False # Step 4: Check for large files if not self._check_and_handle_large_files(project_path): return False # Show progress bar progress_bar = ProgressBar(self.root, "Push zu Gitea") result = {"success": False, "completed": False} def run_push(): try: stages = GitOperationProgress.get_stages('push') start_time = time.time() min_display_time = 2.0 # Perform actual push early logger.info(f"Executing git push for: {project_path}") success, push_result = git_ops.push(project_path) result["success"] = success result["message"] = push_result # Calculate timing elapsed = time.time() - start_time remaining_time = max(0.1, min_display_time - elapsed) stage_delay = remaining_time / len(stages) # Animate through stages for i, (progress, status) in enumerate(stages): self.root.after(int(i * stage_delay * 1000), lambda p=progress, s=status: progress_bar.update_progress(p, s)) # Final update def finish_push(): if success: progress_bar.update_progress(1.0, stages[-1][1]) logger.info(f"Push completed successfully for {project.name}") # Auto-closes after 0.5s else: logger.error(f"Push failed for {project.name}: {push_result}") # Don't destroy immediately - will be handled by error state result["completed"] = True self.root.after(int(min_display_time * 1000), finish_push) except Exception as e: logger.error(f"Exception during push: {str(e)}", exc_info=True) result["message"] = str(e) result["completed"] = True # Run push in thread thread = threading.Thread(target=run_push, daemon=True) thread.start() # Wait for thread and all UI updates to complete while not result.get("completed", False): self.root.update() time.sleep(0.05) # Handle result in main thread BEFORE destroying progress bar if result["success"]: progress_bar.destroy() self._handle_successful_push_result(project_path, remotes) return True else: # Destroy progress bar first to avoid blocking dialogs progress_bar.destroy() # Small delay to ensure progress bar is fully destroyed self.root.update_idletasks() # Check if error is due to remote changes error_msg = result.get("message", "") logger.info(f"Push failed with message: {error_msg}") if "fetch first" in error_msg or "rejected" in error_msg or "non-fast-forward" in error_msg: from tkinter import messagebox logger.info("Showing pull dialog for fetch first error") response = messagebox.askyesno("Push fehlgeschlagen", "Das Remote-Repository hat Änderungen, die lokal nicht vorhanden sind.\n\n" "Möchten Sie jetzt einen Pull durchführen?") if response: # Perform pull logger.info("User requested pull before push") success, pull_result = git_ops.pull(project_path) if success: messagebox.showinfo("Pull erfolgreich", "Die Änderungen wurden erfolgreich gepullt.\n\n" "Versuchen Sie den Push erneut.") else: messagebox.showerror("Pull fehlgeschlagen", f"Pull fehlgeschlagen:\n\n{pull_result}") else: # Show general error from tkinter import messagebox logger.info(f"Showing general error dialog: {error_msg}") messagebox.showerror("Push fehlgeschlagen", f"Push fehlgeschlagen:\n\n{error_msg}") return False def _verify_git_repository(self, project_path: Path) -> bool: """Verify that the project is a git repository""" from tkinter import messagebox if not (project_path / ".git").exists(): messagebox.showerror("Fehler", "Dies ist kein Git-Repository!\n\n" "Bitte verwenden Sie 'Init & Push' um ein neues Repository zu erstellen.") return False return True def _check_remote_configuration(self, project_path: Path) -> tuple[bool, str]: """Check if remote is configured""" from tkinter import messagebox git_ops = self.repo_manager.git_ops success, remotes = git_ops.remote_list(project_path) if not success or not remotes: messagebox.showerror("Fehler", "Kein Remote-Repository konfiguriert!\n\n" "Verwenden Sie 'Link to Gitea' um das Repository zu verknüpfen.") return False, "" return True, remotes def _handle_uncommitted_changes(self, project_path: Path) -> bool: """Check and handle uncommitted changes""" from tkinter import messagebox git_ops = self.repo_manager.git_ops success, status = git_ops.status(project_path) if success and "nothing to commit" not in status: if messagebox.askyesno("Uncommitted Changes", "Es gibt uncommittete Änderungen.\n\n" "Möchten Sie diese Änderungen committen?"): # Add all changes git_ops.add(project_path) # Get commit message from tkinter import simpledialog commit_msg = simpledialog.askstring( "Commit Message", "Commit-Nachricht eingeben:", initialvalue="Update changes" ) if commit_msg: success, msg = git_ops.commit(project_path, commit_msg) if not success: messagebox.showerror("Fehler", f"Commit fehlgeschlagen: {msg}") return False else: return False return True def _check_and_handle_large_files(self, project_path: Path) -> bool: """Check for large files and handle them""" from tkinter import messagebox git_ops = self.repo_manager.git_ops large_files = git_ops.check_large_files(project_path, 50) # 50MB limit if not large_files: return True # Build warning message msg = "⚠️ Große Dateien gefunden!\n\n" msg += "Folgende Dateien überschreiten 50MB:\n\n" total_size = 0 for file, size in large_files[:10]: size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if len(large_files) > 10: msg += f" ... und {len(large_files) - 10} weitere\n" total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" msg += "Diese großen Dateien verhindern den Push zu Gitea.\n\n" msg += "EMPFEHLUNG: Große Dateien aus Git entfernen\n\n" msg += "Möchten Sie diese Dateien aus Git entfernen?\n" msg += "(Die Dateien bleiben auf Ihrer Festplatte erhalten)" if messagebox.askyesno("Große Dateien gefunden", msg, icon='warning'): return self._remove_large_files_from_git(project_path, large_files) else: return self._offer_gitignore_creation(project_path, large_files) def _remove_large_files_from_git(self, project_path: Path, large_files: list) -> bool: """Remove large files from git but keep them locally""" from tkinter import messagebox git_ops = self.repo_manager.git_ops try: # Create/update .gitignore gitignore_path = project_path / ".gitignore" existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") # Remove files from git index removed_count = 0 for file, _ in large_files: cmd = ["git", "rm", "--cached", file] success, _, _ = git_ops._run_git_command(cmd, cwd=project_path) if success: removed_count += 1 if removed_count > 0: # Commit the changes git_ops.add(project_path, [".gitignore"]) success, _ = git_ops.commit(project_path, f"Große Dateien aus Git entfernt ({removed_count} Dateien)") if success: messagebox.showinfo("Erfolg", f"{removed_count} große Dateien wurden aus Git entfernt.\n\n" "Die Dateien sind weiterhin lokal vorhanden.\n" "Sie können jetzt den Push fortsetzen.") return True else: messagebox.showerror("Fehler", "Konnte Änderungen nicht committen.") return False except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Entfernen der Dateien: {str(e)}") return False def _offer_gitignore_creation(self, project_path: Path, large_files: list) -> bool: """Offer to create .gitignore for large files""" from tkinter import messagebox if messagebox.askyesno("Alternative", "Möchten Sie die großen Dateien manuell bearbeiten?\n\n" "Optionen:\n" "- Dateien komprimieren (z.B. ZIP)\n" "- Dateien auf externen Speicher verschieben\n" "- Dateien in kleinere Teile aufteilen\n\n" "Soll ich eine .gitignore-Datei erstellen?"): gitignore_path = project_path / ".gitignore" try: existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") messagebox.showinfo("Erfolg", ".gitignore wurde aktualisiert.\n\n" "Bitte committen Sie die .gitignore-Datei und versuchen Sie es erneut.") except Exception as e: messagebox.showerror("Fehler", f"Konnte .gitignore nicht erstellen: {str(e)}") return False def _handle_successful_push_result(self, project_path: Path, remotes: str) -> None: """Handle successful push result""" from tkinter import messagebox from pathlib import Path import re # Build debug info debug_info = "Push erfolgreich!\n\n" debug_info += f"Remote URLs:\n{remotes}\n\n" # Extract owner from remote URL match = re.search(r'gitea-undso\.intelsight\.de[:/]([^/]+)/([^/\.]+)', remotes) if match: remote_owner = match.group(1) remote_repo = match.group(2) debug_info += f"Push ging an: {remote_owner}/{remote_repo}\n\n" # Verify repository exists try: repo = self.repo_manager.client.get_repo(remote_owner, remote_repo) debug_info += f"✅ Repository gefunden!\n" debug_info += f"URL: {repo.get('html_url', 'Unknown')}\n" debug_info += f"Größe: {repo.get('size', 0)} bytes\n" debug_info += f"Default Branch: {repo.get('default_branch', 'Keiner')}\n\n" except: debug_info += f"❌ Repository nicht unter {remote_owner}/{remote_repo} gefunden!\n\n" debug_info += self._search_repository_in_all_locations(remote_repo) debug_info += f"\nLog-Datei: {Path.home() / '.claude_project_manager' / 'gitea_operations.log'}" messagebox.showinfo("Push Debug Info", debug_info) # Refresh Gitea explorer if hasattr(self.main_window, 'gitea_explorer'): self.main_window.gitea_explorer.refresh_repositories() def _search_repository_in_all_locations(self, repo_name: str) -> str: """Search for repository in all accessible locations""" debug_info = "Suche Repository in allen Orten:\n" # Check user repos try: user_repos = self.repo_manager.list_all_repositories() found_in_user = any(r['name'] == repo_name for r in user_repos) debug_info += f"- In Benutzer-Repos: {'Gefunden' if found_in_user else 'Nicht gefunden'}\n" except: debug_info += "- In Benutzer-Repos: Fehler beim Suchen\n" # Check org repos if hasattr(self.main_window, 'gitea_explorer') and self.main_window.gitea_explorer.organization_name: try: org_name = self.main_window.gitea_explorer.organization_name org_repos = self.repo_manager.list_organization_repositories(org_name) found_in_org = any(r['name'] == repo_name for r in org_repos) debug_info += f"- In Organisation {org_name}: {'Gefunden' if found_in_org else 'Nicht gefunden'}\n" except: debug_info += f"- In Organisation: Fehler beim Suchen\n" return debug_info def test_gitea_connection(self, project: Optional['Project'] = None) -> None: """ Test Gitea connection and show information Consolidated version combining features from both implementations """ try: # Test API connection user_info = self.repo_manager.client.get_user_info() username = user_info.get('username', 'Unknown') info = f"Gitea Verbindungstest erfolgreich!\n" info += "=" * 50 + "\n\n" info += f"Benutzer: {username}\n" info += f"Server: {self.repo_manager.client.config.base_url}\n\n" # Get organizations (from v1) orgs = self.repo_manager.client.get_user_orgs() if orgs: info += "Organisationen:\n" for org in orgs: info += f" - {org.get('username', 'Unknown')}\n" info += "\n" # Enhanced features from v2 # Get teams if organization is available teams_info = self.repo_manager.client.get_user_teams() if teams_info: info += "Team-Mitgliedschaften:\n" for team in teams_info: org_name = team.get('organization', {}).get('username', 'Unknown') team_name = team.get('name', 'Unknown') permission = team.get('permission', 'none') info += f" - {org_name}/{team_name}: {permission}\n" info += "\n" # Project-specific info if provided (from v2) if project: info += f"Projekt-spezifische Informationen:\n" info += f"Name: {project.name}\n" info += f"Pfad: {project.path}\n" # Check if git repo exists from pathlib import Path git_dir = Path(project.path) / ".git" if git_dir.exists(): info += "Git-Status: ✓ Initialisiert\n" # Get remote info git_ops = self.repo_manager.git_ops success, remotes = git_ops.remote(Path(project.path), verbose=True) if success and remotes: info += f"Remote URL: {remotes.strip()}\n" else: info += "Git-Status: ✗ Nicht initialisiert\n" info += "\n" # List repositories repos = self.repo_manager.client.get_user_repos() if repos: info += f"Repositories ({len(repos)}):\n" for repo in repos[:10]: # Show first 10 repo_name = repo.get('full_name', repo.get('name', 'Unknown')) private = "🔒" if repo.get('private', False) else "🌐" info += f" {private} {repo_name}\n" if len(repos) > 10: info += f" ... und {len(repos) - 10} weitere\n" # Show info using scrollable dialog (from v2) self.main_window._show_scrollable_info("Gitea Verbindungstest", info) except Exception as e: # Enhanced error message (from v2) error_msg = f"Gitea Verbindungstest fehlgeschlagen!\n\n" error_msg += f"Fehler: {str(e)}\n\n" error_msg += f"Server: {self.repo_manager.client.config.base_url}\n" error_msg += "\nBitte prüfen Sie:\n" error_msg += "- Netzwerkverbindung\n" error_msg += "- Server Erreichbarkeit\n" error_msg += "- API Token Gültigkeit" from tkinter import messagebox messagebox.showerror("Gitea Verbindungstest", error_msg) logger.error(f"Gitea connection test failed: {e}") def verify_repository_on_gitea(self, project: 'Project') -> None: """ Verify repository status on Gitea Consolidated version combining pre-checks (v1) and debug info (v2) """ import re from pathlib import Path project_path = Path(project.path) project_name = project.name if hasattr(project, 'name') else project.get('name', 'Unknown') info = f"Repository Verifizierung für: {project_name}\n" info += "=" * 50 + "\n\n" # Pre-check from v1: Verify .git directory exists if not (project_path / ".git").exists(): info += "❌ Kein Git-Repository gefunden!\n\n" info += "Das Projekt muss zuerst als Git-Repository initialisiert werden.\n" info += "Verwenden Sie 'Init & Push' um das Repository zu erstellen." self.main_window._show_scrollable_info("Repository Verifizierung", info) return # Get git configuration git_ops = self.repo_manager.git_ops # Check remotes success, remotes = git_ops.remote(project_path, verbose=True) if not success or not remotes: info += "❌ Kein Remote-Repository konfiguriert!\n\n" info += "Verwenden Sie 'Link to Gitea' um das Repository zu verknüpfen." self.main_window._show_scrollable_info("Repository Verifizierung", info) return # Debug info from v2: Show .git/config content git_config_path = project_path / ".git" / "config" if git_config_path.exists(): info += "Git Konfiguration (.git/config):\n" info += "-" * 30 + "\n" try: with open(git_config_path, 'r') as f: config_content = f.read() info += config_content + "\n" except Exception as e: info += f"Fehler beim Lesen: {e}\n" info += "-" * 30 + "\n\n" # Parse remote URL remote_url = remotes.strip() info += f"Remote URL: {remote_url}\n\n" # Extract repo info from URL repo_match = re.search(r'/([^/]+)/([^/]+?)(?:\.git)?$', remote_url) if not repo_match: info += "❌ Konnte Repository-Informationen nicht aus URL extrahieren!\n" self.main_window._show_scrollable_info("Repository Verifizierung", info) return owner = repo_match.group(1) repo_name = repo_match.group(2) info += f"Repository: {owner}/{repo_name}\n\n" # Check if repository exists on Gitea try: repo_data = self.repo_manager.client.get_repo(owner, repo_name) if repo_data: info += "✅ Repository existiert auf Gitea!\n\n" info += f"Name: {repo_data.get('name', 'Unknown')}\n" info += f"Beschreibung: {repo_data.get('description', 'Keine')}\n" info += f"Privat: {'Ja' if repo_data.get('private', False) else 'Nein'}\n" info += f"Default Branch: {repo_data.get('default_branch', 'Unknown')}\n" info += f"Größe: {repo_data.get('size', 0)} KB\n" info += f"URL: {repo_data.get('html_url', 'Unknown')}\n\n" # Check branches info += "Branches:\n" success, local_branches = git_ops.branch(project_path, list_all=True) if success: info += f"Lokal: {local_branches.strip()}\n" # Get current branch success, current_branch = git_ops.branch(project_path) if success: current = current_branch.strip().replace('* ', '') info += f"Aktueller Branch: {current}\n" else: info += "❌ Repository nicht auf Gitea gefunden!\n" info += "Das Repository existiert möglicherweise nicht oder Sie haben keine Berechtigung." except Exception as e: info += f"❌ Fehler bei der Verifizierung: {str(e)}\n" logger.error(f"Repository verification failed: {e}") # Show results using scrollable dialog self.main_window._show_scrollable_info("Repository Verifizierung", info) def manage_branches(self, project: 'Project') -> None: """ Manage git branches Uses the full implementation (second version) """ # The first version was just a placeholder, use the full implementation return self.main_window._original_manage_branches_v2(project) def link_to_gitea(self, project: 'Project') -> None: """ Link local repository to Gitea Uses the full implementation (second version) """ # The first version was just a placeholder, use the full implementation return self.main_window._original_link_to_gitea_v2(project) def show_git_status(self, project: 'Project') -> None: """Show git status for project""" return self.main_window._original_show_git_status(project) def commit_changes(self, project: 'Project') -> None: """Commit changes with progress bar""" import threading from tkinter import messagebox, simpledialog from pathlib import Path project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Check for changes success, status = git_ops.status(project_path) if not success: messagebox.showerror("Fehler", "Konnte Git-Status nicht abrufen") return if "nothing to commit" in status: messagebox.showinfo("Info", "Keine Änderungen zum Committen vorhanden") return # Get commit message commit_msg = simpledialog.askstring( "Commit Message", "Commit-Nachricht eingeben:", parent=self.root ) if not commit_msg: return # Show progress bar progress_bar = ProgressBar(self.root, "Commit erstellen") def run_commit(): start_time = time.time() min_display_time = 2.0 try: stages = GitOperationProgress.get_stages('commit') total_stages = len(stages) # Execute commit early to know the result git_ops.add(project_path) success, result = git_ops.commit(project_path, commit_msg) # Calculate timing for stages elapsed = time.time() - start_time remaining_time = max(0.1, min_display_time - elapsed) stage_delay = remaining_time / total_stages # Animate through stages for i, (progress, status) in enumerate(stages): self.root.after(int(i * stage_delay * 1000), lambda p=progress, s=status: progress_bar.update_progress(p, s)) # Final update def finish_commit(): if success: progress_bar.update_progress(1.0, stages[-1][1]) # Auto-closes after 0.5s else: error_msg = f"Commit fehlgeschlagen: {result}" progress_bar.set_error(error_msg) messagebox.showerror("Fehler", error_msg) self.root.after(int(min_display_time * 1000), finish_commit) except Exception as e: progress_bar.destroy() logger.error(f"Commit error: {e}") messagebox.showerror("Fehler", f"Fehler beim Commit: {str(e)}") # Run in thread thread = threading.Thread(target=run_commit, daemon=True) thread.start() def pull_from_gitea(self, project: 'Project') -> None: """Pull from Gitea repository with progress bar""" import threading from pathlib import Path from tkinter import messagebox project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Verify it's a git repo with remote if not (project_path / ".git").exists(): messagebox.showerror("Fehler", "Kein Git-Repository!") return success, remotes = git_ops.remote(project_path, verbose=True) if not success or not remotes: messagebox.showerror("Fehler", "Kein Remote-Repository konfiguriert!") return # Show progress bar progress_bar = ProgressBar(self.root, "Pull von Gitea") def run_pull(): try: stages = GitOperationProgress.get_stages('pull') start_time = time.time() min_display_time = 2.0 # Perform actual pull early success, result = git_ops.pull(project_path) # Calculate timing elapsed = time.time() - start_time remaining_time = max(0.1, min_display_time - elapsed) stage_delay = remaining_time / len(stages) # Animate through stages for i, (progress, status) in enumerate(stages): self.root.after(int(i * stage_delay * 1000), lambda p=progress, s=status: progress_bar.update_progress(p, s)) # Final update def finish_pull(): if success: progress_bar.update_progress(1.0, stages[-1][1]) # Auto-closes after 0.5s else: error_msg = f"Pull fehlgeschlagen: {result}" progress_bar.set_error(error_msg) messagebox.showerror("Fehler", error_msg) self.root.after(int(min_display_time * 1000), finish_pull) except Exception as e: progress_bar.destroy() logger.error(f"Pull error: {e}") messagebox.showerror("Fehler", f"Fehler beim Pull: {str(e)}") # Run in thread thread = threading.Thread(target=run_pull, daemon=True) thread.start() def fetch_from_gitea(self, project: 'Project') -> None: """Fetch from Gitea repository with progress bar""" import threading from pathlib import Path from tkinter import messagebox project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Verify it's a git repo with remote if not (project_path / ".git").exists(): messagebox.showerror("Fehler", "Kein Git-Repository!") return success, remotes = git_ops.remote(project_path, verbose=True) if not success or not remotes: messagebox.showerror("Fehler", "Kein Remote-Repository konfiguriert!") return # Show progress bar progress_bar = ProgressBar(self.root, "Fetch von Gitea") def run_fetch(): try: stages = GitOperationProgress.get_stages('fetch') start_time = time.time() min_display_time = 2.0 # Perform actual fetch early success, result = git_ops.fetch(project_path) # Calculate timing elapsed = time.time() - start_time remaining_time = max(0.1, min_display_time - elapsed) stage_delay = remaining_time / len(stages) # Animate through stages for i, (progress, status) in enumerate(stages): self.root.after(int(i * stage_delay * 1000), lambda p=progress, s=status: progress_bar.update_progress(p, s)) # Final update def finish_fetch(): if success: progress_bar.update_progress(1.0, stages[-1][1]) # Auto-closes after 0.5s else: error_msg = f"Fetch fehlgeschlagen: {result}" progress_bar.set_error(error_msg) messagebox.showerror("Fehler", error_msg) self.root.after(int(min_display_time * 1000), finish_fetch) except Exception as e: progress_bar.destroy() logger.error(f"Fetch error: {e}") messagebox.showerror("Fehler", f"Fehler beim Fetch: {str(e)}") # Run in thread thread = threading.Thread(target=run_fetch, daemon=True) thread.start() def clone_repository(self, repo_data: dict) -> None: """Clone repository from Gitea with progress bar""" import threading import time from tkinter import messagebox logger.log_method_call("clone_repository", args=(repo_data.get('name', 'unknown'),)) logger.log_git_operation("clone", "started", {"repo": repo_data}) # Show progress bar progress_bar = ProgressBar(self.root, "Repository klonen") progress_bar.update_progress(0.0, "Vorbereitung...", "Repository klonen") def run_clone(): start_time = time.time() min_display_time = 2.0 # Minimum 2 seconds try: # Get stages for clone operation stages = GitOperationProgress.get_stages('clone') stage_index = 0 # Simulate progress through stages def update_stage(): nonlocal stage_index if stage_index < len(stages): progress, status = stages[stage_index] progress_bar.update_progress(progress, status) stage_index += 1 # Start with first stage update_stage() # Get clone directory owner = repo_data.get('owner', {}).get('username', 'unknown') repo_name = repo_data.get('name', 'unknown') # Update progress self.root.after(300, update_stage) # Select directory clone_dir = self.repo_manager.git_ops.select_directory( f"Wählen Sie ein Verzeichnis für '{repo_name}'" ) if not clone_dir: progress_bar.destroy() return # Update progress self.root.after(100, update_stage) # Perform actual clone logger.log_git_operation("clone", "executing", { "owner": owner, "repo": repo_name, "target_dir": str(clone_dir) }) success, local_path = self.repo_manager.git_ops.clone_repository( owner, repo_name, clone_dir ) # Calculate remaining display time elapsed = time.time() - start_time remaining_display_time = max(0, min_display_time - elapsed) # Update through remaining stages with timing remaining_stages = len(stages) - stage_index stage_delay = remaining_display_time / (remaining_stages + 1) if remaining_stages > 0 else 0.3 for i in range(remaining_stages): self.root.after(int((i + 1) * stage_delay * 1000), update_stage) # Final update def finish_clone(): if success: progress_bar.update_progress(1.0, "Repository erfolgreich geklont!") logger.log_git_operation("clone", "completed", { "repo": repo_name, "local_path": str(local_path) }) # Add to projects from project_manager import Project project = Project( id=str(datetime.now().timestamp()), name=repo_name, path=str(local_path), last_accessed=datetime.now(), gitea_repo=f"{owner}/{repo_name}" ) self.project_manager.add_project(project) self.main_window.refresh_projects() # Progress bar will auto-close after 0.5s else: error_msg = f"Klonen fehlgeschlagen: {local_path}" progress_bar.set_error(error_msg) logger.log_git_operation("clone", "failed", { "repo": repo_name, "error": str(local_path) }) messagebox.showerror("Fehler", error_msg) self.root.after(int(remaining_display_time * 1000), finish_clone) except Exception as e: progress_bar.destroy() logger.log_exception(e, "clone_repository") logger.log_git_operation("clone", "error", {"error": str(e)}) messagebox.showerror("Fehler", f"Fehler beim Klonen: {str(e)}") # Run clone in thread thread = threading.Thread(target=run_clone, daemon=True) thread.start() def init_git_repo(self, project: 'Project') -> bool: """Initialize git repository""" return self.main_window._original_init_git_repo(project) def fix_repository_issues(self, project: 'Project') -> None: """ Fix common repository issues Refactored from 110 lines into smaller, focused methods """ from pathlib import Path project_path = Path(project.path) # Step 1: Diagnose repository issues issues = self._diagnose_repository_issues(project_path) # Step 2: Show issues dialog with fix options self._show_fix_repository_dialog(project, issues) def _diagnose_repository_issues(self, project_path: Path) -> list: """Diagnose common repository issues""" git_ops = self.repo_manager.git_ops issues = [] # Check if it's a git repo if not (project_path / ".git").exists(): issues.append("❌ Kein Git-Repository") else: # Check remote configuration remote_issues = self._check_remote_issues(project_path) issues.extend(remote_issues) # Check for LFS configuration lfs_issues = self._check_lfs_issues(project_path) issues.extend(lfs_issues) return issues def _check_remote_issues(self, project_path: Path) -> list: """Check for remote configuration issues""" git_ops = self.repo_manager.git_ops issues = [] success, remotes = git_ops.remote_list(project_path) if success and remotes: issues.append(f"✅ Remote vorhanden:\n{remotes}") # Check for authentication issues if "Authentication failed" in remotes or "21cbba8d" in remotes: issues.append("⚠️ Alter/falscher Token im Remote gefunden") else: issues.append("❌ Kein Remote konfiguriert") return issues def _check_lfs_issues(self, project_path: Path) -> list: """Check for Git LFS issues""" issues = [] gitattributes = project_path / ".gitattributes" if gitattributes.exists(): with open(gitattributes, 'r') as f: if 'filter=lfs' in f.read(): issues.append("⚠️ Git LFS ist konfiguriert (kann Probleme verursachen)") return issues def _show_fix_repository_dialog(self, project: 'Project', issues: list) -> None: """Show dialog with repository issues and fix options""" import customtkinter as ctk msg = "Repository-Status:\n\n" + "\n".join(issues) + "\n\nWas möchten Sie tun?" # Create dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Repository reparieren") dialog.geometry("600x400") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 600) // 2 y = (dialog.winfo_screenheight() - 400) // 2 dialog.geometry(f"600x400+{x}+{y}") # Message text_widget = ctk.CTkTextbox(dialog, width=580, height=250) text_widget.pack(padx=10, pady=10) text_widget.insert("1.0", msg) text_widget.configure(state="disabled") # Buttons button_frame = ctk.CTkFrame(dialog) button_frame.pack(fill="x", padx=10, pady=(0, 10)) # Add fix buttons based on issues self._add_fix_buttons(dialog, button_frame, project, issues) close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100) close_btn.pack(pady=10) dialog.grab_set() def _add_fix_buttons(self, dialog, button_frame, project: 'Project', issues: list) -> None: """Add appropriate fix buttons based on detected issues""" import customtkinter as ctk # Add Remote Fix button if authentication issues detected if any("falscher Token" in issue for issue in issues): fix_btn = ctk.CTkButton( button_frame, text="🔧 Remote korrigieren", command=lambda: self._fix_remote_action(dialog, project), width=180, height=40 ) fix_btn.pack(side="left", padx=5) # Add LFS disable button if LFS issues detected if any("LFS" in issue for issue in issues): lfs_btn = ctk.CTkButton( button_frame, text="🚫 LFS deaktivieren", command=lambda: self._disable_lfs_action(dialog, project), width=180, height=40 ) lfs_btn.pack(side="left", padx=5) # Always add check on Gitea button check_btn = ctk.CTkButton( button_frame, text="🔍 Auf Gitea prüfen", command=lambda: self._check_on_gitea_action(dialog, project), width=180, height=40 ) check_btn.pack(side="left", padx=5) def _fix_remote_action(self, dialog, project: 'Project') -> None: """Fix remote URL with correct credentials""" from pathlib import Path from tkinter import messagebox dialog.destroy() project_path = Path(project.path) git_ops = self.repo_manager.git_ops # Get current organization org_name = self._get_current_organization() # Remove old remote git_ops.remote_remove(project_path, "origin") # Add correct remote success, msg = git_ops.add_remote_to_existing_repo( project_path, org_name, project.name ) if success: messagebox.showinfo("Erfolg", f"Remote wurde korrigiert!\n\n" f"Repository: {org_name}/{project.name}\n" f"Sie können jetzt pushen.") else: messagebox.showerror("Fehler", f"Fehler beim Korrigieren: {msg}") def _get_current_organization(self) -> str: """Get current organization name""" org_name = "IntelSight" if hasattr(self.main_window, 'gitea_explorer') and \ self.main_window.gitea_explorer.organization_name: org_name = self.main_window.gitea_explorer.organization_name return org_name def _disable_lfs_action(self, dialog, project: 'Project') -> None: """Disable LFS temporarily""" from pathlib import Path from tkinter import messagebox dialog.destroy() project_path = Path(project.path) git_ops = self.repo_manager.git_ops success, msg = git_ops.disable_lfs_for_push(project_path) if success: messagebox.showinfo("Erfolg", "Git LFS wurde deaktiviert.\n\n" "Versuchen Sie den Push erneut.") else: messagebox.showerror("Fehler", f"Fehler: {msg}") def _check_on_gitea_action(self, dialog, project: 'Project') -> None: """Check if repository exists on Gitea""" dialog.destroy() self.verify_repository_on_gitea(project) def manage_large_files(self, project: 'Project') -> None: """ Manage large files in the repository Refactored from 160 lines into smaller, focused methods """ from pathlib import Path from tkinter import messagebox project_path = Path(project.path) # Step 1: Check for large files large_files = self._scan_for_large_files(project_path) if not large_files: messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\n" "Ihr Repository kann problemlos gepusht werden.") return # Step 2: Show dialog with large files self._show_large_files_dialog(project, large_files) def _scan_for_large_files(self, project_path: Path) -> list: """Scan repository for large files""" git_ops = self.repo_manager.git_ops return git_ops.check_large_files(project_path, 50) # Files > 50MB def _show_large_files_dialog(self, project: 'Project', large_files: list) -> None: """Show dialog with large files and management options""" import customtkinter as ctk from pathlib import Path project_path = Path(project.path) # Build message msg = self._build_large_files_message(large_files) # Create dialog dialog = ctk.CTkToplevel(self.root) dialog.title("Große Dateien verwalten") dialog.geometry("600x500") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 600) // 2 y = (dialog.winfo_screenheight() - 500) // 2 dialog.geometry(f"600x500+{x}+{y}") # Message text_widget = ctk.CTkTextbox(dialog, width=580, height=300) text_widget.pack(padx=10, pady=10) text_widget.insert("1.0", msg) text_widget.configure(state="disabled") # Buttons button_frame = ctk.CTkFrame(dialog) button_frame.pack(fill="x", padx=10, pady=(0, 10)) # Add action buttons self._add_large_files_action_buttons(dialog, button_frame, project, large_files) # Info text info_label = ctk.CTkLabel(dialog, text="Tipp: 'Aus Git entfernen' ist die einfachste Lösung.\n" "Die Dateien bleiben auf Ihrer Festplatte erhalten!", font=("Arial", 12), text_color="yellow") info_label.pack(pady=10) close_btn = ctk.CTkButton(dialog, text="Schließen", command=dialog.destroy, width=100) close_btn.pack(pady=10) dialog.grab_set() def _build_large_files_message(self, large_files: list) -> str: """Build message showing large files""" msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n" total_size = 0 for i, (file, size) in enumerate(large_files[:15]): size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if i >= 14 and len(large_files) > 15: msg += f" ... und {len(large_files) - 15} weitere\n" break total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" msg += "Was möchten Sie tun?" return msg def _add_large_files_action_buttons(self, dialog, button_frame, project: 'Project', large_files: list) -> None: """Add action buttons for managing large files""" import customtkinter as ctk from pathlib import Path project_path = Path(project.path) # Remove from Git button remove_btn = ctk.CTkButton( button_frame, text="🗑️ Aus Git entfernen\n(Dateien bleiben lokal)", command=lambda: self._remove_large_files_action(dialog, project_path, large_files), width=180, height=60 ) remove_btn.pack(side="left", padx=5) # Show .gitignore button gitignore_btn = ctk.CTkButton( button_frame, text="📝 .gitignore anzeigen", command=lambda: self._show_gitignore_content(dialog, large_files), width=180, height=60 ) gitignore_btn.pack(side="left", padx=5) # Git status button status_btn = ctk.CTkButton( button_frame, text="📊 Git Status", command=lambda: self._show_git_status_action(dialog, project), width=180, height=60 ) status_btn.pack(side="left", padx=5) def _remove_large_files_action(self, dialog, project_path: Path, large_files: list) -> None: """Remove large files from git but keep locally""" from tkinter import messagebox dialog.destroy() try: # Update .gitignore if not self._update_gitignore_with_large_files(project_path, large_files): return # Remove files from git index removed_count, failed_files = self._remove_files_from_git_index( project_path, large_files) if removed_count > 0: # Commit the changes self._commit_large_files_removal(project_path, removed_count) # Show result self._show_removal_result(removed_count, failed_files) else: messagebox.showwarning("Warnung", "Keine Dateien konnten entfernt werden.") except Exception as e: messagebox.showerror("Fehler", f"Fehler beim Entfernen: {str(e)}") def _update_gitignore_with_large_files(self, project_path: Path, large_files: list) -> bool: """Update .gitignore with large files""" try: gitignore_path = project_path / ".gitignore" existing_content = "" if gitignore_path.exists(): with open(gitignore_path, 'r', encoding='utf-8') as f: existing_content = f.read() # Add large files to .gitignore with open(gitignore_path, 'a', encoding='utf-8') as f: if existing_content and not existing_content.endswith('\n'): f.write('\n') f.write("\n# Große Dateien (automatisch hinzugefügt)\n") for file, _ in large_files: f.write(f"{file}\n") return True except Exception as e: from tkinter import messagebox messagebox.showerror("Fehler", f"Fehler beim Aktualisieren von .gitignore: {str(e)}") return False def _remove_files_from_git_index(self, project_path: Path, large_files: list) -> tuple: """Remove files from git index""" git_ops = self.repo_manager.git_ops removed_count = 0 failed_files = [] for file, _ in large_files: cmd = ["git", "rm", "--cached", file] success, _, stderr = git_ops._run_git_command(cmd, cwd=project_path) if success: removed_count += 1 else: failed_files.append((file, stderr)) return removed_count, failed_files def _commit_large_files_removal(self, project_path: Path, removed_count: int) -> None: """Commit the removal of large files""" git_ops = self.repo_manager.git_ops git_ops.add(Path(project_path), [".gitignore"]) git_ops.commit(Path(project_path), f"Große Dateien aus Git entfernt ({removed_count} Dateien)") def _show_removal_result(self, removed_count: int, failed_files: list) -> None: """Show result of file removal""" from tkinter import messagebox result_msg = f"✅ {removed_count} Dateien wurden aus Git entfernt.\n\n" result_msg += "Die Dateien sind weiterhin lokal vorhanden.\n" if failed_files: result_msg += f"\n⚠️ {len(failed_files)} Dateien konnten nicht entfernt werden.\n" result_msg += "\nSie können jetzt pushen!" messagebox.showinfo("Erfolg", result_msg) def _show_gitignore_content(self, dialog, large_files: list) -> None: """Show .gitignore content for large files""" import customtkinter as ctk from tkinter import messagebox dialog.destroy() gitignore_content = "# Große Dateien\n" for file, _ in large_files: gitignore_content += f"{file}\n" # Create dialog to show content info_dialog = ctk.CTkToplevel(self.root) info_dialog.title(".gitignore Inhalt") info_dialog.geometry("500x400") label = ctk.CTkLabel(info_dialog, text="Fügen Sie diese Zeilen zu .gitignore hinzu:", font=("Arial", 14)) label.pack(pady=10) text = ctk.CTkTextbox(info_dialog, width=480, height=300) text.pack(padx=10, pady=10) text.insert("1.0", gitignore_content) def copy_to_clipboard(): self.root.clipboard_clear() self.root.clipboard_append(gitignore_content) messagebox.showinfo("Kopiert", "Inhalt wurde in die Zwischenablage kopiert!") copy_btn = ctk.CTkButton(info_dialog, text="In Zwischenablage kopieren", command=copy_to_clipboard) copy_btn.pack(pady=10) info_dialog.grab_set() def _show_git_status_action(self, dialog, project: 'Project') -> None: """Show git status for project""" from pathlib import Path from tkinter import messagebox dialog.destroy() project_path = Path(project.path) git_ops = self.repo_manager.git_ops success, status = git_ops.status(project_path) if success: self.main_window._show_scrollable_info("Git Status", f"Git Status für {project.name}:\n\n{status}") else: messagebox.showerror("Fehler", "Konnte Git Status nicht abrufen.") def setup_git_lfs(self, project: 'Project') -> None: """ Setup Git LFS for the project Refactored from 131 lines into smaller, focused methods """ from pathlib import Path from tkinter import messagebox project_path = Path(project.path) # Step 1: Check for large files large_files = self._scan_for_large_files(project_path) if not large_files: messagebox.showinfo("Info", "Keine großen Dateien gefunden (>50MB).\n\n" "Git LFS ist möglicherweise nicht erforderlich.") return # Step 2: Show large files and ask for confirmation if not self._confirm_lfs_setup(large_files): return # Step 3: Check and setup Git LFS if not self._initialize_git_lfs(project_path): return # Step 4: Get tracking option from user tracking_choice = self._get_lfs_tracking_choice() if not tracking_choice: return # Step 5: Process patterns based on choice patterns = self._get_lfs_patterns(large_files, tracking_choice) if not patterns: return # Step 6: Track files with LFS self._track_files_with_lfs(project_path, patterns, large_files) def _confirm_lfs_setup(self, large_files: list) -> bool: """Show large files and ask for LFS setup confirmation""" from tkinter import messagebox msg = self._build_lfs_confirmation_message(large_files) return messagebox.askyesno("Git LFS einrichten", msg) def _build_lfs_confirmation_message(self, large_files: list) -> str: """Build confirmation message for LFS setup""" msg = f"Gefundene große Dateien ({len(large_files)} Dateien):\n\n" total_size = 0 for i, (file, size) in enumerate(large_files[:10]): size_mb = size / (1024 * 1024) msg += f" • {file} ({size_mb:.1f} MB)\n" total_size += size if i >= 9 and len(large_files) > 10: msg += f" ... und {len(large_files) - 10} weitere\n" break total_mb = total_size / (1024 * 1024) msg += f"\nGesamtgröße: {total_mb:.1f} MB\n\n" msg += "Möchten Sie Git LFS für diese Dateien einrichten?" return msg def _initialize_git_lfs(self, project_path: Path) -> bool: """Check if Git LFS is installed and initialize it""" from tkinter import messagebox git_ops = self.repo_manager.git_ops success, result = git_ops.setup_lfs(project_path) if not success: messagebox.showerror("Fehler", result) return False return True def _get_lfs_tracking_choice(self) -> Optional[str]: """Get user choice for LFS tracking""" import customtkinter as ctk import tkinter as tk choices = { "all": "Alle großen Dateien (>50MB)", "types": "Nach Dateityp (z.B. *.mp4, *.zip)", "specific": "Spezifische Dateien auswählen" } dialog = ctk.CTkToplevel(self.root) dialog.title("LFS Tracking-Optionen") dialog.geometry("400x300") dialog.transient(self.root) # Center dialog dialog.update_idletasks() x = (dialog.winfo_screenwidth() - 400) // 2 y = (dialog.winfo_screenheight() - 300) // 2 dialog.geometry(f"400x300+{x}+{y}") selected_option = tk.StringVar(value="all") label = ctk.CTkLabel(dialog, text="Wie möchten Sie die Dateien tracken?", font=("Arial", 14)) label.pack(pady=20) for key, text in choices.items(): radio = ctk.CTkRadioButton(dialog, text=text, variable=selected_option, value=key) radio.pack(pady=5, padx=20, anchor="w") result = {"ok": False} def on_ok(): result["ok"] = True result["choice"] = selected_option.get() dialog.destroy() ok_btn = ctk.CTkButton(dialog, text="OK", command=on_ok) ok_btn.pack(pady=20) dialog.grab_set() dialog.wait_window() return result.get("choice") if result.get("ok") else None def _get_lfs_patterns(self, large_files: list, choice: str) -> list: """Get patterns to track based on user choice""" from pathlib import Path if choice == "all": # Track all large files return [file for file, _ in large_files] elif choice == "types": return self._get_file_type_patterns(large_files) else: # specific # For simplicity, track all for now return [file for file, _ in large_files] def _get_file_type_patterns(self, large_files: list) -> list: """Get file type patterns from user""" from pathlib import Path from tkinter import simpledialog # Get unique extensions extensions = set() for file, _ in large_files: ext = Path(file).suffix if ext: extensions.add(ext) # Ask which extensions to track ext_msg = "Folgende Dateitypen wurden gefunden:\n" for ext in extensions: ext_msg += f" • *{ext}\n" ext_msg += "\nWelche möchten Sie mit LFS tracken?\n(Komma-getrennt, z.B. .mp4,.zip)" selected = simpledialog.askstring("Dateitypen auswählen", ext_msg) if not selected: return [] patterns = [] for ext in selected.split(","): ext = ext.strip() if not ext.startswith("."): ext = "." + ext patterns.append(f"*{ext}") return patterns def _track_files_with_lfs(self, project_path: Path, patterns: list, large_files: list) -> None: """Track files with Git LFS""" from tkinter import messagebox git_ops = self.repo_manager.git_ops # Track with LFS success, result = git_ops.track_with_lfs(project_path, patterns) if success: messagebox.showinfo("Erfolg", f"{result}\n\n" "Nächste Schritte:\n" "1. Committen Sie die .gitattributes Datei\n" "2. Große Dateien werden nun über LFS verwaltet\n" "3. Der Push sollte jetzt funktionieren") # Offer to migrate existing files self._offer_lfs_migration(project_path, large_files) else: messagebox.showerror("Fehler", result) def _offer_lfs_migration(self, project_path: Path, large_files: list) -> None: """Offer to migrate existing large files to LFS""" from tkinter import messagebox if messagebox.askyesno("Migration", "Möchten Sie existierende große Dateien zu LFS migrieren?\n\n" "Dies wird die Dateien aus dem Git-History entfernen und neu hinzufügen."): git_ops = self.repo_manager.git_ops success, result = git_ops.migrate_to_lfs(project_path, [f for f, _ in large_files]) if success: messagebox.showinfo("Erfolg", result) else: messagebox.showerror("Fehler", result)