Automatischer Push: 45 Datei(en) geändert
- .claude/agents/planer.md - .claude/agents/quorum-stimme.md - .claude/skills/bookstack-upload/SKILL.md - .claude/skills/bookstack-upload/scripts/bookstack/archiv.py - .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py - .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py - .claude/skills/bookstack-upload/scripts/bookstack/bilder.py - .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py - .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py - .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py - .claude/skills/bookstack-upload/scripts/bookstack/struktur.py - .claude/skills/bookstack-upload/scripts/bookstack/verweise.py - … und 33 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
60dee0ab68
commit
deec79a6ea
45 files changed
+8645
-262
No files matched your search
@@ -0,0 +1,490 @@
|
||||
"""Hängt verwaiste Notizen einmalig an ihren Hub an: nur anhängen, nie ersetzen.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-29
|
||||
Plan: docs/plans/2026-09-29-waisen/ im selben Ordner
|
||||
|
||||
Ohne --anwenden ist es ein Probelauf: Er listet je Zieldatei den Text, der
|
||||
angehängt würde, und schreibt nichts. Mit --anwenden kopiert das Skript jede
|
||||
Zieldatei zuerst in den Sicherungsordner außerhalb des Vaults, prüft jede
|
||||
Kopie Byte für Byte und hängt erst danach an. Das Zeilenende jeder Datei
|
||||
bleibt, wie es war. Kein Commit, kein git add.
|
||||
|
||||
REGELN, alle über Muster, ohne Namen einzelner Projekte oder Kunden
|
||||
Code Projekte/<p>/docs/plans/<ordner>/<datei>.md
|
||||
00-uebersicht.md desselben Ordners bekommt eine Zeile mit relativem Link
|
||||
aufs Projekt-README, wenn sie es nicht schon verlinkt, und den Abschnitt
|
||||
"Dateien in diesem Ordner" mit relativen Links auf jede .md im Ordner.
|
||||
Taskdateien fasst das Skript nie an.
|
||||
Code Projekte/<p>/CHANGELOG.md und jede Datei unter Code Projekte/<p>/docs/
|
||||
außerhalb von Planordnern
|
||||
eine Zeile mit relativem Link aufs Projekt-README.
|
||||
docs/plans/<ordner>/<datei>.md an der Vault-Wurzel
|
||||
wie ein Planordner in Code Projekte, aber die Zeile heißt "Vault:" und
|
||||
verlinkt README.md der Vault-Wurzel.
|
||||
jede andere Datei unter docs/ an der Vault-Wurzel
|
||||
eine Zeile mit relativem Link auf README.md der Vault-Wurzel.
|
||||
IT Workspace/**/_arbeitsstand/**
|
||||
Planordner wie oben, aber der Link nach oben ist ein Wikilink auf die
|
||||
einzige _MOC-*.md im Ordner über _arbeitsstand; jede andere Datei
|
||||
bekommt eine Zeile mit diesem Wikilink.
|
||||
IT Workspace/**/findings/<datei>.md
|
||||
der Bericht im Ordner über findings (ABSCHLUSSBERICHT.md, sonst
|
||||
FULL-AUDIT-REPORT.md) bekommt den Abschnitt "Befunddateien dieses Laufs"
|
||||
mit einem Wikilink mit vollem Pfad je Datei.
|
||||
AI/secondbrain/<ordner>/JJJJ-MM-TT-<thema>.md
|
||||
eine Zeile "Tagesnotiz: [[Inbox/JJJJ-MM-TT]]", nur wenn die Tagesnotiz
|
||||
existiert.
|
||||
Alles andere kommt auf die Liste für den Koordinator. Geraten wird nichts.
|
||||
|
||||
ÜBERSPRUNGEN UND AUFGELISTET wird eine Zieldatei, die
|
||||
* in einem Git-Repo im Vault liegt, auch dem der Vault-Wurzel, und dort in
|
||||
irgendeinem Arbeitsbaum uncommittet geändert oder neu ist oder auf einem
|
||||
anderen lokalen Zweig seit dem gemeinsamen Stand geändert wurde; was das
|
||||
Repo ignoriert, zählt nicht als geändert,
|
||||
* nicht als UTF-8 lesbar ist oder gemischte Zeilenenden hat,
|
||||
* die anzuhängende Überschrift oder Linkzeile schon enthält.
|
||||
Ein Git-Repo, das sich nicht prüfen lässt, sperrt alle seine Zieldateien.
|
||||
|
||||
BENANNTE LÜCKEN
|
||||
* Welche Notiz eine Waise ist, entscheidet waisen.py mit dessen Lücken.
|
||||
* Nur lokale Zweige zählen, entfernte nicht.
|
||||
* Eine Datei, die zwischen dem Lesen und dem Anhängen geändert wird, bricht
|
||||
den Lauf ab; was bis dahin angehängt war, nennt die Ausgabe.
|
||||
|
||||
Exit 0: Lauf durch. Exit 2: abgebrochen, die Ursache steht in der Ausgabe.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/linkcheck/waisen_bestand.py --vault D:/Claude
|
||||
python .vault-tools/linkcheck/waisen_bestand.py --vault D:/Claude --anwenden --sicherung <ordner außerhalb des Vaults>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
if str(HIER) not in sys.path:
|
||||
sys.path.insert(0, str(HIER))
|
||||
|
||||
try:
|
||||
import waisen
|
||||
_IMPORTFEHLER = ""
|
||||
except Exception as _fehler:
|
||||
waisen = None
|
||||
_IMPORTFEHLER = f"{type(_fehler).__name__}: {_fehler}"
|
||||
|
||||
ORDNER_KOPF = "## Dateien in diesem Ordner"
|
||||
BEFUND_KOPF = "## Befunddateien dieses Laufs"
|
||||
BERICHTE = ("ABSCHLUSSBERICHT.md", "FULL-AUDIT-REPORT.md")
|
||||
_DATUM = re.compile(r"^(\d{4}-\d{2}-\d{2})")
|
||||
GESCHRIEBEN = []
|
||||
|
||||
|
||||
class Abbruch(Exception):
|
||||
"""Der Lauf kann nicht sicher weiter."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ziel:
|
||||
rel: str
|
||||
zeilen: list = field(default_factory=list)
|
||||
ordner: Optional[str] = None
|
||||
befunde: Optional[str] = None
|
||||
ausloeser: list = field(default_factory=list)
|
||||
hubs: set = field(default_factory=set)
|
||||
gelistet: list = field(default_factory=list)
|
||||
|
||||
|
||||
def existiert(vault: Path, rel: str) -> bool:
|
||||
return (vault / rel).is_file()
|
||||
|
||||
|
||||
def rel_link(von: str, zu: str) -> str:
|
||||
start = von.split("/")[:-1]
|
||||
ziel = zu.split("/")
|
||||
gemeinsam = 0
|
||||
while gemeinsam < min(len(start), len(ziel) - 1) and start[gemeinsam] == ziel[gemeinsam]:
|
||||
gemeinsam += 1
|
||||
teile = [".."] * (len(start) - gemeinsam) + ziel[gemeinsam:]
|
||||
return quote("/".join(teile), safe="/")
|
||||
|
||||
|
||||
def _code_projekt(rel: str, teile: list, vault: Path) -> tuple:
|
||||
readme = "/".join(teile[:2]) + "/README.md"
|
||||
innen = teile[2:]
|
||||
if innen[:2] == ["docs", "plans"] and len(innen) == 4:
|
||||
uebersicht = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
||||
if not existiert(vault, uebersicht):
|
||||
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
||||
if not existiert(vault, readme):
|
||||
return ("rest", "", f"{readme} fehlt", "")
|
||||
return ("ordner", uebersicht, f"Projekt: [README]({rel_link(uebersicht, readme)})", readme)
|
||||
if innen[:2] == ["docs", "plans"] and len(innen) > 4:
|
||||
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
||||
if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 2):
|
||||
if not existiert(vault, readme):
|
||||
return ("rest", "", f"{readme} fehlt", "")
|
||||
return ("zeile", rel, f"Projekt: [README]({rel_link(rel, readme)})", readme)
|
||||
return ("rest", "", "keine Regel passt", "")
|
||||
|
||||
|
||||
def _arbeitsstand(rel: str, teile: list, vault: Path) -> tuple:
|
||||
stelle = teile.index("_arbeitsstand")
|
||||
projekt = "/".join(teile[:stelle])
|
||||
try:
|
||||
mocs = sorted(
|
||||
name for name in os.listdir(vault / projekt)
|
||||
if name.startswith("_MOC-") and name.lower().endswith(".md") and (vault / projekt / name).is_file()
|
||||
)
|
||||
except OSError as fehler:
|
||||
return ("rest", "", f"Projektordner nicht lesbar ({fehler})", "")
|
||||
if len(mocs) != 1:
|
||||
return ("rest", "", f"{len(mocs)} _MOC-Dateien in {projekt}, erwartet genau eine", "")
|
||||
moc = f"{projekt}/{mocs[0]}"
|
||||
zeile = f"Projekt: [[{moc[:-3]}]]"
|
||||
innen = teile[stelle + 1:]
|
||||
if innen[:2] == ["docs", "plans"] and len(innen) == 4:
|
||||
uebersicht = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
||||
if not existiert(vault, uebersicht):
|
||||
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
||||
return ("ordner", uebersicht, zeile, moc)
|
||||
if innen[:2] == ["docs", "plans"] and len(innen) > 4:
|
||||
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
||||
return ("zeile", rel, zeile, moc)
|
||||
|
||||
|
||||
def _befund(rel: str, teile: list, vault: Path) -> tuple:
|
||||
lauf = "/".join(teile[:-2])
|
||||
for name in BERICHTE:
|
||||
if existiert(vault, f"{lauf}/{name}"):
|
||||
bericht = f"{lauf}/{name}"
|
||||
return ("befund", bericht, "/".join(teile[:-1]), bericht)
|
||||
return ("rest", "", f"kein Bericht ({' oder '.join(BERICHTE)}) in {lauf}", "")
|
||||
|
||||
|
||||
def _dump(rel: str, teile: list, vault: Path) -> tuple:
|
||||
treffer = _DATUM.match(teile[3])
|
||||
if not treffer:
|
||||
return ("rest", "", "Dump ohne Datum am Anfang des Dateinamens", "")
|
||||
tag = treffer.group(1)
|
||||
if not existiert(vault, f"Inbox/{tag}.md"):
|
||||
return ("rest", "", f"Tagesnotiz Inbox/{tag}.md fehlt", "")
|
||||
return ("zeile", rel, f"Tagesnotiz: [[Inbox/{tag}]]", f"Inbox/{tag}.md")
|
||||
|
||||
|
||||
def _wurzel_docs(rel: str, teile: list, vault: Path) -> tuple:
|
||||
# Pläne und Specs unter docs/ an der Vault-Wurzel: Hub ist README.md dort.
|
||||
hub = "README.md"
|
||||
if not existiert(vault, hub):
|
||||
return ("rest", "", "README.md der Vault-Wurzel fehlt", "")
|
||||
if teile[1] == "plans" and len(teile) == 4:
|
||||
kopf = "/".join(teile[:-1]) + "/00-uebersicht.md"
|
||||
if not existiert(vault, kopf):
|
||||
return ("rest", "", "Planordner ohne 00-uebersicht.md", "")
|
||||
return ("ordner", kopf, f"Vault: [README]({rel_link(kopf, hub)})", hub)
|
||||
if teile[1] == "plans" and len(teile) > 4:
|
||||
return ("rest", "", "Plandatei tiefer als ein Ordner unter docs/plans", "")
|
||||
return ("zeile", rel, f"Vault: [README]({rel_link(rel, hub)})", hub)
|
||||
|
||||
|
||||
def klassifiziere(rel: str, vault: Path) -> tuple:
|
||||
"""Liefert (art, ziel, wert, hub). art ist zeile, ordner, befund oder rest."""
|
||||
teile = rel.split("/")
|
||||
if teile[0] == "Code Projekte" and len(teile) >= 3:
|
||||
return _code_projekt(rel, teile, vault)
|
||||
if teile[0] == "IT Workspace" and "_arbeitsstand" in teile[1:-1]:
|
||||
return _arbeitsstand(rel, teile, vault)
|
||||
if teile[0] == "IT Workspace" and len(teile) >= 3 and teile[-2] == "findings":
|
||||
return _befund(rel, teile, vault)
|
||||
if teile[:2] == ["AI", "secondbrain"] and len(teile) == 4:
|
||||
return _dump(rel, teile, vault)
|
||||
if teile[0] == "docs" and len(teile) >= 2:
|
||||
return _wurzel_docs(rel, teile, vault)
|
||||
return ("rest", "", "keine Regel passt", "")
|
||||
|
||||
|
||||
def plane(vault: Path, ergebnis) -> tuple:
|
||||
ziele = {}
|
||||
rest = []
|
||||
for rel in ergebnis.waisen:
|
||||
art, ziel_rel, wert, hub = klassifiziere(rel, vault)
|
||||
if art == "rest":
|
||||
rest.append((rel, wert))
|
||||
continue
|
||||
ziel = ziele.setdefault(ziel_rel, Ziel(ziel_rel))
|
||||
ziel.ausloeser.append(rel)
|
||||
ziel.hubs.add(hub)
|
||||
if art == "befund":
|
||||
ziel.befunde = wert
|
||||
continue
|
||||
if art == "ordner":
|
||||
ziel.ordner = ziel_rel.rsplit("/", 1)[0]
|
||||
if hub in ergebnis.ausgehend.get(ziel_rel, set()):
|
||||
continue
|
||||
if wert not in ziel.zeilen:
|
||||
ziel.zeilen.append(wert)
|
||||
return ziele, rest
|
||||
|
||||
|
||||
def md_dateien(vault: Path, ordner: str) -> list:
|
||||
pfad = vault / ordner
|
||||
return sorted(
|
||||
name for name in os.listdir(pfad)
|
||||
if name.lower().endswith(".md") and not name.startswith(".") and (pfad / name).is_file()
|
||||
)
|
||||
|
||||
|
||||
def anhang(vault: Path, ziel: Ziel) -> list:
|
||||
zeilen = list(ziel.zeilen)
|
||||
if ziel.ordner is not None:
|
||||
namen = [n for n in md_dateien(vault, ziel.ordner) if n != "00-uebersicht.md"]
|
||||
if namen:
|
||||
if zeilen:
|
||||
zeilen.append("")
|
||||
zeilen += [ORDNER_KOPF, ""]
|
||||
zeilen += [f"- [{n}]({quote(n, safe='')})" for n in namen]
|
||||
ziel.gelistet += [f"{ziel.ordner}/{n}" for n in namen]
|
||||
if ziel.befunde is not None:
|
||||
namen = md_dateien(vault, ziel.befunde)
|
||||
if namen:
|
||||
if zeilen:
|
||||
zeilen.append("")
|
||||
zeilen += [BEFUND_KOPF, ""]
|
||||
zeilen += [f"- [[{ziel.befunde}/{n[:-3]}]]" for n in namen]
|
||||
ziel.gelistet += [f"{ziel.befunde}/{n}" for n in namen]
|
||||
return zeilen
|
||||
|
||||
|
||||
def pruefe_ziel(vault: Path, rel: str, zeilen: list) -> tuple:
|
||||
"""Liefert (Grund zum Überspringen oder None, alter Inhalt, Zeilenende)."""
|
||||
alt = (vault / rel).read_bytes()
|
||||
try:
|
||||
text = alt.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "nicht als UTF-8 lesbar", alt, ""
|
||||
crlf = alt.count(b"\r\n")
|
||||
lf = alt.count(b"\n") - crlf
|
||||
if crlf and lf:
|
||||
return "gemischte Zeilenenden", alt, ""
|
||||
ende = "\r\n" if crlf else "\n"
|
||||
vorhanden = text.replace("\r\n", "\n").split("\n")
|
||||
for zeile in zeilen:
|
||||
if zeile and not zeile.startswith("- "):
|
||||
if zeile in vorhanden:
|
||||
return f"enthält schon die Zeile {zeile!r}", alt, ende
|
||||
return None, alt, ende
|
||||
|
||||
|
||||
def anhang_bytes(alt: bytes, zeilen: list, ende: str) -> bytes:
|
||||
vorne = ende if alt and not alt.endswith(b"\n") else ""
|
||||
return (vorne + ende + ende.join(zeilen) + ende).encode("utf-8")
|
||||
|
||||
|
||||
def repo_von(vault: Path, rel: str) -> Optional[Path]:
|
||||
ordner = (vault / rel).parent
|
||||
# Auch die Vault-Wurzel kann ein Repo sein; über ihr wird nicht gesucht.
|
||||
while ordner == vault or vault in ordner.parents:
|
||||
if (ordner / ".git").exists():
|
||||
return ordner
|
||||
if ordner == vault:
|
||||
return None
|
||||
ordner = ordner.parent
|
||||
return None
|
||||
|
||||
|
||||
def git(ordner: Path, *argumente: str) -> bytes:
|
||||
umgebung = dict(os.environ)
|
||||
# Kein Index-Auffrischen: der Probelauf schreibt auch in .git nichts.
|
||||
umgebung["GIT_OPTIONAL_LOCKS"] = "0"
|
||||
try:
|
||||
lauf = subprocess.run(
|
||||
["git", "-C", str(ordner), *argumente],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
timeout=120,
|
||||
env=umgebung,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as fehler:
|
||||
raise Abbruch(f"git {' '.join(argumente)} in {ordner}: {fehler}") from fehler
|
||||
if lauf.returncode != 0:
|
||||
meldung = lauf.stderr.decode("utf-8", errors="replace").strip()
|
||||
raise Abbruch(f"git {' '.join(argumente)} in {ordner}: Exit {lauf.returncode}: {meldung}")
|
||||
return lauf.stdout
|
||||
|
||||
|
||||
def geaenderte_pfade(repo: Path) -> dict:
|
||||
"""Pfad relativ zum Repo, klein geschrieben, und wo er geändert ist."""
|
||||
wo = {}
|
||||
liste = git(repo, "worktree", "list", "--porcelain").decode("utf-8", errors="replace")
|
||||
baeume = [z[len("worktree "):].strip() for z in liste.split("\n") if z.startswith("worktree ")]
|
||||
if not baeume:
|
||||
raise Abbruch("git worktree list nennt keinen Arbeitsbaum")
|
||||
for baum in baeume:
|
||||
roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")
|
||||
eintraege = roh.decode("utf-8", errors="replace").split("\0")
|
||||
i = 0
|
||||
while i < len(eintraege):
|
||||
eintrag = eintraege[i]
|
||||
i += 1
|
||||
if len(eintrag) < 4:
|
||||
continue
|
||||
wo.setdefault(eintrag[3:].lower(), f"Arbeitsbaum {baum}")
|
||||
if "R" in eintrag[:2] or "C" in eintrag[:2]:
|
||||
if i < len(eintraege) and eintraege[i]:
|
||||
wo.setdefault(eintraege[i].lower(), f"Arbeitsbaum {baum}")
|
||||
i += 1
|
||||
aktuell = git(repo, "rev-parse", "--abbrev-ref", "HEAD").decode("utf-8", errors="replace").strip()
|
||||
zweige = git(repo, "for-each-ref", "--format=%(refname:short)", "refs/heads/").decode("utf-8", errors="replace")
|
||||
for zweig in (z.strip() for z in zweige.split("\n")):
|
||||
if not zweig or zweig == aktuell:
|
||||
continue
|
||||
diff = git(repo, "diff", "--name-only", "-z", f"HEAD...{zweig}", "--")
|
||||
for pfad in diff.decode("utf-8", errors="replace").split("\0"):
|
||||
if pfad:
|
||||
wo.setdefault(pfad.lower(), f"Zweig {zweig}")
|
||||
return wo
|
||||
|
||||
|
||||
def pruefe_alle(vault: Path, ziele: dict) -> tuple:
|
||||
repos = {}
|
||||
fertig = []
|
||||
uebersprungen = []
|
||||
for rel in sorted(ziele):
|
||||
ziel = ziele[rel]
|
||||
zeilen = anhang(vault, ziel)
|
||||
repo = repo_von(vault, rel)
|
||||
if repo is not None:
|
||||
if repo not in repos:
|
||||
try:
|
||||
repos[repo] = geaenderte_pfade(repo)
|
||||
except Abbruch as fehler:
|
||||
repos[repo] = str(fehler)
|
||||
stand = repos[repo]
|
||||
if isinstance(stand, str):
|
||||
uebersprungen.append((ziel, f"Git-Repo nicht prüfbar: {stand}"))
|
||||
continue
|
||||
innen = (vault / rel).relative_to(repo).as_posix().lower()
|
||||
if innen in stand:
|
||||
uebersprungen.append((ziel, f"in Git geändert ({stand[innen]})"))
|
||||
continue
|
||||
grund, alt, ende = pruefe_ziel(vault, rel, zeilen)
|
||||
if grund:
|
||||
uebersprungen.append((ziel, grund))
|
||||
continue
|
||||
fertig.append((ziel, zeilen, alt, ende))
|
||||
return fertig, uebersprungen
|
||||
|
||||
|
||||
def zeige(vault: Path, anwenden: bool, vorher: int, fertig: list, uebersprungen: list, rest: list, mitverbunden: int) -> None:
|
||||
print(f"# Bestandslauf - {'Anwenden' if anwenden else 'Probelauf'} - {vault}")
|
||||
print(f"Waisen außerhalb der Ausnahmen vorher: {vorher}")
|
||||
print()
|
||||
print(f"## Anhänge ({len(fertig)} Dateien)")
|
||||
for ziel, zeilen, _alt, ende in fertig:
|
||||
art = "CRLF" if ende == "\r\n" else "LF"
|
||||
print(f"ZIEL {ziel.rel} [{art}] wegen {len(ziel.ausloeser)} Waise(n)")
|
||||
for zeile in zeilen:
|
||||
print(f" | {zeile}")
|
||||
print()
|
||||
print(f"## Übersprungen ({len(uebersprungen)})")
|
||||
for ziel, grund in uebersprungen:
|
||||
print(f"UEBERSPRUNGEN {ziel.rel}: {grund} (betrifft {len(ziel.ausloeser)} Waise(n))")
|
||||
print()
|
||||
print(f"## Liste für den Koordinator ({len(rest)})")
|
||||
for rel, grund in rest:
|
||||
print(f"REST {rel}: {grund}")
|
||||
print(f"Durch Anhänge mitverbunden, ohne eigene Regel: {mitverbunden}")
|
||||
|
||||
|
||||
def sichern_und_anhaengen(vault: Path, sicherung: Path, fertig: list) -> None:
|
||||
sicherung.mkdir(parents=True)
|
||||
for ziel, _zeilen, alt, _ende in fertig:
|
||||
kopie = sicherung / ziel.rel
|
||||
kopie.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(vault / ziel.rel, kopie)
|
||||
if kopie.read_bytes() != alt:
|
||||
raise Abbruch(f"Sicherung weicht vom Original ab: {kopie}")
|
||||
for ziel, zeilen, alt, ende in fertig:
|
||||
pfad = vault / ziel.rel
|
||||
if pfad.read_bytes() != alt:
|
||||
raise Abbruch(f"{ziel.rel} hat sich seit dem Lesen geändert")
|
||||
neu = anhang_bytes(alt, zeilen, ende)
|
||||
with open(pfad, "ab") as datei:
|
||||
datei.write(neu)
|
||||
GESCHRIEBEN.append(ziel.rel)
|
||||
if pfad.read_bytes() != alt + neu:
|
||||
raise Abbruch(f"{ziel.rel}: Inhalt nach dem Anhängen weicht ab")
|
||||
(sicherung / "GEAENDERT.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def lauf(vault: Path, ausnahmen: Path, anwenden: bool, sicherung) -> int:
|
||||
GESCHRIEBEN.clear()
|
||||
if waisen is None:
|
||||
raise Abbruch(f"waisen.py nicht importierbar ({_IMPORTFEHLER})")
|
||||
sicherung_pfad = None
|
||||
if anwenden:
|
||||
if not sicherung:
|
||||
raise Abbruch("--anwenden braucht --sicherung mit einem Ordner außerhalb des Vaults")
|
||||
sicherung_pfad = Path(os.path.abspath(sicherung)).resolve()
|
||||
if sicherung_pfad == vault or vault in sicherung_pfad.parents:
|
||||
raise Abbruch(f"Sicherungsordner liegt im Vault: {sicherung_pfad}")
|
||||
if sicherung_pfad.exists():
|
||||
raise Abbruch(f"Sicherungsordner existiert schon: {sicherung_pfad}")
|
||||
ergebnis = waisen.analysiere(vault, ausnahmen)
|
||||
ziele, rest = plane(vault, ergebnis)
|
||||
fertig, uebersprungen = pruefe_alle(vault, ziele)
|
||||
verbunden = set()
|
||||
for ziel, _zeilen, _alt, _ende in fertig:
|
||||
verbunden.add(ziel.rel)
|
||||
verbunden.update(ziel.hubs)
|
||||
verbunden.update(ziel.gelistet)
|
||||
anzahl = len(rest)
|
||||
rest = [(rel, grund) for rel, grund in rest if rel not in verbunden]
|
||||
zeige(vault, anwenden, len(ergebnis.waisen), fertig, uebersprungen, rest, anzahl - len(rest))
|
||||
if anwenden:
|
||||
sichern_und_anhaengen(vault, sicherung_pfad, fertig)
|
||||
nachher = waisen.analysiere(vault, ausnahmen)
|
||||
print()
|
||||
print(f"Angehängt an {len(GESCHRIEBEN)} Dateien. Sicherung und GEAENDERT.txt: {sicherung_pfad}")
|
||||
print(f"Waisen außerhalb der Ausnahmen nachher: {len(nachher.waisen)}")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
zerleger = argparse.ArgumentParser(description="Hängt verwaiste Notizen einmalig an ihren Hub an")
|
||||
zerleger.add_argument("--vault", required=True)
|
||||
zerleger.add_argument("--ausnahmen", default=str(HIER / "waisen-ausnahmen.txt"))
|
||||
zerleger.add_argument("--anwenden", action="store_true")
|
||||
zerleger.add_argument("--sicherung")
|
||||
args = zerleger.parse_args(argv)
|
||||
vault = Path(os.path.abspath(args.vault)).resolve()
|
||||
try:
|
||||
return lauf(vault, Path(args.ausnahmen), args.anwenden, args.sicherung)
|
||||
except Exception as fehler:
|
||||
erwartet = isinstance(fehler, Abbruch) or (waisen is not None and isinstance(fehler, waisen.Abbruch))
|
||||
art = "" if erwartet else f"{type(fehler).__name__}: "
|
||||
print(f"Bestandslauf - ABGEBROCHEN: {art}{fehler}")
|
||||
if not erwartet:
|
||||
print(traceback.format_exc())
|
||||
if GESCHRIEBEN:
|
||||
print(f"Vor dem Abbruch schon angehängt ({len(GESCHRIEBEN)}), die Sicherung liegt vor:")
|
||||
for rel in GESCHRIEBEN:
|
||||
print(f" - {rel}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user