#include "merge_subcommand.hpp"
#include
#include
#include
#include
#include
#include "../utils/input_output.hpp"
#include "../wrapper/status_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->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();
}
);
}
namespace
{
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;
auto lambda_get_target_ref = [](auto repo, auto is_unborn)
{
if (!is_unborn)
{
return repo->head();
}
else
{
return repo->find_reference("HEAD");
}
};
reference_wrapper target_ref = lambda_get_target_ref(&repo, is_unborn);
object_wrapper target = repo.find_object(target_oid, GIT_OBJECT_COMMIT);
repo.checkout_tree(target, ff_checkout_options);
target_ref.write_new_ref(target_oid);
}
// 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;
}
}
annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_wrapper& repo)
{
std::vector commits_to_merge;
commits_to_merge.reserve(m_branches_to_merge.size());
for (const auto branch_name : m_branches_to_merge)
{
std::optional commit = repo.resolve_local_ref(branch_name);
if (commit.has_value())
{
commits_to_merge.push_back(std::move(commit).value());
}
}
return annotated_commit_list_wrapper(std::move(commits_to_merge));
}
void 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(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);
parents_list.push_back(std::move(head_ref.peel()));
for (size_t i = 0; i < num_commits_to_merge; ++i)
{
parents_list.push_back(repo.find_commit(commits_to_merge[i].oid()));
}
auto parents = commit_list_wrapper(std::move(parents_list));
auto author_committer_sign = signature_wrapper::get_default_signature_from_env(repo);
std::string author_name;
author_name = author_committer_sign.first.name();
std::string author_email;
author_email = author_committer_sign.first.email();
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 = 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);
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();
auto repo = repository_wrapper::open(directory);
auto state = repo.state();
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