Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/env_vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion src/subcommand/clone_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/subcommand/fetch_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/subcommand/push_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions src/utils/credentials.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
2 changes: 2 additions & 0 deletions src/utils/credentials.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ int user_credentials(
unsigned int allowed_types,
void* payload
);

bool want_user_credentials();
41 changes: 26 additions & 15 deletions src/wasm/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# include <emscripten.h>

# include "../utils/common.hpp"
# include "../utils/credentials.hpp"
# include "constants.hpp"
# include "read_buffer.hpp"
# include "response.hpp"
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
11 changes: 11 additions & 0 deletions test/test_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading