From 8a2a7c463cfc545f155e002500e2dcd343bb217f Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Mon, 1 Sep 2025 17:27:53 +0200 Subject: [PATCH] Add merge subcommand --- CMakeLists.txt | 2 + src/main.cpp | 3 + src/subcommand/checkout_subcommand.cpp | 2 +- src/subcommand/commit_subcommand.cpp | 2 +- src/subcommand/merge_subcommand.cpp | 182 +++++++++++++++++++++++ src/subcommand/merge_subcommand.hpp | 16 ++ src/wrapper/annotated_commit_wrapper.cpp | 26 ++++ src/wrapper/annotated_commit_wrapper.hpp | 22 +++ src/wrapper/commit_wrapper.cpp | 20 +++ src/wrapper/commit_wrapper.hpp | 24 ++- src/wrapper/refs_wrapper.cpp | 16 ++ src/wrapper/refs_wrapper.hpp | 5 + src/wrapper/repository_wrapper.cpp | 82 ++++++++-- src/wrapper/repository_wrapper.hpp | 7 +- src/wrapper/signature_wrapper.cpp | 20 +++ src/wrapper/signature_wrapper.hpp | 7 +- test/test_merge.py | 40 +++++ 17 files changed, 461 insertions(+), 15 deletions(-) create mode 100644 src/subcommand/merge_subcommand.cpp create mode 100644 src/subcommand/merge_subcommand.hpp create mode 100644 test/test_merge.py 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..6ae3e42 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 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..cfe9a7f --- /dev/null +++ b/src/subcommand/merge_subcommand.cpp @@ -0,0 +1,182 @@ +#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 resolve_heads(const repository_wrapper& repo, std::vector m_branches_to_merge) +{ + 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"); + } + }; + auto target_ref = lambda_get_target_ref(&repo, is_unborn); + + auto target = repo.find_object(target_oid, GIT_OBJECT_COMMIT); + + repo.checkout_tree(target, &ff_checkout_options); + + auto new_target_ref = target_ref.new_ref(); +} + +static void create_merge_commit(repository_wrapper repo, index_wrapper index, std::vector m_branches_to_merge, + std::vector commits_to_merge) +{ + auto head_ref = repo.head(); + auto merge_ref = repo.find_reference_dwim(m_branches_to_merge.front()); + // if (ref) + // { + // auto merge_ref = std::move(ref).value(); + // } + auto merge_commit = repo.resolve_local_ref(m_branches_to_merge.front()).value(); + + size_t annotated_count = commits_to_merge.size(); + std::vector parents_list; + parents_list.reserve(annotated_count + 1); + parents_list.push_back(std::move(head_ref.peel())); + for (size_t i=0; ishort_name(); + } + else + { + msg_target = git_oid_tostr_s(&(merge_commit.oid())); + } + + std::string msg; + msg = "Merge "; + if (merge_ref) + { + msg.append("branch "); + } + else + { + msg.append("commit "); + } + msg.append(msg_target); + std::cout << msg << std::endl; + + repo.create_commit(author_committer_sign_now, msg, std::optional(std::move(parents))); +} + +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); + + 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))) + { + const git_oid* target_oid; + 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(); + target_oid = &commit.oid(); + assert(num_commits_to_merge == 1); + perform_fastforward(repo, target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN)); + } + else if (analysis & GIT_MERGE_ANALYSIS_NORMAL) + { + git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT; + git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT; + + merge_opts.flags = 0; + merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3; + + checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE|GIT_CHECKOUT_ALLOW_CONFLICTS; + + if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY) + { + std::cout << "Fast-forward is preferred, but only a merge is possible\n" << std::endl; + // how to break ? + } + + // git_merge(repo, + // (const git_annotated_commit **)opts.annotated, opts.annotated_count, + // &merge_opts, &checkout_opts); + } + + // if (git_index_has_conflicts(index)) { + // /* Handle conflicts */ + // output_conflicts(index); + // } else if (!opts.no_commit) { + // create_merge_commit(repo, index, &opts); + // printf("Merge made\n"); + // } +} diff --git a/src/subcommand/merge_subcommand.hpp b/src/subcommand/merge_subcommand.hpp new file mode 100644 index 0000000..306d65d --- /dev/null +++ b/src/subcommand/merge_subcommand.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "../utils/common.hpp" + +class merge_subcommand +{ +public: + + explicit merge_subcommand(const libgit2_object&, CLI::App& app); + void run(); + +private: + std::vector m_branches_to_merge; +}; diff --git a/src/wrapper/annotated_commit_wrapper.cpp b/src/wrapper/annotated_commit_wrapper.cpp index da38620..d7425ba 100644 --- a/src/wrapper/annotated_commit_wrapper.cpp +++ b/src/wrapper/annotated_commit_wrapper.cpp @@ -21,3 +21,29 @@ std::string_view annotated_commit_wrapper::reference_name() const const char* res = git_annotated_commit_ref(*this); return res ? res : std::string_view{}; } + +annotated_commit_list_wrapper::annotated_commit_list_wrapper(std::vector annotated_commit_list) + : m_annotated_commit_list(std::move(annotated_commit_list)) +{ + git_annotated_commit** p_resource = new git_annotated_commit*[m_annotated_commit_list.size()]; + for (size_t i=0; i +#include #include @@ -27,3 +28,24 @@ class annotated_commit_wrapper : public wrapper_base friend class repository_wrapper; }; +class annotated_commit_list_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + explicit annotated_commit_list_wrapper(std::vector annotated_commit_list); + + ~annotated_commit_list_wrapper(); + + annotated_commit_list_wrapper(annotated_commit_list_wrapper&&) noexcept = default; + annotated_commit_list_wrapper& operator=(annotated_commit_list_wrapper&&) noexcept = default; + + size_t size() const; + annotated_commit_wrapper front(); + +private: + + std::vector m_annotated_commit_list; + +}; diff --git a/src/wrapper/commit_wrapper.cpp b/src/wrapper/commit_wrapper.cpp index a5996fc..f079e5b 100644 --- a/src/wrapper/commit_wrapper.cpp +++ b/src/wrapper/commit_wrapper.cpp @@ -26,3 +26,23 @@ std::string commit_wrapper::commit_oid_tostr() const char buf[GIT_OID_SHA1_HEXSIZE + 1]; return git_oid_tostr(buf, sizeof(buf), &this->oid()); } + +commit_list_wrapper::commit_list_wrapper(std::vector commit_list) +{ + git_commit** p_resource = new git_commit*[m_commit_list.size()]; + for (size_t i=0; i +#include +#include -#include "../wrapper/repository_wrapper.hpp" #include "../wrapper/wrapper_base.hpp" class commit_wrapper : public wrapper_base @@ -28,3 +29,24 @@ class commit_wrapper : public wrapper_base friend class repository_wrapper; friend class reference_wrapper; }; + +class commit_list_wrapper : public wrapper_base +{ +public: + + using base_type = wrapper_base; + + explicit commit_list_wrapper(std::vector commit_list); + + ~commit_list_wrapper(); + + commit_list_wrapper(commit_list_wrapper&&) noexcept = default; + commit_list_wrapper& operator=(commit_list_wrapper&&) noexcept = default; + + size_t size() const; + +private: + + std::vector m_commit_list; + +}; diff --git a/src/wrapper/refs_wrapper.cpp b/src/wrapper/refs_wrapper.cpp index 571ca52..11a8669 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,16 @@ 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::new_ref() +{ + git_reference* new_ref; + const git_oid* target_oid = this->target(); + 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..2c013e4 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,9 @@ class reference_wrapper : public wrapper_base std::string short_name() const; bool is_remote() const; + const git_oid* target() const; + reference_wrapper new_ref(); + // object_wrapper peel(git_object_t type); template W peel() const; diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index 6eb00f4..7e300af 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& 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.c_str(), tree, parents_count, parents)); git_tree_free(tree); } +std::optional repository_wrapper::resolve_local_ref +( + const std::string& 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..ba7bda8 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&, const std::optional& parents_list); + std::optional resolve_local_ref(const std::string& 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..7d0eaa0 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,22 @@ signature_wrapper signature_wrapper::get_commit_committer(const commit_wrapper& committer.m_ownership = false; return committer; } + +signature_wrapper signature_wrapper::signature_now(std::string name, std::string email) +{ + signature_wrapper sw; + git_signature* signature; + throw_if_error(git_signature_now(&signature, name.c_str(), email.c_str())); + sw.p_resource = signature; + sw.m_ownership = true; + return sw; +} + +signature_wrapper::author_committer_signatures signature_wrapper::signature_now(std::string author_name, std::string author_email, std::string committer_name, std::string committer_email) +{ + signature_wrapper author_sig = signature_now(author_name.c_str(), author_email.c_str()); + signature_wrapper cmt_sig = signature_now(committer_name.c_str(), committer_email.c_str()); + // 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..e0496e6 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,8 @@ 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 name, std::string email); + static author_committer_signatures signature_now(std::string author_name, std::string autor_email, std::string committer_name, std::string committer_email); private: diff --git a/test/test_merge.py b/test/test_merge.py new file mode 100644 index 0000000..3f801a9 --- /dev/null +++ b/test/test_merge.py @@ -0,0 +1,40 @@ +import subprocess + +import pytest + + +def test_merge(xtl_clone, git2cpp_path, tmp_path): + 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 + + log_cmd = [git2cpp_path, 'log', '--max-count', '1'] + p_log = subprocess.run(log_cmd, capture_output=True, cwd=xtl_path, text=True) + assert p_log.returncode == 0 + assert "Jane Doe" in p_log.stdout + assert (xtl_path / 'mook_file.txt').exists() + +# def test_merge_multiple_branches(xtl_clone, git2cpp_path, tmp_path): +# pass