Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
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=<token>für Gitea-Token-Auth. Andere Credential-Typen (SSH-Keys) werden mitGIT_PASSTHROUGHabgelehnt. Ohne konfigurierten Token wird ebenfalls durchgereicht, sodass libgit2 den rohen "authentication required"-Fehler an den Aufrufer surfaced. - Token-Quelle — bei
sporel_lib_initwird der Token aus der UmgebungsvariableSPOREL_GITEA_TOKENgelatcht (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:
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, <libgit2-Fehlermeldung>.
git.checkout(dir, ref)
Syntax: git.checkout(dir: string, ref: string) -> ok: boolean, err: string|nil
Example:
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, <Fehlermeldung>.
git.describe(dir)
Syntax: git.describe(dir: string) -> desc: string|nil, err: string|nil
Example:
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, <Fehlermeldung>.
git.tags(dir)
Syntax: git.tags(dir: string) -> tags: string[]|nil, err: string|nil
Example:
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, <Fehlermeldung>.
git.has_ref(dir, ref)
Syntax: git.has_ref(dir: string, ref: string) -> boolean
Example:
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:
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:
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, <Fehlermeldung>.
git.set_token(t)
Syntax: git.set_token(t: string|nil) -> void
Example:
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:
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:
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 = <Fehlermeldung>, kind = <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:
-- 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