Nimm die Quellen-Checkouts aus der Ablage heraus
Andreas 04.09.: "nimm den info-checkout aus silverbullet raus". Die Quellen liegen jetzt unter <data>/quellen/<name> neben der Ablage — die Oberfläche sieht sie nicht mehr. Der Index führt sie unter dem virtuellen Pfad <ns>/doku/<name>/…, read löst ihn auf die Datei im Checkout auf (mit Wurzelprüfung). Kein Zurückschreiben mehr: geändert wird im Quell-Repo, gelesen über die gebaute MkDocs-Seite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+15
-17
@@ -7,21 +7,20 @@ in `gitrepo.py` (`sources_of`) und `runner.py`.
|
|||||||
|
|
||||||
## Bauform
|
## Bauform
|
||||||
|
|
||||||
Jede Quelle ist ein eigener Git-Checkout **in** der Ablage:
|
Jede Quelle ist ein eigener Git-Checkout **neben** der Ablage — außerhalb
|
||||||
|
von SilverBullet (Andreas, 04.09.: „nimm den info-checkout aus silverbullet
|
||||||
|
raus"):
|
||||||
|
|
||||||
```
|
```
|
||||||
<ablage>/<ns>/doku/<name>/ ← z. B. familie/doku/info/ = Clone von ahmann/info
|
<data>/quellen/<name>/ ← z. B. /srv/km/data/quellen/info = Clone von ahmann/info
|
||||||
```
|
```
|
||||||
|
|
||||||
- SilverBullet sieht die Seiten wie alle anderen; der Index nimmt sie mit
|
- Im Index erscheinen die Seiten unter dem virtuellen Pfad
|
||||||
(`typ: doku`, `projekt: <name>`, `classification` aus dem Frontmatter).
|
`<ns>/doku/<name>/<pfad>` (`typ: doku`, `projekt: <name>`,
|
||||||
- Die Ablage ignoriert `*/doku/` in ihrer `.gitignore`: die Wahrheit dieser
|
`classification` aus dem Frontmatter); `read` löst ihn auf die Datei im
|
||||||
Seiten liegt im Quell-Repo, nicht ein zweites Mal in `ahmann/km`.
|
Checkout auf.
|
||||||
- **Je Takt:** Änderungen aus der Oberfläche werden im Checkout committet
|
- **Je Takt:** `git pull --rebase`, danach der statische Bau (unten). Es
|
||||||
(„km: Änderungen aus der Oberfläche", Autor `KM_GIT_NAME`) und **in das
|
wird nichts zurückgeschrieben — geändert wird im Quell-Repo.
|
||||||
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
|
## Konfiguration
|
||||||
|
|
||||||
@@ -37,14 +36,13 @@ KM_SOURCES=info=https://git.home/ahmann/info.git@master
|
|||||||
Hat die Quelle ein `mkdocs.yml`, baut km sie nach jedem Pull mit Änderung
|
Hat die Quelle ein `mkdocs.yml`, baut km sie nach jedem Pull mit Änderung
|
||||||
mit `python -m mkdocs build` nach `<data>/site/<name>` — die gewohnte
|
mit `python -m mkdocs build` nach `<data>/site/<name>` — die gewohnte
|
||||||
MkDocs-Darstellung, read-only, ohne laufenden Dienst. Caddy liefert sie unter
|
MkDocs-Darstellung, read-only, ohne laufenden Dienst. Caddy liefert sie unter
|
||||||
`/doku/<name>/` aus (deploy/README.md). SilverBullet bleibt für die Ablage
|
`/doku/<name>/` aus (deploy/README.md). SilverBullet zeigt nur die Ablage
|
||||||
selbst; für die Doku ist die gebaute Seite der Leseweg.
|
selbst; für die Doku ist die gebaute Seite der einzige Leseweg.
|
||||||
|
|
||||||
## Was Andreas wissen sollte
|
## Was Andreas wissen sollte
|
||||||
|
|
||||||
- **Alles ist drin**, auch `corp`- und `lan`-Seiten — bis die Quellen
|
- **Alles ist drin**, auch `corp`- und `lan`-Seiten — bis die Quellen
|
||||||
getrennt werden, ist die Familien-Oberfläche damit nicht nur Familie.
|
getrennt werden, zeigt die gebaute Seite der Familie 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
|
- Der Checkout enthält das ganze Repo (`ai/`, `deployment/`, …), nicht nur
|
||||||
`docs/`; indexiert werden nur `*.md`.
|
`docs/`; indexiert werden alle `*.md`, gebaut wird nur, was `mkdocs.yml`
|
||||||
|
kennt.
|
||||||
|
|||||||
+19
-3
@@ -55,9 +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>,
|
# weitere Quellen als eigene Git-Checkouts unter <data>/quellen/<name>
|
||||||
# komplett und ungefiltert (Andreas 04.09.: "vorerst alles komplett rein,
|
# (ausserhalb der Ablage, nicht in SilverBullet), im Index unter dem
|
||||||
# sobald hive.corp steht, trennen wir die Quellen").
|
# virtuellen Pfad <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"
|
# "info=https://git.home/ahmann/info.git@master"
|
||||||
sources: list[tuple[str, str, str]] = field(default_factory=list)
|
sources: list[tuple[str, str, str]] = field(default_factory=list)
|
||||||
interval: int = 300
|
interval: int = 300
|
||||||
@@ -75,6 +77,20 @@ class Config:
|
|||||||
def state(self) -> Path:
|
def state(self) -> Path:
|
||||||
return self.ablage.parent / "km-ingest-state.json"
|
return self.ablage.parent / "km-ingest-state.json"
|
||||||
|
|
||||||
|
def source_path(self, name: str) -> Path:
|
||||||
|
return self.ablage.parent / "quellen" / name
|
||||||
|
|
||||||
|
def source_prefix(self, name: str) -> str:
|
||||||
|
return f"{self.namespace}/doku/{name}/"
|
||||||
|
|
||||||
|
def resolve(self, rel: str) -> Path:
|
||||||
|
"""Index-Pfad -> Datei: Ablage, oder Quelle hinter ihrem virtuellen Praefix."""
|
||||||
|
for name, _, _ in self.sources:
|
||||||
|
pre = self.source_prefix(name)
|
||||||
|
if rel.startswith(pre):
|
||||||
|
return self.source_path(name) / rel[len(pre):]
|
||||||
|
return self.ablage / rel
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "Config":
|
def from_env(cls) -> "Config":
|
||||||
e = lambda k, d="": _clean(os.environ.get(k)) or d # noqa: E731
|
e = lambda k, d="": _clean(os.environ.get(k)) or d # noqa: E731
|
||||||
|
|||||||
+6
-7
@@ -89,11 +89,9 @@ class Ablage:
|
|||||||
# Wahrheit; die Quellen haben ihr eigenes Remote und werden hier ignoriert.
|
# Wahrheit; die Quellen haben ihr eigenes Remote und werden hier ignoriert.
|
||||||
gi = a / ".gitignore"
|
gi = a / ".gitignore"
|
||||||
existing = gi.read_text() if gi.exists() else ""
|
existing = gi.read_text() if gi.exists() else ""
|
||||||
# */doku/ sind die Quellen-Checkouts -- deren Wahrheit liegt im
|
|
||||||
# jeweiligen Remote (z. B. ahmann/info), nicht ein zweites Mal hier.
|
|
||||||
# _plug/ bleibt drin: die Plug-Dateien (TreeView) gehoeren zur Ablage,
|
# _plug/ bleibt drin: die Plug-Dateien (TreeView) gehoeren zur Ablage,
|
||||||
# sonst fehlt nach einem frischen Checkout die Seitenleiste.
|
# sonst fehlt nach einem frischen Checkout die Seitenleiste.
|
||||||
for line in (".silverbullet*", "*/doku/"):
|
for line in (".silverbullet*",):
|
||||||
if line not in existing.splitlines():
|
if line not in existing.splitlines():
|
||||||
existing += line + "\n"
|
existing += line + "\n"
|
||||||
gi.write_text(existing)
|
gi.write_text(existing)
|
||||||
@@ -141,8 +139,9 @@ class Ablage:
|
|||||||
|
|
||||||
|
|
||||||
def sources_of(cfg: Config) -> list[Ablage]:
|
def sources_of(cfg: Config) -> list[Ablage]:
|
||||||
"""Die Quellen als Checkouts unter <ns>/doku/<name> in der Ablage: die
|
"""Die Quellen als Checkouts NEBEN der Ablage (<data>/quellen/<name>):
|
||||||
Oberflaeche sieht sie, km zieht sie je Takt und liefert Aenderungen aus
|
SilverBullet sieht sie nicht (Andreas 04.09.: "nimm den info-checkout aus
|
||||||
der Oberflaeche in ihr eigenes Remote ein."""
|
silverbullet raus"); km zieht sie je Takt, baut die statische Seite und
|
||||||
return [Ablage(cfg, cfg.ablage / cfg.namespace / "doku" / name, url, branch, name=name)
|
indexiert sie unter dem virtuellen Pfad <ns>/doku/<name>/…"""
|
||||||
|
return [Ablage(cfg, cfg.source_path(name), url, branch, name=name)
|
||||||
for name, url, branch in cfg.sources]
|
for name, url, branch in cfg.sources]
|
||||||
|
|||||||
+5
-4
@@ -224,8 +224,8 @@ 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, default_cls: str = "lan") -> bool:
|
max_chars: int, overlap: int, default_cls: str = "lan", rel: str | None = None) -> bool:
|
||||||
rel = f.relative_to(ablage).as_posix()
|
rel = rel or f.relative_to(ablage).as_posix()
|
||||||
raw = f.read_bytes()
|
raw = f.read_bytes()
|
||||||
digest = sha256(raw)
|
digest = sha256(raw)
|
||||||
row = con.execute("SELECT sha256 FROM docs WHERE path=?", (rel,)).fetchone()
|
row = con.execute("SELECT sha256 FROM docs WHERE path=?", (rel,)).fetchone()
|
||||||
@@ -254,8 +254,9 @@ def index_file(con: sqlite3.Connection, ablage: Path, f: Path, default_ns: str,
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def remove_missing(con: sqlite3.Connection, ablage: Path) -> int:
|
def remove_missing(con: sqlite3.Connection, ablage: Path, resolve=None) -> int:
|
||||||
gone = [r[0] for r in con.execute("SELECT path FROM docs") if not (ablage / r[0]).exists()]
|
resolve = resolve or (lambda rel: ablage / rel)
|
||||||
|
gone = [r[0] for r in con.execute("SELECT path FROM docs") if not resolve(r[0]).exists()]
|
||||||
with con:
|
with con:
|
||||||
for rel in gone:
|
for rel in gone:
|
||||||
ids = [r[0] for r in con.execute("SELECT id FROM chunks WHERE path=?", (rel,))]
|
ids = [r[0] for r in con.execute("SELECT id FROM chunks WHERE path=?", (rel,))]
|
||||||
|
|||||||
+10
-8
@@ -41,13 +41,8 @@ class Runner:
|
|||||||
# Quellen: erst Aenderungen aus der Oberflaeche in ihr Remote,
|
# Quellen: erst Aenderungen aus der Oberflaeche in ihr Remote,
|
||||||
# dann ziehen -- die Wahrheit einer info-Seite bleibt in info.
|
# dann ziehen -- die Wahrheit einer info-Seite bleibt in info.
|
||||||
for src in self.sources:
|
for src in self.sources:
|
||||||
if src.commit_all("km: Änderungen aus der Oberfläche"):
|
|
||||||
src.push()
|
|
||||||
result[f"commit_{src.name}"] = src.head()
|
|
||||||
before = src.head()
|
before = src.head()
|
||||||
src.pull()
|
src.pull()
|
||||||
if src.last_push_error:
|
|
||||||
result[f"push_fehler_{src.name}"] = src.last_push_error
|
|
||||||
built = self._build_site(src, changed=src.head() != before)
|
built = self._build_site(src, changed=src.head() != before)
|
||||||
if built:
|
if built:
|
||||||
result[f"site_{src.name}"] = built
|
result[f"site_{src.name}"] = built
|
||||||
@@ -115,9 +110,16 @@ class Runner:
|
|||||||
c = self.cfg
|
c = self.cfg
|
||||||
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))
|
def md(root):
|
||||||
changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60, c.classification) for f in files)
|
return sorted(p for p in root.rglob("*.md")
|
||||||
gone = km_index.remove_missing(con, c.ablage)
|
if not any(s.startswith(".") for s in p.relative_to(root).parts))
|
||||||
|
files = [(f, None) for f in md(c.ablage)]
|
||||||
|
for src in self.sources:
|
||||||
|
pre = c.source_prefix(src.name)
|
||||||
|
files += [(f, pre + f.relative_to(src.path).as_posix()) for f in md(src.path)]
|
||||||
|
changed = sum(km_index.index_file(con, c.ablage, f, c.namespace, 600, 60, c.classification, rel)
|
||||||
|
for f, rel in files)
|
||||||
|
gone = km_index.remove_missing(con, c.ablage, c.resolve)
|
||||||
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:
|
||||||
out["eingebettet"] = km_index.embed_pending(
|
out["eingebettet"] = km_index.embed_pending(
|
||||||
|
|||||||
+3
-2
@@ -112,8 +112,9 @@ class Api:
|
|||||||
def read(self, rel: str) -> dict:
|
def read(self, rel: str) -> dict:
|
||||||
if not rel or rel.startswith("/") or ".." in rel.split("/"):
|
if not rel or rel.startswith("/") or ".." in rel.split("/"):
|
||||||
raise ValueError("path ungueltig")
|
raise ValueError("path ungueltig")
|
||||||
f = (self.cfg.ablage / rel).resolve()
|
f = self.cfg.resolve(rel).resolve()
|
||||||
if not str(f).startswith(str(self.cfg.ablage.resolve())) or not f.is_file():
|
roots = [self.cfg.ablage.resolve(), *[self.cfg.source_path(n).resolve() for n, _, _ in self.cfg.sources]]
|
||||||
|
if not any(str(f).startswith(str(r)) for r in roots) or not f.is_file():
|
||||||
raise FileNotFoundError(rel)
|
raise FileNotFoundError(rel)
|
||||||
text = f.read_text(encoding="utf-8")
|
text = f.read_text(encoding="utf-8")
|
||||||
meta, body = km_index.parse_frontmatter(text)
|
meta, body = km_index.parse_frontmatter(text)
|
||||||
|
|||||||
Reference in New Issue
Block a user