diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp index 9e2af17..dcd3b32 100644 --- a/src/subcommand/push_subcommand.cpp +++ b/src/subcommand/push_subcommand.cpp @@ -1,9 +1,14 @@ #include "../subcommand/push_subcommand.hpp" +#include #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 +18,14 @@ 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_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,135 @@ 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) + { + auto iter = repo.iterate_branches(GIT_BRANCH_LOCAL); + auto br = iter.next(); + while (br) + { + 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; try { auto head_ref = repo.head(); - std::string short_name = head_ref.short_name(); - std::string refspec = "refs/heads/" + short_name; - m_refspecs.push_back(refspec); + 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); + // } + + // Take a snapshot of repo's references to check which ones are new after push + auto repo_refs = repo.reference_list(); + std::vector repo_refs_remote; + for (int i = 0; i < repo_refs.size(); ++i) + { + std::string prefix_remote = "refs/remote/"; + if (repo_refs[i].substr(0, prefix_remote.size()) == prefix_remote) + { + std::string remote_short_name = repo_refs[i].substr(prefix_remote.size()); + repo_refs_remote.push_back(remote_short_name); + } + } + 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_local = "refs/heads/"; + std::string local_short_name; + if (ref_view.substr(0, prefix_local.size()) == prefix_local) + { + local_short_name = ref_view.substr(prefix_local.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 = std::find(repo_refs_remote.begin(), repo_refs_remote.end(), remote_ref); + if (iter == repo_refs_remote.end()) + { + std::cout << " * [new branch] " << local_short_name << " -> " << remote_branch << std::endl; + continue; + } + + git_oid remote_oid = repo.ref_name_to_id(*iter); + + 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..615931e 100644 --- a/src/subcommand/push_subcommand.hpp +++ b/src/subcommand/push_subcommand.hpp @@ -18,4 +18,5 @@ class push_subcommand std::string m_remote_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/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..6246865 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -3,6 +3,14 @@ #include #include #include +#include +#include + +#include +#include +#include +#include +#include #include "../utils/git_exception.hpp" #include "../wrapper/commit_wrapper.hpp" @@ -134,6 +142,26 @@ std::optional repository_wrapper::find_reference_dwim(std::st return rc == 0 ? std::make_optional(reference_wrapper(ref)) : std::nullopt; } +std::vector repository_wrapper::reference_list() const +{ + git_strarray* array; + throw_if_error(git_reference_list(array, *this)); + std::vector result; + for (size_t i = 0; i < array->count; ++i) + { + result.push_back(array->strings[i]); + } + git_strarray_free(array); + return result; +} + +const git_oid& repository_wrapper::ref_name_to_id(std::string ref_name) const +{ + git_oid* ref_id; + throw_if_error(git_reference_name_to_id(ref_id, *this, ref_name.c_str())); + return *ref_id; +} + // Index index_wrapper repository_wrapper::make_index() @@ -194,6 +222,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 +469,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..9ca193c 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" @@ -63,6 +62,8 @@ class repository_wrapper : public wrapper_base // References reference_wrapper find_reference(std::string_view ref_name) const; std::optional find_reference_dwim(std::string_view ref_name) const; + std::vector reference_list() const; + const git_oid& ref_name_to_id(std::string ref_name) const; // Index index_wrapper make_index(); @@ -74,6 +75,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)