Files
ClaudeProjectManager-main/gui/handlers/gitea_operations.py
Claude Project Manager ec92da8a64 Initial commit
2025-07-07 22:11:38 +02:00

1891 Zeilen
76 KiB
Python

"""
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)