diff --git a/CMakeLists.txt b/CMakeLists.txt index ba71a22..d18cbd9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,8 @@ set(GIT2CPP_SRC ${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/reset_subcommand.cpp ${GIT2CPP_SOURCE_DIR}/subcommand/reset_subcommand.hpp ${GIT2CPP_SOURCE_DIR}/subcommand/status_subcommand.cpp diff --git a/src/main.cpp b/src/main.cpp index 71140ad..e8479c8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include +#include #include // For version number only #include @@ -11,6 +12,7 @@ #include "subcommand/commit_subcommand.hpp" #include "subcommand/init_subcommand.hpp" #include "subcommand/log_subcommand.hpp" +#include "subcommand/merge_subcommand.hpp" #include "subcommand/reset_subcommand.hpp" #include "subcommand/status_subcommand.hpp" @@ -35,6 +37,7 @@ int main(int argc, char** argv) commit_subcommand commit(lg2_obj, app); reset_subcommand reset(lg2_obj, app); log_subcommand log(lg2_obj, app); + merge_subcommand merge(lg2_obj, app); app.require_subcommand(/* min */ 0, /* max */ 1); diff --git a/src/subcommand/checkout_subcommand.cpp b/src/subcommand/checkout_subcommand.cpp index 09491b8..d0103d7 100644 --- a/src/subcommand/checkout_subcommand.cpp +++ b/src/subcommand/checkout_subcommand.cpp @@ -43,7 +43,7 @@ void checkout_subcommand::run() } else { - auto optional_commit = resolve_local_ref(repo, m_branch_name); + auto optional_commit = repo.resolve_local_ref(m_branch_name); if (!optional_commit) { // TODO: handle remote refs @@ -56,26 +56,6 @@ void checkout_subcommand::run() } } -std::optional checkout_subcommand::resolve_local_ref -( - const repository_wrapper& repo, - const std::string& target_name -) -{ - if (auto ref = repo.find_reference_dwim(target_name)) - { - return repo.find_annotated_commit(*ref); - } - else if (auto obj = repo.revparse_single(target_name)) - { - return repo.find_annotated_commit(obj->oid()); - } - else - { - return std::nullopt; - } -} - annotated_commit_wrapper checkout_subcommand::create_local_branch ( repository_wrapper& repo, diff --git a/src/subcommand/checkout_subcommand.hpp b/src/subcommand/checkout_subcommand.hpp index 533b46a..2aab79e 100644 --- a/src/subcommand/checkout_subcommand.hpp +++ b/src/subcommand/checkout_subcommand.hpp @@ -17,12 +17,6 @@ class checkout_subcommand private: - std::optional resolve_local_ref - ( - const repository_wrapper& repo, - const std::string& target_name - ); - annotated_commit_wrapper create_local_branch ( repository_wrapper& repo, diff --git a/src/subcommand/commit_subcommand.cpp b/src/subcommand/commit_subcommand.cpp index d281ec6..bca7f39 100644 --- a/src/subcommand/commit_subcommand.cpp +++ b/src/subcommand/commit_subcommand.cpp @@ -32,5 +32,5 @@ void commit_subcommand::run() } } - repo.create_commit(author_committer_signatures, m_commit_message); + repo.create_commit(author_committer_signatures, m_commit_message, std::nullopt); } diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp new file mode 100644 index 0000000..a9e62be --- /dev/null +++ b/src/subcommand/merge_subcommand.cpp @@ -0,0 +1,100 @@ +#include +#include + +#include "merge_subcommand.hpp" +// #include "../wrapper/repository_wrapper.hpp" + + +merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app) +{ + auto *sub = app.add_subcommand("merge", "Join two or more development histories together"); + + sub->add_option("", m_branches_to_merge, "Branch(es) to merge"); + + sub->callback([this]() { this->run(); }); +} + +annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_wrapper& repo) +{ + std::vector commits_to_merge; + commits_to_merge.reserve(m_branches_to_merge.size()); + + for (const auto branch_name:m_branches_to_merge) + { + std::optional commit = repo.resolve_local_ref(branch_name); + if (commit.has_value()) + { + commits_to_merge.push_back(std::move(commit).value()); + } + } + return annotated_commit_list_wrapper(std::move(commits_to_merge)); +} + +void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int is_unborn) +{ + const git_checkout_options ff_checkout_options = GIT_CHECKOUT_OPTIONS_INIT; + + auto lambda_get_target_ref = [] (auto repo, auto is_unborn) + { + if (!is_unborn) + { + return repo->head(); + } + else + { + return repo->find_reference("HEAD"); + } + }; + reference_wrapper target_ref = lambda_get_target_ref(&repo, is_unborn); + + object_wrapper target = repo.find_object(target_oid, GIT_OBJECT_COMMIT); + + repo.checkout_tree(target, ff_checkout_options); + + target_ref.write_new_ref(target_oid); +} + +void merge_subcommand::run() +{ + auto directory = get_current_git_path(); + auto bare = false; + auto repo = repository_wrapper::open(directory); + + auto state = repo.state(); + if (state != GIT_REPOSITORY_STATE_NONE) + { + std::cout << "repository is in unexpected state " << state <(c_commits_to_merge); + + throw_if_error(git_merge_analysis(&analysis, &preference, repo, commits_to_merge_const, num_commits_to_merge)); + + if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) + { + std::cout << "Already up-to-date" << std::endl; + } + else if (analysis & GIT_MERGE_ANALYSIS_UNBORN || + (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD && + !(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD))) + { + if (analysis & GIT_MERGE_ANALYSIS_UNBORN) + { + std::cout << "Unborn" << std::endl; + } + else + { + std::cout << "Fast-forward" << std::endl; + } + const annotated_commit_wrapper& commit = commits_to_merge.front(); + const git_oid target_oid = commit.oid(); + // Since this is a fast-forward, there can be only one merge head. + assert(num_commits_to_merge == 1); + perform_fastforward(repo, target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN)); + } +} diff --git a/src/subcommand/merge_subcommand.hpp b/src/subcommand/merge_subcommand.hpp new file mode 100644 index 0000000..3d73f47 --- /dev/null +++ b/src/subcommand/merge_subcommand.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "../utils/common.hpp" +#include "../wrapper/repository_wrapper.hpp" + +class merge_subcommand +{ +public: + + explicit merge_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + + annotated_commit_list_wrapper resolve_heads(const repository_wrapper& repo); + + std::vector m_branches_to_merge; +}; diff --git a/src/wrapper/annotated_commit_wrapper.hpp b/src/wrapper/annotated_commit_wrapper.hpp index 7bb5a4c..c390e2f 100644 --- a/src/wrapper/annotated_commit_wrapper.hpp +++ b/src/wrapper/annotated_commit_wrapper.hpp @@ -27,3 +27,4 @@ class annotated_commit_wrapper : public wrapper_base friend class repository_wrapper; }; +using annotated_commit_list_wrapper = list_wrapper; diff --git a/src/wrapper/commit_wrapper.hpp b/src/wrapper/commit_wrapper.hpp index 0560007..1327beb 100644 --- a/src/wrapper/commit_wrapper.hpp +++ b/src/wrapper/commit_wrapper.hpp @@ -1,8 +1,9 @@ #pragma once #include +#include +#include -#include "../wrapper/repository_wrapper.hpp" #include "../wrapper/wrapper_base.hpp" class commit_wrapper : public wrapper_base @@ -28,3 +29,5 @@ class commit_wrapper : public wrapper_base friend class repository_wrapper; friend class reference_wrapper; }; + +using commit_list_wrapper = list_wrapper; diff --git a/src/wrapper/refs_wrapper.cpp b/src/wrapper/refs_wrapper.cpp index 571ca52..4af5906 100644 --- a/src/wrapper/refs_wrapper.cpp +++ b/src/wrapper/refs_wrapper.cpp @@ -1,4 +1,7 @@ #include "../utils/git_exception.hpp" +#include "object_wrapper.hpp" +#include +#include #include "../wrapper/refs_wrapper.hpp" reference_wrapper::reference_wrapper(git_reference* ref) @@ -21,3 +24,15 @@ bool reference_wrapper::is_remote() const { return git_reference_is_remote(*this); } + +const git_oid* reference_wrapper::target() const +{ + return git_reference_target(p_resource); +} + +reference_wrapper reference_wrapper::write_new_ref(const git_oid target_oid) +{ + git_reference* new_ref; + throw_if_error(git_reference_set_target(&new_ref, p_resource, &target_oid, NULL)); + return reference_wrapper(new_ref); +} diff --git a/src/wrapper/refs_wrapper.hpp b/src/wrapper/refs_wrapper.hpp index e7509c2..d7645ca 100644 --- a/src/wrapper/refs_wrapper.hpp +++ b/src/wrapper/refs_wrapper.hpp @@ -5,7 +5,9 @@ #include +#include "../utils/git_exception.hpp" #include "../wrapper/wrapper_base.hpp" +#include "../wrapper/object_wrapper.hpp" class reference_wrapper : public wrapper_base { @@ -20,6 +22,8 @@ class reference_wrapper : public wrapper_base std::string short_name() const; bool is_remote() const; + const git_oid* target() const; + reference_wrapper write_new_ref(const git_oid target_oid); template W peel() const; diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index 6eb00f4..d099382 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -1,6 +1,7 @@ #include "../utils/git_exception.hpp" #include "../wrapper/index_wrapper.hpp" #include "../wrapper/object_wrapper.hpp" +#include "../wrapper/commit_wrapper.hpp" #include "../wrapper/repository_wrapper.hpp" repository_wrapper::~repository_wrapper() @@ -35,6 +36,8 @@ git_repository_state_t repository_wrapper::state() const return git_repository_state_t(git_repository_state(*this)); } +// References + reference_wrapper repository_wrapper::head() const { git_reference* ref; @@ -56,12 +59,16 @@ std::optional repository_wrapper::find_reference_dwim(std::st return rc == 0 ? std::make_optional(reference_wrapper(ref)) : std::nullopt; } +// Index + index_wrapper repository_wrapper::make_index() { index_wrapper index = index_wrapper::init(*this); return index; } +// Branches + branch_wrapper repository_wrapper::create_branch(std::string_view name, bool force) { return create_branch(name, find_commit(), force); @@ -95,6 +102,8 @@ branch_iterator repository_wrapper::iterate_branches(git_branch_t type) const return branch_iterator(iter); } +// Commits + commit_wrapper repository_wrapper::find_commit(std::string_view ref_name) const { git_oid oid_parent_commit; @@ -110,20 +119,36 @@ commit_wrapper repository_wrapper::find_commit(const git_oid& id) const } void repository_wrapper::create_commit(const signature_wrapper::author_committer_signatures& author_committer_signatures, - const std::string& message) + const std::string_view message, const std::optional& parents_list) { const char* message_encoding = "UTF-8"; git_oid commit_id; std::string update_ref = "HEAD"; - auto parent = revparse_single(update_ref); - std::size_t parent_count = 0; - const git_commit* parents[1] = {nullptr}; - if (parent) + const git_commit* placeholder[1] = {nullptr}; + + auto [parents, parents_count] = [&]() -> std::pair { - parent_count = 1; - parents[0] = *parent; - } + if (parents_list) + { + // TODO: write a "as_const" function to replace the following + auto pl_size = parents_list.value().size(); + git_commit** pl_value = parents_list.value(); + auto pl_value_const = const_cast(pl_value); + return {pl_value_const, pl_size}; + } + else + { + auto parent = revparse_single(update_ref); + size_t parents_count = 0; + if (parent) + { + parents_count = 1; + placeholder[0] = *parent; + } + return {placeholder, parents_count}; + } + }(); git_tree* tree; index_wrapper index = this->make_index(); @@ -133,11 +158,32 @@ void repository_wrapper::create_commit(const signature_wrapper::author_committer throw_if_error(git_tree_lookup(&tree, *this, &tree_id)); throw_if_error(git_commit_create(&commit_id, *this, update_ref.c_str(), author_committer_signatures.first, author_committer_signatures.second, - message_encoding, message.c_str(), tree, parent_count, parents)); + message_encoding, message.data(), tree, parents_count, parents)); git_tree_free(tree); } +std::optional repository_wrapper::resolve_local_ref +( + const std::string_view target_name +) const +{ + if (auto ref = this->find_reference_dwim(target_name)) + { + return this->find_annotated_commit(*ref); + } + else if (auto obj = this->revparse_single(target_name)) + { + return this->find_annotated_commit(obj->oid()); + } + else + { + return std::nullopt; + } +} + +// Annotated commits + annotated_commit_wrapper repository_wrapper::find_annotated_commit(const git_oid& id) const { git_annotated_commit* commit; @@ -145,6 +191,8 @@ annotated_commit_wrapper repository_wrapper::find_annotated_commit(const git_oid return annotated_commit_wrapper(commit); } +// Objects + std::optional repository_wrapper::revparse_single(std::string_view spec) const { git_object* obj; @@ -152,6 +200,15 @@ std::optional repository_wrapper::revparse_single(std::string_vi return rc == 0 ? std::make_optional(object_wrapper(obj)) : std::nullopt; } +object_wrapper repository_wrapper::find_object(const git_oid id, git_object_t type) +{ + git_object* object; + git_object_lookup(&object, *this, &id, type); + return object_wrapper(object); +} + +// Head manipulations + void repository_wrapper::set_head(std::string_view ref_name) { throw_if_error(git_repository_set_head(*this, ref_name.data())); @@ -168,3 +225,10 @@ void repository_wrapper::reset(const object_wrapper& target, git_reset_t reset_t throw_if_error(git_reset(*this, target, reset_type, &checkout_options)); } + +// Trees + +void repository_wrapper::checkout_tree(const object_wrapper& target, const git_checkout_options opts) +{ + throw_if_error(git_checkout_tree(*this, target, &opts)); +} diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index 7258724..78212cc 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -51,7 +51,8 @@ class repository_wrapper : public wrapper_base // Commits commit_wrapper find_commit(std::string_view ref_name = "HEAD") const; commit_wrapper find_commit(const git_oid& id) const; - void create_commit(const signature_wrapper::author_committer_signatures&, const std::string&); + void create_commit(const signature_wrapper::author_committer_signatures&, const std::string_view, const std::optional& parents_list); + std::optional resolve_local_ref(const std::string_view target_name) const; // Annotated commits annotated_commit_wrapper find_annotated_commit(const git_oid& id) const; @@ -61,12 +62,16 @@ class repository_wrapper : public wrapper_base // Objects std::optional revparse_single(std::string_view spec) const; + object_wrapper find_object(const git_oid id, git_object_t type); // Head manipulations void set_head(std::string_view ref_name); void set_head_detached(const annotated_commit_wrapper& commit); void reset(const object_wrapper& target, git_reset_t reset_type, const git_checkout_options& checkout_options); + // Trees + void checkout_tree(const object_wrapper& target, const git_checkout_options opts); + private: repository_wrapper() = default; diff --git a/src/wrapper/signature_wrapper.cpp b/src/wrapper/signature_wrapper.cpp index d16eaf9..b6148d9 100644 --- a/src/wrapper/signature_wrapper.cpp +++ b/src/wrapper/signature_wrapper.cpp @@ -1,6 +1,7 @@ #include "../wrapper/repository_wrapper.hpp" #include "../wrapper/signature_wrapper.hpp" #include "../utils/git_exception.hpp" +#include signature_wrapper::~signature_wrapper() { @@ -49,3 +50,23 @@ signature_wrapper signature_wrapper::get_commit_committer(const commit_wrapper& committer.m_ownership = false; return committer; } + +signature_wrapper signature_wrapper::signature_now(std::string_view name, std::string_view email) +{ + signature_wrapper sw; + git_signature* signature; + throw_if_error(git_signature_now(&signature, name.data(), email.data())); + sw.p_resource = signature; + sw.m_ownership = true; + return sw; +} + +signature_wrapper::author_committer_signatures signature_wrapper::signature_now( + std::string_view author_name, std::string_view author_email, std::string_view committer_name, std::string_view committer_email) +{ + signature_wrapper author_sig = signature_now(author_name.data(), author_email.data()); + signature_wrapper cmt_sig = signature_now(committer_name.data(), committer_email.data()); + // Deep copy of "when", which contains only copiable values, not pointers + cmt_sig.p_resource->when = author_sig.p_resource->when; + return std::pair(std::move(author_sig), std::move(cmt_sig)); +} diff --git a/src/wrapper/signature_wrapper.hpp b/src/wrapper/signature_wrapper.hpp index 2ebc861..68d9caa 100644 --- a/src/wrapper/signature_wrapper.hpp +++ b/src/wrapper/signature_wrapper.hpp @@ -13,12 +13,13 @@ class repository_wrapper; class signature_wrapper : public wrapper_base { public: + using author_committer_signatures = std::pair; ~signature_wrapper(); - signature_wrapper(signature_wrapper&&) = default; - signature_wrapper& operator=(signature_wrapper&&) = default; + signature_wrapper(signature_wrapper&&) noexcept = default; + signature_wrapper& operator=(signature_wrapper&&) noexcept = default; std::string_view name() const; std::string_view email() const; @@ -27,6 +28,14 @@ class signature_wrapper : public wrapper_base static author_committer_signatures get_default_signature_from_env(repository_wrapper&); static signature_wrapper get_commit_author(const commit_wrapper&); static signature_wrapper get_commit_committer(const commit_wrapper&); + static signature_wrapper signature_now(std::string_view name, std::string_view email); + static author_committer_signatures signature_now + ( + std::string_view author_name, + std::string_view author_email, + std::string_view committer_name, + std::string_view committer_email + ); private: diff --git a/src/wrapper/wrapper_base.hpp b/src/wrapper/wrapper_base.hpp index 08a5662..16e5dd2 100644 --- a/src/wrapper/wrapper_base.hpp +++ b/src/wrapper/wrapper_base.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include template class wrapper_base @@ -38,3 +39,46 @@ class wrapper_base resource_type* p_resource = nullptr; }; + +template +class list_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + explicit list_wrapper(std::vector list) + : m_list(std::move(list)) + { + this->p_resource = new base_type::resource_type[m_list.size()]; + for (size_t i=0; i< m_list.size(); ++i) + { + this->p_resource[i] = m_list[i]; + } + } + + ~list_wrapper() + { + delete[] this->p_resource; + this->p_resource = nullptr; + } + + list_wrapper(list_wrapper&&) noexcept = default; + list_wrapper& operator=(list_wrapper&&) noexcept = default; + + size_t size() const + { + return m_list.size(); + } + + T front() + { + // TODO: rework wrapper so they can have references + // on libgit2 object without taking ownership + return T(std::move(m_list.front())); + } + +private: + + std::vector m_list; +}; diff --git a/test/conftest.py b/test/conftest.py index 576b5a3..aaf2ff5 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,7 +4,7 @@ import subprocess -# Fixture to run test in current tmp_path +# Fixture to run test in current tmp_path @pytest.fixture def run_in_tmp_path(tmp_path): original_cwd = os.getcwd() @@ -12,15 +12,18 @@ def run_in_tmp_path(tmp_path): yield os.chdir(original_cwd) -@pytest.fixture(scope='session') + +@pytest.fixture(scope="session") def git2cpp_path(): - return Path(__file__).parent.parent / 'build' / 'git2cpp' + return Path(__file__).parent.parent / "build" / "git2cpp" + @pytest.fixture def xtl_clone(git2cpp_path, tmp_path, run_in_tmp_path): - url = 'https://github.com/xtensor-stack/xtl.git' - clone_cmd = [git2cpp_path, 'clone', url] - subprocess.run(clone_cmd, capture_output=True, cwd = tmp_path, text=True) + url = "https://github.com/xtensor-stack/xtl.git" + clone_cmd = [git2cpp_path, "clone", url] + subprocess.run(clone_cmd, capture_output=True, cwd=tmp_path, text=True) + @pytest.fixture def git_config(monkeypatch): diff --git a/test/test_merge.py b/test/test_merge.py new file mode 100644 index 0000000..95bcfc2 --- /dev/null +++ b/test/test_merge.py @@ -0,0 +1,46 @@ +import subprocess +import time + +import pytest + + +# TODO: Have a different "person" for the commit and for the merge +# TODO: Test "unborn" case, but how ? +def test_merge_fast_forward(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): + 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, cwd=xtl_path, text=True + ) + assert p_checkout.returncode == 0 + + p = xtl_path / "mook_file.txt" + p.write_text("blablabla") + + add_cmd = [git2cpp_path, "add", "mook_file.txt"] + p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + commit_cmd = [git2cpp_path, "commit", "-m", "test commit"] + p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_commit.returncode == 0 + + checkout_cmd_2 = [git2cpp_path, "checkout", "master"] + p_checkout_2 = subprocess.run( + checkout_cmd_2, capture_output=True, cwd=xtl_path, text=True + ) + assert p_checkout_2.returncode == 0 + + merge_cmd = [git2cpp_path, "merge", "foregone"] + p_merge = subprocess.run(merge_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_merge.returncode == 0 + assert "Fast-forward" in p_merge.stdout + + log_cmd = [git2cpp_path, "log", "--format=full", "--max-count", "1"] + p_log = subprocess.run(log_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_log.returncode == 0 + assert "Author: Jane Doe" in p_log.stdout + # assert "Commit: John Doe" in p_log.stdout + assert (xtl_path / "mook_file.txt").exists()