Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 76 additions & 1 deletion src/subcommand/merge_subcommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
#include <git2/types.h>

#include "merge_subcommand.hpp"
// #include "../wrapper/repository_wrapper.hpp"
#include <iostream>


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("--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(); });
}
Expand Down Expand Up @@ -54,6 +57,42 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int
target_ref.write_new_ref(target_oid);
}

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());
auto merge_commit = repo.resolve_local_ref(m_branches_to_merge.front()).value();

std::vector<commit_wrapper> parents_list;
parents_list.reserve(num_commits_to_merge + 1);
parents_list.push_back(std::move(head_ref.peel<commit_wrapper>()));
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()));
std::string msg = merge_ref ? "Merge branch " : "Merge commit ";
msg.append(msg_target);

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

repo.state_cleanup();
}

void merge_subcommand::run()
{
auto directory = get_current_git_path();
Expand All @@ -78,6 +117,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 &&
Expand All @@ -97,4 +137,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, commits_to_merge, num_commits_to_merge);
printf("Merge made\n");
}
}
8 changes: 8 additions & 0 deletions src/subcommand/merge_subcommand.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ 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<std::string> m_branches_to_merge;
// bool m_no_ff = false;
// bool m_commit = false;
bool m_no_commit = false;
};
6 changes: 6 additions & 0 deletions src/wrapper/repository_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "../wrapper/index_wrapper.hpp"
#include "../wrapper/object_wrapper.hpp"
#include "../wrapper/commit_wrapper.hpp"
#include <git2/repository.h>
#include "../wrapper/repository_wrapper.hpp"

repository_wrapper::~repository_wrapper()
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/wrapper/repository_wrapper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class repository_wrapper : public wrapper_base<git_repository>
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;
Expand Down
11 changes: 7 additions & 4 deletions src/wrapper/wrapper_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,14 @@ class list_wrapper : public wrapper_base<typename T::resource_type*>
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:
Expand Down
68 changes: 68 additions & 0 deletions test/test_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"