181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
"""Ordnerstruktur-Manager für das Monorepo.
|
|
|
|
Verwaltet die 3-Ebenen-Ordnerhierarchie:
|
|
Ebene 1 (Kontextordner) → Ebene 2 (Projektordner) → Ebene 3 (Modulordner)
|
|
|
|
Verantwortung: Anlegen, Validieren und Verwalten der Monorepo-Ordnerstruktur.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from monorepo.models import Context, ProjectInfo
|
|
|
|
|
|
class StructureManager:
|
|
"""Verwaltet die 3-Ebenen-Ordnerhierarchie des Monorepos."""
|
|
|
|
CONTEXTS = ("privat", "dhive", "bahn", "shared")
|
|
NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-]{0,48}[a-z0-9]$")
|
|
|
|
def __init__(self, root_path: Path) -> None:
|
|
"""Initialisiert den StructureManager.
|
|
|
|
Args:
|
|
root_path: Wurzelverzeichnis des Monorepos.
|
|
"""
|
|
self.root_path = root_path
|
|
|
|
def validate_name(self, name: str) -> bool:
|
|
"""Prüft ob ein Name der kebab-case-Konvention entspricht.
|
|
|
|
Regeln:
|
|
- Ausschließlich Kleinbuchstaben, Ziffern und Bindestriche
|
|
- Zwischen 2 und 50 Zeichen lang
|
|
- Beginnt und endet nicht mit einem Bindestrich
|
|
|
|
Args:
|
|
name: Der zu prüfende Projektname.
|
|
|
|
Returns:
|
|
True wenn der Name gültig ist, False sonst.
|
|
"""
|
|
if not name:
|
|
return False
|
|
return self.NAME_PATTERN.match(name) is not None
|
|
|
|
def create_project(self, context: str, name: str) -> Path:
|
|
"""Erstellt ein neues Projekt im gegebenen Kontext.
|
|
|
|
Validiert den Namen, prüft auf Namenskonflikte und erstellt
|
|
den Projektordner im korrekten Kontextordner.
|
|
|
|
Args:
|
|
context: Der Arbeitskontext (privat, dhive, bahn, shared).
|
|
name: Der Projektname in kebab-case.
|
|
|
|
Returns:
|
|
Pfad zum erstellten Projektordner.
|
|
|
|
Raises:
|
|
ValueError: Wenn der Kontext ungültig ist, der Name die
|
|
Namenskonvention verletzt, oder ein Projekt mit
|
|
diesem Namen bereits existiert.
|
|
"""
|
|
# Kontext validieren
|
|
if context not in self.CONTEXTS:
|
|
raise ValueError(
|
|
f"Ungültiger Kontext '{context}'. "
|
|
f"Erlaubte Kontexte: {', '.join(self.CONTEXTS)}"
|
|
)
|
|
|
|
# Name validieren
|
|
if not self.validate_name(name):
|
|
raise ValueError(
|
|
f"Ungültiger Projektname '{name}'. "
|
|
f"Der Name muss kebab-case sein (Kleinbuchstaben, Ziffern, "
|
|
f"Bindestriche), zwischen 2 und 50 Zeichen lang, und darf "
|
|
f"nicht mit einem Bindestrich beginnen oder enden."
|
|
)
|
|
|
|
# Namenskonflikt prüfen
|
|
project_path = self.root_path / context / name
|
|
if project_path.exists():
|
|
raise ValueError(
|
|
f"Namenskonflikt: Ein Projekt mit dem Namen '{name}' "
|
|
f"existiert bereits im Kontext '{context}' "
|
|
f"(Pfad: {project_path})."
|
|
)
|
|
|
|
# Projektordner erstellen
|
|
project_path.mkdir(parents=True, exist_ok=False)
|
|
return project_path
|
|
|
|
def list_projects(self, context: str | None = None) -> list[ProjectInfo]:
|
|
"""Listet alle Projekte, optional gefiltert nach Kontext.
|
|
|
|
Args:
|
|
context: Optionaler Kontext-Filter. Wenn None, werden alle
|
|
Projekte aus allen Kontexten aufgelistet.
|
|
|
|
Returns:
|
|
Liste von ProjectInfo-Objekten für alle gefundenen Projekte.
|
|
|
|
Raises:
|
|
ValueError: Wenn ein ungültiger Kontext angegeben wird.
|
|
"""
|
|
if context is not None and context not in self.CONTEXTS:
|
|
raise ValueError(
|
|
f"Ungültiger Kontext '{context}'. "
|
|
f"Erlaubte Kontexte: {', '.join(self.CONTEXTS)}"
|
|
)
|
|
|
|
contexts_to_scan = (context,) if context else self.CONTEXTS
|
|
projects: list[ProjectInfo] = []
|
|
|
|
for ctx in contexts_to_scan:
|
|
context_dir = self.root_path / ctx
|
|
if not context_dir.exists():
|
|
continue
|
|
|
|
for entry in sorted(context_dir.iterdir()):
|
|
if entry.is_dir() and not entry.name.startswith("."):
|
|
projects.append(
|
|
ProjectInfo(
|
|
name=entry.name,
|
|
context=ctx,
|
|
path=entry,
|
|
)
|
|
)
|
|
|
|
return projects
|
|
|
|
def resolve_context(self, project_path: Path) -> str:
|
|
"""Ermittelt den Arbeitskontext eines Projekts anhand seines Pfads.
|
|
|
|
Analysiert den Pfad relativ zum Monorepo-Root und extrahiert
|
|
den Kontextordner (erste Ebene).
|
|
|
|
Args:
|
|
project_path: Absoluter oder relativer Pfad zum Projekt.
|
|
|
|
Returns:
|
|
Der ermittelte Arbeitskontext als String.
|
|
|
|
Raises:
|
|
ValueError: Wenn der Pfad nicht innerhalb eines bekannten
|
|
Kontextordners liegt.
|
|
"""
|
|
# Pfad auflösen (absolut machen)
|
|
resolved = project_path.resolve()
|
|
root_resolved = self.root_path.resolve()
|
|
|
|
# Prüfen ob der Pfad innerhalb des Monorepos liegt
|
|
try:
|
|
relative = resolved.relative_to(root_resolved)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Der Pfad '{project_path}' liegt nicht innerhalb "
|
|
f"des Monorepos ({self.root_path})."
|
|
)
|
|
|
|
# Ersten Pfadteil (Kontextordner) extrahieren
|
|
parts = relative.parts
|
|
if not parts:
|
|
raise ValueError(
|
|
f"Der Pfad '{project_path}' zeigt auf das Monorepo-Root, "
|
|
f"nicht auf ein Projekt in einem Kontext."
|
|
)
|
|
|
|
context_name = parts[0]
|
|
if context_name not in self.CONTEXTS:
|
|
raise ValueError(
|
|
f"Der Pfad '{project_path}' liegt nicht in einem bekannten "
|
|
f"Kontextordner. Gefunden: '{context_name}', "
|
|
f"erwartet: {', '.join(self.CONTEXTS)}."
|
|
)
|
|
|
|
return context_name
|