Nimm weitere Git-Repos als Quellen in die Ablage — zuerst die info-Doku
KM_SOURCES=name=url@branch legt je Quelle einen Checkout unter <ns>/doku/<name> in der Ablage an. SilverBullet sieht die Seiten, der Index nimmt sie mit (typ doku, projekt = Name), und je Takt gehen Änderungen aus der Oberfläche als Commit ins Quell-Repo zurück, danach pull --rebase. Die Ablage ignoriert */doku/, damit die Wahrheit nur im Quell-Repo liegt. Vorerst komplett und ungefiltert (Andreas 04.09.: "sobald hive.corp steht, trennen wir die Quellen"). Doku in docs/quellen.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,8 @@ services:
|
|||||||
KM_GITEA_PASSWORD: ${KM_GITEA_PASSWORD}
|
KM_GITEA_PASSWORD: ${KM_GITEA_PASSWORD}
|
||||||
KM_GITEA_ORGS: ${KM_GITEA_ORGS}
|
KM_GITEA_ORGS: ${KM_GITEA_ORGS}
|
||||||
KM_GITEA_REPOS: ${KM_GITEA_REPOS}
|
KM_GITEA_REPOS: ${KM_GITEA_REPOS}
|
||||||
|
# weitere Git-Quellen als Checkout unter <ns>/doku/<name> (docs/quellen.md)
|
||||||
|
KM_SOURCES: ${KM_SOURCES}
|
||||||
KM_INTERVAL: ${KM_INTERVAL}
|
KM_INTERVAL: ${KM_INTERVAL}
|
||||||
# Vektoren erst, wenn es einen Abnehmer gibt (Andreas 04.09.); die
|
# Vektoren erst, wenn es einen Abnehmer gibt (Andreas 04.09.); die
|
||||||
# Ollama-Instanz auf s18 ist vom LXC aus nur mit VLAN5-Route erreichbar.
|
# Ollama-Instanz auf s18 ist vom LXC aus nur mit VLAN5-Route erreichbar.
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ KM_INTERVAL=300
|
|||||||
KM_EMBED=0
|
KM_EMBED=0
|
||||||
KM_OLLAMA_URL=http://10.18.5.30:11434
|
KM_OLLAMA_URL=http://10.18.5.30:11434
|
||||||
SB_USER=andreas:aus-openbao
|
SB_USER=andreas:aus-openbao
|
||||||
|
KM_SOURCES=info=https://git.home/ahmann/info.git@master
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Quellen — fremde Git-Repos als Checkout in der Ablage
|
||||||
|
|
||||||
|
Festlegung Andreas, 2026-09-04: die `info`-Doku kommt als zweite Quelle in
|
||||||
|
km — **vorerst komplett**, ohne Filter nach Klassifikation. *„Sobald
|
||||||
|
hive.corp steht, trennen wir die Quellen."* Konfiguration `KM_SOURCES`, Code
|
||||||
|
in `gitrepo.py` (`sources_of`) und `runner.py`.
|
||||||
|
|
||||||
|
## Bauform
|
||||||
|
|
||||||
|
Jede Quelle ist ein eigener Git-Checkout **in** der Ablage:
|
||||||
|
|
||||||
|
```
|
||||||
|
<ablage>/<ns>/doku/<name>/ ← z. B. familie/doku/info/ = Clone von ahmann/info
|
||||||
|
```
|
||||||
|
|
||||||
|
- SilverBullet sieht die Seiten wie alle anderen; der Index nimmt sie mit
|
||||||
|
(`typ: doku`, `projekt: <name>`, `classification` aus dem Frontmatter).
|
||||||
|
- Die Ablage ignoriert `*/doku/` in ihrer `.gitignore`: die Wahrheit dieser
|
||||||
|
Seiten liegt im Quell-Repo, nicht ein zweites Mal in `ahmann/km`.
|
||||||
|
- **Je Takt:** Änderungen aus der Oberfläche werden im Checkout committet
|
||||||
|
(„km: Änderungen aus der Oberfläche", Autor `KM_GIT_NAME`) und **in das
|
||||||
|
Quell-Repo gepusht**, danach `git pull --rebase`. Ein Commit aus
|
||||||
|
SilverBullet landet also Minuten später auf `master` von `ahmann/info` —
|
||||||
|
ohne PR und ohne Umlaut-Riegel. Das ist gewollt (Auto-Commit und Push).
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
```
|
||||||
|
KM_SOURCES=info=https://git.home/ahmann/info.git@master
|
||||||
|
```
|
||||||
|
|
||||||
|
`name=url[@branch]`, mehrere durch Komma. Zugang: dieselben `KM_GIT_USER`/
|
||||||
|
`KM_GIT_PASSWORD` wie für die Ablage; die `.netrc` bekommt jeden Remote-Host.
|
||||||
|
|
||||||
|
## Was Andreas wissen sollte
|
||||||
|
|
||||||
|
- **Alles ist drin**, auch `corp`- und `lan`-Seiten — bis die Quellen
|
||||||
|
getrennt werden, ist die Familien-Oberfläche damit nicht nur Familie.
|
||||||
|
- **Admonitions** (`!!! warning`) aus MkDocs zeigt SilverBullet als Rohtext
|
||||||
|
(Test vom 04.09., spine/core#8).
|
||||||
|
- Der Checkout enthält das ganze Repo (`ai/`, `deployment/`, …), nicht nur
|
||||||
|
`docs/`; indexiert werden nur `*.md`.
|
||||||
@@ -9,6 +9,7 @@ und nichts tut, ist teurer als einer, der gar nicht startet (spine/images).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,6 +22,19 @@ def _clean(v: str | None) -> str:
|
|||||||
return "" if v.startswith("${") else v
|
return "" if v.startswith("${") else v
|
||||||
|
|
||||||
|
|
||||||
|
def _sources(v: str) -> list[tuple[str, str, str]]:
|
||||||
|
out = []
|
||||||
|
for item in _list(v):
|
||||||
|
if "=" not in item:
|
||||||
|
raise SystemExit(f"km: KM_SOURCES: '{item}' braucht die Form name=url[@branch]")
|
||||||
|
name, rest = item.split("=", 1)
|
||||||
|
url, _, branch = rest.rpartition("@") if "@" in rest.split("/")[-1] else (rest, "", "")
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9_.-]+", name):
|
||||||
|
raise SystemExit(f"km: KM_SOURCES: Name '{name}' ungueltig")
|
||||||
|
out.append((name, url, branch or "main"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _list(v: str) -> list[str]:
|
def _list(v: str) -> list[str]:
|
||||||
return [x.strip() for x in _clean(v).replace(";", ",").split(",") if x.strip()]
|
return [x.strip() for x in _clean(v).replace(";", ",").split(",") if x.strip()]
|
||||||
|
|
||||||
@@ -41,6 +55,11 @@ class Config:
|
|||||||
gitea_password: str | None
|
gitea_password: str | None
|
||||||
gitea_orgs: list[str] = field(default_factory=list)
|
gitea_orgs: list[str] = field(default_factory=list)
|
||||||
gitea_repos: list[str] = field(default_factory=list)
|
gitea_repos: list[str] = field(default_factory=list)
|
||||||
|
# weitere Quellen als eigene Git-Checkouts unter <ns>/doku/<name>,
|
||||||
|
# komplett und ungefiltert (Andreas 04.09.: "vorerst alles komplett rein,
|
||||||
|
# sobald hive.corp steht, trennen wir die Quellen").
|
||||||
|
# "info=https://git.home/ahmann/info.git@master"
|
||||||
|
sources: list[tuple[str, str, str]] = field(default_factory=list)
|
||||||
interval: int = 300
|
interval: int = 300
|
||||||
embed: bool = False
|
embed: bool = False
|
||||||
ollama_url: str = "http://10.18.5.30:11434"
|
ollama_url: str = "http://10.18.5.30:11434"
|
||||||
@@ -74,6 +93,7 @@ class Config:
|
|||||||
gitea_password=e("KM_GITEA_PASSWORD") or None,
|
gitea_password=e("KM_GITEA_PASSWORD") or None,
|
||||||
gitea_orgs=_list(e("KM_GITEA_ORGS", "")),
|
gitea_orgs=_list(e("KM_GITEA_ORGS", "")),
|
||||||
gitea_repos=_list(e("KM_GITEA_REPOS", "")),
|
gitea_repos=_list(e("KM_GITEA_REPOS", "")),
|
||||||
|
sources=_sources(e("KM_SOURCES", "")),
|
||||||
interval=int(e("KM_INTERVAL", "300")),
|
interval=int(e("KM_INTERVAL", "300")),
|
||||||
embed=e("KM_EMBED", "0").lower() in ("1", "true", "yes", "ja"),
|
embed=e("KM_EMBED", "0").lower() in ("1", "true", "yes", "ja"),
|
||||||
ollama_url=e("KM_OLLAMA_URL", "http://10.18.5.30:11434"),
|
ollama_url=e("KM_OLLAMA_URL", "http://10.18.5.30:11434"),
|
||||||
@@ -86,6 +106,8 @@ class Config:
|
|||||||
missing.append("KM_NAMESPACE (z. B. familie oder 42i)")
|
missing.append("KM_NAMESPACE (z. B. familie oder 42i)")
|
||||||
if cfg.git_remote and cfg.git_remote.startswith("http") and not (cfg.git_user and cfg.git_password):
|
if cfg.git_remote and cfg.git_remote.startswith("http") and not (cfg.git_user and cfg.git_password):
|
||||||
missing.append("KM_GIT_USER und KM_GIT_PASSWORD (Push an KM_GIT_REMOTE)")
|
missing.append("KM_GIT_USER und KM_GIT_PASSWORD (Push an KM_GIT_REMOTE)")
|
||||||
|
if any(u.startswith("http") for _, u, _ in cfg.sources) and not (cfg.git_user and cfg.git_password):
|
||||||
|
missing.append("KM_GIT_USER und KM_GIT_PASSWORD (Quellen in KM_SOURCES)")
|
||||||
if cfg.gitea_url and not (cfg.gitea_user and cfg.gitea_password):
|
if cfg.gitea_url and not (cfg.gitea_user and cfg.gitea_password):
|
||||||
missing.append("KM_GITEA_USER und KM_GITEA_PASSWORD (Ingest von KM_GITEA_URL)")
|
missing.append("KM_GITEA_USER und KM_GITEA_PASSWORD (Ingest von KM_GITEA_URL)")
|
||||||
if cfg.gitea_url and not (cfg.gitea_orgs or cfg.gitea_repos):
|
if cfg.gitea_url and not (cfg.gitea_orgs or cfg.gitea_repos):
|
||||||
|
|||||||
+52
-27
@@ -26,65 +26,82 @@ _REJECTED = re.compile(r"\b(rejected|non-fast-forward|fetch first|behind)\b", re
|
|||||||
|
|
||||||
|
|
||||||
class Ablage:
|
class Ablage:
|
||||||
def __init__(self, cfg: Config) -> None:
|
"""Ein Git-Checkout, den km fuehrt: die Ablage selbst oder eine Quelle
|
||||||
|
(z. B. die info-Doku), die unter <ns>/quellen/<name> eingehaengt ist und
|
||||||
|
ihr eigenes Remote behaelt -- Aenderungen aus der Oberflaeche gehen dort
|
||||||
|
zurueck, nicht in eine Kopie."""
|
||||||
|
|
||||||
|
def __init__(self, cfg: Config, path: Path | None = None, remote: str | None = None,
|
||||||
|
branch: str | None = None, name: str = "ablage") -> None:
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
|
self.name = name
|
||||||
|
self.path = path or cfg.ablage
|
||||||
|
self.remote = cfg.git_remote if path is None else remote
|
||||||
|
self.branch = branch or cfg.git_branch
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.last_push_error: str | None = None
|
self.last_push_error: str | None = None
|
||||||
self.last_push: float | None = None
|
self.last_push: float | None = None
|
||||||
|
|
||||||
# ------------------------------------------------------------ Hilfen
|
# ------------------------------------------------------------ Hilfen
|
||||||
def _git(self, *args: str, check: bool = True) -> subprocess.CompletedProcess:
|
def _git(self, *args: str, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
return subprocess.run(["git", *args], cwd=self.cfg.ablage, check=check,
|
return subprocess.run(["git", *args], cwd=self.path, check=check,
|
||||||
capture_output=True, text=True)
|
capture_output=True, text=True)
|
||||||
|
|
||||||
def write_netrc(self) -> None:
|
def write_netrc(self) -> None:
|
||||||
if not (self.cfg.git_remote and self.cfg.git_user and self.cfg.git_password):
|
"""Zugang fuer jeden Remote-Host (Ablage und Quellen) in ~/.netrc, 0600."""
|
||||||
return
|
if not (self.cfg.git_user and self.cfg.git_password):
|
||||||
host = urlparse(self.cfg.git_remote).hostname
|
|
||||||
if not host:
|
|
||||||
return
|
return
|
||||||
|
hosts = {urlparse(u).hostname for u in [self.cfg.git_remote, *[src[1] for src in self.cfg.sources]] if u}
|
||||||
netrc = Path.home() / ".netrc"
|
netrc = Path.home() / ".netrc"
|
||||||
line = f"machine {host} login {self.cfg.git_user} password {self.cfg.git_password}\n"
|
|
||||||
existing = netrc.read_text() if netrc.exists() else ""
|
existing = netrc.read_text() if netrc.exists() else ""
|
||||||
|
for host in sorted(h for h in hosts if h):
|
||||||
if f"machine {host} " not in existing:
|
if f"machine {host} " not in existing:
|
||||||
netrc.write_text(existing + line)
|
existing += f"machine {host} login {self.cfg.git_user} password {self.cfg.git_password}\n"
|
||||||
|
netrc.write_text(existing)
|
||||||
netrc.chmod(0o600)
|
netrc.chmod(0o600)
|
||||||
|
|
||||||
# ------------------------------------------------------------ Lebenszyklus
|
# ------------------------------------------------------------ Lebenszyklus
|
||||||
def ensure(self) -> None:
|
def ensure(self) -> None:
|
||||||
"""Checkout vorhanden machen: klonen, sonst init -- nie stillschweigend leer."""
|
"""Checkout vorhanden machen: klonen, sonst init -- nie stillschweigend leer."""
|
||||||
a = self.cfg.ablage
|
a = self.path
|
||||||
if not (a / ".git").exists():
|
if not (a / ".git").exists():
|
||||||
# Kein clone: das Verzeichnis existiert oft schon, weil SilverBullet
|
# Kein clone: das Verzeichnis existiert oft schon, weil SilverBullet
|
||||||
# dasselbe Volume mountet und dort seine Dateien anlegt, bevor km
|
# dasselbe Volume mountet und dort seine Dateien anlegt, bevor km
|
||||||
# dran ist. Also in place initialisieren und den Remote-Stand holen.
|
# dran ist. Also in place initialisieren und den Remote-Stand holen.
|
||||||
a.mkdir(parents=True, exist_ok=True)
|
a.mkdir(parents=True, exist_ok=True)
|
||||||
self._git("init", "-q", "-b", self.cfg.git_branch)
|
self._git("init", "-q", "-b", self.branch)
|
||||||
if self.cfg.git_remote:
|
if self.remote:
|
||||||
self._git("remote", "add", "origin", self.cfg.git_remote)
|
self._git("remote", "add", "origin", self.remote)
|
||||||
r = self._git("fetch", "-q", "origin", check=False)
|
r = self._git("fetch", "-q", "origin", check=False)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
raise SystemExit(f"km: fetch von {self.cfg.git_remote} fehlgeschlagen: {r.stderr.strip()[:300]}")
|
raise SystemExit(f"km: fetch von {self.remote} fehlgeschlagen: {r.stderr.strip()[:300]}")
|
||||||
has_branch = self._git("rev-parse", "--verify", "-q", f"origin/{self.cfg.git_branch}", check=False).returncode == 0
|
has_branch = self._git("rev-parse", "--verify", "-q", f"origin/{self.branch}", check=False).returncode == 0
|
||||||
if has_branch:
|
if has_branch:
|
||||||
r = self._git("checkout", "-q", "-B", self.cfg.git_branch, f"origin/{self.cfg.git_branch}", check=False)
|
r = self._git("checkout", "-q", "-B", self.branch, f"origin/{self.branch}", check=False)
|
||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
raise SystemExit(f"km: checkout kollidiert mit vorhandenen Dateien: {r.stderr.strip()[:300]}")
|
raise SystemExit(f"km: checkout kollidiert mit vorhandenen Dateien: {r.stderr.strip()[:300]}")
|
||||||
# sonst: leeres Remote, der erste Push legt den Branch an
|
# sonst: leeres Remote, der erste Push legt den Branch an
|
||||||
self._git("config", "user.name", self.cfg.git_name)
|
self._git("config", "user.name", self.cfg.git_name)
|
||||||
self._git("config", "user.email", self.cfg.git_email)
|
self._git("config", "user.email", self.cfg.git_email)
|
||||||
# Was die Oberflaeche fuer sich selbst ablegt, gehoert nicht in die Wahrheit.
|
if self.name != "ablage":
|
||||||
|
return
|
||||||
|
# Was die Oberflaeche fuer sich selbst ablegt, gehoert nicht in die
|
||||||
|
# Wahrheit; die Quellen haben ihr eigenes Remote und werden hier ignoriert.
|
||||||
gi = a / ".gitignore"
|
gi = a / ".gitignore"
|
||||||
wanted = ".silverbullet*\n_plug/\n"
|
existing = gi.read_text() if gi.exists() else ""
|
||||||
if not gi.exists() or wanted not in gi.read_text():
|
# */doku/ sind die Quellen-Checkouts -- deren Wahrheit liegt im
|
||||||
gi.write_text((gi.read_text() if gi.exists() else "") + wanted)
|
# jeweiligen Remote (z. B. ahmann/info), nicht ein zweites Mal hier.
|
||||||
|
for line in (".silverbullet*", "_plug/", "*/doku/"):
|
||||||
|
if line not in existing.splitlines():
|
||||||
|
existing += line + "\n"
|
||||||
|
gi.write_text(existing)
|
||||||
|
|
||||||
def pull(self) -> None:
|
def pull(self) -> None:
|
||||||
if not self.cfg.git_remote:
|
if not self.remote:
|
||||||
return
|
return
|
||||||
r = self._git("pull", "--rebase", "--autostash", "origin", self.cfg.git_branch, check=False)
|
r = self._git("pull", "--rebase", "--autostash", "origin", self.branch, check=False)
|
||||||
if r.returncode != 0 and "couldn't find remote ref" not in r.stderr:
|
if r.returncode != 0 and "couldn't find remote ref" not in r.stderr:
|
||||||
print(f"km: pull: {r.stderr.strip()[:300]}", flush=True)
|
print(f"km: pull {self.name}: {r.stderr.strip()[:300]}", flush=True)
|
||||||
|
|
||||||
def commit_all(self, message: str) -> bool:
|
def commit_all(self, message: str) -> bool:
|
||||||
"""Alles Geaenderte einliefern (auch Aenderungen aus SilverBullet). True, wenn ein Commit entstand."""
|
"""Alles Geaenderte einliefern (auch Aenderungen aus SilverBullet). True, wenn ein Commit entstand."""
|
||||||
@@ -96,19 +113,19 @@ class Ablage:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def push(self) -> None:
|
def push(self) -> None:
|
||||||
if not self.cfg.git_remote:
|
if not self.remote:
|
||||||
return
|
return
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
r = self._git("push", "-u", "origin", self.cfg.git_branch, check=False)
|
r = self._git("push", "-u", "origin", self.branch, check=False)
|
||||||
if r.returncode == 0:
|
if r.returncode == 0:
|
||||||
self.last_push_error = None
|
self.last_push_error = None
|
||||||
self.last_push = time.time()
|
self.last_push = time.time()
|
||||||
return
|
return
|
||||||
if not _REJECTED.search(r.stderr):
|
if not _REJECTED.search(r.stderr):
|
||||||
self.last_push_error = r.stderr.strip()[:300]
|
self.last_push_error = r.stderr.strip()[:300]
|
||||||
print(f"km: push fehlgeschlagen: {self.last_push_error}", flush=True)
|
print(f"km: push {self.name} fehlgeschlagen: {self.last_push_error}", flush=True)
|
||||||
return
|
return
|
||||||
self._git("pull", "--rebase", "origin", self.cfg.git_branch, check=False)
|
self._git("pull", "--rebase", "origin", self.branch, check=False)
|
||||||
self.last_push_error = "push nach 3 Versuchen abgelehnt"
|
self.last_push_error = "push nach 3 Versuchen abgelehnt"
|
||||||
print(f"km: {self.last_push_error}", flush=True)
|
print(f"km: {self.last_push_error}", flush=True)
|
||||||
|
|
||||||
@@ -117,5 +134,13 @@ class Ablage:
|
|||||||
return r.stdout.strip() or None
|
return r.stdout.strip() or None
|
||||||
|
|
||||||
def status(self) -> dict:
|
def status(self) -> dict:
|
||||||
return {"head": self.head(), "remote": bool(self.cfg.git_remote),
|
return {"head": self.head(), "remote": bool(self.remote),
|
||||||
"letzter_push": self.last_push, "push_fehler": self.last_push_error}
|
"letzter_push": self.last_push, "push_fehler": self.last_push_error}
|
||||||
|
|
||||||
|
|
||||||
|
def sources_of(cfg: Config) -> list[Ablage]:
|
||||||
|
"""Die Quellen als Checkouts unter <ns>/doku/<name> in der Ablage: die
|
||||||
|
Oberflaeche sieht sie, km zieht sie je Takt und liefert Aenderungen aus
|
||||||
|
der Oberflaeche in ihr eigenes Remote ein."""
|
||||||
|
return [Ablage(cfg, cfg.ablage / cfg.namespace / "doku" / name, url, branch, name=name)
|
||||||
|
for name, url, branch in cfg.sources]
|
||||||
|
|||||||
+6
-4
@@ -110,17 +110,19 @@ def derive_title(body: str, fallback: str) -> str:
|
|||||||
return m.group(1).strip() if m else fallback
|
return m.group(1).strip() if m else fallback
|
||||||
|
|
||||||
|
|
||||||
def doc_fields(rel: str, meta: dict[str, str], body: str, default_ns: str) -> dict:
|
def doc_fields(rel: str, meta: dict[str, str], body: str, default_ns: str, default_cls: str = "lan") -> dict:
|
||||||
parts = rel.split("/")
|
parts = rel.split("/")
|
||||||
namespace = meta.get("namespace") or (parts[0] if len(parts) > 1 else default_ns)
|
namespace = meta.get("namespace") or (parts[0] if len(parts) > 1 else default_ns)
|
||||||
typ = meta.get("typ") or meta.get("type") or ("vorgang" if meta.get("quelle") == "gitea" else "doku")
|
typ = meta.get("typ") or meta.get("type") or ("vorgang" if meta.get("quelle") == "gitea" else "doku")
|
||||||
projekt = meta.get("projekt") or meta.get("project")
|
projekt = meta.get("projekt") or meta.get("project")
|
||||||
if not projekt and typ == "vorgang" and meta.get("repo"):
|
if not projekt and typ == "vorgang" and meta.get("repo"):
|
||||||
projekt = meta["repo"].split("/")[0]
|
projekt = meta["repo"].split("/")[0]
|
||||||
|
if not projekt and len(parts) > 2 and parts[1] == "doku":
|
||||||
|
projekt = parts[2] # <ns>/doku/<name>/… (Quellen-Checkout)
|
||||||
return {
|
return {
|
||||||
"namespace": namespace,
|
"namespace": namespace,
|
||||||
"projekt": projekt or None,
|
"projekt": projekt or None,
|
||||||
"classification": meta.get("classification") or "lan",
|
"classification": meta.get("classification") or default_cls,
|
||||||
"typ": typ,
|
"typ": typ,
|
||||||
"zustand": meta.get("zustand") or meta.get("state"),
|
"zustand": meta.get("zustand") or meta.get("state"),
|
||||||
"titel": meta.get("titel") or meta.get("title") or derive_title(body, rel),
|
"titel": meta.get("titel") or meta.get("title") or derive_title(body, rel),
|
||||||
@@ -222,7 +224,7 @@ def sha256(data: bytes) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str,
|
def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str,
|
||||||
max_chars: int, overlap: int) -> bool:
|
max_chars: int, overlap: int, default_cls: str = "lan") -> bool:
|
||||||
rel = f.relative_to(ablage).as_posix()
|
rel = f.relative_to(ablage).as_posix()
|
||||||
raw = f.read_bytes()
|
raw = f.read_bytes()
|
||||||
digest = sha256(raw)
|
digest = sha256(raw)
|
||||||
@@ -230,7 +232,7 @@ def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str,
|
|||||||
if row and row[0] == digest:
|
if row and row[0] == digest:
|
||||||
return False
|
return False
|
||||||
meta, body = parse_frontmatter(raw.decode("utf-8", errors="replace"))
|
meta, body = parse_frontmatter(raw.decode("utf-8", errors="replace"))
|
||||||
fields = doc_fields(rel, meta, body, default_ns)
|
fields = doc_fields(rel, meta, body, default_ns, default_cls)
|
||||||
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||||
with con:
|
with con:
|
||||||
old_ids = [r[0] for r in con.execute("SELECT id FROM chunks WHERE path=?", (rel,))]
|
old_ids = [r[0] for r in con.execute("SELECT id FROM chunks WHERE path=?", (rel,))]
|
||||||
|
|||||||
+5
-2
@@ -7,7 +7,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
from config import Config
|
from config import Config
|
||||||
from gitrepo import Ablage
|
from gitrepo import Ablage, sources_of
|
||||||
from runner import Runner
|
from runner import Runner
|
||||||
from server import Api, serve
|
from server import Api, serve
|
||||||
|
|
||||||
@@ -17,9 +17,12 @@ def main() -> None:
|
|||||||
ablage = Ablage(cfg)
|
ablage = Ablage(cfg)
|
||||||
ablage.write_netrc()
|
ablage.write_netrc()
|
||||||
ablage.ensure()
|
ablage.ensure()
|
||||||
|
for src in sources_of(cfg):
|
||||||
|
src.ensure()
|
||||||
runner = Runner(cfg, ablage)
|
runner = Runner(cfg, ablage)
|
||||||
print(f"km: namespace={cfg.namespace} ablage={cfg.ablage} remote={'ja' if cfg.git_remote else 'nein'} "
|
print(f"km: namespace={cfg.namespace} ablage={cfg.ablage} remote={'ja' if cfg.git_remote else 'nein'} "
|
||||||
f"gitea={cfg.gitea_url or '-'} takt={cfg.interval}s embed={'an' if cfg.embed else 'aus'}",
|
f"gitea={cfg.gitea_url or '-'} quellen={','.join(n for n, _, _ in cfg.sources) or '-'} "
|
||||||
|
f"takt={cfg.interval}s embed={'an' if cfg.embed else 'aus'}",
|
||||||
file=sys.stderr, flush=True)
|
file=sys.stderr, flush=True)
|
||||||
threading.Thread(target=runner.loop, name="takt", daemon=True).start()
|
threading.Thread(target=runner.loop, name="takt", daemon=True).start()
|
||||||
serve(cfg, Api(cfg, ablage, runner))
|
serve(cfg, Api(cfg, ablage, runner))
|
||||||
|
|||||||
+12
-2
@@ -15,7 +15,7 @@ import traceback
|
|||||||
|
|
||||||
import index as km_index
|
import index as km_index
|
||||||
from config import Config
|
from config import Config
|
||||||
from gitrepo import Ablage
|
from gitrepo import Ablage, sources_of
|
||||||
from ingest_gitea import Forge, ingest_repo, load_state, save_state
|
from ingest_gitea import Forge, ingest_repo, load_state, save_state
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ class Runner:
|
|||||||
def __init__(self, cfg: Config, ablage: Ablage) -> None:
|
def __init__(self, cfg: Config, ablage: Ablage) -> None:
|
||||||
self.cfg = cfg
|
self.cfg = cfg
|
||||||
self.ablage = ablage
|
self.ablage = ablage
|
||||||
|
self.sources = sources_of(cfg)
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.last: dict = {"zeit": None, "dauer": None, "fehler": None, "ergebnis": None}
|
self.last: dict = {"zeit": None, "dauer": None, "fehler": None, "ergebnis": None}
|
||||||
self._wake = threading.Event()
|
self._wake = threading.Event()
|
||||||
@@ -36,6 +37,15 @@ class Runner:
|
|||||||
result: dict = {}
|
result: dict = {}
|
||||||
try:
|
try:
|
||||||
self.ablage.pull()
|
self.ablage.pull()
|
||||||
|
# Quellen: erst Aenderungen aus der Oberflaeche in ihr Remote,
|
||||||
|
# dann ziehen -- die Wahrheit einer info-Seite bleibt in info.
|
||||||
|
for src in self.sources:
|
||||||
|
if src.commit_all("km: Änderungen aus der Oberfläche"):
|
||||||
|
src.push()
|
||||||
|
result[f"commit_{src.name}"] = src.head()
|
||||||
|
src.pull()
|
||||||
|
if src.last_push_error:
|
||||||
|
result[f"push_fehler_{src.name}"] = src.last_push_error
|
||||||
result["ingest"] = self._ingest(full)
|
result["ingest"] = self._ingest(full)
|
||||||
result["index"] = self._index()
|
result["index"] = self._index()
|
||||||
if self.ablage.commit_all(self._message(result)):
|
if self.ablage.commit_all(self._message(result)):
|
||||||
@@ -81,7 +91,7 @@ class Runner:
|
|||||||
con = km_index.connect(c.db)
|
con = km_index.connect(c.db)
|
||||||
try:
|
try:
|
||||||
files = sorted(p for p in c.ablage.rglob("*.md") if not any(s.startswith(".") for s in p.parts))
|
files = sorted(p for p in c.ablage.rglob("*.md") if not any(s.startswith(".") for s in p.parts))
|
||||||
changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60) for f in files)
|
changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60, c.classification) for f in files)
|
||||||
gone = km_index.remove_missing(con, c.ablage)
|
gone = km_index.remove_missing(con, c.ablage)
|
||||||
out = {"dateien": len(files), "geaendert": changed, "entfernt": gone}
|
out = {"dateien": len(files), "geaendert": changed, "entfernt": gone}
|
||||||
if c.embed and changed:
|
if c.embed and changed:
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class Api:
|
|||||||
last = self.runner.last
|
last = self.runner.last
|
||||||
return {"ok": last.get("fehler") is None, "namespace": self.cfg.namespace,
|
return {"ok": last.get("fehler") is None, "namespace": self.cfg.namespace,
|
||||||
"dokumente": docs, "eingebettet": embedded, "git": self.ablage.status(),
|
"dokumente": docs, "eingebettet": embedded, "git": self.ablage.status(),
|
||||||
|
"quellen": {s.name: s.status() for s in self.runner.sources},
|
||||||
"letzter_lauf": last}
|
"letzter_lauf": last}
|
||||||
|
|
||||||
def search(self, a: _Args) -> list[dict]:
|
def search(self, a: _Args) -> list[dict]:
|
||||||
|
|||||||
Reference in New Issue
Block a user