diff --git a/CMakeLists.txt b/CMakeLists.txt index d18cbd9..a9a266b 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 @@ -68,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 @@ -82,6 +90,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/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 new file mode 100644 index 0000000..4d07e1a --- /dev/null +++ b/src/subcommand/fetch_subcommand.cpp @@ -0,0 +1,54 @@ +#include + +#include + +#include "../subcommand/fetch_subcommand.hpp" +#include "../utils/output.hpp" +#include "../utils/progress.hpp" +#include "../wrapper/repository_wrapper.hpp" + +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 + 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 " + << 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..bc607c1 --- /dev/null +++ b/src/subcommand/fetch_subcommand.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include + +#include "../utils/common.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..be04267 --- /dev/null +++ b/src/subcommand/push_subcommand.cpp @@ -0,0 +1,54 @@ +#include + +#include + +#include "../subcommand/push_subcommand.hpp" +#include "../utils/progress.hpp" +#include "../wrapper/repository_wrapper.hpp" + +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; + + if (m_refspecs.empty()) + { + 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; + + 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 new file mode 100644 index 0000000..07c301e --- /dev/null +++ b/src/subcommand/push_subcommand.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include + +#include "../utils/common.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..f76ad70 --- /dev/null +++ b/src/subcommand/remote_subcommand.cpp @@ -0,0 +1,194 @@ +#include +#include + +#include "../subcommand/remote_subcommand.hpp" +#include "../wrapper/repository_wrapper.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") + { + if (extras.size() == 2) + { + m_remote_name = extras[0]; + m_url = extras[1]; + } + run_add(repo); + } + else if (m_operation == "remove" || m_operation == "rm") + { + if (extras.size() == 1) + { + m_remote_name = extras[0]; + } + run_remove(repo); + } + else if (m_operation == "rename") + { + if (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); + } +} + +void remote_subcommand::run_add(repository_wrapper& repo) +{ + if (m_remote_name.empty()) + { + throw std::runtime_error("usage: git remote add "); // TODO: add [] when implemented + } + 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("usage: git remote remove "); + } + repo.delete_remote(m_remote_name); +} + +void remote_subcommand::run_rename(repository_wrapper& repo) +{ + if (m_old_name.empty()) + { + throw std::runtime_error("usage: git remote rename "); // TODO: add [--[no-]progress] when implemented + } + 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..b6f3bf2 --- /dev/null +++ b/src/subcommand/remote_subcommand.hpp @@ -0,0 +1,34 @@ +#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/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 new file mode 100644 index 0000000..08420ca --- /dev/null +++ b/src/wrapper/remote_wrapper.cpp @@ -0,0 +1,63 @@ +#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; +} + +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 new file mode 100644 index 0000000..1fa1632 --- /dev/null +++ b/src/wrapper/remote_wrapper.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#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; + + 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/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_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 new file mode 100644 index 0000000..56f6dc4 --- /dev/null +++ b/test/test_remote.py @@ -0,0 +1,383 @@ +import subprocess + +import pytest + +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 + 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 + + +def test_remote_add(git2cpp_path, tmp_path, run_in_tmp_path): + """Test adding a remote.""" + 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"] + 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.""" + 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 + + +@pytest.mark.parametrize("remove", ["rm", "remove"]) +def test_remote_remove(git2cpp_path, tmp_path, run_in_tmp_path, remove): + """Test removing a remote.""" + 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 + + # 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.""" + 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 + 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"] + 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.""" + 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" + 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"] + 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.""" + 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] + 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.""" + 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 repo_url in p.stdout + + +def test_remote_show_verbose(git2cpp_path, tmp_path, run_in_tmp_path): + """Test showing remotes with verbose flag.""" + 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 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.""" + 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.""" + 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 + 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.""" + 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.""" + 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.""" + 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 + + +@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() + 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 + local_path = tmp_path / "local_repo" + local_path.mkdir() + + # Initialize repo in the directory + p_init_2 = subprocess.run( + [git2cpp_path, "init"], capture_output=True, check=True, cwd=local_path + ) + assert p_init_2.returncode == 0 + + # Add remote + 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.""" + 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=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.""" + 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] + + +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 + + +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" + + 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