From 294cbb93e499e14390225c80cbe247bd95a420b7 Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 26 Jun 2026 10:56:18 +0100 Subject: [PATCH] Use GIT_CREDENTIAL_CALLBACK=0 to disable use of credential callback --- docs/env_vars.md | 6 +++++ src/subcommand/clone_subcommand.cpp | 5 +++- src/subcommand/fetch_subcommand.cpp | 5 +++- src/subcommand/push_subcommand.cpp | 5 +++- src/utils/credentials.cpp | 6 +++++ src/utils/credentials.hpp | 2 ++ src/wasm/stream.cpp | 41 ++++++++++++++++++----------- test/conftest.py | 8 ++++++ test/test_clone.py | 11 ++++++++ 9 files changed, 71 insertions(+), 18 deletions(-) diff --git a/docs/env_vars.md b/docs/env_vars.md index 166cc1c..40bcc76 100644 --- a/docs/env_vars.md +++ b/docs/env_vars.md @@ -19,6 +19,12 @@ time. `GIT_COMMITTER_NAME` : The human-readable name for the "committer" field. +`GIT_CREDENTIAL_CALLBACK` +: By default, `git2cpp` will prompt the user to enter a username and password if they are required + for remote authentication, using a + [libgit2 credential callback](https://libgit2.org/docs/reference/main/credential/git_credential_acquire_cb.html). + To disable the callback use `export GIT_CREDENTIAL_CALLBACK=0`. + ## In WebAssembly build only diff --git a/src/subcommand/clone_subcommand.cpp b/src/subcommand/clone_subcommand.cpp index af945ce..b93af35 100644 --- a/src/subcommand/clone_subcommand.cpp +++ b/src/subcommand/clone_subcommand.cpp @@ -53,7 +53,10 @@ void clone_subcommand::run() checkout_opts.progress_cb = checkout_progress; checkout_opts.progress_payload = &pd; clone_opts.checkout_opts = checkout_opts; - clone_opts.fetch_opts.callbacks.credentials = user_credentials; + if (want_user_credentials()) + { + clone_opts.fetch_opts.callbacks.credentials = user_credentials; + } clone_opts.fetch_opts.callbacks.sideband_progress = sideband_progress; clone_opts.fetch_opts.callbacks.transfer_progress = fetch_progress; clone_opts.fetch_opts.callbacks.payload = &pd; diff --git a/src/subcommand/fetch_subcommand.cpp b/src/subcommand/fetch_subcommand.cpp index e853f82..6d4544d 100644 --- a/src/subcommand/fetch_subcommand.cpp +++ b/src/subcommand/fetch_subcommand.cpp @@ -44,7 +44,10 @@ void fetch_subcommand::run() git_indexer_progress pd = {0}; git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; - fetch_opts.callbacks.credentials = user_credentials; + if (want_user_credentials()) + { + fetch_opts.callbacks.credentials = user_credentials; + } fetch_opts.callbacks.sideband_progress = sideband_progress; fetch_opts.callbacks.transfer_progress = fetch_progress; fetch_opts.callbacks.payload = &pd; diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp index 5c9bee4..a4984b8 100644 --- a/src/subcommand/push_subcommand.cpp +++ b/src/subcommand/push_subcommand.cpp @@ -172,7 +172,10 @@ void push_subcommand::run() auto remote = repo.find_remote(remote_name); git_push_options push_opts = GIT_PUSH_OPTIONS_INIT; - push_opts.callbacks.credentials = user_credentials; + if (want_user_credentials()) + { + push_opts.callbacks.credentials = user_credentials; + } push_opts.callbacks.push_transfer_progress = push_transfer_progress; push_opts.callbacks.push_update_reference = push_update_reference; diff --git a/src/utils/credentials.cpp b/src/utils/credentials.cpp index 4ed8307..152417d 100644 --- a/src/utils/credentials.cpp +++ b/src/utils/credentials.cpp @@ -47,3 +47,9 @@ int user_credentials( giterr_set_str(GIT_ERROR_HTTP, "Unexpected credentials request"); return GIT_ERROR; } + +bool want_user_credentials() +{ + const char* env_var = std::getenv("GIT_CREDENTIAL_CALLBACK"); + return env_var == nullptr || std::string_view(env_var) != "0"; +} diff --git a/src/utils/credentials.hpp b/src/utils/credentials.hpp index ba970e6..8e60ce3 100644 --- a/src/utils/credentials.hpp +++ b/src/utils/credentials.hpp @@ -11,3 +11,5 @@ int user_credentials( unsigned int allowed_types, void* payload ); + +bool want_user_credentials(); diff --git a/src/wasm/stream.cpp b/src/wasm/stream.cpp index 7b12109..8716209 100644 --- a/src/wasm/stream.cpp +++ b/src/wasm/stream.cpp @@ -8,6 +8,7 @@ # include # include "../utils/common.hpp" +# include "../utils/credentials.hpp" # include "constants.hpp" # include "read_buffer.hpp" # include "response.hpp" @@ -464,28 +465,38 @@ static int create_credential(wasm_http_stream* stream) } subtransport->m_authorization_header = ""; - // Check that response headers show support for 'www-authenticate: Basic'. - if (!stream->m_response.has_header_starts_with("www-authenticate", "Basic")) + if (!want_user_credentials()) { - git_error_set( - GIT_ERROR_HTTP, - "remote host for request %s does not support Basic authentication", - stream->m_unconverted_url.c_str() - ); - return -1; + // Check that response headers show support for 'www-authenticate: Basic'. + if (!stream->m_response.has_header_starts_with("www-authenticate", "Basic")) + { + git_error_set( + GIT_ERROR_HTTP, + "remote host for request %s does not support Basic authentication", + stream->m_unconverted_url.c_str() + ); + return -1; + } } // Get credentials from user via libgit2 registered callback. - if (git_transport_smart_credentials( - &subtransport->m_credential, - subtransport->m_owner, - nullptr, - GIT_CREDENTIAL_USERPASS_PLAINTEXT - ) + int err; + if ((err = git_transport_smart_credentials( + &subtransport->m_credential, + subtransport->m_owner, + nullptr, + GIT_CREDENTIAL_USERPASS_PLAINTEXT + )) < 0) { + if (err == GIT_PASSTHROUGH) + { + // Use same error message as libgit2 + git_error_set(GIT_ERROR_HTTP, "remote authentication required but no callback set"); + } + // credentials_callback will have set git error. - return -1; + return err; } if (subtransport->m_credential->credtype != GIT_CREDENTIAL_USERPASS_PLAINTEXT) diff --git a/test/conftest.py b/test/conftest.py index eccc89d..d521b5d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -51,6 +51,14 @@ def commit_env_config(monkeypatch): monkeypatch.setenv(key, value) +@pytest.fixture +def disable_credential_callback(monkeypatch): + if GIT2CPP_TEST_WASM: + subprocess.run(["export", "GIT_CREDENTIAL_CALLBACK=0"], check=True) + else: + monkeypatch.setenv("GIT_CREDENTIAL_CALLBACK", "0") + + @pytest.fixture def repo_init_with_commit(commit_env_config, git2cpp_path, tmp_path): cmd_init = [git2cpp_path, "init", ".", "-b", "main"] diff --git a/test/test_clone.py b/test/test_clone.py index 35693ae..1f81f33 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -128,6 +128,17 @@ def test_clone_private_repo_fails_on_no_password( assert p_clone.stdout.count("Password:") == 1 +def test_clone_private_repo_fails_with_no_credential_callback( + git2cpp_path, tmp_path, run_in_tmp_path, disable_credential_callback +): + clone_cmd = [git2cpp_path, "clone", "https://github.com/QuantStack/git2cpp-test-private"] + p_clone = subprocess.run(clone_cmd, capture_output=True, text=True) + + assert p_clone.returncode != 0 + assert "Cloning into 'git2cpp-test-private'..." in p_clone.stdout + assert "error: remote authentication required but no callback set" in p_clone.stderr + + @pytest.mark.parametrize("protocol", ["http", "https"]) def test_clone_gitlab(git2cpp_path, tmp_path, run_in_tmp_path, protocol): repo_url = f"{protocol}://gitlab.quantstack.net/ianthomas23_group/cockle-playground"