From 1c4b6d94bea57a6b64ce975ec650173ca016999f Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Thu, 30 Oct 2025 11:46:45 +0100 Subject: [PATCH 1/3] Add merge commit --- src/subcommand/merge_subcommand.cpp | 91 ++++++++++++++++++++++++++++- src/subcommand/merge_subcommand.hpp | 3 + src/wrapper/repository_wrapper.cpp | 6 ++ src/wrapper/repository_wrapper.hpp | 1 + src/wrapper/wrapper_base.hpp | 11 ++-- test/test_merge.py | 68 +++++++++++++++++++++ 6 files changed, 175 insertions(+), 5 deletions(-) diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp index a9e62be..24ef3d9 100644 --- a/src/subcommand/merge_subcommand.cpp +++ b/src/subcommand/merge_subcommand.cpp @@ -2,7 +2,7 @@ #include #include "merge_subcommand.hpp" -// #include "../wrapper/repository_wrapper.hpp" +#include merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app) @@ -10,6 +10,9 @@ 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->add_flag("--no-ff", m_no_ff, ""); + // sub->add_flag("--commit", m_commit, "Perform the merge and commit the result. This option can be used to override --no-commit."); + sub->add_flag("--no-commit", m_no_commit, "With --no-commit perform the merge and stop just before creating a merge commit, to give the user a chance to inspect and further tweak the merge result before committing. \nNote that fast-forward updates do not create a merge commit and therefore there is no way to stop those merges with --no-commit. Thus, if you want to ensure your branch is not changed or updated by the merge command, use --no-ff with --no-commit."); sub->callback([this]() { this->run(); }); } @@ -54,6 +57,56 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int target_ref.write_new_ref(target_oid); } +void create_merge_commit(repository_wrapper& repo, const index_wrapper& index, std::vector m_branches_to_merge, + const annotated_commit_list_wrapper& commits_to_merge, size_t num_commits_to_merge) +{ + auto head_ref = repo.head(); + auto merge_ref = repo.find_reference_dwim(m_branches_to_merge.front()); + auto merge_commit = repo.resolve_local_ref(m_branches_to_merge.front()).value(); + + std::vector parents_list; + parents_list.reserve(num_commits_to_merge + 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 = "Merge "; + if (merge_ref) + { + msg.append("branch "); + } + else + { + msg.append("commit "); + } + msg.append(msg_target); + + repo.create_commit(author_committer_sign_now, msg, std::optional(std::move(parents))); + + repo.state_cleanup(); +} + void merge_subcommand::run() { auto directory = get_current_git_path(); @@ -78,6 +131,7 @@ void merge_subcommand::run() if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) { std::cout << "Already up-to-date" << std::endl; + return; } else if (analysis & GIT_MERGE_ANALYSIS_UNBORN || (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD && @@ -97,4 +151,39 @@ void merge_subcommand::run() 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; + } + + throw_if_error(git_merge(repo, + (const git_annotated_commit**)c_commits_to_merge, + num_commits_to_merge, + &merge_opts, + &checkout_opts)); + } + + index_wrapper index = repo.make_index(); + + if (git_index_has_conflicts(index)) + { + std::cout << "Conflict. To be implemented" << std::endl; + /* Handle conflicts */ + // output_conflicts(index); + } + else if (!m_no_commit) + { + create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge); + printf("Merge made\n"); + } } diff --git a/src/subcommand/merge_subcommand.hpp b/src/subcommand/merge_subcommand.hpp index 3d73f47..1ec1c20 100644 --- a/src/subcommand/merge_subcommand.hpp +++ b/src/subcommand/merge_subcommand.hpp @@ -17,4 +17,7 @@ class merge_subcommand annotated_commit_list_wrapper resolve_heads(const repository_wrapper& repo); std::vector m_branches_to_merge; + // bool m_no_ff = false; + // bool m_commit = false; + bool m_no_commit = false; }; diff --git a/src/wrapper/repository_wrapper.cpp b/src/wrapper/repository_wrapper.cpp index d099382..fcbd365 100644 --- a/src/wrapper/repository_wrapper.cpp +++ b/src/wrapper/repository_wrapper.cpp @@ -2,6 +2,7 @@ #include "../wrapper/index_wrapper.hpp" #include "../wrapper/object_wrapper.hpp" #include "../wrapper/commit_wrapper.hpp" +#include #include "../wrapper/repository_wrapper.hpp" repository_wrapper::~repository_wrapper() @@ -36,6 +37,11 @@ git_repository_state_t repository_wrapper::state() const return git_repository_state_t(git_repository_state(*this)); } +void repository_wrapper::state_cleanup() +{ + throw_if_error(git_repository_state_cleanup(*this)); +} + // References reference_wrapper repository_wrapper::head() const diff --git a/src/wrapper/repository_wrapper.hpp b/src/wrapper/repository_wrapper.hpp index 78212cc..99e36ae 100644 --- a/src/wrapper/repository_wrapper.hpp +++ b/src/wrapper/repository_wrapper.hpp @@ -30,6 +30,7 @@ class repository_wrapper : public wrapper_base static repository_wrapper clone(std::string_view url, std::string_view path, const git_clone_options& opts); git_repository_state_t state() const; + void state_cleanup(); // References reference_wrapper head() const; diff --git a/src/wrapper/wrapper_base.hpp b/src/wrapper/wrapper_base.hpp index 16e5dd2..def9b69 100644 --- a/src/wrapper/wrapper_base.hpp +++ b/src/wrapper/wrapper_base.hpp @@ -71,11 +71,14 @@ class list_wrapper : public wrapper_base return m_list.size(); } - T front() + const T& operator[](size_t pos) const { - // TODO: rework wrapper so they can have references - // on libgit2 object without taking ownership - return T(std::move(m_list.front())); + return m_list[pos]; + } + + const T& front() const + { + return m_list.front(); } private: diff --git a/test/test_merge.py b/test/test_merge.py index 95bcfc2..c123553 100644 --- a/test/test_merge.py +++ b/test/test_merge.py @@ -44,3 +44,71 @@ def test_merge_fast_forward(xtl_clone, git_config, git2cpp_path, tmp_path, monke assert "Author: Jane Doe" in p_log.stdout # assert "Commit: John Doe" in p_log.stdout assert (xtl_path / "mook_file.txt").exists() + + merge_cmd_2 = [git2cpp_path, "merge", "foregone"] + p_merge_2 = subprocess.run( + merge_cmd_2, capture_output=True, cwd=xtl_path, text=True + ) + assert p_merge_2.returncode == 0 + assert p_merge_2.stdout == "Already up-to-date\n" + + +def test_merge(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 foregone"] + 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 + + p = xtl_path / "mook_file_2.txt" + p.write_text("BLABLABLA") + + add_cmd_2 = [git2cpp_path, "add", "mook_file_2.txt"] + p_add_2 = subprocess.run(add_cmd_2, capture_output=True, cwd=xtl_path, text=True) + assert p_add_2.returncode == 0 + + commit_cmd_2 = [git2cpp_path, "commit", "-m", "test commit master"] + p_commit_2 = subprocess.run( + commit_cmd_2, capture_output=True, cwd=xtl_path, text=True + ) + assert p_commit_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", "--format=full", "--max-count", "2"] + 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 "Johan" not in p_log.stdout + assert (xtl_path / "mook_file.txt").exists() + assert (xtl_path / "mook_file.txt").exists() + + merge_cmd_2 = [git2cpp_path, "merge", "foregone"] + p_merge_2 = subprocess.run( + merge_cmd_2, capture_output=True, cwd=xtl_path, text=True + ) + assert p_merge_2.returncode == 0 + assert p_merge_2.stdout == "Already up-to-date\n" From 62e24e25c310b25f1df0edecaaf63f8a855010b8 Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 31 Oct 2025 13:24:44 +0100 Subject: [PATCH 2/3] address review comments --- src/subcommand/merge_subcommand.cpp | 20 +++++++------------- src/subcommand/merge_subcommand.hpp | 5 +++++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp index 24ef3d9..7629e4e 100644 --- a/src/subcommand/merge_subcommand.cpp +++ b/src/subcommand/merge_subcommand.cpp @@ -57,8 +57,11 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int target_ref.write_new_ref(target_oid); } -void create_merge_commit(repository_wrapper& repo, const index_wrapper& index, std::vector m_branches_to_merge, - const annotated_commit_list_wrapper& commits_to_merge, size_t num_commits_to_merge) +void merge_subcommand::create_merge_commit( + repository_wrapper& repo, + const index_wrapper& index, + const annotated_commit_list_wrapper& commits_to_merge, + size_t num_commits_to_merge) { auto head_ref = repo.head(); auto merge_ref = repo.find_reference_dwim(m_branches_to_merge.front()); @@ -90,16 +93,7 @@ void create_merge_commit(repository_wrapper& repo, const index_wrapper& index, s { msg_target = git_oid_tostr_s(&(merge_commit.oid())); } - - std::string msg = "Merge "; - if (merge_ref) - { - msg.append("branch "); - } - else - { - msg.append("commit "); - } + std::string msg = merge_ref ? "Merge branch " : "Merge commit "; msg.append(msg_target); repo.create_commit(author_committer_sign_now, msg, std::optional(std::move(parents))); @@ -183,7 +177,7 @@ void merge_subcommand::run() } else if (!m_no_commit) { - create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge); + create_merge_commit(repo, index, commits_to_merge, num_commits_to_merge); printf("Merge made\n"); } } diff --git a/src/subcommand/merge_subcommand.hpp b/src/subcommand/merge_subcommand.hpp index 1ec1c20..c72855c 100644 --- a/src/subcommand/merge_subcommand.hpp +++ b/src/subcommand/merge_subcommand.hpp @@ -15,6 +15,11 @@ class merge_subcommand private: annotated_commit_list_wrapper resolve_heads(const repository_wrapper& repo); + void create_merge_commit( + repository_wrapper& repo, + const index_wrapper& index, + const annotated_commit_list_wrapper& commits_to_merge, + size_t num_commits_to_merge); std::vector m_branches_to_merge; // bool m_no_ff = false; From 5efcd8501e19b825c7ad84ed95a57ed68a5fe7bc Mon Sep 17 00:00:00 2001 From: Sandrine Pataut Date: Fri, 31 Oct 2025 13:30:25 +0100 Subject: [PATCH 3/3] small fix --- src/subcommand/merge_subcommand.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp index 7629e4e..d537e1e 100644 --- a/src/subcommand/merge_subcommand.cpp +++ b/src/subcommand/merge_subcommand.cpp @@ -84,15 +84,7 @@ void merge_subcommand::create_merge_commit( auto author_committer_sign_now = signature_wrapper::signature_now(author_name, author_email, author_name, author_email); // TODO: add a prompt to edit the merge message - std::string msg_target = ""; - if (merge_ref) - { - msg_target = merge_ref->short_name(); - } - else - { - msg_target = git_oid_tostr_s(&(merge_commit.oid())); - } + std::string msg_target = merge_ref ? merge_ref->short_name() : git_oid_tostr_s(&(merge_commit.oid())); std::string msg = merge_ref ? "Merge branch " : "Merge commit "; msg.append(msg_target);