diff --git a/src/subcommand/merge_subcommand.cpp b/src/subcommand/merge_subcommand.cpp index d537e1e..53d2115 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 +#include "../wrapper/status_wrapper.hpp" merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app) @@ -10,9 +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, ""); + // 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(); }); } @@ -33,7 +39,23 @@ annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_w return annotated_commit_list_wrapper(std::move(commits_to_merge)); } -void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int is_unborn) +annotated_commit_list_wrapper resolve_mergeheads(const repository_wrapper& repo, const 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; @@ -60,12 +82,13 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int void merge_subcommand::create_merge_commit( repository_wrapper& repo, const index_wrapper& index, + const std::vector& 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(); + auto merge_ref = repo.find_reference_dwim(branches_to_merge.front()); + auto merge_commit = repo.resolve_local_ref(branches_to_merge.front()).value(); std::vector parents_list; parents_list.reserve(num_commits_to_merge + 1); @@ -85,6 +108,7 @@ void merge_subcommand::create_merge_commit( // TODO: add a prompt to edit the merge message std::string msg_target = merge_ref ? merge_ref->short_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); @@ -93,15 +117,159 @@ void merge_subcommand::create_merge_commit( repo.state_cleanup(); } +// This function is used as a callback in git_repository_mergehead_foreach and therefore its type must be git_repository_mergehead_foreach_cb. +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 (const 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 <& branches_to_merge, const annotated_commit_list_wrapper& commits_to_merge, size_t num_commits_to_merge); @@ -25,4 +26,7 @@ class merge_subcommand // 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 1b9c1f1..36aa213 100644 --- a/src/subcommand/status_subcommand.cpp +++ b/src/subcommand/status_subcommand.cpp @@ -27,41 +27,13 @@ status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app) }; 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 restore --staged ...\" to unstage)\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 (use \"git add ...\" to update what will be committed)\n"; -// (use \"git restore ...\" to discard changes in working directory)\n -// (use \"git checkout -- ...\" to discard changes in working directory)\n" -const std::string nothingtocommit_msg = "No changes added to commit (use \"git add\" and/or \"git commit -a\")"; -const std::string uptodate_msg = "Nothing to commit, working tree clean."; -const std::string nothingtocommit_untrackedfiles_msg = "Nothing added to commit but untracked files present (use \"git add\" to track)"; -// 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, {"", ""} }, -}; +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)\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 { @@ -81,18 +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)) { - if (status == GIT_STATUS_WT_NEW) - { - entry_status = status_msg_map.at(status).long_mod + "\t"; - } - else - { - 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; } @@ -173,8 +138,7 @@ void print_not_tracked(const std::vector& entries_to_print, const s const size_t first_slash_idx = e.item.find('/'); if (std::string::npos != first_slash_idx) { - auto directory = e.item.substr(0, first_slash_idx); - auto directory_print = e.item.substr(0, first_slash_idx) + "/"; + auto directory = e.item.substr(0, first_slash_idx) + "/"; if (tracked_dir_set.contains(directory)) { not_tracked_entries_to_print.push_back(e); @@ -185,7 +149,7 @@ void print_not_tracked(const std::vector& entries_to_print, const s {} else { - not_tracked_entries_to_print.push_back({e.status, directory_print}); + not_tracked_entries_to_print.push_back({e.status, directory}); untracked_dir_set.insert(std::string(directory)); } } @@ -228,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 { @@ -273,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; @@ -287,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; @@ -300,21 +290,4 @@ void status_subcommand::run() // std::cout << std::endl; // } // } - - if (!sl.has_tobecommited_header() && (sl.has_notstagged_header() || sl.has_untracked_header())) - { - if (sl.has_untracked_header()) - { - std::cout << nothingtocommit_untrackedfiles_msg << std::endl; - } - else - { - std::cout << nothingtocommit_msg << std::endl; - } - } - - if (!sl.has_notstagged_header() && !sl.has_untracked_header()) - { - std::cout << uptodate_msg << std::endl; - } } 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/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/test/test_merge.py b/test/test_merge.py index c123553..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) @@ -53,7 +52,7 @@ def test_merge_fast_forward(xtl_clone, git_config, git2cpp_path, tmp_path, monke assert p_merge_2.stdout == "Already up-to-date\n" -def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): +def test_merge_commit(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): assert (tmp_path / "xtl").exists() xtl_path = tmp_path / "xtl" @@ -63,8 +62,8 @@ def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): ) 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) @@ -80,8 +79,8 @@ def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): ) assert p_checkout_2.returncode == 0 - p = xtl_path / "mook_file_2.txt" - p.write_text("BLABLABLA") + 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) @@ -104,7 +103,7 @@ def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): # 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() + assert (xtl_path / "mook_file_2.txt").exists() merge_cmd_2 = [git2cpp_path, "merge", "foregone"] p_merge_2 = subprocess.run( @@ -112,3 +111,122 @@ def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch): ) 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"