From 9161abae630c5de0cde04f1d0238913b481ace80 Mon Sep 17 00:00:00 2001 From: KI-Sparringpartner Date: Mon, 3 Aug 2026 17:23:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20NEUE=20FEATURES:=20Push-to-Gitea?= =?UTF-8?q?,=20Inotify-Watcher,=20Changelog=20mit=20git=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- app/__init__.py | 5 ++- app/changelog_gen.py | 89 ++++++++++++++++++++++++++++++++++--- app/file_watcher.py | 39 ++++++++--------- app/git_manager.py | 16 +++++-- app/routes.py | 102 ++++++++++++++++++++++++++++--------------- 6 files changed, 187 insertions(+), 66 deletions(-) diff --git a/Dockerfile b/Dockerfile index 540f2a3..a7e708f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app # Git und System-Tools installieren + User erstellen ALS ROOT! RUN apt-get update && \ - apt-get install -y git curl && \ + apt-get install -y git curl inotify-tools && \ rm -rf /var/lib/apt/lists/* # Alle Paketeinstallations-Schritte als ROOT durchführen: diff --git a/app/__init__.py b/app/__init__.py index d7a803f..6057989 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1,4 @@ -from .main import app \ No newline at end of file +# App package init +from .main import app + +__all__ = ["app"] \ No newline at end of file diff --git a/app/changelog_gen.py b/app/changelog_gen.py index 89a10fe..1a747ad 100644 --- a/app/changelog_gen.py +++ b/app/changelog_gen.py @@ -1,22 +1,97 @@ +import subprocess from datetime import datetime from pathlib import Path def generate_changelog(project_path: Path, output_file: str = "CHANGELOG.md") -> dict: - """Generiert einen einfachen Changelog (manuell oder statisch).""" - changelog_path = project_path / output_file + """Generiert einen Git-basierten Changelog mit Commit-History.""" + + # Versuche git log zu holen + try: + result = subprocess.run( + ["git", "-C", str(project_path), "log", "-10", "--oneline"], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0 and result.stdout.strip(): + # Git log erfolgreich + recent_commits = "\n".join([f"- {line}" for line in result.stdout.strip().split("\n")[:10]]) + changelog_content = f"""# 📋 Projekt-Changelog - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") +## 🧠 Letzte Änderungen ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) - content = f"""# 📋 Projekt-Changelog +### Zuletzt committed (git log): +{recent_commits} -## 🧠 Letzte Änderungen ({timestamp}) +💡 *Automatisch generiert vom Gitea-Sync-AI-Service* +""" + else: + # Fallback: statischer Changelog + changelog_content = f"""# 📋 Projekt-Changelog + +## 🧠 Letzte Änderungen ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) - Dateiüberwachung aktiviert - AI-basierte Analysen möglich via OpenWebUI 💡 *Automatisch generiert vom Gitea-Sync-AI-Service* """ + except Exception as e: + # Fallback bei Fehler + changelog_content = f"""# 📋 Projekt-Changelog - changelog_path.write_text(content) - return {"success": True, "path": str(changelog_path), "content": content} \ No newline at end of file +## 🧠 Letzte Änderungen ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) + +- Dateiüberwachung aktiviert +- AI-basierte Analysen möglich via OpenWebUI +- Git log fehlgeschlagen: {str(e)} + +💡 *Automatisch generiert vom Gitea-Sync-AI-Service* +""" + + changelog_path = project_path / output_file + changelog_path.write_text(changelog_content) + return {"success": True, "path": str(changelog_path), "content": changelog_content} + + +def get_recent_commits(project_path: Path, count: int = 5) -> list: + """Holt die letzten commits eines Repos.""" + try: + result = subprocess.run( + ["git", "-C", str(project_path), "log", f"-{count}", "--pretty=format:%h - %s"], + capture_output=True, + text=True, + timeout=5 + ) + + if result.returncode == 0: + return result.stdout.strip().split("\n") + return [] + except Exception: + return [] + + +def update_changelog_with_changes(project_path: Path, changes: list) -> dict: + """Aktualisiert den Changelog mit neuen Änderungen.""" + + recent_commits = get_recent_commits(project_path) + + changelog_content = f"""# 📋 Projekt-Changelog + +## 🧠 Letzte Änderungen ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")}) + +### Lokale Änderungen: +{chr(10).join(f"- {change}" for change in changes)}\n\n""" + + if recent_commits: + changelog_content += "### Zuletzt committed:\n" + changelog_content += "\n".join(f"- {commit}" for commit in recent_commits[:5]) + + changelog_content += "\n💡 *Gemeinsam gestaltet mit KI-Sparringpartner*\n" + + changelog_path = project_path / "CHANGELOG.md" + changelog_path.write_text(changelog_content) + + return {"success": True, "path": str(changelog_path)} \ No newline at end of file diff --git a/app/file_watcher.py b/app/file_watcher.py index e33e55d..880ec0b 100644 --- a/app/file_watcher.py +++ b/app/file_watcher.py @@ -1,25 +1,24 @@ -import time -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler +import subprocess from pathlib import Path -class FileChangeHandler(FileSystemEventHandler): - def __init__(self, callback=None): - self.callback = callback +def start_inotify_watcher(path: str, callback=None): + """ + Startet einen Inotify-Watcher mit dem Befehl 'inotifywait'. + Callback wird aufgerufen bei Dateiänderungen. + """ + # Useinotifywait from inotify-tools package + command = [ + "inotifywait", + "-m", "-r", "--format '%w%f %e'", + str(path) + ] - def on_modified(self, event): - if not event.is_directory: - if self.callback: - self.callback(event.src_path) - print(f"[Watcher] Datei geändert: {event.src_path}") + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True + ) - -def start_watcher(path: Path, callback=None): - """Startet den File-Watcher in einem Hintergrundthread.""" - event_handler = FileChangeHandler(callback=callback) - observer = Observer() - observer.schedule(event_handler, str(path), recursive=True) - observer.start() - - return observer \ No newline at end of file + return process \ No newline at end of file diff --git a/app/git_manager.py b/app/git_manager.py index 90ede92..c309262 100644 --- a/app/git_manager.py +++ b/app/git_manager.py @@ -109,8 +109,13 @@ def unsubscribe_repo(project_name: str, projects_dir: Path): return {"success": False, "error": f"Konnte nicht loeschen: {e}"} -def push_changes(repo_path: Path, message: str = "Update 🧠"): - """Pusht lokale Änderungen ins Repository.""" +def push_changes(repo_path: Path, message: str = "Update 🧠", changelog_content: str = None): + """Pusht lokale Änderungen ins Repository mit optionalem Changelog-Content.""" + # Optional: Changelog aktualisieren + if changelog_content: + changelog_path = repo_path / "CHANGELOG.md" + changelog_path.write_text(changelog_content) + result = subprocess.run( ["git", "add", "."], cwd=repo_path, capture_output=True, text=True ) @@ -118,8 +123,13 @@ def push_changes(repo_path: Path, message: str = "Update 🧠"): if result.returncode != 0: return {"success": False, "error": f"git add fehlgeschlagen: {result.stderr}"} + # Commit message mit optionaler Info aus Changelog + commit_msg = message + if changelog_content: + commit_msg += "\n\nChangelog aktualisiert" + result = subprocess.run( - ["git", "commit", "-m", message], cwd=repo_path, capture_output=True, text=True + ["git", "commit", "-m", commit_msg], cwd=repo_path, capture_output=True, text=True ) if result.returncode != 0 and "nothing to commit" not in result.stderr.lower(): diff --git a/app/routes.py b/app/routes.py index c981f33..1ac9dba 100644 --- a/app/routes.py +++ b/app/routes.py @@ -19,6 +19,13 @@ class UnsubscribeRequest(BaseModel): project_name: str +class PushRequest(BaseModel): + """Schema für Push mit Changelog-Content""" + project_name: str + commit_message: str = "AI-bearbeitete Änderungen pushen" + changelog_content: str | None = None + + @router.get("/projects") # Listet abonierte Projekte! async def list_projects(): """Listet lokale (abonierte) Projekte an.""" @@ -27,13 +34,12 @@ async def list_projects(): projects = [] if PROJECTS_DIR.exists() and str(PROJECTS_DIR).strip(): - # Sortiere nach dem Status der Changes zuerst all_projects = [] for project_dir in sorted(PROJECTS_DIR.iterdir()): if project_dir.is_dir() and not project_dir.name.startswith('.'): has_git = (project_dir / '.git').exists() - # Pruefe ob dieses Projekt bearbeitet werden darf (chmod-check) + # Pruefe ob dieses Projekt bearbeitet werden darf can_write = os.access(str(project_dir), os.W_OK) if project_dir.exists() else False all_projects.append({ @@ -44,8 +50,7 @@ async def list_projects(): "can_write": can_write }) - # Sortiere: Projects mit Changes zuerst (kan signale anzeigen) - projects = sorted(all_projects, key=lambda x: ("." in x["name"]), reverse=True) + projects = sorted(all_projects, key=lambda x: ("" in x["name"]), reverse=True) return {"projects": projects} @@ -53,7 +58,7 @@ async def list_projects(): @router.post("/unsubscribe") # Deabonination Endpoint! async def unsubscribe_repo(req: UnsubscribeRequest): """Loeschen eines abonnierten Projekts.""" - from .main import PROJECTS_DIR, get_token + from .main import PROJECTS_DIR target_dir = PROJECTS_DIR / req.project_name @@ -61,37 +66,13 @@ async def unsubscribe_repo(req: UnsubscribeRequest): return {"success": False, "error": f"Projekt '{req.project_name}' nicht gefunden"} try: - import shutil - - # ✅ SCHRITT 1: Alle Berechtigungen normalisieren damit wir es loeschen können! + # Alles auf Schreibbar setzen fuer Loeschen set_permissions(str(target_dir)) - - # ✅ SCHRITT 2: Mit FORCE-LöSCHUNG entfernen (wegen root-Besitzern) - import stat - def force_remove_dir(dir_path): - """Erzwingt das Loeschen von Verzeichnissen.""" - for root, dirs, files in os.walk(str(dir_path), topdown=False): - for d in dirs: - dir_full = Path(root) / d - try: - dir_full.chmod(0o755 | stat.S_IWUSR | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - except Exception: - pass - for f in files: - file_full = Path(root) / f - try: - # Schreibrechte wiederherstellen - os.chmod(str(file_full), 0o666) - file_full.unlink() - except Exception: - pass - - force_remove_dir(target_dir) + import shutil shutil.rmtree(str(target_dir)) - + return {"success": True, "message": f"{req.project_name} erfolgreich geloescht"} - except Exception as e: # Fallback mit rm -rf (Unix-Force) try: @@ -136,7 +117,7 @@ async def subscribe_repo(req: SubscribeRequest): if not result["success"]: return {"error": result.get("error"), "success": False} - # ✅ BERECHTIGUNGEN SETZEN NACH DEM KLOONEN! + # BERECHTIGUNGEN SETZEN NACH DEM KLOONEN! set_permissions(str(target_dir)) generate_changelog(target_dir) @@ -149,6 +130,59 @@ async def subscribe_repo(req: SubscribeRequest): } +@router.post("/push") # Push zu Gitea! +async def push_to_gitea(req: PushRequest): + """Pusht Changes eines Projekts zurueck zu Gitea.""" + from .main import PROJECTS_DIR + import stat + + target_dir = PROJECTS_DIR / req.project_name + + if not target_dir.exists(): + return {"success": False, "error": f"Projekt '{req.project_name}' nicht gefunden"} + + # Alles auf Schreibbar setzen fuer Push + force_remove_all(str(target_dir)) + + result = push_changes(target_dir, req.commit_message, req.changelog_content) + + if result["success"]: + return { + "message": f"✅ {req.project_name} erfolgreich gepusht!", + "path": str(target_dir), + "output": result.get("output", ""), + "success": True + } + else: + return {"error": result.get("error"), "success": False} + + +@router.post("/push/{project_name}") # Alternative Push-Route! +async def push_project(project_name: str): + """Pusht Changes eines Projekts zurueck zu Gitea (ohne ChangelogContent).""" + from .main import PROJECTS_DIR + + target_dir = PROJECTS_DIR / project_name + + if not target_dir.exists(): + return {"success": False, "error": f"Projekt '{project_name}' nicht gefunden"} + + # Alles auf Schreibbar setzen fuer Push + force_remove_all(str(target_dir)) + + result = push_changes(target_dir, "AI-bearbeitete Änderungen") + + if result["success"]: + return { + "message": f"✅ {project_name} erfolgreich gepusht!", + "path": str(target_dir), + "output": result.get("output", ""), + "success": True + } + else: + return {"error": result.get("error"), "success": False} + + # Importiere die Funktionen am Ende damit sie verfügbar sind! -from .git_manager import clone_repo, set_permissions +from .git_manager import clone_repo, set_permissions, push_changes, force_remove_all from .changelog_gen import generate_changelog \ No newline at end of file