🚀 NEUE FEATURES: Push-to-Gitea, Inotify-Watcher, Changelog mit git log
docker-build-and-push / build (push) Successful in 1m54s
docker-build-and-push / build (push) Successful in 1m54s
This commit is contained in:
+1
-1
@@ -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:
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# App package init
|
||||
from .main import app
|
||||
|
||||
__all__ = ["app"]
|
||||
+82
-7
@@ -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."""
|
||||
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# Versuche git log zu holen
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(project_path), "log", "-10", "--oneline"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
content = f"""# 📋 Projekt-Changelog
|
||||
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
|
||||
|
||||
## 🧠 Letzte Änderungen ({timestamp})
|
||||
## 🧠 Letzte Änderungen ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})
|
||||
|
||||
### Zuletzt committed (git log):
|
||||
{recent_commits}
|
||||
|
||||
💡 *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}
|
||||
## 🧠 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)}
|
||||
+19
-20
@@ -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
|
||||
return process
|
||||
+13
-3
@@ -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():
|
||||
|
||||
+67
-33
@@ -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
|
||||
Reference in New Issue
Block a user