diff --git a/manifest.lib b/manifest.lib index 244bdda..381b7d8 100644 --- a/manifest.lib +++ b/manifest.lib @@ -1,6 +1,6 @@ { "id": "lib-core.git", - "version": "0.1.0", + "version": "0.2.0", "api_min": "0.1", "native": true, "deps": [] diff --git a/sporel_git.dll b/sporel_git.dll index 73cba42..d864a9e 100644 Binary files a/sporel_git.dll and b/sporel_git.dll differ diff --git a/src/sporel_git.c b/src/sporel_git.c index d44b6a6..deb25de 100644 --- a/src/sporel_git.c +++ b/src/sporel_git.c @@ -33,6 +33,15 @@ * sporel_lib_init; used by the async wrappers below. */ static const sporel_engine_api *g_api = NULL; +/* Optional Gitea / HTTP-Basic token. Populated at sporel_lib_init from + * SPOREL_GITEA_TOKEN. When set, every libgit2 transport request that + * asks for credentials gets username="token", password=g_token via the + * cred-acquire callback. Empty string = anonymous (callback declines). + * + * Read once at init; no per-call override yet. A future engine.config + * binding can refresh it (rebind via a setter) when settings UI lands. */ +static char g_token[512] = ""; + /* Call once per process; thread-safe via libgit2's own internal flag. */ static void ensure_libgit2_init(void) { static int initialized = 0; @@ -42,6 +51,45 @@ static void ensure_libgit2_init(void) { } } +/* HTTP-Basic credential callback. libgit2 invokes this when the remote + * demands auth. We service USERPASS_PLAINTEXT (Gitea token via + * Basic-Auth, username literally "token") and decline everything else + * (SSH-key types are unsupported since we ship no SSH transport). */ +static int sporel_cred_acquire_cb(git_credential **out, + const char *url, + const char *username_from_url, + unsigned int allowed_types, + void *payload) { + (void)url; (void)username_from_url; (void)payload; + if (!(allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT)) { + return GIT_PASSTHROUGH; + } + if (g_token[0] == '\0') { + /* No token configured — fall through so libgit2 surfaces the + * raw "authentication required" error to the caller. */ + return GIT_PASSTHROUGH; + } + return git_credential_userpass_plaintext_new(out, "token", g_token); +} + +/* Map a libgit2 error to a stable "kind" string the Lua side surfaces + * in result.kind. Lets the launcher distinguish local-only repos + * (404 with creds) from need-auth (401 without creds) from genuine + * network failures. Pattern-matches the error message because + * libgit2's error-class taxonomy varies across HTTP backends. */ +static const char *classify_git_error(int rc, const git_error *e) { + const char *msg = (e && e->message) ? e->message : ""; + if (rc == GIT_EAUTH) return "auth-required"; + if (strstr(msg, "401") || strstr(msg, "authentication required") + || strstr(msg, "Authentication")) return "auth-required"; + if (strstr(msg, "404") || strstr(msg, "Not Found") + || strstr(msg, "not found")) return "not-found"; + if (e && (e->klass == GIT_ERROR_NET || e->klass == GIT_ERROR_HTTP + || e->klass == GIT_ERROR_SSL)) + return "network"; + return "other"; +} + /* ---- Synchronous bindings ------------------------------------------------ */ /* git.clone(url, dest_dir, ref) -> ok, err @@ -54,6 +102,7 @@ static int l_clone(lua_State *L) { git_repository *repo = NULL; git_clone_options opts = GIT_CLONE_OPTIONS_INIT; if (ref) opts.checkout_branch = ref; + opts.fetch_opts.callbacks.credentials = sporel_cred_acquire_cb; int rc = git_clone(&repo, url, dest, &opts); if (rc != 0) { @@ -215,6 +264,7 @@ static int l_fetch(lua_State *L) { if (git_remote_create_anonymous(&remote, repo, url) != 0) goto err; git_fetch_options fopts = GIT_FETCH_OPTIONS_INIT; + fopts.callbacks.credentials = sporel_cred_acquire_cb; int rc = git_remote_fetch(remote, NULL, &fopts, NULL); git_remote_free(remote); git_repository_free(repo); @@ -247,6 +297,7 @@ typedef struct { /* outputs: */ int ok; char err[512]; + char kind[32]; /* "auth-required" | "not-found" | "network" | "other" */ int tag_count; char tags[GIT_ASYNC_MAX_TAGS][GIT_ASYNC_TAG_LEN]; } git_async_ctx; @@ -261,6 +312,7 @@ static void do_ls_remote_tags(void *vctx) { if (rc != 0) goto fail; git_remote_callbacks cbs = GIT_REMOTE_CALLBACKS_INIT; + cbs.credentials = sporel_cred_acquire_cb; rc = git_remote_connect(remote, GIT_DIRECTION_FETCH, &cbs, NULL, NULL); if (rc != 0) goto fail; @@ -289,6 +341,7 @@ fail:; const git_error *e = git_error_last(); snprintf(c->err, sizeof c->err, "%s", e && e->message ? e->message : "ls_remote failed"); + snprintf(c->kind, sizeof c->kind, "%s", classify_git_error(rc, e)); c->ok = 0; if (remote) git_remote_free(remote); } @@ -301,6 +354,22 @@ typedef struct { git_async_ctx ctx; /* Lua-owned via userdata */ } git_handle_userdata; +/* git.set_token(token | nil) -> nil + * Replaces the in-memory Gitea HTTP-Basic token used by the credential + * callback. Passing nil / empty string reverts to anonymous. Source of + * truth at lib-init is SPOREL_GITEA_TOKEN env; the launcher (or any + * consumer) calls this to override with engine.config("gitea_token") + * once the Lua engine bindings are live. */ +static int l_set_token(lua_State *L) { + if (lua_isnoneornil(L, 1)) { + g_token[0] = '\0'; + return 0; + } + const char *t = luaL_checkstring(L, 1); + snprintf(g_token, sizeof g_token, "%s", t); + return 0; +} + /* git.start_ls_remote_tags(url) -> handle (userdata) */ static int l_start_ls_remote_tags(lua_State *L) { if (!g_api) { @@ -349,6 +418,8 @@ static int l_take_result(lua_State *L) { } else { lua_pushstring(L, u->ctx.err); lua_setfield(L, -2, "error"); + lua_pushstring(L, u->ctx.kind); + lua_setfield(L, -2, "kind"); } return 1; } @@ -359,6 +430,12 @@ SPOREL_EXPORT int sporel_lib_init(lua_State *L, const sporel_engine_api *api) { g_api = api; ensure_libgit2_init(); + /* Latch the Gitea token from env, if present. Empty/unset → anon. */ + const char *tok = getenv("SPOREL_GITEA_TOKEN"); + if (tok && *tok) { + snprintf(g_token, sizeof g_token, "%s", tok); + } + lua_newtable(L); lua_pushcfunction(L, l_clone); lua_setfield(L, -2, "clone"); @@ -372,6 +449,7 @@ SPOREL_EXPORT int sporel_lib_init(lua_State *L, const sporel_engine_api *api) { lua_pushcfunction(L, l_start_ls_remote_tags); lua_setfield(L, -2, "start_ls_remote_tags"); lua_pushcfunction(L, l_is_done); lua_setfield(L, -2, "is_done"); lua_pushcfunction(L, l_take_result); lua_setfield(L, -2, "take_result"); + lua_pushcfunction(L, l_set_token); lua_setfield(L, -2, "set_token"); return 1; }