diff --git a/README.md b/README.md new file mode 100644 index 0000000..440af71 --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +# lib-core.git + +Native libgit2-Wrapper, der synchrone und asynchrone Git-Operationen an Lua +exponiert. Bindet libgit2 statisch (vendored, ADR-0047) und folgt dem +Native-Lib-Pattern aus ADR-0046: Die Lib wird als Shared-Library geladen und +über `sporel_lib_init(L, api)` initialisiert, wobei der Engine-API-Struct +(u. a. der Async-Worker-Pool) hereingereicht wird. Transport ausschließlich +über **HTTPS** — kein SSH (hält die statische Link-Surface klein und vermeidet +libssh2 + Crypto-Abhängigkeiten). Für private Gitea-Repos liefert ein +HTTP-Basic-Credential-Callback den Token (Username literal `token`). + +**Version:** 0.2.0 +**Lib-ID:** lib-core.git +**Requires:** (none — native; nutzt den Engine-Async-Pool via `engine_api`) +**Tags:** git, libgit2, native, clone, async, ls-remote, gitea, https + +## Topology + + +```mermaid +graph LR + this["lib-core.git"] + engine["engine.*"] + this --> engine +``` + + +Native-Lib: bindet libgit2 statisch ein und greift über den +`sporel_engine_api`-Struct auf den Engine-Async-Worker-Pool zu +(`async_submit` / `async_is_done` / `async_take_result`). Die einzige +libgit2-Berührung von einem Nicht-Lua-Thread ist der `ls-remote`-Worker; +libgit2 ist nach `git_libgit2_init` voll thread-safe. + +## Transport & Auth + +- **HTTPS only** — kein SSH-Transport. Backend ist WinHTTP (Windows) bzw. + OpenSSL/mbedTLS (Linux) je nach libgit2-Autodetection. +- **HTTP-Basic-Credential-Callback** — fordert ein Remote Credentials an, + liefert der Callback `username="token"`, `password=` für + Gitea-Token-Auth. Andere Credential-Typen (SSH-Keys) werden mit + `GIT_PASSTHROUGH` abgelehnt. Ohne konfigurierten Token wird ebenfalls + durchgereicht, sodass libgit2 den rohen "authentication required"-Fehler + an den Aufrufer surfaced. +- **Token-Quelle** — bei `sporel_lib_init` wird der Token aus der + Umgebungsvariable `SPOREL_GITEA_TOKEN` gelatcht (leer/ungesetzt → + anonym). `git.set_token(t)` überschreibt diesen Default zur Laufzeit. + +## API + +### `git.clone(url, dest, ref?)` + +**Syntax:** `git.clone(url: string, dest: string, ref?: string) -> ok: boolean, err: string|nil` + +**Example:** +```lua +local git = require("lib-core.git") +local ok, err = git.clone("https://git.davoryn.de/calic/foo.git", "/tmp/foo", "v0.2.0") +if not ok then engine.print("clone failed: "..err) end +``` + +**Description:** Klont `url` nach `dest`. Ist `ref` gesetzt, wird dieser +Branch nach dem Clone ausgecheckt (`checkout_branch`). Nutzt den +HTTP-Basic-Credential-Callback. Liefert `true, nil` bei Erfolg, sonst +`false, `. + +### `git.checkout(dir, ref)` + +**Syntax:** `git.checkout(dir: string, ref: string) -> ok: boolean, err: string|nil` + +**Example:** +```lua +local ok, err = git.checkout("/tmp/foo", "v0.2.0") +``` + +**Description:** Öffnet das Repo in `dir`, löst `ref` auf (Tag, Branch oder +SHA via `git_revparse_single`), checkt den Tree mit `GIT_CHECKOUT_SAFE` aus +und setzt HEAD detached auf den Commit. Liefert `true, nil` bei Erfolg, sonst +`false, `. + +### `git.describe(dir)` + +**Syntax:** `git.describe(dir: string) -> desc: string|nil, err: string|nil` + +**Example:** +```lua +local desc, err = git.describe("/tmp/foo") -- z. B. "v0.2.0-3-g1a2b3c4-dirty" +``` + +**Description:** `git describe --tags` auf den Workdir. Strategie +`GIT_DESCRIBE_TAGS`, mit `-dirty`-Suffix bei uncommitteten Änderungen. +Liefert den Describe-String + `nil`, oder `nil, `. + +### `git.tags(dir)` + +**Syntax:** `git.tags(dir: string) -> tags: string[]|nil, err: string|nil` + +**Example:** +```lua +local tags, err = git.tags("/tmp/foo") +for _, t in ipairs(tags or {}) do engine.print(t) end +``` + +**Description:** Listet alle lokalen Tags des Repos in `dir` +(`git_tag_list`). Liefert ein Array von Tag-Namen + `nil`, oder +`nil, "not a repo"` / `nil, `. + +### `git.has_ref(dir, ref)` + +**Syntax:** `git.has_ref(dir: string, ref: string) -> boolean` + +**Example:** +```lua +if git.has_ref("/tmp/foo", "v0.2.0") then ... end +``` + +**Description:** `true` iff `ref` (Tag, Branch oder SHA) im Repo unter `dir` +auflösbar ist. `false`, wenn das Repo nicht geöffnet werden kann oder `ref` +nicht auflösbar ist. Wirft keinen Fehler. + +### `git.is_repo(dir)` + +**Syntax:** `git.is_repo(dir: string) -> boolean` + +**Example:** +```lua +if not git.is_repo("/tmp/foo") then git.clone(url, "/tmp/foo") end +``` + +**Description:** `true` iff `dir` ein öffenbares Git-Repository ist, sonst +`false`. Wirft keinen Fehler. + +### `git.fetch(url, dir)` + +**Syntax:** `git.fetch(url: string, dir: string) -> ok: boolean, err: string|nil` + +**Example:** +```lua +local ok, err = git.fetch("https://git.davoryn.de/calic/foo.git", "/tmp/foo") +``` + +**Description:** Anonym-Remote-Fetch in das Repo unter `dir` — erzeugt ein +detached anonymous remote für `url` und fetcht, **ohne** die Refspecs von +`origin` zu mutieren. Nutzt den HTTP-Basic-Credential-Callback. Liefert +`true, nil` bei Erfolg, sonst `false, `. + +### `git.set_token(t)` + +**Syntax:** `git.set_token(t: string|nil) -> void` + +**Example:** +```lua +git.set_token(engine.config("gitea_token")) -- override env-default +git.set_token(nil) -- zurück auf anonym +``` + +**Description:** Überschreibt den In-Memory-Gitea-HTTP-Basic-Token, den der +Credential-Callback verwendet. `nil` oder leerer String setzt auf anonym +zurück. Source-of-Truth bei Lib-Init ist die Env-Variable +`SPOREL_GITEA_TOKEN`; dieser Setter lässt den Launcher (oder jeden Consumer) +den Default zur Laufzeit überschreiben — etwa mit `engine.config("gitea_token")`, +sobald die Engine-Bindings live sind. + +### `git.start_ls_remote_tags(url)` + +**Syntax:** `git.start_ls_remote_tags(url: string) -> handle: userdata` + +**Example:** +```lua +local h = git.start_ls_remote_tags("https://git.davoryn.de/calic/foo.git") +-- später, pro Frame: +if git.is_done(h) then + local res = git.take_result(h) + -- res.tags / res.error+res.kind +end +``` + +**Description:** Startet ein **asynchrones** `ls-remote --tags` gegen `url` +über den Engine-Async-Worker-Pool und liefert sofort ein Lua-owned +Handle (Userdata). Der Worker berührt den Lua-State nicht; das Handle wird +mit `is_done` gepollt und mit `take_result` eingelöst. Peeled-Tag-Duplikate +(`^{}`) werden herausgefiltert. Wirft, wenn der Engine-API-Struct nicht +verdrahtet ist (`sporel_lib_init`-Bug). Bildet zusammen mit `is_done` + +`take_result` das Async-Triple. + +### `git.is_done(handle)` + +**Syntax:** `git.is_done(handle: userdata) -> boolean` + +**Description:** `true`, sobald der Async-`ls-remote`-Worker für `handle` +fertig ist. Non-blocking — pro Frame pollen, bis `true`. Wirft bei +Nicht-Userdata oder nicht verdrahtetem Engine-API. + +### `git.take_result(handle)` + +**Syntax:** `git.take_result(handle: userdata) -> result: table|nil, err: string|nil` + +**Example:** +```lua +local res = git.take_result(h) +if res.tags then + for _, t in ipairs(res.tags) do engine.print(t) end +else + engine.print(("ls-remote %s: %s"):format(res.kind, res.error)) +end +``` + +**Description:** Löst ein **fertiges** Async-Handle ein und gibt das +Engine-Handle frei (danach ist `handle` verbraucht). Bei Erfolg: +`{ tags = { "v0.1.0", "v0.2.0", ... } }`. Bei Fehler: +`{ error = , kind = }` mit +`kind ∈ { "auth-required", "not-found", "network", "other" }` — +abgeleitet aus `GIT_EAUTH` bzw. Pattern-Matching auf die libgit2-Fehlermeldung +(401/Authentication → `auth-required`, 404/Not Found → `not-found`, +Net/HTTP/SSL-Fehlerklasse → `network`, sonst → `other`). Ist das Handle noch +nicht fertig, liefert die Funktion `nil, "not done"`. + +## Async-Pattern + +Das Async-Triple ist non-blocking und für den Per-Frame-Poll im +Engine-Game-Loop ausgelegt: + +```lua +-- einmalig starten: +local h = git.start_ls_remote_tags(url) + +-- in on_update, pro Frame: +if h and git.is_done(h) then + local res = git.take_result(h) + h = nil + if res.tags then + -- Tags verarbeiten + elseif res.kind == "auth-required" then + -- Token setzen und erneut versuchen + end +end +``` + +Die synchronen Operationen (`clone`, `checkout`, `describe`, `tags`, +`has_ref`, `is_repo`, `fetch`) blockieren den aufrufenden Thread und sind für +Tooling- / CLI-Kontexte gedacht, nicht für den Hot-Loop. + +## Build + +Native-Lib (`manifest.lib`: `"native": true`). Gebaut via CMake +(`CMakeLists.txt`), libgit2 wird vendored statisch gelinkt (ADR-0047). Das +Artefakt (`sporel_git.dll` bzw. `.so`) exportiert `sporel_lib_init`, das die +Lua-Funktionstabelle aufbaut und den Engine-API-Struct latcht. + +## References + +- ADR-0046 (Native-Lib-Pattern — `sporel_lib_init`, Engine-API-Struct, Async-Pool) +- ADR-0047 (libgit2-Vendoring — statischer Link, HTTPS-only) +- ADR-0001 (engine knows verbs, libs bring nouns) +- Quelle: `src/sporel_git.c`