diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp index 9e2af17..c59e7e2 100644 --- a/src/subcommand/push_subcommand.cpp +++ b/src/subcommand/push_subcommand.cpp @@ -1,9 +1,13 @@ #include "../subcommand/push_subcommand.hpp" #include +#include +#include +#include -#include +#include +#include "../utils/ansi_code.hpp" #include "../utils/credentials.hpp" #include "../utils/progress.hpp" #include "../wrapper/repository_wrapper.hpp" @@ -13,8 +17,15 @@ push_subcommand::push_subcommand(const libgit2_object&, CLI::App& app) auto* sub = app.add_subcommand("push", "Update remote refs along with associated objects"); sub->add_option("", m_remote_name, "The remote to push to")->default_val("origin"); - + sub->add_option("", m_branch_name, "The branch to push"); sub->add_option("", m_refspecs, "The refspec(s) to push"); + sub->add_flag( + "--all,--branches", + m_branches_flag, + "Push all branches (i.e. refs under " + ansi_code::bold + "refs/heads/" + ansi_code::reset + + "); cannot be used with other ." + ); + sub->callback( [this]() @@ -24,6 +35,15 @@ push_subcommand::push_subcommand(const libgit2_object&, CLI::App& app) ); } +// TODO: put in common +static std::string oid_to_hex(const git_oid& oid) +{ + char oid_str[GIT_OID_SHA1_HEXSIZE + 1]; + git_oid_fmt(oid_str, &oid); + oid_str[GIT_OID_SHA1_HEXSIZE] = '\0'; + return std::string(oid_str); +} + void push_subcommand::run() { auto directory = get_current_git_path(); @@ -37,25 +57,127 @@ void push_subcommand::run() push_opts.callbacks.push_transfer_progress = push_transfer_progress; push_opts.callbacks.push_update_reference = push_update_reference; - if (m_refspecs.empty()) + if (m_branches_flag) { - try + auto iter = repo.iterate_branches(GIT_BRANCH_LOCAL); + auto br = iter.next(); + while (br) { - auto head_ref = repo.head(); - std::string short_name = head_ref.short_name(); - std::string refspec = "refs/heads/" + short_name; + std::string refspec = "refs/heads/" + std::string(br->name()); m_refspecs.push_back(refspec); + br = iter.next(); + } + } + else if (m_refspecs.empty()) + { + std::string branch; + if (!m_branch_name.empty()) + { + branch = m_branch_name; } - catch (...) + else { - std::cerr << "Could not determine current branch to push." << std::endl; - return; + try + { + auto head_ref = repo.head(); + branch = head_ref.short_name(); + } + catch (...) + { + std::cerr << "Could not determine current branch to push." << std::endl; + return; + } } + std::string refspec = "refs/heads/" + branch; + m_refspecs.push_back(refspec); } git_strarray_wrapper refspecs_wrapper(m_refspecs); git_strarray* refspecs_ptr = nullptr; refspecs_ptr = refspecs_wrapper; + // Take a snapshot of remote branches to check which ones are new after push + git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT; + callbacks.credentials = user_credentials; + credentials_payload creds_payload; + callbacks.payload = &creds_payload; + push_opts.callbacks.payload = &creds_payload; + + auto remote_heads = remote.list_heads(&callbacks); + + // Map with names of branches and their oids before push + std::unordered_map remote_heads_map; + for (const auto& h : remote_heads) + { + remote_heads_map.emplace(h.name, h.oid); + } + remote.push(refspecs_ptr, &push_opts); - std::cout << "Pushed to " << remote_name << std::endl; + + std::cout << "To " << remote.url() << std::endl; + for (const auto& refspec : m_refspecs) + { + std::string_view ref_view(refspec); + std::string_view prefix = "refs/heads/"; + std::string local_short_name; + if (ref_view.substr(0, prefix.size()) == prefix) + { + local_short_name = ref_view.substr(prefix.size()); + } + else + { + local_short_name = refspec; + } + + std::optional upstream_opt = repo.branch_upstream_name(local_short_name); + + std::string remote_branch = local_short_name; + std::string remote_ref = "refs/heads/" + local_short_name; + if (upstream_opt.has_value()) + { + const std::string up_name = upstream_opt.value(); + auto pos = up_name.find('/'); + if (pos != std::string::npos && pos + 1 < up_name.size()) + { + std::string up_remote = up_name.substr(0, pos); + std::string up_branch = up_name.substr(pos + 1); + if(up_remote == remote_name) + { + remote_branch = up_name.substr(pos + 1); + remote_ref = "refs/heads/" + remote_branch; + } + } + } + + auto iter = remote_heads_map.find(remote_ref); + if (iter == remote_heads_map.end()) + { + std::cout << " * [new branch] " << local_short_name << " -> " << remote_branch << std::endl; + continue; + } + + git_oid remote_oid = iter->second; + + std::optional local_oid_opt; + if (auto ref_opt = repo.find_reference_dwim(("refs/heads/" + local_short_name))) + { + const git_oid* target = ref_opt->target(); + local_oid_opt = *target; // TODO: pas comprenu pourquoi je ne peux pas faire local_oid_opt = ref_opt->target(); + } + + if (!local_oid_opt) + { + std::cout << " " << local_short_name << " -> " << remote_branch << std::endl; + continue; + } + git_oid local_oid = local_oid_opt.value(); + + if (!git_oid_equal(&remote_oid, &local_oid)) + { + std::string old_hex = oid_to_hex(remote_oid); + std::string new_hex = oid_to_hex(local_oid); + // TODO: check order of hex codes + std::cout << " " << old_hex.substr(0, 7) << ".." << new_hex.substr(0, 7) + << " " << local_short_name << " -> " << local_short_name << std::endl; + } + } } diff --git a/src/subcommand/push_subcommand.hpp b/src/subcommand/push_subcommand.hpp index 07c301e..c4450bf 100644 --- a/src/subcommand/push_subcommand.hpp +++ b/src/subcommand/push_subcommand.hpp @@ -17,5 +17,7 @@ class push_subcommand private: std::string m_remote_name; + std::string m_branch_name; std::vector m_refspecs; + bool m_branches_flag = false; }; diff --git a/src/utils/ansi_code.hpp b/src/utils/ansi_code.hpp index 90b1e25..becc5a9 100644 --- a/src/utils/ansi_code.hpp +++ b/src/utils/ansi_code.hpp @@ -19,6 +19,9 @@ namespace ansi_code const std::string hide_cursor = "\e[?25l"; const std::string show_cursor = "\e[?25h"; + const std::string bold = "\033[1m"; + const std::string reset = "\033[0m"; + // Functions. std::string cursor_to_row(size_t row); diff --git a/src/utils/credentials.cpp b/src/utils/credentials.cpp index c8d43e3..8c9ae74 100644 --- a/src/utils/credentials.cpp +++ b/src/utils/credentials.cpp @@ -15,6 +15,8 @@ int user_credentials( void* payload ) { + credentials_payload* cached = payload ? static_cast(payload) : nullptr; + // Check for cached credentials here, if desired. // It might be necessary to make this function stateful to avoid repeating unnecessary checks. @@ -22,11 +24,24 @@ int user_credentials( if (allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) { - std::string username = username_from_url ? username_from_url : ""; - if (username.empty()) + std::string username; + if (username_from_url && username_from_url[0] != '\0') + { + username = username_from_url; + } + else if (cached && cached->username.has_value()) + { + username = *cached->username; + } + else { username = prompt_input("Username: "); + if (cached && !username.empty()) + { + cached->username = username; + } } + if (username.empty()) { giterr_set_str(GIT_ERROR_HTTP, "No username specified"); @@ -34,6 +49,19 @@ int user_credentials( } std::string password = prompt_input("Password: ", false); + if (cached && cached->password.has_value()) + { + password = *cached->password; + } + else + { + password = prompt_input("Password: ", false); + if (cached && !password.empty()) + { + cached->password = password; + } + } + if (password.empty()) { giterr_set_str(GIT_ERROR_HTTP, "No password specified"); diff --git a/src/utils/credentials.hpp b/src/utils/credentials.hpp index ba970e6..f7a9fb8 100644 --- a/src/utils/credentials.hpp +++ b/src/utils/credentials.hpp @@ -1,7 +1,16 @@ #pragma once +#include +#include + #include +struct credentials_payload +{ + std::optional username; + std::optional password; +}; + // Libgit2 callback of type git_credential_acquire_cb to obtain user credentials // (username and password) to authenticate remote https access. int user_credentials( diff --git a/src/utils/progress.cpp b/src/utils/progress.cpp index 12b7c63..9af2d13 100644 --- a/src/utils/progress.cpp +++ b/src/utils/progress.cpp @@ -139,11 +139,9 @@ int push_update_reference(const char* refname, const char* status, void*) { if (status) { - std::cout << " " << refname << " " << status << std::endl; - } - else - { - std::cout << " " << refname << std::endl; + std::cout << " ! [remote rejected] " << refname << " (" << status << ")" << std::endl; + return -1; } + return 0; } diff --git a/src/utils/progress.hpp b/src/utils/progress.hpp index 861c8d9..fc70509 100644 --- a/src/utils/progress.hpp +++ b/src/utils/progress.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include int sideband_progress(const char* str, int len, void*); @@ -7,4 +9,11 @@ int fetch_progress(const git_indexer_progress* stats, void* payload); void checkout_progress(const char* path, size_t cur, size_t tot, void* payload); int update_refs(const char* refname, const git_oid* a, const git_oid* b, git_refspec*, void*); int push_transfer_progress(unsigned int current, unsigned int total, size_t bytes, void*); + +struct push_update_payload +{ + std::string url; + bool header_printed = false; +}; + int push_update_reference(const char* refname, const char* status, void*); diff --git a/src/wrapper/remote_wrapper.cpp b/src/wrapper/remote_wrapper.cpp index 3f603dd..7ec5d85 100644 --- a/src/wrapper/remote_wrapper.cpp +++ b/src/wrapper/remote_wrapper.cpp @@ -3,8 +3,6 @@ #include #include -#include - #include "../utils/git_exception.hpp" remote_wrapper::remote_wrapper(git_remote* remote) @@ -62,3 +60,41 @@ void remote_wrapper::push(const git_strarray* refspecs, const git_push_options* { throw_if_error(git_remote_push(*this, refspecs, opts)); } + +void remote_wrapper::connect(git_direction direction, const git_remote_callbacks* callbacks) const +{ + throw_if_error(git_remote_connect(*this, direction, callbacks, nullptr, nullptr)); +} + +std::vector remote_wrapper::list_heads(const git_remote_callbacks* callbacks = nullptr) const +{ + std::vector result; + + this->connect(GIT_DIRECTION_FETCH, callbacks); + + const git_remote_head** heads = nullptr; + size_t heads_len = 0; + int err = git_remote_ls(&heads, &heads_len, *this); + if (err != 0) + { + git_remote_disconnect(*this); + throw_if_error(err); + } + + for (size_t i = 0; i < heads_len; ++i) + { + const git_remote_head* h = heads[i]; + if (!h || !h->name) + { + continue; + } + + remote_head rh; + rh.name = std::string(h->name); + rh.oid = h->oid; + result.push_back(std::move(rh)); + } + + git_remote_disconnect(*this); + return result; +} diff --git a/src/wrapper/remote_wrapper.hpp b/src/wrapper/remote_wrapper.hpp index a933fb8..1337072 100644 --- a/src/wrapper/remote_wrapper.hpp +++ b/src/wrapper/remote_wrapper.hpp @@ -1,13 +1,19 @@ #pragma once +#include #include #include #include -#include #include "../wrapper/wrapper_base.hpp" +struct remote_head +{ + std::string name; + git_oid oid; +}; + class remote_wrapper : public wrapper_base { public: @@ -27,6 +33,9 @@ class remote_wrapper : public wrapper_base void fetch(const git_strarray* refspecs, const git_fetch_options* opts, const char* reflog_message); void push(const git_strarray* refspecs, const git_push_options* opts); + void connect(git_direction direction, const git_remote_callbacks* callbacks) const; + + std::vector list_heads(const git_remote_callbacks* callbacks) const; private: diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index ccc0408..81b8372 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -3,6 +3,9 @@ #include #include #include +#include +#include +#include #include "../utils/git_exception.hpp" #include "../wrapper/commit_wrapper.hpp" @@ -194,6 +197,21 @@ std::optional repository_wrapper::upstream() const } } +std::optional repository_wrapper::branch_upstream_name(std::string local_branch) const +{ + git_buf buf = GIT_BUF_INIT; + int error = git_branch_upstream_name(&buf, *this, local_branch.c_str()); + if (error != 0) + { + git_buf_dispose(&buf); + return std::nullopt; + } + + std::string result(buf.ptr ? buf.ptr : ""); + git_buf_dispose(&buf); + return result; +} + branch_tracking_info repository_wrapper::get_tracking_info() const { branch_tracking_info info; @@ -426,7 +444,7 @@ size_t repository_wrapper::shallow_depth_from_head() const if (parent_list.size() > 0u) { has_parent = true; - for (size_t j = 0u; parent_list.size(); j++) + for (size_t j = 0u; j < parent_list.size(); ++j) { const commit_wrapper& c = parent_list[j]; temp_commits_list.push_back(std::move(const_cast(c))); diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index d630343..429617b 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -6,7 +6,6 @@ #include -#include "../utils/common.hpp" #include "../utils/git_exception.hpp" #include "../wrapper/annotated_commit_wrapper.hpp" #include "../wrapper/branch_wrapper.hpp" @@ -74,6 +73,7 @@ class repository_wrapper : public wrapper_base branch_wrapper find_branch(std::string_view name) const; branch_iterator iterate_branches(git_branch_t type) const; std::optional upstream() const; + std::optional branch_upstream_name(std::string local_branch) const; branch_tracking_info get_tracking_info() const; // Commits diff --git a/test/test_push.py b/test/test_push.py index 313f201..3d2177d 100644 --- a/test/test_push.py +++ b/test/test_push.py @@ -61,4 +61,101 @@ def test_push_private_repo( assert p_push.returncode == 0 assert p_push.stdout.count("Username:") == 2 assert p_push.stdout.count("Password:") == 2 - assert "Pushed to origin" in p_push.stdout + assert " * [new branch] test-" in p_push.stdout + print(p_push.stdout) + + +def test_push_branch_private_repo( + git2cpp_path, tmp_path, run_in_tmp_path, private_test_repo, commit_env_config +): + """Test push with an explicit branch name: git2cpp push .""" + branch_name = f"test-{uuid4()}" + + username = "abc" + password = private_test_repo["token"] + input = f"{username}\n{password}" + repo_path = tmp_path / private_test_repo["repo_name"] + url = private_test_repo["https_url"] + + # Clone the private repo. + clone_cmd = [git2cpp_path, "clone", url] + p_clone = subprocess.run(clone_cmd, capture_output=True, text=True, input=input) + assert p_clone.returncode == 0 + assert repo_path.exists() + + # Create a new branch and commit on it. + checkout_cmd = [git2cpp_path, "checkout", "-b", branch_name] + p_checkout = subprocess.run(checkout_cmd, cwd=repo_path, capture_output=True, text=True) + assert p_checkout.returncode == 0 + + (repo_path / "push_branch_file.txt").write_text("push branch test") + subprocess.run([git2cpp_path, "add", "push_branch_file.txt"], cwd=repo_path, check=True) + subprocess.run([git2cpp_path, "commit", "-m", "branch commit"], cwd=repo_path, check=True) + + # Switch back to main so HEAD is NOT on the branch we want to push. + subprocess.run( + [git2cpp_path, "checkout", "main"], capture_output=True, check=True, cwd=repo_path + ) + + status_cmd = [git2cpp_path, "status"] + p_status = subprocess.run(status_cmd, cwd=repo_path, capture_output=True, text=True) + assert p_status.returncode == 0 + assert "On branch main" in p_status.stdout + + # Push specifying the branch explicitly (HEAD is on main, not the test branch). + input = f"{username}\n{password}" + push_cmd = [git2cpp_path, "push", "origin", branch_name] + p_push = subprocess.run(push_cmd, cwd=repo_path, capture_output=True, text=True, input=input) + assert p_push.returncode == 0 + assert " * [new branch] test-" in p_push.stdout + print("\n\n", p_push.stdout) + + +def test_push_branches_flag_private_repo( + git2cpp_path, tmp_path, run_in_tmp_path, private_test_repo, commit_env_config +): + """Test push --branches pushes all local branches.""" + branch_a = f"test-a-{uuid4()}" + branch_b = f"test-b-{uuid4()}" + + username = "abc" + password = private_test_repo["token"] + input = f"{username}\n{password}" + repo_path = tmp_path / private_test_repo["repo_name"] + url = private_test_repo["https_url"] + + # Clone the private repo. + clone_cmd = [git2cpp_path, "clone", url] + p_clone = subprocess.run(clone_cmd, capture_output=True, text=True, input=input) + assert p_clone.returncode == 0 + assert repo_path.exists() + + # Create two extra branches with commits. + for branch_name in [branch_a, branch_b]: + subprocess.run( + [git2cpp_path, "checkout", "-b", branch_name], + capture_output=True, + check=True, + cwd=repo_path, + ) + (repo_path / f"{branch_name}.txt").write_text(f"content for {branch_name}") + subprocess.run([git2cpp_path, "add", f"{branch_name}.txt"], cwd=repo_path, check=True) + subprocess.run( + [git2cpp_path, "commit", "-m", f"commit on {branch_name}"], + cwd=repo_path, + check=True, + ) + + # Go back to main. + subprocess.run( + [git2cpp_path, "checkout", "main"], capture_output=True, check=True, cwd=repo_path + ) + + # Push all branches at once. + input = f"{username}\n{password}" + push_cmd = [git2cpp_path, "push", "origin", "--branches"] + p_push = subprocess.run(push_cmd, cwd=repo_path, capture_output=True, text=True, input=input) + assert p_push.returncode == 0 + assert " * [new branch] test-" in p_push.stdout + # assert "main" not in p_push.stdout + print("\n\n", p_push.stdout)