Skip to content

Commit 437e2b7

Browse files
authored
Add merge commit (#53)
* Add merge commit * address review comments * small fix
1 parent 69b86a4 commit 437e2b7

File tree

6 files changed

+166
-5
lines changed

6 files changed

+166
-5
lines changed

src/subcommand/merge_subcommand.cpp

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
#include <git2/types.h>
33

44
#include "merge_subcommand.hpp"
5-
// #include "../wrapper/repository_wrapper.hpp"
5+
#include <iostream>
66

77

88
merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app)
99
{
1010
auto *sub = app.add_subcommand("merge", "Join two or more development histories together");
1111

1212
sub->add_option("<branch>", m_branches_to_merge, "Branch(es) to merge");
13+
// sub->add_flag("--no-ff", m_no_ff, "");
14+
// sub->add_flag("--commit", m_commit, "Perform the merge and commit the result. This option can be used to override --no-commit.");
15+
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.");
1316

1417
sub->callback([this]() { this->run(); });
1518
}
@@ -54,6 +57,42 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int
5457
target_ref.write_new_ref(target_oid);
5558
}
5659

60+
void merge_subcommand::create_merge_commit(
61+
repository_wrapper& repo,
62+
const index_wrapper& index,
63+
const annotated_commit_list_wrapper& commits_to_merge,
64+
size_t num_commits_to_merge)
65+
{
66+
auto head_ref = repo.head();
67+
auto merge_ref = repo.find_reference_dwim(m_branches_to_merge.front());
68+
auto merge_commit = repo.resolve_local_ref(m_branches_to_merge.front()).value();
69+
70+
std::vector<commit_wrapper> parents_list;
71+
parents_list.reserve(num_commits_to_merge + 1);
72+
parents_list.push_back(std::move(head_ref.peel<commit_wrapper>()));
73+
for (size_t i=0; i<num_commits_to_merge; ++i)
74+
{
75+
parents_list.push_back(repo.find_commit(commits_to_merge[i].oid()));
76+
}
77+
auto parents = commit_list_wrapper(std::move(parents_list));
78+
79+
auto author_committer_sign = signature_wrapper::get_default_signature_from_env(repo);
80+
std::string author_name;
81+
author_name = author_committer_sign.first.name();
82+
std::string author_email;
83+
author_email = author_committer_sign.first.email();
84+
auto author_committer_sign_now = signature_wrapper::signature_now(author_name, author_email, author_name, author_email);
85+
86+
// TODO: add a prompt to edit the merge message
87+
std::string msg_target = merge_ref ? merge_ref->short_name() : git_oid_tostr_s(&(merge_commit.oid()));
88+
std::string msg = merge_ref ? "Merge branch " : "Merge commit ";
89+
msg.append(msg_target);
90+
91+
repo.create_commit(author_committer_sign_now, msg, std::optional<commit_list_wrapper>(std::move(parents)));
92+
93+
repo.state_cleanup();
94+
}
95+
5796
void merge_subcommand::run()
5897
{
5998
auto directory = get_current_git_path();
@@ -78,6 +117,7 @@ void merge_subcommand::run()
78117
if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE)
79118
{
80119
std::cout << "Already up-to-date" << std::endl;
120+
return;
81121
}
82122
else if (analysis & GIT_MERGE_ANALYSIS_UNBORN ||
83123
(analysis & GIT_MERGE_ANALYSIS_FASTFORWARD &&
@@ -97,4 +137,39 @@ void merge_subcommand::run()
97137
assert(num_commits_to_merge == 1);
98138
perform_fastforward(repo, target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN));
99139
}
140+
else if (analysis & GIT_MERGE_ANALYSIS_NORMAL)
141+
{
142+
git_merge_options merge_opts = GIT_MERGE_OPTIONS_INIT;
143+
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
144+
145+
merge_opts.flags = 0;
146+
merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3;
147+
148+
checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE|GIT_CHECKOUT_ALLOW_CONFLICTS;
149+
150+
if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY)
151+
{
152+
std::cout << "Fast-forward is preferred, but only a merge is possible\n" << std::endl;
153+
}
154+
155+
throw_if_error(git_merge(repo,
156+
(const git_annotated_commit**)c_commits_to_merge,
157+
num_commits_to_merge,
158+
&merge_opts,
159+
&checkout_opts));
160+
}
161+
162+
index_wrapper index = repo.make_index();
163+
164+
if (git_index_has_conflicts(index))
165+
{
166+
std::cout << "Conflict. To be implemented" << std::endl;
167+
/* Handle conflicts */
168+
// output_conflicts(index);
169+
}
170+
else if (!m_no_commit)
171+
{
172+
create_merge_commit(repo, index, commits_to_merge, num_commits_to_merge);
173+
printf("Merge made\n");
174+
}
100175
}

src/subcommand/merge_subcommand.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class merge_subcommand
1515
private:
1616

1717
annotated_commit_list_wrapper resolve_heads(const repository_wrapper& repo);
18+
void create_merge_commit(
19+
repository_wrapper& repo,
20+
const index_wrapper& index,
21+
const annotated_commit_list_wrapper& commits_to_merge,
22+
size_t num_commits_to_merge);
1823

1924
std::vector<std::string> m_branches_to_merge;
25+
// bool m_no_ff = false;
26+
// bool m_commit = false;
27+
bool m_no_commit = false;
2028
};

src/wrapper/repository_wrapper.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "../wrapper/index_wrapper.hpp"
33
#include "../wrapper/object_wrapper.hpp"
44
#include "../wrapper/commit_wrapper.hpp"
5+
#include <git2/repository.h>
56
#include "../wrapper/repository_wrapper.hpp"
67

78
repository_wrapper::~repository_wrapper()
@@ -36,6 +37,11 @@ git_repository_state_t repository_wrapper::state() const
3637
return git_repository_state_t(git_repository_state(*this));
3738
}
3839

40+
void repository_wrapper::state_cleanup()
41+
{
42+
throw_if_error(git_repository_state_cleanup(*this));
43+
}
44+
3945
// References
4046

4147
reference_wrapper repository_wrapper::head() const

src/wrapper/repository_wrapper.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class repository_wrapper : public wrapper_base<git_repository>
3030
static repository_wrapper clone(std::string_view url, std::string_view path, const git_clone_options& opts);
3131

3232
git_repository_state_t state() const;
33+
void state_cleanup();
3334

3435
// References
3536
reference_wrapper head() const;

src/wrapper/wrapper_base.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,14 @@ class list_wrapper : public wrapper_base<typename T::resource_type*>
7171
return m_list.size();
7272
}
7373

74-
T front()
74+
const T& operator[](size_t pos) const
7575
{
76-
// TODO: rework wrapper so they can have references
77-
// on libgit2 object without taking ownership
78-
return T(std::move(m_list.front()));
76+
return m_list[pos];
77+
}
78+
79+
const T& front() const
80+
{
81+
return m_list.front();
7982
}
8083

8184
private:

test/test_merge.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,71 @@ def test_merge_fast_forward(xtl_clone, git_config, git2cpp_path, tmp_path, monke
4444
assert "Author: Jane Doe" in p_log.stdout
4545
# assert "Commit: John Doe" in p_log.stdout
4646
assert (xtl_path / "mook_file.txt").exists()
47+
48+
merge_cmd_2 = [git2cpp_path, "merge", "foregone"]
49+
p_merge_2 = subprocess.run(
50+
merge_cmd_2, capture_output=True, cwd=xtl_path, text=True
51+
)
52+
assert p_merge_2.returncode == 0
53+
assert p_merge_2.stdout == "Already up-to-date\n"
54+
55+
56+
def test_merge(xtl_clone, git_config, git2cpp_path, tmp_path, monkeypatch):
57+
assert (tmp_path / "xtl").exists()
58+
xtl_path = tmp_path / "xtl"
59+
60+
checkout_cmd = [git2cpp_path, "checkout", "-b", "foregone"]
61+
p_checkout = subprocess.run(
62+
checkout_cmd, capture_output=True, cwd=xtl_path, text=True
63+
)
64+
assert p_checkout.returncode == 0
65+
66+
p = xtl_path / "mook_file.txt"
67+
p.write_text("blablabla")
68+
69+
add_cmd = [git2cpp_path, "add", "mook_file.txt"]
70+
p_add = subprocess.run(add_cmd, capture_output=True, cwd=xtl_path, text=True)
71+
assert p_add.returncode == 0
72+
73+
commit_cmd = [git2cpp_path, "commit", "-m", "test commit foregone"]
74+
p_commit = subprocess.run(commit_cmd, capture_output=True, cwd=xtl_path, text=True)
75+
assert p_commit.returncode == 0
76+
77+
checkout_cmd_2 = [git2cpp_path, "checkout", "master"]
78+
p_checkout_2 = subprocess.run(
79+
checkout_cmd_2, capture_output=True, cwd=xtl_path, text=True
80+
)
81+
assert p_checkout_2.returncode == 0
82+
83+
p = xtl_path / "mook_file_2.txt"
84+
p.write_text("BLABLABLA")
85+
86+
add_cmd_2 = [git2cpp_path, "add", "mook_file_2.txt"]
87+
p_add_2 = subprocess.run(add_cmd_2, capture_output=True, cwd=xtl_path, text=True)
88+
assert p_add_2.returncode == 0
89+
90+
commit_cmd_2 = [git2cpp_path, "commit", "-m", "test commit master"]
91+
p_commit_2 = subprocess.run(
92+
commit_cmd_2, capture_output=True, cwd=xtl_path, text=True
93+
)
94+
assert p_commit_2.returncode == 0
95+
96+
merge_cmd = [git2cpp_path, "merge", "foregone"]
97+
p_merge = subprocess.run(merge_cmd, capture_output=True, cwd=xtl_path, text=True)
98+
assert p_merge.returncode == 0
99+
100+
log_cmd = [git2cpp_path, "log", "--format=full", "--max-count", "2"]
101+
p_log = subprocess.run(log_cmd, capture_output=True, cwd=xtl_path, text=True)
102+
assert p_log.returncode == 0
103+
assert "Author: Jane Doe" in p_log.stdout
104+
# assert "Commit: John Doe" in p_log.stdout
105+
assert "Johan" not in p_log.stdout
106+
assert (xtl_path / "mook_file.txt").exists()
107+
assert (xtl_path / "mook_file.txt").exists()
108+
109+
merge_cmd_2 = [git2cpp_path, "merge", "foregone"]
110+
p_merge_2 = subprocess.run(
111+
merge_cmd_2, capture_output=True, cwd=xtl_path, text=True
112+
)
113+
assert p_merge_2.returncode == 0
114+
assert p_merge_2.stdout == "Already up-to-date\n"

0 commit comments

Comments
 (0)