From 50621159640688d14fec25464b3ee7b4bae6f16a Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Tue, 24 Mar 2026 10:13:11 +0100 Subject: [PATCH] Add branches to push subcommand --- src/subcommand/push_subcommand.cpp | 64 +++++++++++++++++--- src/subcommand/push_subcommand.hpp | 2 + src/utils/ansi_code.hpp | 3 + src/utils/progress.cpp | 8 +-- src/utils/progress.hpp | 9 +++ test/test_push.py | 97 +++++++++++++++++++++++++++++- 6 files changed, 167 insertions(+), 16 deletions(-) diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp index 9e2af17..e6e7e4a 100644 --- a/src/subcommand/push_subcommand.cpp +++ b/src/subcommand/push_subcommand.cpp @@ -3,7 +3,9 @@ #include #include +#include +#include "../utils/ansi_code.hpp" #include "../utils/credentials.hpp" #include "../utils/progress.hpp" #include "../wrapper/repository_wrapper.hpp" @@ -13,8 +15,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]() @@ -37,25 +46,60 @@ 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; 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 short_name; + if (ref_view.substr(0, prefix.size()) == prefix) + { + short_name = ref_view.substr(prefix.size()); + } + else + { + short_name = refspec; + } + std::cout << " * " << short_name << " -> " << 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/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/test/test_push.py b/test/test_push.py index 313f201..6e92715 100644 --- a/test/test_push.py +++ b/test/test_push.py @@ -61,4 +61,99 @@ 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 + + +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 + print("\n\n", p_push.stdout)