diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp index a9e62be..2a73d7b 100644 --- a/src/subcommand/merge_subcommand.cpp +++ b/src/subcommand/merge_subcommand.cpp @@ -1,8 +1,11 @@ #include +#include #include +#include +#include #include "merge_subcommand.hpp" -// #include "../wrapper/repository_wrapper.hpp" +#include "../wrapper/status_wrapper.hpp" merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app) @@ -10,6 +13,12 @@ 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, "Create a merge commit in all cases, even when the merge could instead be resolved as a fast-forward."); + // 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->add_flag("--abort", m_abort, "Abort the current conflict resolution process, and try to reconstruct the pre-merge state. If an autostash entry is present, apply it to the worktree.\nIf there were uncommitted worktree changes present when the merge started, git merge --abort will in some cases be unable to reconstruct these changes. It is therefore recommended to always commit or stash your changes before running git merge.\ngit merge --abort is equivalent to git reset --merge when MERGE_HEAD is present unless MERGE_AUTOSTASH is also present in which case git merge --abort applies the stash entry to the worktree whereas git reset --merge will save the stashed changes in the stash list."); + sub->add_flag("--quit", m_quit, "Forget about the current merge in progress. Leave the index and the working tree as-is. If MERGE_AUTOSTASH is present, the stash entry will be saved to the stash list."); + sub->add_flag("--continue", m_continue, "After a git merge stops due to conflicts you can conclude the merge by running git merge --continue"); // (see "HOW TO RESOLVE CONFLICTS" section below). sub->callback([this]() { this->run(); }); } @@ -30,6 +39,22 @@ annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_w return annotated_commit_list_wrapper(std::move(commits_to_merge)); } +annotated_commit_list_wrapper resolve_mergeheads(const repository_wrapper& repo, std::vector oid_list) +{ + std::vector commits_to_merge; + commits_to_merge.reserve(oid_list.size()); + + for (const auto id:oid_list) + { + std::optional commit = repo.find_annotated_commit(id); + 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; @@ -54,15 +79,192 @@ 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() : git_oid_tostr_s(&(merge_commit.oid())); + msg_target = "\'" + msg_target + "\'"; + 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))); + + repo.state_cleanup(); +} + +int populate_list(const git_oid* oid, void* payload) +{ + auto* l = reinterpret_cast*>(payload); + l->push_back(*oid); + return 0; +} + 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) + index_wrapper index = repo.make_index(); + stream_colour_fn yellow = termcolor::yellow; + + if (state == GIT_REPOSITORY_STATE_MERGE) { + if (m_abort) + { + // git merge --abort is equivalent to git reset --merge when MERGE_HEAD is present + // unless MERGE_AUTOSTASH is also present in which case git merge --abort applies + // the stash entry to the worktree whereas git reset --merge will save the stashed + // changes in the stash list. + + if (m_quit | m_continue) + { + std::cout << "fatal: --abort expects no arguments" << std::endl; // TODO: add the help info + return; + } + + std::cout << "Warning: 'merge --abort' is not implemented yet. A 'reset --hard HEAD' will be executed." << std::endl; + std::cout << "Do you want to continue [y/N] ?" << std::endl; + std::string answer; + std::cin >> answer; + if (answer == "y") + { + repo.state_cleanup(); + index.conflict_cleanup(); + + git_checkout_options options; + git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION); + auto head_ref = repo.head(); + repo.reset(head_ref.peel(), GIT_RESET_HARD, options); + } + else + { + std::cout << "Abort." << std::endl; // maybe another message would be more clear? + } + return; + } + else if (m_quit) + { + // Forget about the current merge in progress. Leave the index and the working tree as-is. + // If MERGE_AUTOSTASH is present, the stash entry will be saved to the stash list. + // + + // if (m_continue) + // { + // std::cout << "fatal: --abort expects no arguments" << std::endl; // TODO: add the help info + // return; + // } + + // problem: can't do a reset if the state is not cleaned up, but it shouldn't be. + // Idem for the index and the conflicts. + + // repo.state_cleanup(); + // index.conflict_cleanup(); + + // git_checkout_options options; + // git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION); + // auto head_ref = repo.head(); + // repo.reset(head_ref.peel(), GIT_RESET_SOFT, options); + + std::cout << "merge --quit is not implemented yet." << std::endl; + return; + } + else if (m_continue) + { + auto sl = status_list_wrapper::status_list(repo); + if (!sl.has_unmerged_header()) + { + // std::string commit_message = "Merge branch "; // how to get the name of the branch the merge was started on ? + // auto author_committer_signatures = signature_wrapper::get_default_signature_from_env(repo); + // repo.create_commit(author_committer_signatures, commit_message, std::nullopt); + + std::vector oid_list; + git_repository_mergehead_foreach(repo, populate_list, &oid_list); + + annotated_commit_list_wrapper commits_to_merge = resolve_mergeheads(repo, oid_list); + size_t num_commits_to_merge = commits_to_merge.size(); + + std::vector branches_to_merge_names; + for (auto id:oid_list) + { + git_reference_iterator* iter; + git_reference_iterator_new(&iter, repo); + git_reference* ref; + git_reference_next(&ref, iter); + if (git_oid_equal(git_reference_target(ref), &id)) + { + auto name = git_reference_name(ref); + branches_to_merge_names.push_back(name); + } + git_reference_free(ref); + } + + create_merge_commit(repo, index, branches_to_merge_names, commits_to_merge, num_commits_to_merge); + std::cout << "Merge made" << std::endl; // TODO: change the outpout to something like this: 3c22161 (HEAD -> master) Merge branch 'foregone' + + repo.state_cleanup(); + index.conflict_cleanup(); + return; + } + else + { + auto entry_status = get_status_msg(GIT_STATUS_CONFLICTED).short_mod; + const auto& entry_list = sl.get_entry_list(GIT_STATUS_CONFLICTED); + for (auto* entry : entry_list) + { + git_diff_delta* diff_delta = entry->head_to_index; //ou entry->index_to_workdir ??? + const char* old_path = diff_delta->old_file.path; + std::cout << entry_status << "\t" << old_path << std::endl; + } + std::cout << "error: Committing is not possible because you have unmerged files." << std::endl; + } + } + else + { + std::cout << "error: Merging is not possible because you have unmerged files." << std::endl; + } + std::cout << yellow << "hint: Fix them up in the work tree, and then use 'git add/rm '" << std::endl; + std::cout << "hint: as appropriate to mark resolution and make a commit." << termcolor::reset << std::endl; + std::cout << "fatal: Exiting because of an unresolved conflict." << std::endl; + return; + } + else + { + if (m_abort) + { + std::cout << "fatal: There is no merge to abort (MERGE_HEAD missing)." << std::endl; + return; + } + if (m_continue) + { + std::cout << "fatal: There is no merge in progress (MERGE_HEAD missing)." << std::endl; + return; + } + } + + if (state != GIT_REPOSITORY_STATE_NONE) // Could this be a "else if before the "else" above ? + { std::cout << "repository is in unexpected state " << state < m_branches_to_merge; + // bool m_no_ff = false; + // bool m_commit = false; + bool m_no_commit = false; + bool m_abort = false; + bool m_quit = false; + bool m_continue = false; }; diff --git a/src/subcommand/status_subcommand.cpp b/src/subcommand/status_subcommand.cpp index c24a3e2..f0acb1f 100644 --- a/src/subcommand/status_subcommand.cpp +++ b/src/subcommand/status_subcommand.cpp @@ -26,40 +26,14 @@ status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app) sub->callback([this]() { this->run(); }); }; -const std::string untracked_header = "Untracked files:\n"; -// "Untracked files:\n (use \"git add ...\" to include in what will be committed)"; -const std::string tobecommited_header = "Changes to be committed:\n"; -// "Changes to be committed:\n (use \"git reset HEAD ...\" to unstage)"; -const std::string ignored_header = "Ignored files:\n"; -// "Ignored files:\n (use \"git add -f ...\" to include in what will be committed)" +const std::string untracked_header = "Untracked files:\n (use \"git add ...\" to include in what will be committed)\n"; +const std::string tobecommited_header = "Changes to be committed:\n (use \"git reset HEAD ...\" to unstage)\n"; +const std::string ignored_header = "Ignored files:\n (use \"git add -f ...\" to include in what will be committed)\n"; const std::string notstagged_header = "Changes not staged for commit:\n"; -// "Changes not staged for commit:\n (use \"git add%s ...\" to update what will be committed)\n (use \"git checkout -- ...\" to discard changes in working directory)" -const std::string nothingtocommit_message = "No changes added to commit"; -// "No changes added to commit (use \"git add\" and/or \"git commit -a\")" - -struct status_messages -{ - std::string short_mod; - std::string long_mod; -}; - -const std::map status_msg_map = //TODO : check spaces in short_mod -{ - { GIT_STATUS_CURRENT, {"", ""} }, - { GIT_STATUS_INDEX_NEW, {"A ", "\tnew file:"} }, - { GIT_STATUS_INDEX_MODIFIED, {"M ", "\tmodified:"} }, - { GIT_STATUS_INDEX_DELETED, {"D ", "\tdeleted:"} }, - { GIT_STATUS_INDEX_RENAMED, {"R ", "\trenamed:"} }, - { GIT_STATUS_INDEX_TYPECHANGE, {"T ", "\ttypechange:"} }, - { GIT_STATUS_WT_NEW, {"?? ", ""} }, - { GIT_STATUS_WT_MODIFIED, {" M " , "\tmodified:"} }, - { GIT_STATUS_WT_DELETED, {" D ", "\tdeleted:"} }, - { GIT_STATUS_WT_TYPECHANGE, {" T ", "\ttypechange:"} }, - { GIT_STATUS_WT_RENAMED, {" R ", "\trenamed:"} }, - { GIT_STATUS_WT_UNREADABLE, {"", ""} }, - { GIT_STATUS_IGNORED, {"!! ", ""} }, - { GIT_STATUS_CONFLICTED, {"", ""} }, -}; +// "Changes not staged for commit:\n (use \"git add%s ...\" to update what will be committed)\n (use \"git checkout -- ...\" to discard changes in working directory)\n" +const std::string unmerged_header = "Unmerged paths:\n (use \"git add ...\" to mark resolution)\n"; +// const std::string nothingtocommit_message = "No changes added to commit (use \"git add\" and/or \"git commit -a\")"; +const std::string treeclean_message = "Nothing to commit, working tree clean"; enum class output_format { @@ -79,11 +53,11 @@ std::string get_print_status(git_status_t status, output_format of) std::string entry_status; if ((of == output_format::DEFAULT) || (of == output_format::LONG)) { - entry_status = status_msg_map.at(status).long_mod + " "; + entry_status = get_status_msg(status).long_mod; } else if (of == output_format::SHORT) { - entry_status = status_msg_map.at(status).short_mod; + entry_status = get_status_msg(status).short_mod; } return entry_status; } @@ -218,7 +192,12 @@ void status_subcommand::run() is_long = ((of == output_format::DEFAULT) || (of == output_format::LONG)); if (is_long) { - std::cout << "On branch " << branch_name << std::endl; + std::cout << "On branch " << branch_name << "\n" << std::endl; + + if (sl.has_unmerged_header()) + { + std::cout << "You have unmerged paths.\n (fix conflicts and run \"git commit\")\n (use \"git merge --abort\" to abort the merge)\n" << std::endl; + } } else { @@ -263,6 +242,21 @@ void status_subcommand::run() } } + // TODO: check if should be printed before "not stagged" files + if (sl.has_unmerged_header()) + { + stream_colour_fn colour = termcolor::red; + if (is_long) + { + std::cout << unmerged_header; + } + print_not_tracked(get_entries_to_print(GIT_STATUS_CONFLICTED, sl, false, of), tracked_dir_set, untracked_dir_set, is_long, colour); + if (is_long) + { + std::cout << std::endl; + } + } + if (sl.has_untracked_header()) { stream_colour_fn colour = termcolor::red; @@ -277,6 +271,12 @@ void status_subcommand::run() } } + // TODO: check if this message should be displayed even if there are untracked files + if (!(sl.has_tobecommited_header() | sl.has_notstagged_header() | sl.has_unmerged_header() | sl.has_untracked_header())) + { + std::cout << treeclean_message << std::endl; + } + // if (sl.has_ignored_header()) // { // stream_colour_fn colour = termcolor::red; diff --git a/src/utils/common.cpp b/src/utils/common.cpp index 9a5787c..a9b84d4 100644 --- a/src/utils/common.cpp +++ b/src/utils/common.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -26,6 +27,33 @@ std::string get_current_git_path() // ->check(CLI::ExistingDirectory | CLI::NonexistentPath) // ->default_val(std::filesystem::current_path()); +const std::map& get_status_msg_map() +{ + static std::map status_msg_map = //TODO : check spaces in short_mod + { + { GIT_STATUS_CURRENT, {"", ""} }, + { GIT_STATUS_INDEX_NEW, {"A ", "\tnew file: "} }, + { GIT_STATUS_INDEX_MODIFIED, {"M ", "\tmodified: "} }, + { GIT_STATUS_INDEX_DELETED, {"D ", "\tdeleted: "} }, + { GIT_STATUS_INDEX_RENAMED, {"R ", "\trenamed: "} }, + { GIT_STATUS_INDEX_TYPECHANGE, {"T ", "\ttypechange: "} }, + { GIT_STATUS_WT_NEW, {"?? ", "\t"} }, + { GIT_STATUS_WT_MODIFIED, {" M " , "\tmodified: "} }, + { GIT_STATUS_WT_DELETED, {" D ", "\tdeleted: "} }, + { GIT_STATUS_WT_TYPECHANGE, {" T ", "\ttypechange: "} }, + { GIT_STATUS_WT_RENAMED, {" R ", "\trenamed: "} }, + { GIT_STATUS_WT_UNREADABLE, {"", ""} }, + { GIT_STATUS_IGNORED, {"!! ", ""} }, + { GIT_STATUS_CONFLICTED, {"AA ", "\tboth added: "} }, + }; + return status_msg_map; +} + +status_messages get_status_msg(git_status_t st) +{ + return get_status_msg_map().find(st)->second; +} + git_strarray_wrapper::git_strarray_wrapper(std::vector patterns) : m_patterns(std::move(patterns)) { diff --git a/src/utils/common.hpp b/src/utils/common.hpp index e3b959c..6751b46 100644 --- a/src/utils/common.hpp +++ b/src/utils/common.hpp @@ -28,6 +28,14 @@ class libgit2_object : private noncopyable_nonmovable std::string get_current_git_path(); +struct status_messages +{ + std::string short_mod; + std::string long_mod; +}; + +status_messages get_status_msg(git_status_t); + using stream_colour_fn = std::ostream& (*)(std::ostream&); class git_strarray_wrapper diff --git a/src/wrapper/index_wrapper.cpp b/src/wrapper/index_wrapper.cpp index 03e8afb..7ff0ce2 100644 --- a/src/wrapper/index_wrapper.cpp +++ b/src/wrapper/index_wrapper.cpp @@ -1,9 +1,12 @@ +#include +#include +#include + #include "index_wrapper.hpp" #include "../utils/common.hpp" #include "../utils/git_exception.hpp" #include "../wrapper/repository_wrapper.hpp" -#include index_wrapper::~index_wrapper() { @@ -32,7 +35,6 @@ void index_wrapper::add_impl(std::vector patterns) { git_strarray_wrapper array{patterns}; throw_if_error(git_index_add_all(*this, array, 0, NULL, NULL)); - // throw_if_error(git_index_write(*this)); } void index_wrapper::write() @@ -46,3 +48,51 @@ git_oid index_wrapper::write_tree() throw_if_error(git_index_write_tree(&tree_id, *this)); return tree_id; } + +bool index_wrapper::has_conflict() const +{ + return git_index_has_conflicts(*this); +} + +git_index_conflict_iterator* index_wrapper::create_conflict_iterator() +{ + git_index_conflict_iterator* conflict_iterator; + throw_if_error(git_index_conflict_iterator_new(&conflict_iterator, *this)); + return conflict_iterator; +} + +void index_wrapper::output_conflicts() +{ + git_index_conflict_iterator* conflicts = create_conflict_iterator(); + + const git_index_entry* ancestor; + const git_index_entry* our; + const git_index_entry* their; + int err = 0; + std::string msg_conflict; + + while ((err = git_index_conflict_next(&ancestor, &our, &their, conflicts)) == 0) + { + std::string ancestor_path = ancestor ? ancestor->path : ""; + std::string our_path = our->path ? our->path : "NULL"; + std::string their_path = their->path ? their->path : "NULL"; + msg_conflict = "conflict: " + ancestor_path + " " + our_path + " " + their_path; + std::cout << msg_conflict << std::endl; +// Message with git is a bit different: +// Auto-merging mook_file.txt +// CONFLICT (add/add): Merge conflict in mook_file.txt +// Automatic merge failed; fix conflicts and then commit the result. + } + + if (err != GIT_ITEROVER) + { + std::cout << "error iterating conflicts" << std::endl; + } + + git_index_conflict_iterator_free(conflicts); +} + +void index_wrapper::conflict_cleanup() +{ + throw_if_error(git_index_conflict_cleanup(*this)); +} diff --git a/src/wrapper/index_wrapper.hpp b/src/wrapper/index_wrapper.hpp index 2095fe0..0fa8b55 100644 --- a/src/wrapper/index_wrapper.hpp +++ b/src/wrapper/index_wrapper.hpp @@ -26,9 +26,15 @@ class index_wrapper : public wrapper_base void add_entries(std::vector patterns); void add_all(); + bool has_conflict() const; + void output_conflicts(); + void conflict_cleanup(); + private: index_wrapper() = default; void add_impl(std::vector patterns); + + git_index_conflict_iterator* create_conflict_iterator(); }; 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/status_wrapper.cpp b/src/wrapper/status_wrapper.cpp index a1962d8..a6fe876 100644 --- a/src/wrapper/status_wrapper.cpp +++ b/src/wrapper/status_wrapper.cpp @@ -35,6 +35,10 @@ status_list_wrapper status_list_wrapper::status_list(const repository_wrapper& r { res.m_ignored_header_flag = true; } + if (!res.get_entry_list(GIT_STATUS_CONFLICTED).empty()) + { + res.m_unmerged_header_flag = true; + } // if (!res.tobecommited_header_flag) // { // res.m_nothingtocommit_message_flag = true; @@ -59,6 +63,10 @@ bool status_list_wrapper::has_notstagged_header() const { return m_notstagged_header_flag; } +bool status_list_wrapper::has_unmerged_header() const +{ + return m_unmerged_header_flag; +} bool status_list_wrapper::has_nothingtocommit_message() const { return m_nothingtocommit_message_flag; diff --git a/src/wrapper/status_wrapper.hpp b/src/wrapper/status_wrapper.hpp index 2a8335d..b20e18a 100644 --- a/src/wrapper/status_wrapper.hpp +++ b/src/wrapper/status_wrapper.hpp @@ -26,6 +26,7 @@ class status_list_wrapper : public wrapper_base bool has_tobecommited_header() const; bool has_ignored_header() const; bool has_notstagged_header() const; + bool has_unmerged_header() const; bool has_nothingtocommit_message() const; private: @@ -39,5 +40,6 @@ class status_list_wrapper : public wrapper_base bool m_tobecommited_header_flag = false; bool m_ignored_header_flag = false; bool m_notstagged_header_flag = false; + bool m_unmerged_header_flag = false; bool m_nothingtocommit_message_flag = false; }; 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..0f531f0 100644 --- a/test/test_merge.py +++ b/test/test_merge.py @@ -1,5 +1,4 @@ import subprocess -import time import pytest @@ -16,8 +15,8 @@ def test_merge_fast_forward(xtl_clone, git_config, git2cpp_path, tmp_path, monke ) assert p_checkout.returncode == 0 - p = xtl_path / "mook_file.txt" - p.write_text("blablabla") + file_path = xtl_path / "mook_file.txt" + file_path.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) @@ -44,3 +43,190 @@ 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_commit(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 + + file_path = xtl_path / "mook_file.txt" + file_path.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 + + file_path_2 = xtl_path / "mook_file_2.txt" + file_path_2.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_2.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" + + +@pytest.mark.parametrize("flag", ["--abort", "--quit", "--continue"]) +def test_merge_conflict( + xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch, flag +): + 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 + + file_path = xtl_path / "mook_file.txt" + file_path.write_text("blablabla") + + file_path_2 = xtl_path / "mook_file_2.txt" + file_path_2.write_text("Second file") + + add_cmd = [git2cpp_path, "add", "--all"] + 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 + + file_path.write_text("BLABLABLA") + + add_cmd_2 = [git2cpp_path, "add", "mook_file.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 + assert "conflict: " in p_merge.stdout + + flag_cmd = [git2cpp_path, "merge", flag] + if flag == "--abort": + for answer in {"y", ""}: + p_abort = subprocess.run( + flag_cmd, input=answer, capture_output=True, cwd=xtl_path, text=True + ) + assert p_abort.returncode == 0 + assert (xtl_path / "mook_file.txt").exists() + with open(xtl_path / "mook_file.txt") as f: + if answer == "y": + assert "BLA" in f.read() + assert "bla" not in f.read() + else: + assert "Abort." in p_abort.stdout + + elif flag == "--quit": + pass + # p_quit = subprocess.run(flag_cmd, capture_output=True, cwd=xtl_path, text=True) + # assert p_quit.returncode == 0 + # assert (xtl_path / "mook_file.txt").exists() + # with open(xtl_path / "mook_file.txt") as f: + # lines = f.readlines() + # assert "<<<<<<< HEAD" in lines[0] + # assert ">>>>>>> foregone" in lines[-1] + + # p_merge_2 = subprocess.run( + # merge_cmd, capture_output=True, cwd=xtl_path, text=True + # ) + # assert p_merge_2.returncode != 0 + # print(p_merge_2.stdout) + # assert "error: Merging is not possible because you have unmerged files." in p_merge_2.stdout + + elif flag == "--continue": + # Create another branch pointing to the same commit (alias branch). + # This checks the merge behaviour when a different branch name points to the same commit. + branch_alias_cmd = [git2cpp_path, "branch", "foregone_alias"] + p_branch_alias = subprocess.run( + branch_alias_cmd, capture_output=True, cwd=xtl_path, text=True + ) + assert p_branch_alias.returncode == 0 + + file_path.write_text("blablabla") + + cmd_add = [git2cpp_path, "add", "mook_file.txt"] + p_add = subprocess.run(cmd_add, cwd=xtl_path, text=True) + assert p_add.returncode == 0 + + p_continue = subprocess.run( + flag_cmd, capture_output=True, cwd=xtl_path, text=True + ) + assert p_continue.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_2.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"