#!/usr/bin/env bash # # Werkbaum — pull-doc: ein Server-Dokument in eine lokale Datei holen; auf # Wunsch committet es dazu. Gedacht als Archiv-Schritt (auch per Cron): Der # Notationstext eines geteilten Plans (D76) bekommt so eine Git-Historie. # # Verwendung: # pull-doc [--git-commit] [--open] # # --git-commit Die Zieldatei zusätzlich committen. Sie muss dann in einem # git-Worktree liegen; committet wird nur diese eine Datei, # die Commit-Nachricht nennt Datum, Titel und Version des # Server-Stands. Ohne den Schalter wird nur geschrieben. # --open Die Zieldatei danach mit `idea` (IntelliJ IDEA) öffnen — # auch wenn sie unverändert war. # Die Dokument-Adresse des Backends # (https://…/api/v1/documents/) — oder gleich der # geteilte Editor-Link (…?live=); der Parameter # wird erkannt und ausgepackt. # Datei, in die der Notationstext geschrieben wird. # # Unverändert heißt: nichts geschrieben, kein Commit — der Lauf meldet es und # endet mit 0. Geschrieben wird byte-getreu, was der Server hält (der Text ist # das führende Datenformat, D14); andere vorgemerkte Änderungen im Worktree # bleiben unangetastet (Commit mit Pfadangabe). # # Siehe docs/DECISIONS.md D88. set -euo pipefail usage(){ sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; } do_commit=0; do_open=0 while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0;; --git-commit) do_commit=1; shift;; --open) do_open=1; shift;; --*) echo "Fehler: unbekannter Schalter $1" >&2; echo >&2; usage >&2; exit 2;; *) break;; esac done [ $# -eq 2 ] || { echo "Fehler: erwartet [--git-commit] [--open] URL und Zieldatei." >&2; echo >&2; usage >&2; exit 2; } url="$1"; target="$2" command -v python3 >/dev/null || { echo "Fehler: python3 wird gebraucht (JSON lesen)." >&2; exit 2; } command -v curl >/dev/null || { echo "Fehler: curl wird gebraucht." >&2; exit 2; } # Ein geteilter Editor-Link traegt die Dokument-Adresse als ?live=…-Parameter — # auspacken (samt Prozent-Dekodierung), sonst gilt die URL selbst. url="$(python3 - "$url" <<'PY' import sys, urllib.parse u = sys.argv[1] q = urllib.parse.parse_qs(urllib.parse.urlsplit(u).query) print(q["live"][0] if "live" in q else u) PY )" case "$url" in http://*|https://*) ;; *) echo "Fehler: keine http(s)-URL: $url" >&2; exit 2;; esac # Ziel pruefen: Verzeichnis muss existieren und in einem git-Worktree liegen. [ -d "$target" ] && { echo "Fehler: $target ist ein Verzeichnis." >&2; exit 2; } dir="$(dirname -- "$target")"; base="$(basename -- "$target")" [ -d "$dir" ] || { echo "Fehler: Verzeichnis $dir gibt es nicht." >&2; exit 2; } if [ "$do_commit" = 1 ]; then if [ "$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then echo "Fehler: $dir liegt in keinem git-Worktree (nötig für --git-commit)." >&2; exit 2 fi if [ "$(git -C "$dir" rev-parse --is-inside-git-dir 2>/dev/null)" = "true" ]; then echo "Fehler: $dir liegt im .git-Verzeichnis." >&2; exit 2 fi fi tmp_json="$(mktemp)"; tmp_doc="$dir/.$base.pull-doc.$$" trap 'rm -f "$tmp_json" "$tmp_doc"' EXIT curl -fsS --max-time 30 -H 'Accept: application/json' -o "$tmp_json" "$url" \ || { echo "Fehler: Abruf gescheitert: $url" >&2; exit 1; } # JSON lesen; den Inhalt schreibt python selbst (byte-getreu — eine # Shell-Variable verschluckte abschliessende Zeilenumbrueche). meta="$(python3 - "$tmp_json" "$tmp_doc" <<'PY' import io, json, sys try: with io.open(sys.argv[1], encoding="utf-8") as f: doc = json.load(f) content, title, version = doc["content"], doc["title"], doc["version"] except Exception as e: sys.exit(f"Fehler: Antwort ist kein Werkbaum-Dokument ({e})") with io.open(sys.argv[2], "w", encoding="utf-8", newline="") as f: f.write(content) print(f"{title}\t{version}") PY )" title="${meta%$'\t'*}"; version="${meta##*$'\t'}" # Geoeffnet wird auch Unveraendertes — wer --open sagt, will die Datei sehen. # `idea` forkt sich selbst; das Subshell-& haelt den Lauf trotzdem frei davon. open_target(){ [ "$do_open" = 1 ] || return 0 if command -v idea >/dev/null; then (idea "$target" >/dev/null 2>&1 &) else echo "Hinweis: 'idea' nicht im PATH — Datei nicht geöffnet." >&2 fi } # Unveraendert heisst: Datei gleich dem Server-Stand — und im Commit-Modus # zusaetzlich in git sauber, sonst bliebe eine liegende Aenderung (etwa aus # einem Lauf ohne --git-commit) fuer immer uncommittet. if [ -f "$target" ] && cmp -s "$tmp_doc" "$target"; then if [ "$do_commit" != 1 ] || [ -z "$(git -C "$dir" status --porcelain -- "$base")" ]; then echo "Unverändert: „$title“ (Version $version) — nichts zu tun." open_target exit 0 fi fi mv -- "$tmp_doc" "$target" if [ "$do_commit" = 1 ]; then msg="Werkbaum-Stand vom $(date '+%Y-%m-%d %H:%M') — „$title“ (Version $version)" git -C "$dir" add -- "$base" git -C "$dir" commit -q -m "$msg" -- "$base" echo "Committet $(git -C "$dir" rev-parse --short HEAD): $msg" else echo "Geschrieben: $target — „$title“ (Version $version)" fi open_target