Files
Projektwissen/scripts/setup_workspace.py
T
Michael Otto 85a34e4d1d Lokal abweichenden Ablageort der Plugin-Klone unterstuetzen (#6)
hallo.py, setup_workspace.py und link_qgis_profile.py gingen bisher fest
davon aus, dass die Repos aus workspace.toml direkte Unterordner von
Projektwissen sind. Eine optionale, nicht versionierte workspace.local.toml
mit [workspace] repos_root = "..." kann das jetzt lokal ueberschreiben, z. B.
wenn die Klone zusaetzlich unter einem separaten src-Ordner liegen und von
dort in mehrere QGIS-Profile verlinkt werden. Ohne diese Datei bleibt das
Verhalten fuer alle unveraendert.
2026-09-14 11:36:55 +02:00

125 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Richtet den AG_QGIS-Workspace ein bzw. aktualisiert ihn.
1. Klont fehlende Repositories aus workspace.toml in Unterordner (Default-Branch).
2. Holt für vorhandene Klone den Stand vom Server (git fetch) und zeigt Branch,
Vorsprung/Rückstand und ungesicherte Änderungen an. Ein Pull passiert nur
mit --pull und nur bei sauberem Arbeitsverzeichnis (fast-forward).
3. Spiegelt die Skills aus .agents/skills/ nach .claude/skills/ (Claude Code liest
nur dort; der Agent-Skills-Standard und andere Werkzeuge lesen .agents/skills/).
Aufrufe:
python scripts/setup_workspace.py # klonen + Status + Skills
python scripts/setup_workspace.py --pull # zusätzlich fast-forward pull
python scripts/setup_workspace.py --nur-skills
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml"
def repos_root() -> Path:
"""Ordner für die Plugin-/Daten-Klone, siehe workspace.local.toml (nicht versioniert)."""
if not LOKALE_ÜBERSCHREIBUNG.is_file():
return ROOT
with LOKALE_ÜBERSCHREIBUNG.open("rb") as f:
lokal = tomllib.load(f)
pfad = lokal.get("workspace", {}).get("repos_root")
return Path(pfad).expanduser() if pfad else ROOT
def git(*args: str, cwd: Path) -> tuple[int, str]:
p = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, errors="replace")
return p.returncode, (p.stdout + p.stderr).strip()
def sync_skills() -> None:
quelle = ROOT / ".agents" / "skills"
ziel = ROOT / ".claude" / "skills"
if not quelle.is_dir():
print("Keine Skills in .agents/skills/ gefunden.")
return
ziel.parent.mkdir(exist_ok=True)
# Alten Spiegel entfernen (Symlink, Junction oder Kopie)
if ziel.is_symlink() or (ziel.exists() and not ziel.is_dir()):
ziel.unlink()
elif ziel.is_dir():
try:
os.rmdir(ziel) # leere Junction/Ordner
except OSError:
shutil.rmtree(ziel)
art = "symlink"
try:
os.symlink(quelle, ziel, target_is_directory=True)
except (OSError, NotImplementedError):
art = "kopie"
if os.name == "nt":
try:
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
art = "junction"
except Exception:
pass
if art == "kopie":
shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("__pycache__"))
anzahl = len([p for p in quelle.iterdir() if (p / "SKILL.md").is_file()])
print(f"Skills: {anzahl} aus .agents/skills/ nach .claude/skills/ gespiegelt ({art}).")
if art == "kopie":
print(" Hinweis: Kopie statt Link – nach Änderungen an .agents/skills/ dieses Skript erneut ausführen.")
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--pull", action="store_true", help="saubere Klone per fast-forward aktualisieren")
ap.add_argument("--nur-skills", action="store_true", help="nur Skills spiegeln, kein Git")
args = ap.parse_args()
if not args.nur_skills:
with (ROOT / "workspace.toml").open("rb") as f:
manifest = tomllib.load(f)
ws = manifest["workspace"]
basis_url = f"{ws['gitea']}/{ws['org']}"
rroot = repos_root()
print(f"Workspace: {ROOT}\nGitea: {basis_url}\n")
if rroot != ROOT:
print(f"Repositories liegen laut workspace.local.toml unter: {rroot}\n")
rroot.mkdir(parents=True, exist_ok=True)
for repo in manifest["repos"]:
name, branch = repo["name"], repo["branch"]
pfad = rroot / name
if not (pfad / ".git").is_dir():
print(f"[{name}] klone Branch {branch} ...")
rc, out = git("clone", "--branch", branch, f"{basis_url}/{name}.git", name, cwd=rroot)
print(" " + ("ok" if rc == 0 else f"FEHLER: {out}"))
continue
git("fetch", "--quiet", "--prune", cwd=pfad)
_, aktuell = git("rev-parse", "--abbrev-ref", "HEAD", cwd=pfad)
_, dirty = git("status", "--porcelain", cwd=pfad)
n_dirty = len([z for z in dirty.splitlines() if z.strip()])
_, ab = git("rev-list", "--left-right", "--count", f"HEAD...origin/{aktuell}", cwd=pfad)
vor, _, zurueck = ab.partition("\t") if "\t" in ab else ("?", "", "?")
meldung = f"[{name}] Branch {aktuell} (+{vor}/-{zurueck.strip()} zu origin), {n_dirty} ungesicherte Änderungen"
if aktuell != branch:
meldung += f" <- Hinweis: Manifest erwartet {branch}"
if args.pull and n_dirty == 0 and aktuell == branch:
rc, out = git("pull", "--ff-only", "--quiet", cwd=pfad)
meldung += " -> pull " + ("ok" if rc == 0 else f"FEHLER: {out.splitlines()[-1] if out else rc}")
print(meldung)
print()
sync_skills()
return 0
if __name__ == "__main__":
raise SystemExit(main())