Skip to content

Commit 2881ad0

Browse files
committed
Add merge conflicts
1 parent 1c4b6d9 commit 2881ad0

File tree

10 files changed

+439
-79
lines changed

10 files changed

+439
-79
lines changed

src/subcommand/merge_subcommand.cpp

Lines changed: 177 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
#include <cassert>
2+
#include <git2/reset.h>
23
#include <git2/types.h>
4+
#include <iostream>
5+
#include <termcolor/termcolor.hpp>
36

47
#include "merge_subcommand.hpp"
5-
#include <iostream>
8+
#include "../wrapper/status_wrapper.hpp"
69

710

811
merge_subcommand::merge_subcommand(const libgit2_object&, CLI::App& app)
912
{
1013
auto *sub = app.add_subcommand("merge", "Join two or more development histories together");
1114

1215
sub->add_option("<branch>", m_branches_to_merge, "Branch(es) to merge");
13-
// sub->add_flag("--no-ff", m_no_ff, "");
16+
// 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.");
1417
// sub->add_flag("--commit", m_commit, "Perform the merge and commit the result. This option can be used to override --no-commit.");
1518
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.");
19+
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.");
20+
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.");
21+
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).
1622

1723
sub->callback([this]() { this->run(); });
1824
}
@@ -33,6 +39,22 @@ annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_w
3339
return annotated_commit_list_wrapper(std::move(commits_to_merge));
3440
}
3541

42+
annotated_commit_list_wrapper resolve_mergeheads(const repository_wrapper& repo, std::vector<git_oid> oid_list)
43+
{
44+
std::vector<annotated_commit_wrapper> commits_to_merge;
45+
commits_to_merge.reserve(oid_list.size());
46+
47+
for (const auto id:oid_list)
48+
{
49+
std::optional<annotated_commit_wrapper> commit = repo.find_annotated_commit(id);
50+
if (commit.has_value())
51+
{
52+
commits_to_merge.push_back(std::move(commit).value());
53+
}
54+
}
55+
return annotated_commit_list_wrapper(std::move(commits_to_merge));
56+
}
57+
3658
void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int is_unborn)
3759
{
3860
const git_checkout_options ff_checkout_options = GIT_CHECKOUT_OPTIONS_INIT;
@@ -81,41 +103,168 @@ void create_merge_commit(repository_wrapper& repo, const index_wrapper& index, s
81103
auto author_committer_sign_now = signature_wrapper::signature_now(author_name, author_email, author_name, author_email);
82104

83105
// TODO: add a prompt to edit the merge message
84-
std::string msg_target = "";
85-
if (merge_ref)
86-
{
87-
msg_target = merge_ref->short_name();
88-
}
89-
else
90-
{
91-
msg_target = git_oid_tostr_s(&(merge_commit.oid()));
92-
}
93-
94-
std::string msg = "Merge ";
95-
if (merge_ref)
96-
{
97-
msg.append("branch ");
98-
}
99-
else
100-
{
101-
msg.append("commit ");
102-
}
103-
msg.append(msg_target);
106+
std::string msg_target = merge_ref ? merge_ref->short_name() : git_oid_tostr_s(&(merge_commit.oid()));
107+
msg_target = "\'" + msg_target + "\'";
108+
std::string msg = merge_ref ? "Merge branch " : "Merge commit ";
109+
msg.append( msg_target);
104110

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

107113
repo.state_cleanup();
108114
}
109115

116+
int populate_list(const git_oid* oid, void* payload)
117+
{
118+
auto* l = reinterpret_cast<std::vector<git_oid>*>(payload);
119+
l->push_back(*oid);
120+
return 0;
121+
}
122+
110123
void merge_subcommand::run()
111124
{
112125
auto directory = get_current_git_path();
113126
auto bare = false;
114127
auto repo = repository_wrapper::open(directory);
115-
116128
auto state = repo.state();
117-
if (state != GIT_REPOSITORY_STATE_NONE)
129+
index_wrapper index = repo.make_index();
130+
stream_colour_fn yellow = termcolor::yellow;
131+
132+
if (state == GIT_REPOSITORY_STATE_MERGE)
133+
{
134+
if (m_abort)
135+
{
136+
// git merge --abort is equivalent to git reset --merge when MERGE_HEAD is present
137+
// unless MERGE_AUTOSTASH is also present in which case git merge --abort applies
138+
// the stash entry to the worktree whereas git reset --merge will save the stashed
139+
// changes in the stash list.
140+
141+
if (m_quit | m_continue)
142+
{
143+
std::cout << "fatal: --abort expects no arguments" << std::endl; // TODO: add the help info
144+
return;
145+
}
146+
147+
std::cout << "Warning: 'merge --abort' is not implemented yet. A 'reset --hard HEAD' will be executed." << std::endl;
148+
std::cout << "Do you want to continue [y/N] ?" << std::endl;
149+
std::string answer;
150+
std::cin >> answer;
151+
if (answer == "y")
152+
{
153+
repo.state_cleanup();
154+
index.conflict_cleanup();
155+
156+
git_checkout_options options;
157+
git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION);
158+
auto head_ref = repo.head();
159+
repo.reset(head_ref.peel<object_wrapper>(), GIT_RESET_HARD, options);
160+
}
161+
else
162+
{
163+
std::cout << "Abort." << std::endl; // maybe another message would be more clear?
164+
}
165+
return;
166+
}
167+
else if (m_quit)
168+
{
169+
// Forget about the current merge in progress. Leave the index and the working tree as-is.
170+
// If MERGE_AUTOSTASH is present, the stash entry will be saved to the stash list.
171+
//
172+
173+
// if (m_continue)
174+
// {
175+
// std::cout << "fatal: --abort expects no arguments" << std::endl; // TODO: add the help info
176+
// return;
177+
// }
178+
179+
// problem: can't do a reset if the state is not cleaned up, but it shouldn't be.
180+
// Idem for the index and the conflicts.
181+
182+
// repo.state_cleanup();
183+
// index.conflict_cleanup();
184+
185+
// git_checkout_options options;
186+
// git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION);
187+
// auto head_ref = repo.head();
188+
// repo.reset(head_ref.peel<object_wrapper>(), GIT_RESET_SOFT, options);
189+
190+
std::cout << "merge --quit is not implemented yet." << std::endl;
191+
return;
192+
}
193+
else if (m_continue)
194+
{
195+
auto sl = status_list_wrapper::status_list(repo);
196+
if (!sl.has_unmerged_header())
197+
{
198+
// std::string commit_message = "Merge branch "; // how to get the name of the branch the merge was started on ?
199+
// auto author_committer_signatures = signature_wrapper::get_default_signature_from_env(repo);
200+
// repo.create_commit(author_committer_signatures, commit_message, std::nullopt);
201+
202+
std::vector<git_oid> oid_list;
203+
git_repository_mergehead_foreach(repo, populate_list, &oid_list);
204+
205+
annotated_commit_list_wrapper commits_to_merge = resolve_mergeheads(repo, oid_list);
206+
size_t num_commits_to_merge = commits_to_merge.size();
207+
208+
std::vector<std::string> branches_to_merge_names;
209+
for (auto id:oid_list)
210+
{
211+
git_reference_iterator* iter;
212+
git_reference_iterator_new(&iter, repo);
213+
git_reference* ref;
214+
git_reference_next(&ref, iter);
215+
if (git_oid_equal(git_reference_target(ref), &id))
216+
{
217+
auto name = git_reference_name(ref);
218+
branches_to_merge_names.push_back(name);
219+
}
220+
git_reference_free(ref);
221+
}
222+
223+
create_merge_commit(repo, index, branches_to_merge_names, commits_to_merge, num_commits_to_merge);
224+
std::cout << "Merge made" << std::endl; // TODO: change the outpout to something like this: 3c22161 (HEAD -> master) Merge branch 'foregone'
225+
226+
repo.state_cleanup();
227+
index.conflict_cleanup();
228+
return;
229+
}
230+
else
231+
{
232+
auto entry_status = get_status_msg(GIT_STATUS_CONFLICTED).short_mod;
233+
const auto& entry_list = sl.get_entry_list(GIT_STATUS_CONFLICTED);
234+
for (auto* entry : entry_list)
235+
{
236+
git_diff_delta* diff_delta = entry->head_to_index; //ou entry->index_to_workdir ???
237+
const char* old_path = diff_delta->old_file.path;
238+
std::cout << entry_status << "\t" << old_path << std::endl;
239+
}
240+
std::cout << "error: Committing is not possible because you have unmerged files." << std::endl;
241+
}
242+
}
243+
else
244+
{
245+
std::cout << "error: Merging is not possible because you have unmerged files." << std::endl;
246+
}
247+
std::cout << yellow << "hint: Fix them up in the work tree, and then use 'git add/rm <file>'" << std::endl;
248+
std::cout << "hint: as appropriate to mark resolution and make a commit." << termcolor::reset << std::endl;
249+
std::cout << "fatal: Exiting because of an unresolved conflict." << std::endl;
250+
return;
251+
}
252+
else
118253
{
254+
if (m_abort)
255+
{
256+
std::cout << "fatal: There is no merge to abort (MERGE_HEAD missing)." << std::endl;
257+
return;
258+
}
259+
if (m_continue)
260+
{
261+
std::cout << "fatal: There is no merge in progress (MERGE_HEAD missing)." << std::endl;
262+
return;
263+
}
264+
}
265+
266+
if (state != GIT_REPOSITORY_STATE_NONE) // Could this be a "else if before the "else" above ?
267+
{
119268
std::cout << "repository is in unexpected state " << state <<std::endl;
120269
}
121270

@@ -173,17 +322,13 @@ void merge_subcommand::run()
173322
&checkout_opts));
174323
}
175324

176-
index_wrapper index = repo.make_index();
177-
178-
if (git_index_has_conflicts(index))
325+
if (index.has_conflict())
179326
{
180-
std::cout << "Conflict. To be implemented" << std::endl;
181-
/* Handle conflicts */
182-
// output_conflicts(index);
327+
index.output_conflicts();
183328
}
184329
else if (!m_no_commit)
185330
{
186-
create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge);
187-
printf("Merge made\n");
331+
create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge);
332+
std::cout << "Merge made" << std::endl;
188333
}
189334
}

src/subcommand/merge_subcommand.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ class merge_subcommand
2020
// bool m_no_ff = false;
2121
// bool m_commit = false;
2222
bool m_no_commit = false;
23+
bool m_abort = false;
24+
bool m_quit = false;
25+
bool m_continue = false;
2326
};

src/subcommand/status_subcommand.cpp

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,14 @@ status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app)
2626
sub->callback([this]() { this->run(); });
2727
};
2828

29-
const std::string untracked_header = "Untracked files:\n";
30-
// "Untracked files:\n (use \"git add <file>...\" to include in what will be committed)";
31-
const std::string tobecommited_header = "Changes to be committed:\n";
32-
// "Changes to be committed:\n (use \"git reset HEAD <file>...\" to unstage)";
33-
const std::string ignored_header = "Ignored files:\n";
34-
// "Ignored files:\n (use \"git add -f <file>...\" to include in what will be committed)"
29+
const std::string untracked_header = "Untracked files:\n (use \"git add <file>...\" to include in what will be committed)\n";
30+
const std::string tobecommited_header = "Changes to be committed:\n (use \"git reset HEAD <file>...\" to unstage)\n";
31+
const std::string ignored_header = "Ignored files:\n (use \"git add -f <file>...\" to include in what will be committed)\n";
3532
const std::string notstagged_header = "Changes not staged for commit:\n";
36-
// "Changes not staged for commit:\n (use \"git add%s <file>...\" to update what will be committed)\n (use \"git checkout -- <file>...\" to discard changes in working directory)"
37-
const std::string nothingtocommit_message = "No changes added to commit";
38-
// "No changes added to commit (use \"git add\" and/or \"git commit -a\")"
39-
40-
struct status_messages
41-
{
42-
std::string short_mod;
43-
std::string long_mod;
44-
};
45-
46-
const std::map<git_status_t, status_messages> status_msg_map = //TODO : check spaces in short_mod
47-
{
48-
{ GIT_STATUS_CURRENT, {"", ""} },
49-
{ GIT_STATUS_INDEX_NEW, {"A ", "\tnew file:"} },
50-
{ GIT_STATUS_INDEX_MODIFIED, {"M ", "\tmodified:"} },
51-
{ GIT_STATUS_INDEX_DELETED, {"D ", "\tdeleted:"} },
52-
{ GIT_STATUS_INDEX_RENAMED, {"R ", "\trenamed:"} },
53-
{ GIT_STATUS_INDEX_TYPECHANGE, {"T ", "\ttypechange:"} },
54-
{ GIT_STATUS_WT_NEW, {"?? ", ""} },
55-
{ GIT_STATUS_WT_MODIFIED, {" M " , "\tmodified:"} },
56-
{ GIT_STATUS_WT_DELETED, {" D ", "\tdeleted:"} },
57-
{ GIT_STATUS_WT_TYPECHANGE, {" T ", "\ttypechange:"} },
58-
{ GIT_STATUS_WT_RENAMED, {" R ", "\trenamed:"} },
59-
{ GIT_STATUS_WT_UNREADABLE, {"", ""} },
60-
{ GIT_STATUS_IGNORED, {"!! ", ""} },
61-
{ GIT_STATUS_CONFLICTED, {"", ""} },
62-
};
33+
// "Changes not staged for commit:\n (use \"git add%s <file>...\" to update what will be committed)\n (use \"git checkout -- <file>...\" to discard changes in working directory)\n"
34+
const std::string unmerged_header = "Unmerged paths:\n (use \"git add <file>...\" to mark resolution)\n";
35+
// const std::string nothingtocommit_message = "No changes added to commit (use \"git add\" and/or \"git commit -a\")";
36+
const std::string treeclean_message = "Nothing to commit, working tree clean";
6337

6438
enum class output_format
6539
{
@@ -79,11 +53,11 @@ std::string get_print_status(git_status_t status, output_format of)
7953
std::string entry_status;
8054
if ((of == output_format::DEFAULT) || (of == output_format::LONG))
8155
{
82-
entry_status = status_msg_map.at(status).long_mod + " ";
56+
entry_status = get_status_msg(status).long_mod;
8357
}
8458
else if (of == output_format::SHORT)
8559
{
86-
entry_status = status_msg_map.at(status).short_mod;
60+
entry_status = get_status_msg(status).short_mod;
8761
}
8862
return entry_status;
8963
}
@@ -218,7 +192,12 @@ void status_subcommand::run()
218192
is_long = ((of == output_format::DEFAULT) || (of == output_format::LONG));
219193
if (is_long)
220194
{
221-
std::cout << "On branch " << branch_name << std::endl;
195+
std::cout << "On branch " << branch_name << "\n" << std::endl;
196+
197+
if (sl.has_unmerged_header())
198+
{
199+
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;
200+
}
222201
}
223202
else
224203
{
@@ -263,6 +242,21 @@ void status_subcommand::run()
263242
}
264243
}
265244

245+
// TODO: check if should be printed before "not stagged" files
246+
if (sl.has_unmerged_header())
247+
{
248+
stream_colour_fn colour = termcolor::red;
249+
if (is_long)
250+
{
251+
std::cout << unmerged_header;
252+
}
253+
print_not_tracked(get_entries_to_print(GIT_STATUS_CONFLICTED, sl, false, of), tracked_dir_set, untracked_dir_set, is_long, colour);
254+
if (is_long)
255+
{
256+
std::cout << std::endl;
257+
}
258+
}
259+
266260
if (sl.has_untracked_header())
267261
{
268262
stream_colour_fn colour = termcolor::red;
@@ -277,6 +271,12 @@ void status_subcommand::run()
277271
}
278272
}
279273

274+
// TODO: check if this message should be displayed even if there are untracked files
275+
if (!(sl.has_tobecommited_header() | sl.has_notstagged_header() | sl.has_unmerged_header() | sl.has_untracked_header()))
276+
{
277+
std::cout << treeclean_message << std::endl;
278+
}
279+
280280
// if (sl.has_ignored_header())
281281
// {
282282
// stream_colour_fn colour = termcolor::red;

0 commit comments

Comments
 (0)