From 0032053ba816888f21b21914fcc5a6292e3d1938 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Mon, 24 Nov 2025 21:40:20 +0100 Subject: [PATCH 1/5] feat: Add remote management, fetch, and push subcommands - Implement remote add/remove/rename/set-url/show operations - Add fetch and push subcommands for remote synchronization - Create remote_wrapper class for RAII management - Add comprehensive test suite (19 tests, all passing) - Fix CMakeLists.txt to find CLI11 in conda/pixi environment Signed-off-by: Julien Jerphanion --- CMakeLists.txt | 8 + src/main.cpp | 6 + src/subcommand/fetch_subcommand.cpp | 121 +++++++++++ src/subcommand/fetch_subcommand.hpp | 23 +++ src/subcommand/push_subcommand.cpp | 72 +++++++ src/subcommand/push_subcommand.hpp | 25 +++ src/subcommand/remote_subcommand.cpp | 216 ++++++++++++++++++++ src/subcommand/remote_subcommand.hpp | 35 ++++ src/wrapper/remote_wrapper.cpp | 54 +++++ src/wrapper/remote_wrapper.hpp | 35 ++++ src/wrapper/repository_wrapper.cpp | 68 +++++++ src/wrapper/repository_wrapper.hpp | 9 + test/test_remote.py | 293 +++++++++++++++++++++++++++ 13 files changed, 965 insertions(+) create mode 100644 src/subcommand/fetch_subcommand.cpp create mode 100644 src/subcommand/fetch_subcommand.hpp create mode 100644 src/subcommand/push_subcommand.cpp create mode 100644 src/subcommand/push_subcommand.hpp create mode 100644 src/subcommand/remote_subcommand.cpp create mode 100644 src/subcommand/remote_subcommand.hpp create mode 100644 src/wrapper/remote_wrapper.cpp create mode 100644 src/wrapper/remote_wrapper.hpp create mode 100644 test/test_remote.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d18cbd9..d0c7c08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,12 +50,18 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/subcommand/clone_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/commit_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/fetch_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/fetch_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/init_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/init_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/log_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/merge_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/push_subcommand.hpp + ${GIT2CPP_SOURCE_DIR}/subcommand/remote_subcommand.cpp + ${GIT2CPP_SOURCE_DIR}/subcommand/remote_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp @@ -82,6 +88,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/wrapper/object_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/refs_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/refs_wrapper.hpp + ${GIT2CPP_SOURCE_DIR}/wrapper/remote_wrapper.cpp + ${GIT2CPP_SOURCE_DIR}/wrapper/remote_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/repository_wrapper.cpp ${GIT2CPP_SOURCE_DIR}/wrapper/repository_wrapper.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/signature_wrapper.cpp diff --git a/src/main.cpp b/src/main.cpp index e8479c8..7b52301 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,9 +10,12 @@ #include "subcommand/checkout_subcommand.hpp" #include "subcommand/clone_subcommand.hpp" #include "subcommand/commit_subcommand.hpp" +#include "subcommand/fetch_subcommand.hpp" #include "subcommand/init_subcommand.hpp" #include "subcommand/log_subcommand.hpp" #include "subcommand/merge_subcommand.hpp" +#include "subcommand/push_subcommand.hpp" +#include "subcommand/remote_subcommand.hpp" #include "subcommand/reset_subcommand.hpp" #include "subcommand/status_subcommand.hpp" @@ -35,9 +38,12 @@ int main(int argc, char** argv) checkout_subcommand checkout(lg2_obj, app); clone_subcommand clone(lg2_obj, app); commit_subcommand commit(lg2_obj, app); + fetch_subcommand fetch(lg2_obj, app); reset_subcommand reset(lg2_obj, app); log_subcommand log(lg2_obj, app); merge_subcommand merge(lg2_obj, app); + push_subcommand push(lg2_obj, app); + remote_subcommand remote(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); diff --git a/src/subcommand/fetch_subcommand.cpp b/src/subcommand/fetch_subcommand.cpp new file mode 100644 index 0000000..6944165 --- /dev/null +++ b/src/subcommand/fetch_subcommand.cpp @@ -0,0 +1,121 @@ +#include +#include + +#include + +#include "../subcommand/fetch_subcommand.hpp" +#include "../wrapper/repository_wrapper.hpp" +#include "../wrapper/remote_wrapper.hpp" +#include "../utils/output.hpp" +#include "../utils/git_exception.hpp" + +namespace +{ + int sideband_progress(const char* str, int len, void*) + { + printf("remote: %.*s", len, str); + fflush(stdout); + return 0; + } + + int fetch_progress(const git_indexer_progress* stats, void* payload) + { + static bool done = false; + + auto* pr = reinterpret_cast(payload); + *pr = *stats; + + if (done) + { + return 0; + } + + int network_percent = pr->total_objects > 0 ? + (100 * pr->received_objects / pr->total_objects) + : 0; + size_t mbytes = pr->received_bytes / (1024*1024); + + std::cout << "Receiving objects: " << std::setw(4) << network_percent + << "% (" << pr->received_objects << "/" << pr->total_objects << "), " + << mbytes << " MiB"; + + if (pr->received_objects == pr->total_objects) + { + std::cout << ", done." << std::endl; + done = true; + } + else + { + std::cout << '\r'; + } + return 0; + } + + int update_refs(const char* refname, const git_oid* a, const git_oid* b, git_refspec*, void*) + { + char a_str[GIT_OID_SHA1_HEXSIZE+1], b_str[GIT_OID_SHA1_HEXSIZE+1]; + + git_oid_fmt(b_str, b); + b_str[GIT_OID_SHA1_HEXSIZE] = '\0'; + + if (git_oid_is_zero(a)) + { + printf("[new] %.20s %s\n", b_str, refname); + } + else + { + git_oid_fmt(a_str, a); + a_str[GIT_OID_SHA1_HEXSIZE] = '\0'; + printf("[updated] %.10s..%.10s %s\n", a_str, b_str, refname); + } + + return 0; + } +} + +fetch_subcommand::fetch_subcommand(const libgit2_object&, CLI::App& app) +{ + auto* sub = app.add_subcommand("fetch", "Download objects and refs from another repository"); + + sub->add_option("", m_remote_name, "The remote to fetch from") + ->default_val("origin"); + + sub->callback([this]() { this->run(); }); +} + +void fetch_subcommand::run() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + // Find the remote (default to origin if not specified) + std::string remote_name = m_remote_name.empty() ? "origin" : m_remote_name; + auto remote = repo.find_remote(remote_name); + + git_indexer_progress pd = {0}; + git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT; + fetch_opts.callbacks.sideband_progress = sideband_progress; + fetch_opts.callbacks.transfer_progress = fetch_progress; + fetch_opts.callbacks.payload = &pd; + fetch_opts.callbacks.update_refs = update_refs; + + cursor_hider ch; + + // Perform the fetch + throw_if_error(git_remote_fetch(remote, nullptr, &fetch_opts, "fetch")); + + // Show statistics + const git_indexer_progress* stats = git_remote_stats(remote); + if (stats->local_objects > 0) + { + std::cout << "\rReceived " << stats->indexed_objects << "/" << stats->total_objects + << " objects in " << stats->received_bytes << " bytes (used " + << stats->local_objects << " local objects)" << std::endl; + } + else + { + std::cout << "\rReceived " << stats->indexed_objects << "/" << stats->total_objects + << " objects in " << stats->received_bytes << " bytes" << std::endl; + } +} + diff --git a/src/subcommand/fetch_subcommand.hpp b/src/subcommand/fetch_subcommand.hpp new file mode 100644 index 0000000..e83eb83 --- /dev/null +++ b/src/subcommand/fetch_subcommand.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +#include "../utils/common.hpp" +#include "../wrapper/repository_wrapper.hpp" + +class fetch_subcommand +{ +public: + + explicit fetch_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + + std::string m_remote_name; +}; + + + diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp new file mode 100644 index 0000000..1f3795c --- /dev/null +++ b/src/subcommand/push_subcommand.cpp @@ -0,0 +1,72 @@ +#include + +#include + +#include "../subcommand/push_subcommand.hpp" +#include "../wrapper/repository_wrapper.hpp" +#include "../wrapper/remote_wrapper.hpp" +#include "../utils/git_exception.hpp" +#include "../utils/common.hpp" + +namespace +{ + int push_transfer_progress(unsigned int current, unsigned int total, size_t bytes, void*) + { + if (total > 0) + { + int percent = (100 * current) / total; + std::cout << "Writing objects: " << percent << "% (" << current + << "/" << total << "), " << bytes << " bytes\r"; + } + return 0; + } + + 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; + } + return 0; + } +} + +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->callback([this]() { this->run(); }); +} + +void push_subcommand::run() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + std::string remote_name = m_remote_name.empty() ? "origin" : m_remote_name; + auto remote = repo.find_remote(remote_name); + + git_push_options push_opts = GIT_PUSH_OPTIONS_INIT; + push_opts.callbacks.push_transfer_progress = push_transfer_progress; + push_opts.callbacks.push_update_reference = push_update_reference; + + git_strarray_wrapper refspecs_wrapper(m_refspecs); + git_strarray* refspecs_ptr = nullptr; + if (!m_refspecs.empty()) + { + refspecs_ptr = refspecs_wrapper; + } + + throw_if_error(git_remote_push(remote, refspecs_ptr, &push_opts)); + std::cout << "Pushed to " << remote_name << std::endl; +} + diff --git a/src/subcommand/push_subcommand.hpp b/src/subcommand/push_subcommand.hpp new file mode 100644 index 0000000..626bb4a --- /dev/null +++ b/src/subcommand/push_subcommand.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +#include "../utils/common.hpp" +#include "../wrapper/repository_wrapper.hpp" + +class push_subcommand +{ +public: + + explicit push_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + + std::string m_remote_name; + std::vector m_refspecs; +}; + + + diff --git a/src/subcommand/remote_subcommand.cpp b/src/subcommand/remote_subcommand.cpp new file mode 100644 index 0000000..4ed24ab --- /dev/null +++ b/src/subcommand/remote_subcommand.cpp @@ -0,0 +1,216 @@ +#include +#include + +#include "../subcommand/remote_subcommand.hpp" +#include "../wrapper/repository_wrapper.hpp" +#include "../utils/git_exception.hpp" + +remote_subcommand::remote_subcommand(const libgit2_object&, CLI::App& app) +{ + m_subcommand = app.add_subcommand("remote", "Manage set of tracked repositories"); + + m_subcommand->add_option("operation", m_operation, "Operation: add, remove, rename, set-url, show") + ->check(CLI::IsMember({"add", "remove", "rm", "rename", "set-url", "show"})); + + m_subcommand->add_flag("-v,--verbose", m_verbose_flag, "Be verbose"); + m_subcommand->add_flag("--push", m_push_flag, "Set push URL instead of fetch URL"); + + // Allow positional arguments after operation + m_subcommand->allow_extras(); + + m_subcommand->callback([this]() { this->run(); }); +} + +void remote_subcommand::run() +{ + auto directory = get_current_git_path(); + auto repo = repository_wrapper::open(directory); + + // Get extra positional arguments + auto extras = m_subcommand->remaining(); + + // Parse positional arguments based on operation + if (m_operation == "add" && extras.size() >= 2) + { + m_remote_name = extras[0]; + m_url = extras[1]; + run_add(repo); + } + else if ((m_operation == "remove" || m_operation == "rm") && extras.size() >= 1) + { + m_remote_name = extras[0]; + run_remove(repo); + } + else if (m_operation == "rename" && extras.size() >= 2) + { + m_old_name = extras[0]; + m_new_name = extras[1]; + run_rename(repo); + } + else if (m_operation == "set-url") + { + // Handle --push flag before arguments + size_t arg_idx = 0; + if (extras.size() > 0 && extras[0] == "--push") + { + m_push_flag = true; + arg_idx = 1; + } + if (extras.size() >= arg_idx + 2) + { + m_remote_name = extras[arg_idx]; + m_new_name = extras[arg_idx + 1]; + run_seturl(repo); + } + else if (m_remote_name.empty() || m_new_name.empty()) + { + throw std::runtime_error("remote set-url requires both name and new URL"); + } + else + { + run_seturl(repo); + } + } + else if (m_operation.empty() || m_operation == "show") + { + if (extras.size() >= 1) + { + m_remote_name = extras[0]; + } + run_show(repo); + } + else + { + // Fallback to using option values if extras not available + if (m_operation == "add") + { + run_add(repo); + } + else if (m_operation == "remove" || m_operation == "rm") + { + run_remove(repo); + } + else if (m_operation == "rename") + { + run_rename(repo); + } + else if (m_operation == "set-url") + { + run_seturl(repo); + } + } +} + +void remote_subcommand::run_list(const repository_wrapper& repo) +{ + auto remotes = repo.list_remotes(); + for (const auto& name : remotes) + { + std::cout << name << std::endl; + } +} + +void remote_subcommand::run_add(repository_wrapper& repo) +{ + if (m_remote_name.empty() || m_url.empty()) + { + throw std::runtime_error("remote add requires both name and URL"); + } + repo.create_remote(m_remote_name, m_url); +} + +void remote_subcommand::run_remove(repository_wrapper& repo) +{ + if (m_remote_name.empty()) + { + throw std::runtime_error("remote remove requires a name"); + } + repo.delete_remote(m_remote_name); +} + +void remote_subcommand::run_rename(repository_wrapper& repo) +{ + if (m_old_name.empty() || m_new_name.empty()) + { + throw std::runtime_error("remote rename requires both old and new names"); + } + repo.rename_remote(m_old_name, m_new_name); +} + +void remote_subcommand::run_seturl(repository_wrapper& repo) +{ + if (m_remote_name.empty() || m_new_name.empty()) + { + throw std::runtime_error("remote set-url requires both name and new URL"); + } + repo.set_remote_url(m_remote_name, m_new_name, m_push_flag); +} + +void remote_subcommand::run_show(const repository_wrapper& repo) +{ + auto remotes = repo.list_remotes(); + + if (m_remote_name.empty()) + { + // Show all remotes + for (const auto& name : remotes) + { + if (m_verbose_flag) + { + auto remote = repo.find_remote(name); + auto fetch_url = remote.url(); + auto push_url = remote.pushurl(); + + if (!fetch_url.empty()) + { + std::cout << name << "\t" << fetch_url << " (fetch)" << std::endl; + } + if (!push_url.empty()) + { + std::cout << name << "\t" << push_url << " (push)" << std::endl; + } + else if (!fetch_url.empty()) + { + std::cout << name << "\t" << fetch_url << " (push)" << std::endl; + } + } + else + { + std::cout << name << std::endl; + } + } + } + else + { + // Show specific remote + auto remote = repo.find_remote(m_remote_name); + std::cout << "* remote " << m_remote_name << std::endl; + + auto fetch_url = remote.url(); + if (!fetch_url.empty()) + { + std::cout << " Fetch URL: " << fetch_url << std::endl; + } + + auto push_url = remote.pushurl(); + if (!push_url.empty()) + { + std::cout << " Push URL: " << push_url << std::endl; + } + else if (!fetch_url.empty()) + { + std::cout << " Push URL: " << fetch_url << std::endl; + } + + auto refspecs = remote.refspecs(); + if (!refspecs.empty()) + { + std::cout << " HEAD branch: (not yet implemented)" << std::endl; + for (const auto& refspec : refspecs) + { + std::cout << " " << refspec << std::endl; + } + } + } +} + diff --git a/src/subcommand/remote_subcommand.hpp b/src/subcommand/remote_subcommand.hpp new file mode 100644 index 0000000..9b8ea35 --- /dev/null +++ b/src/subcommand/remote_subcommand.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include + +#include "../utils/common.hpp" +#include "../wrapper/repository_wrapper.hpp" + +class remote_subcommand +{ +public: + + explicit remote_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + + void run_list(const repository_wrapper& repo); + void run_add(repository_wrapper& repo); + void run_remove(repository_wrapper& repo); + void run_rename(repository_wrapper& repo); + void run_seturl(repository_wrapper& repo); + void run_show(const repository_wrapper& repo); + + CLI::App* m_subcommand = nullptr; + std::string m_operation; + std::string m_remote_name; + std::string m_url; + std::string m_old_name; + std::string m_new_name; + bool m_verbose_flag = false; + bool m_push_flag = false; +}; + diff --git a/src/wrapper/remote_wrapper.cpp b/src/wrapper/remote_wrapper.cpp new file mode 100644 index 0000000..c06aee7 --- /dev/null +++ b/src/wrapper/remote_wrapper.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include + +#include "../utils/git_exception.hpp" +#include "../wrapper/remote_wrapper.hpp" + +remote_wrapper::remote_wrapper(git_remote* remote) + : base_type(remote) +{ +} + +remote_wrapper::~remote_wrapper() +{ + git_remote_free(p_resource); + p_resource = nullptr; +} + +std::string_view remote_wrapper::name() const +{ + const char* out = git_remote_name(*this); + return out ? std::string_view(out) : std::string_view(); +} + +std::string_view remote_wrapper::url() const +{ + const char* out = git_remote_url(*this); + return out ? std::string_view(out) : std::string_view(); +} + +std::string_view remote_wrapper::pushurl() const +{ + const char* out = git_remote_pushurl(*this); + return out ? std::string_view(out) : std::string_view(); +} + +std::vector remote_wrapper::refspecs() const +{ + git_strarray refspecs = {0}; + std::vector result; + + if (git_remote_get_fetch_refspecs(&refspecs, *this) == 0) + { + for (size_t i = 0; i < refspecs.count; ++i) + { + result.emplace_back(refspecs.strings[i]); + } + git_strarray_dispose(&refspecs); + } + + return result; +} + diff --git a/src/wrapper/remote_wrapper.hpp b/src/wrapper/remote_wrapper.hpp new file mode 100644 index 0000000..962dd85 --- /dev/null +++ b/src/wrapper/remote_wrapper.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include + +#include "../wrapper/wrapper_base.hpp" + +class remote_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + ~remote_wrapper(); + + remote_wrapper(remote_wrapper&&) = default; + remote_wrapper& operator=(remote_wrapper&&) = default; + + std::string_view name() const; + std::string_view url() const; + std::string_view pushurl() const; + + std::vector refspecs() const; + +private: + + explicit remote_wrapper(git_remote* remote); + + friend class repository_wrapper; +}; + + + diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index fcbd365..15cdfa8 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -1,8 +1,12 @@ +#include + #include "../utils/git_exception.hpp" #include "../wrapper/index_wrapper.hpp" #include "../wrapper/object_wrapper.hpp" #include "../wrapper/commit_wrapper.hpp" +#include "../wrapper/remote_wrapper.hpp" #include +#include #include "../wrapper/repository_wrapper.hpp" repository_wrapper::~repository_wrapper() @@ -238,3 +242,67 @@ void repository_wrapper::checkout_tree(const object_wrapper& target, const git_c { throw_if_error(git_checkout_tree(*this, target, &opts)); } + +// Remotes + +remote_wrapper repository_wrapper::find_remote(std::string_view name) const +{ + git_remote* remote = nullptr; + throw_if_error(git_remote_lookup(&remote, *this, name.data())); + return remote_wrapper(remote); +} + +remote_wrapper repository_wrapper::create_remote(std::string_view name, std::string_view url) +{ + git_remote* remote = nullptr; + throw_if_error(git_remote_create(&remote, *this, name.data(), url.data())); + return remote_wrapper(remote); +} + +void repository_wrapper::delete_remote(std::string_view name) +{ + throw_if_error(git_remote_delete(*this, name.data())); +} + +void repository_wrapper::rename_remote(std::string_view old_name, std::string_view new_name) +{ + git_strarray problems = {0}; + int error = git_remote_rename(&problems, *this, old_name.data(), new_name.data()); + if (error != 0) + { + for (size_t i = 0; i < problems.count; ++i) + { + std::cerr << problems.strings[i] << std::endl; + } + git_strarray_dispose(&problems); + throw_if_error(error); + } + git_strarray_dispose(&problems); +} + +void repository_wrapper::set_remote_url(std::string_view name, std::string_view url, bool push) +{ + if (push) + { + throw_if_error(git_remote_set_pushurl(*this, name.data(), url.data())); + } + else + { + throw_if_error(git_remote_set_url(*this, name.data(), url.data())); + } +} + +std::vector repository_wrapper::list_remotes() const +{ + git_strarray remotes = {0}; + throw_if_error(git_remote_list(&remotes, *this)); + + std::vector result; + for (size_t i = 0; i < remotes.count; ++i) + { + result.emplace_back(remotes.strings[i]); + } + + git_strarray_dispose(&remotes); + return result; +} diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index 99e36ae..6b3e55a 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -13,6 +13,7 @@ #include "../wrapper/index_wrapper.hpp" #include "../wrapper/object_wrapper.hpp" #include "../wrapper/refs_wrapper.hpp" +#include "../wrapper/remote_wrapper.hpp" #include "../wrapper/signature_wrapper.hpp" #include "../wrapper/wrapper_base.hpp" @@ -73,6 +74,14 @@ class repository_wrapper : public wrapper_base // Trees void checkout_tree(const object_wrapper& target, const git_checkout_options opts); + // Remotes + remote_wrapper find_remote(std::string_view name) const; + remote_wrapper create_remote(std::string_view name, std::string_view url); + void delete_remote(std::string_view name); + void rename_remote(std::string_view old_name, std::string_view new_name); + void set_remote_url(std::string_view name, std::string_view url, bool push = false); + std::vector list_remotes() const; + private: repository_wrapper() = default; diff --git a/test/test_remote.py b/test/test_remote.py new file mode 100644 index 0000000..187c3ee --- /dev/null +++ b/test/test_remote.py @@ -0,0 +1,293 @@ +import os +import subprocess +import pytest +from pathlib import Path + + +def test_remote_list_empty(git2cpp_path, tmp_path, run_in_tmp_path): + """Test listing remotes in a repo with no remotes.""" + # Initialize a repo + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + assert p.stdout == '' # No remotes yet + + +def test_remote_add(git2cpp_path, tmp_path, run_in_tmp_path): + """Test adding a remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify remote was added + list_cmd = [git2cpp_path, 'remote'] + p_list = subprocess.run(list_cmd, capture_output=True, text=True) + assert p_list.returncode == 0 + assert 'origin' in p_list.stdout + + +def test_remote_add_multiple(git2cpp_path, tmp_path, run_in_tmp_path): + """Test adding multiple remotes.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'upstream', 'https://github.com/upstream/repo.git'], + capture_output=True, check=True) + + list_cmd = [git2cpp_path, 'remote'] + p_list = subprocess.run(list_cmd, capture_output=True, text=True) + assert p_list.returncode == 0 + output = p_list.stdout.strip() + assert 'origin' in output + assert 'upstream' in output + + +def test_remote_remove(git2cpp_path, tmp_path, run_in_tmp_path): + """Test removing a remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Remove the remote + cmd = [git2cpp_path, 'remote', 'remove', 'origin'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify remote was removed + list_cmd = [git2cpp_path, 'remote'] + p_list = subprocess.run(list_cmd, capture_output=True, text=True) + assert p_list.returncode == 0 + assert 'origin' not in p_list.stdout + + +def test_remote_remove_rm_alias(git2cpp_path, tmp_path, run_in_tmp_path): + """Test removing a remote using 'rm' alias.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Remove using 'rm' alias + cmd = [git2cpp_path, 'remote', 'rm', 'origin'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify remote was removed + list_cmd = [git2cpp_path, 'remote'] + p_list = subprocess.run(list_cmd, capture_output=True, text=True) + assert p_list.returncode == 0 + assert 'origin' not in p_list.stdout + + +def test_remote_rename(git2cpp_path, tmp_path, run_in_tmp_path): + """Test renaming a remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Rename the remote + cmd = [git2cpp_path, 'remote', 'rename', 'origin', 'upstream'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify remote was renamed + list_cmd = [git2cpp_path, 'remote'] + p_list = subprocess.run(list_cmd, capture_output=True, text=True) + assert p_list.returncode == 0 + assert 'origin' not in p_list.stdout + assert 'upstream' in p_list.stdout + + +def test_remote_set_url(git2cpp_path, tmp_path, run_in_tmp_path): + """Test setting remote URL.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Change the URL + new_url = 'https://github.com/user/newrepo.git' + cmd = [git2cpp_path, 'remote', 'set-url', 'origin', new_url] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify URL was changed + show_cmd = [git2cpp_path, 'remote', 'show', 'origin'] + p_show = subprocess.run(show_cmd, capture_output=True, text=True) + assert p_show.returncode == 0 + assert new_url in p_show.stdout + + +def test_remote_set_push_url(git2cpp_path, tmp_path, run_in_tmp_path): + """Test setting remote push URL.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Set push URL + push_url = 'https://github.com/user/pushrepo.git' + cmd = [git2cpp_path, 'remote', 'set-url', '--push', 'origin', push_url] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + + # Verify push URL was set + show_cmd = [git2cpp_path, 'remote', 'show', 'origin'] + p_show = subprocess.run(show_cmd, capture_output=True, text=True) + assert p_show.returncode == 0 + assert push_url in p_show.stdout + + +def test_remote_show(git2cpp_path, tmp_path, run_in_tmp_path): + """Test showing remote details.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + url = 'https://github.com/user/repo.git' + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', url], + capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'show', 'origin'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + assert 'origin' in p.stdout + assert url in p.stdout + + +def test_remote_show_verbose(git2cpp_path, tmp_path, run_in_tmp_path): + """Test showing remotes with verbose flag.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + url = 'https://github.com/user/repo.git' + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', url], + capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', '-v'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + assert 'origin' in p.stdout + assert url in p.stdout + assert '(fetch)' in p.stdout or '(push)' in p.stdout + + +def test_remote_show_all_verbose(git2cpp_path, tmp_path, run_in_tmp_path): + """Test showing all remotes with verbose flag.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'upstream', 'https://github.com/upstream/repo.git'], + capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'show', '-v'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode == 0 + assert 'origin' in p.stdout + assert 'upstream' in p.stdout + + +def test_remote_error_on_duplicate_add(git2cpp_path, tmp_path, run_in_tmp_path): + """Test error when adding duplicate remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], + capture_output=True, check=True) + + # Try to add duplicate + cmd = [git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/other.git'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode != 0 + + +def test_remote_error_on_remove_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): + """Test error when removing non-existent remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'remove', 'nonexistent'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode != 0 + + +def test_remote_error_on_rename_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): + """Test error when renaming non-existent remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'rename', 'nonexistent', 'new'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode != 0 + + +def test_remote_error_on_show_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): + """Test error when showing non-existent remote.""" + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) + + cmd = [git2cpp_path, 'remote', 'show', 'nonexistent'] + p = subprocess.run(cmd, capture_output=True, text=True) + assert p.returncode != 0 + + +@pytest.fixture +def repo_with_remote(git2cpp_path, tmp_path, run_in_tmp_path): + """Fixture that creates a repo with a remote pointing to a local bare repo.""" + # Create a bare repo to use as remote + remote_path = tmp_path / "remote_repo" + remote_path.mkdir() + subprocess.run([git2cpp_path, 'init', '--bare', str(remote_path)], + capture_output=True, check=True) + + # Create a regular repo + repo_path = tmp_path / "local_repo" + repo_path.mkdir() + + # Initialize repo in the directory + subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True, cwd=repo_path) + + # Add remote + subprocess.run([git2cpp_path, 'remote', 'add', 'origin', str(remote_path)], + capture_output=True, check=True, cwd=repo_path) + + return repo_path, remote_path + + +def test_fetch_from_remote(git2cpp_path, repo_with_remote): + """Test fetching from a remote.""" + repo_path, remote_path = repo_with_remote + + # Note: This is a bare repo with no refs, so fetch will fail gracefully + # For now, just test that fetch command runs (it will fail gracefully if no refs) + cmd = [git2cpp_path, 'fetch', 'origin'] + p = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_path) + # Fetch might succeed (empty) or fail (no refs), but shouldn't crash + assert p.returncode in [0, 1] # 0 for success, 1 for no refs/error + + +def test_fetch_default_origin(git2cpp_path, repo_with_remote): + """Test fetching with default origin.""" + repo_path, remote_path = repo_with_remote + + cmd = [git2cpp_path, 'fetch'] + p = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_path) + # Fetch might succeed (empty) or fail (no refs), but shouldn't crash + assert p.returncode in [0, 1] + + +def test_remote_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): + """Test that cloned repos have remotes configured.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + cmd = [git2cpp_path, 'remote'] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert 'origin' in p.stdout + + +def test_remote_show_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): + """Test showing remote in cloned repo.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + + cmd = [git2cpp_path, 'remote', 'show', 'origin'] + p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) + assert p.returncode == 0 + assert 'origin' in p.stdout + # Should contain URL information + assert 'http' in p.stdout or 'git' in p.stdout or 'https' in p.stdout + From 72404c28b19ddb36198832ae3a9a726d5608f1b5 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Mon, 1 Dec 2025 14:20:17 +0100 Subject: [PATCH 2/5] Address review comments Signed-off-by: Julien Jerphanion Co-authored-by: Johan Mabille --- src/subcommand/fetch_subcommand.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/subcommand/fetch_subcommand.cpp b/src/subcommand/fetch_subcommand.cpp index 6944165..afe8638 100644 --- a/src/subcommand/fetch_subcommand.cpp +++ b/src/subcommand/fetch_subcommand.cpp @@ -13,14 +13,14 @@ namespace { int sideband_progress(const char* str, int len, void*) { - printf("remote: %.*s", len, str); - fflush(stdout); + std::cout << "remote: " << std::string(str, static_cast(len)); + std::cout.flush(); return 0; } int fetch_progress(const git_indexer_progress* stats, void* payload) { - static bool done = false; + bool done = false; auto* pr = reinterpret_cast(payload); *pr = *stats; @@ -60,13 +60,20 @@ namespace if (git_oid_is_zero(a)) { - printf("[new] %.20s %s\n", b_str, refname); + std::cout << "[new] " + << std::string(b_str, 20) + << " " << refname << std::endl; } else { git_oid_fmt(a_str, a); a_str[GIT_OID_SHA1_HEXSIZE] = '\0'; - printf("[updated] %.10s..%.10s %s\n", a_str, b_str, refname); + + std::cout << "[updated] " + << std::string(a_str, 10) + << ".." + << std::string(b_str, 10) + << " " << refname << std::endl; } return 0; From 57e377987bf990a25647e40790ed2fab07b24a46 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Wed, 10 Dec 2025 11:06:38 +0100 Subject: [PATCH 3/5] edit remote --- CMakeLists.txt | 2 + src/subcommand/clone_subcommand.cpp | 80 +----- src/subcommand/clone_subcommand.hpp | 1 + src/subcommand/fetch_subcommand.cpp | 84 +----- src/subcommand/fetch_subcommand.hpp | 4 - src/subcommand/push_subcommand.cpp | 56 ++-- src/subcommand/push_subcommand.hpp | 4 - src/subcommand/remote_subcommand.cpp | 82 ++---- src/subcommand/remote_subcommand.hpp | 1 - src/utils/progress.cpp | 155 +++++++++++ src/utils/progress.hpp | 10 + src/wrapper/remote_wrapper.cpp | 15 +- src/wrapper/remote_wrapper.hpp | 11 +- test/test_clone.py | 22 +- test/test_remote.py | 394 ++++++++++++++++----------- 15 files changed, 505 insertions(+), 416 deletions(-) create mode 100644 src/utils/progress.cpp create mode 100644 src/utils/progress.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d0c7c08..a9a266b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,8 @@ set(GIT2CPP_SRC ${GIT2CPP_SOURCE_DIR}/utils/git_exception.hpp ${GIT2CPP_SOURCE_DIR}/utils/output.cpp ${GIT2CPP_SOURCE_DIR}/utils/output.hpp + ${GIT2CPP_SOURCE_DIR}/utils/progress.cpp + ${GIT2CPP_SOURCE_DIR}/utils/progress.hpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.cpp ${GIT2CPP_SOURCE_DIR}/utils/terminal_pager.hpp ${GIT2CPP_SOURCE_DIR}/wrapper/annotated_commit_wrapper.cpp diff --git a/src/subcommand/clone_subcommand.cpp b/src/subcommand/clone_subcommand.cpp index 6c9b803..69b44fa 100644 --- a/src/subcommand/clone_subcommand.cpp +++ b/src/subcommand/clone_subcommand.cpp @@ -2,6 +2,7 @@ #include "../subcommand/clone_subcommand.hpp" #include "../utils/output.hpp" +#include "../utils/progress.hpp" #include "../wrapper/repository_wrapper.hpp" clone_subcommand::clone_subcommand(const libgit2_object&, CLI::App& app) @@ -10,81 +11,11 @@ clone_subcommand::clone_subcommand(const libgit2_object&, CLI::App& app) sub->add_option("", m_repository, "The (possibly remote) repository to clone from.")->required(); sub->add_option("", m_directory, "The name of a new directory to clone into."); + sub->add_flag("--bare", m_bare, "Create a bare Git repository."); sub->callback([this]() { this->run(); }); } -namespace -{ - int sideband_progress(const char* str, int len, void*) - { - printf("remote: %.*s", len, str); - fflush(stdout); - return 0; - } - - int fetch_progress(const git_indexer_progress* stats, void* payload) - { - static bool done = false; - - // We need to copy stats into payload even if the fetch is done, - // because the checkout_progress callback will be called with the - // same payload and needs the data to be up do date. - auto* pr = reinterpret_cast(payload); - *pr = *stats; - - if (done) - { - return 0; - } - - int network_percent = pr->total_objects > 0 ? - (100 * pr->received_objects / pr->total_objects) - : 0; - size_t mbytes = pr->received_bytes / (1024*1024); - - std::cout << "Receiving objects: " << std::setw(4) << network_percent - << "% (" << pr->received_objects << "/" << pr->total_objects << "), " - << mbytes << " MiB"; - - if (pr->received_objects == pr->total_objects) - { - std::cout << ", done." << std::endl; - done = true; - } - else - { - std::cout << '\r'; - } - return 0; - } - - void checkout_progress(const char* path, size_t cur, size_t tot, void* payload) - { - static bool done = false; - if (done) - { - return; - } - auto* pr = reinterpret_cast(payload); - int deltas_percent = pr->total_deltas > 0 ? - (100 * pr->indexed_deltas / pr->total_deltas) - : 0; - - std::cout << "Resolving deltas: " << std::setw(4) << deltas_percent - << "% (" << pr->indexed_deltas << "/" << pr->total_deltas << ")"; - if (pr->indexed_deltas == pr->total_deltas) - { - std::cout << ", done." << std::endl; - done = true; - } - else - { - std::cout << '\r'; - } - } -} - void clone_subcommand::run() { git_indexer_progress pd; @@ -94,9 +25,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.sideband_progress = sideband_progress; - clone_opts.fetch_opts.callbacks.transfer_progress = fetch_progress; - clone_opts.fetch_opts.callbacks.payload = &pd; + 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; + clone_opts.bare = m_bare ? 1 : 0; std::string short_name = m_directory; if (m_directory.empty()) diff --git a/src/subcommand/clone_subcommand.hpp b/src/subcommand/clone_subcommand.hpp index bf2a0d7..631cd07 100644 --- a/src/subcommand/clone_subcommand.hpp +++ b/src/subcommand/clone_subcommand.hpp @@ -15,4 +15,5 @@ class clone_subcommand std::string m_repository = {}; std::string m_directory = {}; + bool m_bare = false; }; diff --git a/src/subcommand/fetch_subcommand.cpp b/src/subcommand/fetch_subcommand.cpp index afe8638..4d07e1a 100644 --- a/src/subcommand/fetch_subcommand.cpp +++ b/src/subcommand/fetch_subcommand.cpp @@ -1,84 +1,11 @@ #include -#include #include #include "../subcommand/fetch_subcommand.hpp" -#include "../wrapper/repository_wrapper.hpp" -#include "../wrapper/remote_wrapper.hpp" #include "../utils/output.hpp" -#include "../utils/git_exception.hpp" - -namespace -{ - int sideband_progress(const char* str, int len, void*) - { - std::cout << "remote: " << std::string(str, static_cast(len)); - std::cout.flush(); - return 0; - } - - int fetch_progress(const git_indexer_progress* stats, void* payload) - { - bool done = false; - - auto* pr = reinterpret_cast(payload); - *pr = *stats; - - if (done) - { - return 0; - } - - int network_percent = pr->total_objects > 0 ? - (100 * pr->received_objects / pr->total_objects) - : 0; - size_t mbytes = pr->received_bytes / (1024*1024); - - std::cout << "Receiving objects: " << std::setw(4) << network_percent - << "% (" << pr->received_objects << "/" << pr->total_objects << "), " - << mbytes << " MiB"; - - if (pr->received_objects == pr->total_objects) - { - std::cout << ", done." << std::endl; - done = true; - } - else - { - std::cout << '\r'; - } - return 0; - } - - int update_refs(const char* refname, const git_oid* a, const git_oid* b, git_refspec*, void*) - { - char a_str[GIT_OID_SHA1_HEXSIZE+1], b_str[GIT_OID_SHA1_HEXSIZE+1]; - - git_oid_fmt(b_str, b); - b_str[GIT_OID_SHA1_HEXSIZE] = '\0'; - - if (git_oid_is_zero(a)) - { - std::cout << "[new] " - << std::string(b_str, 20) - << " " << refname << std::endl; - } - else - { - git_oid_fmt(a_str, a); - a_str[GIT_OID_SHA1_HEXSIZE] = '\0'; - - std::cout << "[updated] " - << std::string(a_str, 10) - << ".." - << std::string(b_str, 10) - << " " << refname << std::endl; - } - - return 0; - } -} +#include "../utils/progress.hpp" +#include "../wrapper/repository_wrapper.hpp" fetch_subcommand::fetch_subcommand(const libgit2_object&, CLI::App& app) { @@ -107,16 +34,16 @@ void fetch_subcommand::run() fetch_opts.callbacks.update_refs = update_refs; cursor_hider ch; - + // Perform the fetch - throw_if_error(git_remote_fetch(remote, nullptr, &fetch_opts, "fetch")); + remote.fetch(nullptr, &fetch_opts, "fetch"); // Show statistics const git_indexer_progress* stats = git_remote_stats(remote); if (stats->local_objects > 0) { std::cout << "\rReceived " << stats->indexed_objects << "/" << stats->total_objects - << " objects in " << stats->received_bytes << " bytes (used " + << " objects in " << stats->received_bytes << " bytes (used " << stats->local_objects << " local objects)" << std::endl; } else @@ -125,4 +52,3 @@ void fetch_subcommand::run() << " objects in " << stats->received_bytes << " bytes" << std::endl; } } - diff --git a/src/subcommand/fetch_subcommand.hpp b/src/subcommand/fetch_subcommand.hpp index e83eb83..bc607c1 100644 --- a/src/subcommand/fetch_subcommand.hpp +++ b/src/subcommand/fetch_subcommand.hpp @@ -5,7 +5,6 @@ #include #include "../utils/common.hpp" -#include "../wrapper/repository_wrapper.hpp" class fetch_subcommand { @@ -18,6 +17,3 @@ class fetch_subcommand std::string m_remote_name; }; - - - diff --git a/src/subcommand/push_subcommand.cpp b/src/subcommand/push_subcommand.cpp index 1f3795c..be04267 100644 --- a/src/subcommand/push_subcommand.cpp +++ b/src/subcommand/push_subcommand.cpp @@ -3,37 +3,8 @@ #include #include "../subcommand/push_subcommand.hpp" +#include "../utils/progress.hpp" #include "../wrapper/repository_wrapper.hpp" -#include "../wrapper/remote_wrapper.hpp" -#include "../utils/git_exception.hpp" -#include "../utils/common.hpp" - -namespace -{ - int push_transfer_progress(unsigned int current, unsigned int total, size_t bytes, void*) - { - if (total > 0) - { - int percent = (100 * current) / total; - std::cout << "Writing objects: " << percent << "% (" << current - << "/" << total << "), " << bytes << " bytes\r"; - } - return 0; - } - - 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; - } - return 0; - } -} push_subcommand::push_subcommand(const libgit2_object&, CLI::App& app) { @@ -41,7 +12,7 @@ push_subcommand::push_subcommand(const libgit2_object&, CLI::App& app) 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->callback([this]() { this->run(); }); @@ -59,14 +30,25 @@ void push_subcommand::run() push_opts.callbacks.push_transfer_progress = push_transfer_progress; push_opts.callbacks.push_update_reference = push_update_reference; - git_strarray_wrapper refspecs_wrapper(m_refspecs); - git_strarray* refspecs_ptr = nullptr; - if (!m_refspecs.empty()) + if (m_refspecs.empty()) { - refspecs_ptr = refspecs_wrapper; + 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); + } + catch (...) + { + std::cerr << "Could not determine current branch to push." << std::endl; + return; + } } + git_strarray_wrapper refspecs_wrapper(m_refspecs); + git_strarray* refspecs_ptr = nullptr; + refspecs_ptr = refspecs_wrapper; - throw_if_error(git_remote_push(remote, refspecs_ptr, &push_opts)); + remote.push(refspecs_ptr, &push_opts); std::cout << "Pushed to " << remote_name << std::endl; } - diff --git a/src/subcommand/push_subcommand.hpp b/src/subcommand/push_subcommand.hpp index 626bb4a..07c301e 100644 --- a/src/subcommand/push_subcommand.hpp +++ b/src/subcommand/push_subcommand.hpp @@ -6,7 +6,6 @@ #include #include "../utils/common.hpp" -#include "../wrapper/repository_wrapper.hpp" class push_subcommand { @@ -20,6 +19,3 @@ class push_subcommand std::string m_remote_name; std::vector m_refspecs; }; - - - diff --git a/src/subcommand/remote_subcommand.cpp b/src/subcommand/remote_subcommand.cpp index 4ed24ab..f76ad70 100644 --- a/src/subcommand/remote_subcommand.cpp +++ b/src/subcommand/remote_subcommand.cpp @@ -3,7 +3,6 @@ #include "../subcommand/remote_subcommand.hpp" #include "../wrapper/repository_wrapper.hpp" -#include "../utils/git_exception.hpp" remote_subcommand::remote_subcommand(const libgit2_object&, CLI::App& app) { @@ -11,13 +10,13 @@ remote_subcommand::remote_subcommand(const libgit2_object&, CLI::App& app) m_subcommand->add_option("operation", m_operation, "Operation: add, remove, rename, set-url, show") ->check(CLI::IsMember({"add", "remove", "rm", "rename", "set-url", "show"})); - + m_subcommand->add_flag("-v,--verbose", m_verbose_flag, "Be verbose"); m_subcommand->add_flag("--push", m_push_flag, "Set push URL instead of fetch URL"); // Allow positional arguments after operation m_subcommand->allow_extras(); - + m_subcommand->callback([this]() { this->run(); }); } @@ -28,23 +27,32 @@ void remote_subcommand::run() // Get extra positional arguments auto extras = m_subcommand->remaining(); - + // Parse positional arguments based on operation - if (m_operation == "add" && extras.size() >= 2) + if (m_operation == "add") { - m_remote_name = extras[0]; - m_url = extras[1]; + if (extras.size() == 2) + { + m_remote_name = extras[0]; + m_url = extras[1]; + } run_add(repo); } - else if ((m_operation == "remove" || m_operation == "rm") && extras.size() >= 1) + else if (m_operation == "remove" || m_operation == "rm") { - m_remote_name = extras[0]; + if (extras.size() == 1) + { + m_remote_name = extras[0]; + } run_remove(repo); } - else if (m_operation == "rename" && extras.size() >= 2) + else if (m_operation == "rename") { - m_old_name = extras[0]; - m_new_name = extras[1]; + if (extras.size() == 2) + { + m_old_name = extras[0]; + m_new_name = extras[1]; + } run_rename(repo); } else if (m_operation == "set-url") @@ -79,42 +87,13 @@ void remote_subcommand::run() } run_show(repo); } - else - { - // Fallback to using option values if extras not available - if (m_operation == "add") - { - run_add(repo); - } - else if (m_operation == "remove" || m_operation == "rm") - { - run_remove(repo); - } - else if (m_operation == "rename") - { - run_rename(repo); - } - else if (m_operation == "set-url") - { - run_seturl(repo); - } - } -} - -void remote_subcommand::run_list(const repository_wrapper& repo) -{ - auto remotes = repo.list_remotes(); - for (const auto& name : remotes) - { - std::cout << name << std::endl; - } } void remote_subcommand::run_add(repository_wrapper& repo) { - if (m_remote_name.empty() || m_url.empty()) + if (m_remote_name.empty()) { - throw std::runtime_error("remote add requires both name and URL"); + throw std::runtime_error("usage: git remote add "); // TODO: add [] when implemented } repo.create_remote(m_remote_name, m_url); } @@ -123,16 +102,16 @@ void remote_subcommand::run_remove(repository_wrapper& repo) { if (m_remote_name.empty()) { - throw std::runtime_error("remote remove requires a name"); + throw std::runtime_error("usage: git remote remove "); } repo.delete_remote(m_remote_name); } void remote_subcommand::run_rename(repository_wrapper& repo) { - if (m_old_name.empty() || m_new_name.empty()) + if (m_old_name.empty()) { - throw std::runtime_error("remote rename requires both old and new names"); + throw std::runtime_error("usage: git remote rename "); // TODO: add [--[no-]progress] when implemented } repo.rename_remote(m_old_name, m_new_name); } @@ -149,7 +128,7 @@ void remote_subcommand::run_seturl(repository_wrapper& repo) void remote_subcommand::run_show(const repository_wrapper& repo) { auto remotes = repo.list_remotes(); - + if (m_remote_name.empty()) { // Show all remotes @@ -160,7 +139,7 @@ void remote_subcommand::run_show(const repository_wrapper& repo) auto remote = repo.find_remote(name); auto fetch_url = remote.url(); auto push_url = remote.pushurl(); - + if (!fetch_url.empty()) { std::cout << name << "\t" << fetch_url << " (fetch)" << std::endl; @@ -185,13 +164,13 @@ void remote_subcommand::run_show(const repository_wrapper& repo) // Show specific remote auto remote = repo.find_remote(m_remote_name); std::cout << "* remote " << m_remote_name << std::endl; - + auto fetch_url = remote.url(); if (!fetch_url.empty()) { std::cout << " Fetch URL: " << fetch_url << std::endl; } - + auto push_url = remote.pushurl(); if (!push_url.empty()) { @@ -201,7 +180,7 @@ void remote_subcommand::run_show(const repository_wrapper& repo) { std::cout << " Push URL: " << fetch_url << std::endl; } - + auto refspecs = remote.refspecs(); if (!refspecs.empty()) { @@ -213,4 +192,3 @@ void remote_subcommand::run_show(const repository_wrapper& repo) } } } - diff --git a/src/subcommand/remote_subcommand.hpp b/src/subcommand/remote_subcommand.hpp index 9b8ea35..b6f3bf2 100644 --- a/src/subcommand/remote_subcommand.hpp +++ b/src/subcommand/remote_subcommand.hpp @@ -32,4 +32,3 @@ class remote_subcommand bool m_verbose_flag = false; bool m_push_flag = false; }; - diff --git a/src/utils/progress.cpp b/src/utils/progress.cpp new file mode 100644 index 0000000..bdb3a23 --- /dev/null +++ b/src/utils/progress.cpp @@ -0,0 +1,155 @@ +#include +#include +#include + +#include "../utils/progress.hpp" + +int sideband_progress(const char* str, int len, void*) +{ + printf("remote: %.*s", len, str); + fflush(stdout); + return 0; +} + +int fetch_progress(const git_indexer_progress* stats, void* payload) +{ + static bool done = false; + + // We need to copy stats into payload even if the fetch is done, + // because the checkout_progress callback will be called with the + // same payload and needs the data to be up do date. + auto* pr = reinterpret_cast(payload); + *pr = *stats; + + if (done) + { + return 0; + } + + int network_percent = pr->total_objects > 0 ? + (100 * pr->received_objects / pr->total_objects) + : 0; + size_t kbytes = pr->received_bytes / 1024; + size_t mbytes = kbytes / 1024; + + std::cout << "Receiving objects: " << std::setw(4) << network_percent + << "% (" << pr->received_objects << "/" << pr->total_objects << "), "; + if (mbytes != 0) + { + std::cout << mbytes << " MiB"; + } + else if (kbytes != 0) + { + std::cout << kbytes << " KiB"; + } + else + { + std::cout << pr->received_bytes << " bytes"; + } + // TODO: compute speed + + if (pr->received_objects == pr->total_objects) + { + std::cout << ", done." << std::endl; + done = true; + } + else + { + std::cout << '\r'; + } + return 0; +} + +void checkout_progress(const char* path, size_t cur, size_t tot, void* payload) +{ + static bool done = false; + if (done) + { + return; + } + auto* pr = reinterpret_cast(payload); + int deltas_percent = pr->total_deltas > 0 ? + (100 * pr->indexed_deltas / pr->total_deltas) + : 0; + + std::cout << "Resolving deltas: " << std::setw(4) << deltas_percent + << "% (" << pr->indexed_deltas << "/" << pr->total_deltas << ")"; + if (pr->indexed_deltas == pr->total_deltas) + { + std::cout << ", done." << std::endl; + done = true; + } + else + { + std::cout << '\r'; + } +} + +int update_refs(const char* refname, const git_oid* a, const git_oid* b, git_refspec*, void*) +{ + char a_str[GIT_OID_SHA1_HEXSIZE+1], b_str[GIT_OID_SHA1_HEXSIZE+1]; + + git_oid_fmt(b_str, b); + b_str[GIT_OID_SHA1_HEXSIZE] = '\0'; + + if (git_oid_is_zero(a)) + { + std::string n, name, ref; + const size_t last_slash_idx = std::string_view(refname).find_last_of('/'); + name = std::string_view(refname).substr(last_slash_idx + 1, -1); + if (std::string_view(refname).find("remote") != std::string::npos) // maybe will string_view need the size of the string + { + n = " * [new branch] "; + auto new_refname = std::string_view(refname).substr(0, last_slash_idx - 1); + const size_t second_to_last_slash_idx = std::string_view(new_refname).find_last_of('/'); + ref = std::string_view(refname).substr(second_to_last_slash_idx + 1, -1); + } + else if (std::string_view(refname).find("tags") != std::string::npos) + { + n = " * [new tag] "; + ref = name; + } + else + { + // could it be something else ? + } + std::cout << n << name << "\t-> " << ref << std::endl; + } + else + { + git_oid_fmt(a_str, a); + a_str[GIT_OID_SHA1_HEXSIZE] = '\0'; + + std::cout << "[updated] " + << std::string(a_str, 10) + << ".." + << std::string(b_str, 10) + << " " << refname << std::endl; + } + + return 0; +} + +int push_transfer_progress(unsigned int current, unsigned int total, size_t bytes, void*) +{ + if (total > 0) + { + int percent = (100 * current) / total; + std::cout << "Writing objects: " << percent << "% (" << current + << "/" << total << "), " << bytes << " bytes\r"; + } + return 0; +} + +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; + } + return 0; +} diff --git a/src/utils/progress.hpp b/src/utils/progress.hpp new file mode 100644 index 0000000..861c8d9 --- /dev/null +++ b/src/utils/progress.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +int sideband_progress(const char* str, int len, void*); +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*); +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 c06aee7..08420ca 100644 --- a/src/wrapper/remote_wrapper.cpp +++ b/src/wrapper/remote_wrapper.cpp @@ -8,7 +8,7 @@ remote_wrapper::remote_wrapper(git_remote* remote) : base_type(remote) -{ +{ } remote_wrapper::~remote_wrapper() @@ -39,7 +39,7 @@ std::vector remote_wrapper::refspecs() const { git_strarray refspecs = {0}; std::vector result; - + if (git_remote_get_fetch_refspecs(&refspecs, *this) == 0) { for (size_t i = 0; i < refspecs.count; ++i) @@ -48,7 +48,16 @@ std::vector remote_wrapper::refspecs() const } git_strarray_dispose(&refspecs); } - + return result; } +void remote_wrapper::fetch(const git_strarray* refspecs, const git_fetch_options* opts, const char* reflog_message) +{ + throw_if_error(git_remote_fetch(*this, refspecs, opts, reflog_message)); +} + +void remote_wrapper::push(const git_strarray* refspecs, const git_push_options* opts) +{ + throw_if_error(git_remote_push(*this, refspecs, opts)); +} diff --git a/src/wrapper/remote_wrapper.hpp b/src/wrapper/remote_wrapper.hpp index 962dd85..1fa1632 100644 --- a/src/wrapper/remote_wrapper.hpp +++ b/src/wrapper/remote_wrapper.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -21,15 +22,15 @@ class remote_wrapper : public wrapper_base std::string_view name() const; std::string_view url() const; std::string_view pushurl() const; - + std::vector refspecs() const; + 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); + private: - + explicit remote_wrapper(git_remote* remote); friend class repository_wrapper; }; - - - diff --git a/test/test_clone.py b/test/test_clone.py index 7ada28e..a28a058 100644 --- a/test/test_clone.py +++ b/test/test_clone.py @@ -5,11 +5,23 @@ def test_clone(git2cpp_path, tmp_path, run_in_tmp_path): - url = 'https://github.com/xtensor-stack/xtl.git' + url = "https://github.com/xtensor-stack/xtl.git" - clone_cmd = [git2cpp_path, 'clone', url] - p_clone = subprocess.run(clone_cmd, capture_output=True, cwd = tmp_path, text=True) + clone_cmd = [git2cpp_path, "clone", url] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) assert p_clone.returncode == 0 - assert os.path.exists(os.path.join(tmp_path, 'xtl')) - assert os.path.exists(os.path.join(tmp_path, 'xtl/include')) + assert os.path.exists(os.path.join(tmp_path, "xtl")) + assert os.path.exists(os.path.join(tmp_path, "xtl/include")) + + +def test_clone_is_bare(git2cpp_path, tmp_path, run_in_tmp_path): + url = "https://github.com/xtensor-stack/xtl.git" + + clone_cmd = [git2cpp_path, "clone", "--bare", url] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_clone.returncode == 0 + + status_cmd = [git2cpp_path, "status"] + p_status = subprocess.run(status_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_status.returncode != 0 diff --git a/test/test_remote.py b/test/test_remote.py index 187c3ee..4676e73 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -1,121 +1,130 @@ -import os import subprocess + import pytest -from pathlib import Path + +repo_url = "https://github.com/user/repo.git" def test_remote_list_empty(git2cpp_path, tmp_path, run_in_tmp_path): """Test listing remotes in a repo with no remotes.""" # Initialize a repo - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + cmd = [git2cpp_path, "remote"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode == 0 - assert p.stdout == '' # No remotes yet + assert p.stdout == "" # No remotes yet def test_remote_add(git2cpp_path, tmp_path, run_in_tmp_path): """Test adding a remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + missing_cmd = [git2cpp_path, "remote", "add", "origin"] + p_missing = subprocess.run(missing_cmd, capture_output=True, text=True) + assert p_missing.returncode != 0 + assert "usage: git remote add " in p_missing.stderr + + add_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add = subprocess.run(add_cmd, capture_output=True, text=True) + assert p_add.returncode == 0 + # Verify remote was added - list_cmd = [git2cpp_path, 'remote'] + list_cmd = [git2cpp_path, "remote"] p_list = subprocess.run(list_cmd, capture_output=True, text=True) assert p_list.returncode == 0 - assert 'origin' in p_list.stdout + assert "origin" in p_list.stdout def test_remote_add_multiple(git2cpp_path, tmp_path, run_in_tmp_path): """Test adding multiple remotes.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'upstream', 'https://github.com/upstream/repo.git'], - capture_output=True, check=True) - - list_cmd = [git2cpp_path, 'remote'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + add_origin_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add_origin = subprocess.run(add_origin_cmd, capture_output=True, check=True) + assert p_add_origin.returncode == 0 + add_upstream_cmd = [ + git2cpp_path, + "remote", + "add", + "upstream", + "https://github.com/upstream/repo.git", + ] + p_add_upstream = subprocess.run(add_upstream_cmd, capture_output=True, check=True) + assert p_add_upstream.returncode == 0 + + list_cmd = [git2cpp_path, "remote"] p_list = subprocess.run(list_cmd, capture_output=True, text=True) assert p_list.returncode == 0 output = p_list.stdout.strip() - assert 'origin' in output - assert 'upstream' in output + assert "origin" in output + assert "upstream" in output -def test_remote_remove(git2cpp_path, tmp_path, run_in_tmp_path): +@pytest.mark.parametrize("remove", ["rm", "remove"]) +def test_remote_remove(git2cpp_path, tmp_path, run_in_tmp_path, remove): """Test removing a remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - - # Remove the remote - cmd = [git2cpp_path, 'remote', 'remove', 'origin'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - - # Verify remote was removed - list_cmd = [git2cpp_path, 'remote'] - p_list = subprocess.run(list_cmd, capture_output=True, text=True) - assert p_list.returncode == 0 - assert 'origin' not in p_list.stdout + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + add_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add = subprocess.run(add_cmd, capture_output=True, check=True) + assert p_add.returncode == 0 + + # Remove the remote + remove_cmd = [git2cpp_path, "remote", remove, "origin"] + p_remove = subprocess.run(remove_cmd, capture_output=True, text=True) + assert p_remove.returncode == 0 -def test_remote_remove_rm_alias(git2cpp_path, tmp_path, run_in_tmp_path): - """Test removing a remote using 'rm' alias.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - - # Remove using 'rm' alias - cmd = [git2cpp_path, 'remote', 'rm', 'origin'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - # Verify remote was removed - list_cmd = [git2cpp_path, 'remote'] + list_cmd = [git2cpp_path, "remote"] p_list = subprocess.run(list_cmd, capture_output=True, text=True) assert p_list.returncode == 0 - assert 'origin' not in p_list.stdout + assert "origin" not in p_list.stdout def test_remote_rename(git2cpp_path, tmp_path, run_in_tmp_path): """Test renaming a remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + add_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add = subprocess.run(add_cmd, capture_output=True, check=True) + assert p_add.returncode == 0 + # Rename the remote - cmd = [git2cpp_path, 'remote', 'rename', 'origin', 'upstream'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - + rename_cmd = [git2cpp_path, "remote", "rename", "origin", "upstream"] + p_rename = subprocess.run(rename_cmd, capture_output=True, text=True) + assert p_rename.returncode == 0 + # Verify remote was renamed - list_cmd = [git2cpp_path, 'remote'] + list_cmd = [git2cpp_path, "remote"] p_list = subprocess.run(list_cmd, capture_output=True, text=True) assert p_list.returncode == 0 - assert 'origin' not in p_list.stdout - assert 'upstream' in p_list.stdout + assert "origin" not in p_list.stdout + assert "upstream" in p_list.stdout def test_remote_set_url(git2cpp_path, tmp_path, run_in_tmp_path): """Test setting remote URL.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + add_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add = subprocess.run(add_cmd, capture_output=True, check=True) + assert p_add.returncode == 0 + # Change the URL - new_url = 'https://github.com/user/newrepo.git' - cmd = [git2cpp_path, 'remote', 'set-url', 'origin', new_url] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - + new_url = "https://github.com/user/newrepo.git" + set_url_cmd = [git2cpp_path, "remote", "set-url", "origin", new_url] + p_set_url = subprocess.run(set_url_cmd, capture_output=True, text=True) + assert p_set_url.returncode == 0 + # Verify URL was changed - show_cmd = [git2cpp_path, 'remote', 'show', 'origin'] + show_cmd = [git2cpp_path, "remote", "show", "origin"] p_show = subprocess.run(show_cmd, capture_output=True, text=True) assert p_show.returncode == 0 assert new_url in p_show.stdout @@ -123,18 +132,23 @@ def test_remote_set_url(git2cpp_path, tmp_path, run_in_tmp_path): def test_remote_set_push_url(git2cpp_path, tmp_path, run_in_tmp_path): """Test setting remote push URL.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + subprocess.run( + [git2cpp_path, "remote", "add", "origin", repo_url], + capture_output=True, + check=True, + ) + # Set push URL - push_url = 'https://github.com/user/pushrepo.git' - cmd = [git2cpp_path, 'remote', 'set-url', '--push', 'origin', push_url] + push_url = "https://github.com/user/pushrepo.git" + cmd = [git2cpp_path, "remote", "set-url", "--push", "origin", push_url] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode == 0 - + # Verify push URL was set - show_cmd = [git2cpp_path, 'remote', 'show', 'origin'] + show_cmd = [git2cpp_path, "remote", "show", "origin"] p_show = subprocess.run(show_cmd, capture_output=True, text=True) assert p_show.returncode == 0 assert push_url in p_show.stdout @@ -142,83 +156,113 @@ def test_remote_set_push_url(git2cpp_path, tmp_path, run_in_tmp_path): def test_remote_show(git2cpp_path, tmp_path, run_in_tmp_path): """Test showing remote details.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - url = 'https://github.com/user/repo.git' - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', url], - capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'show', 'origin'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + subprocess.run( + [git2cpp_path, "remote", "add", "origin", repo_url], + capture_output=True, + check=True, + ) + + cmd = [git2cpp_path, "remote", "show", "origin"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode == 0 - assert 'origin' in p.stdout - assert url in p.stdout + assert "origin" in p.stdout + assert repo_url in p.stdout def test_remote_show_verbose(git2cpp_path, tmp_path, run_in_tmp_path): """Test showing remotes with verbose flag.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - url = 'https://github.com/user/repo.git' - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', url], - capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', '-v'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + subprocess.run( + [git2cpp_path, "remote", "add", "origin", repo_url], + capture_output=True, + check=True, + ) + + cmd = [git2cpp_path, "remote", "-v"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode == 0 - assert 'origin' in p.stdout - assert url in p.stdout - assert '(fetch)' in p.stdout or '(push)' in p.stdout + assert "origin" in p.stdout + assert repo_url in p.stdout + assert "(fetch)" in p.stdout or "(push)" in p.stdout def test_remote_show_all_verbose(git2cpp_path, tmp_path, run_in_tmp_path): """Test showing all remotes with verbose flag.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'upstream', 'https://github.com/upstream/repo.git'], - capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'show', '-v'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode == 0 - assert 'origin' in p.stdout - assert 'upstream' in p.stdout + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + add_origin_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add_origin = subprocess.run(add_origin_cmd, capture_output=True, check=True) + assert p_add_origin.returncode == 0 + add_upstream_cmd = [ + git2cpp_path, + "remote", + "add", + "upstream", + "https://github.com/upstream/repo.git", + ] + p_add_upstream = subprocess.run(add_upstream_cmd, capture_output=True, check=True) + assert p_add_upstream.returncode == 0 + + show_cmd = [git2cpp_path, "remote", "show", "-v"] + p_show = subprocess.run(show_cmd, capture_output=True, text=True) + assert p_show.returncode == 0 + assert "origin" in p_show.stdout + assert "upstream" in p_show.stdout def test_remote_error_on_duplicate_add(git2cpp_path, tmp_path, run_in_tmp_path): """Test error when adding duplicate remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/repo.git'], - capture_output=True, check=True) - + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + add_cmd = [git2cpp_path, "remote", "add", "origin", repo_url] + p_add = subprocess.run(add_cmd, capture_output=True, check=True) + assert p_add.returncode == 0 + # Try to add duplicate - cmd = [git2cpp_path, 'remote', 'add', 'origin', 'https://github.com/user/other.git'] - p = subprocess.run(cmd, capture_output=True, text=True) - assert p.returncode != 0 + add_dup_cmd = [ + git2cpp_path, + "remote", + "add", + "origin", + "https://github.com/user/other.git", + ] + p_add_dup = subprocess.run(add_dup_cmd, capture_output=True, text=True) + assert p_add_dup.returncode != 0 def test_remote_error_on_remove_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): """Test error when removing non-existent remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'remove', 'nonexistent'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + cmd = [git2cpp_path, "remote", "remove", "nonexistent"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode != 0 def test_remote_error_on_rename_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): """Test error when renaming non-existent remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'rename', 'nonexistent', 'new'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + cmd = [git2cpp_path, "remote", "rename", "nonexistent", "new"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode != 0 def test_remote_error_on_show_nonexistent(git2cpp_path, tmp_path, run_in_tmp_path): """Test error when showing non-existent remote.""" - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True) - - cmd = [git2cpp_path, 'remote', 'show', 'nonexistent'] + p_init = subprocess.run([git2cpp_path, "init"], capture_output=True, check=True) + assert p_init.returncode == 0 + + cmd = [git2cpp_path, "remote", "show", "nonexistent"] p = subprocess.run(cmd, capture_output=True, text=True) assert p.returncode != 0 @@ -229,41 +273,46 @@ def repo_with_remote(git2cpp_path, tmp_path, run_in_tmp_path): # Create a bare repo to use as remote remote_path = tmp_path / "remote_repo" remote_path.mkdir() - subprocess.run([git2cpp_path, 'init', '--bare', str(remote_path)], - capture_output=True, check=True) - + init_cmd = [git2cpp_path, "init", "--bare", str(remote_path)] + p_init = subprocess.run(init_cmd, capture_output=True, check=True) + assert p_init.returncode == 0 + # Create a regular repo - repo_path = tmp_path / "local_repo" - repo_path.mkdir() - + local_path = tmp_path / "local_repo" + local_path.mkdir() + # Initialize repo in the directory - subprocess.run([git2cpp_path, 'init'], capture_output=True, check=True, cwd=repo_path) - + p_init_2 = subprocess.run( + [git2cpp_path, "init"], capture_output=True, check=True, cwd=local_path + ) + assert p_init_2.returncode == 0 + # Add remote - subprocess.run([git2cpp_path, 'remote', 'add', 'origin', str(remote_path)], - capture_output=True, check=True, cwd=repo_path) - - return repo_path, remote_path + add_cmd = [git2cpp_path, "remote", "add", "origin", str(remote_path)] + p_add = subprocess.run(add_cmd, capture_output=True, check=True, cwd=local_path) + assert p_add.returncode == 0 + + return local_path, remote_path def test_fetch_from_remote(git2cpp_path, repo_with_remote): """Test fetching from a remote.""" - repo_path, remote_path = repo_with_remote - + local_path, remote_path = repo_with_remote + # Note: This is a bare repo with no refs, so fetch will fail gracefully # For now, just test that fetch command runs (it will fail gracefully if no refs) - cmd = [git2cpp_path, 'fetch', 'origin'] - p = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_path) + cmd = [git2cpp_path, "fetch", "origin"] + p = subprocess.run(cmd, capture_output=True, text=True, cwd=local_path) # Fetch might succeed (empty) or fail (no refs), but shouldn't crash assert p.returncode in [0, 1] # 0 for success, 1 for no refs/error def test_fetch_default_origin(git2cpp_path, repo_with_remote): """Test fetching with default origin.""" - repo_path, remote_path = repo_with_remote - - cmd = [git2cpp_path, 'fetch'] - p = subprocess.run(cmd, capture_output=True, text=True, cwd=repo_path) + local_path, remote_path = repo_with_remote + + cmd = [git2cpp_path, "fetch"] + p = subprocess.run(cmd, capture_output=True, text=True, cwd=local_path) # Fetch might succeed (empty) or fail (no refs), but shouldn't crash assert p.returncode in [0, 1] @@ -272,22 +321,63 @@ def test_remote_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): """Test that cloned repos have remotes configured.""" assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - - cmd = [git2cpp_path, 'remote'] + + cmd = [git2cpp_path, "remote"] p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) assert p.returncode == 0 - assert 'origin' in p.stdout + assert "origin" in p.stdout def test_remote_show_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): """Test showing remote in cloned repo.""" assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" - - cmd = [git2cpp_path, 'remote', 'show', 'origin'] + + cmd = [git2cpp_path, "remote", "show", "origin"] p = subprocess.run(cmd, capture_output=True, cwd=xtl_path, text=True) assert p.returncode == 0 - assert 'origin' in p.stdout + assert "origin" in p.stdout # Should contain URL information - assert 'http' in p.stdout or 'git' in p.stdout or 'https' in p.stdout + assert "http" in p.stdout or "git" in p.stdout or "https" in p.stdout + + +def test_push_local(xtl_clone, git2cpp_path, tmp_path): + """Test setting push on a local remote.""" + assert (tmp_path / "xtl").exists() + xtl_path = tmp_path / "xtl" + checkout_cmd = [git2cpp_path, "checkout", "-b", "foregone"] + p_checkout = subprocess.run( + checkout_cmd, capture_output=True, check=True, cwd=xtl_path + ) + assert p_checkout.returncode == 0 + + p = xtl_path / "mook_file.txt" + p.write_text("") + + cmd_add = [git2cpp_path, "add", "mook_file.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + cmd_commit = [git2cpp_path, "commit", "-m", "test commit"] + p_commit = subprocess.run(cmd_commit, cwd=xtl_path, text=True) + assert p_commit.returncode == 0 + + url = "https://github.com/xtensor-stack/xtl.git" + local_path = tmp_path / "local_repo" + clone_cmd = [git2cpp_path, "clone", "--bare", url, local_path] + p_clone = subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + assert p_clone.returncode == 0 + + add_cmd = [git2cpp_path, "remote", "add", "local_repo", str(local_path)] + p_add = subprocess.run(add_cmd, capture_output=True, check=True, cwd=xtl_path) + assert p_add.returncode == 0 + + cmd_push = [git2cpp_path, "push", "local_repo"] # "foregone" + p_push = subprocess.run(cmd_push, capture_output=True, check=True, cwd=xtl_path) + assert p_push.returncode == 0 + + list_cmd = [git2cpp_path, "branch"] + p_list = subprocess.run(list_cmd, capture_output=True, cwd=local_path, text=True) + assert p_list.returncode == 0 + assert "foregone" in p_list.stdout From a3bd345660c63cbb4aa7915c32e7146a4559c9f3 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Thu, 11 Dec 2025 16:47:11 +0100 Subject: [PATCH 4/5] small fix in test --- test/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 4676e73..9f6f4b9 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -341,7 +341,7 @@ def test_remote_show_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): assert "http" in p.stdout or "git" in p.stdout or "https" in p.stdout -def test_push_local(xtl_clone, git2cpp_path, tmp_path): +def test_push_local(xtl_clone, git2cpp_path, tmp_path, monkeypatch): """Test setting push on a local remote.""" assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" From 7b23e65100a1121558e3e28945316f5b3c6d9dad Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Thu, 11 Dec 2025 16:52:50 +0100 Subject: [PATCH 5/5] another small fix --- test/test_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_remote.py b/test/test_remote.py index 9f6f4b9..56f6dc4 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -341,7 +341,7 @@ def test_remote_show_in_cloned_repo(xtl_clone, git2cpp_path, tmp_path): assert "http" in p.stdout or "git" in p.stdout or "https" in p.stdout -def test_push_local(xtl_clone, git2cpp_path, tmp_path, monkeypatch): +def test_push_local(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): """Test setting push on a local remote.""" assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl"