Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 176 additions & 13 deletions src/subcommand/merge_subcommand.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
#include <cassert>
#include <git2/reset.h>
#include <git2/types.h>
#include <iostream>
#include <termcolor/termcolor.hpp>

#include "merge_subcommand.hpp"
#include <iostream>
#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("<branch>", 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(); });
}
Expand All @@ -33,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<git_oid> oid_list)
{
std::vector<annotated_commit_wrapper> commits_to_merge;
commits_to_merge.reserve(oid_list.size());

for (const auto id:oid_list)
{
std::optional<annotated_commit_wrapper> 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;
Expand Down Expand Up @@ -60,6 +82,7 @@ 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,
std::vector<std::string> m_branches_to_merge,
const annotated_commit_list_wrapper& commits_to_merge,
size_t num_commits_to_merge)
{
Expand All @@ -85,23 +108,167 @@ 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);
msg.append( msg_target);

repo.create_commit(author_committer_sign_now, msg, std::optional<commit_list_wrapper>(std::move(parents)));

repo.state_cleanup();
}

int populate_list(const git_oid* oid, void* payload)
{
auto* l = reinterpret_cast<std::vector<git_oid>*>(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<object_wrapper>(), 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<object_wrapper>(), 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<git_oid> 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<std::string> 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 <file>'" << 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 <<std::endl;
}

Expand Down Expand Up @@ -159,17 +326,13 @@ void merge_subcommand::run()
&checkout_opts));
}

index_wrapper index = repo.make_index();

if (git_index_has_conflicts(index))
if (index.has_conflict())
{
std::cout << "Conflict. To be implemented" << std::endl;
/* Handle conflicts */
// output_conflicts(index);
index.output_conflicts();
}
else if (!m_no_commit)
{
create_merge_commit(repo, index, commits_to_merge, num_commits_to_merge);
printf("Merge made\n");
create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge);
std::cout << "Merge made" << std::endl;
}
}
4 changes: 4 additions & 0 deletions src/subcommand/merge_subcommand.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@ class merge_subcommand
void create_merge_commit(
repository_wrapper& repo,
const index_wrapper& index,
std::vector<std::string> m_branches_to_merge,
const annotated_commit_list_wrapper& commits_to_merge,
size_t num_commits_to_merge);

std::vector<std::string> 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;
};
Loading