Skip to content

Commit 31972c4

Browse files
committed
Merge conflicts
1 parent 9e6938b commit 31972c4

File tree

10 files changed

+445
-69
lines changed

10 files changed

+445
-69
lines changed

src/subcommand/merge_subcommand.cpp

Lines changed: 175 additions & 13 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;
@@ -85,23 +107,167 @@ void merge_subcommand::create_merge_commit(
85107

86108
// TODO: add a prompt to edit the merge message
87109
std::string msg_target = merge_ref ? merge_ref->short_name() : git_oid_tostr_s(&(merge_commit.oid()));
110+
msg_target = "\'" + msg_target + "\'";
88111
std::string msg = merge_ref ? "Merge branch " : "Merge commit ";
89-
msg.append(msg_target);
112+
msg.append( msg_target);
90113

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

93116
repo.state_cleanup();
94117
}
95118

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

@@ -159,17 +325,13 @@ void merge_subcommand::run()
159325
&checkout_opts));
160326
}
161327

162-
index_wrapper index = repo.make_index();
163-
164-
if (git_index_has_conflicts(index))
328+
if (index.has_conflict())
165329
{
166-
std::cout << "Conflict. To be implemented" << std::endl;
167-
/* Handle conflicts */
168-
// output_conflicts(index);
330+
index.output_conflicts();
169331
}
170332
else if (!m_no_commit)
171333
{
172-
create_merge_commit(repo, index, commits_to_merge, num_commits_to_merge);
173-
printf("Merge made\n");
334+
create_merge_commit(repo, index, m_branches_to_merge, commits_to_merge, num_commits_to_merge);
335+
std::cout << "Merge made" << std::endl;
174336
}
175337
}

src/subcommand/merge_subcommand.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ class merge_subcommand
2525
// bool m_no_ff = false;
2626
// bool m_commit = false;
2727
bool m_no_commit = false;
28+
bool m_abort = false;
29+
bool m_quit = false;
30+
bool m_continue = false;
2831
};

src/subcommand/status_subcommand.cpp

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,41 +27,13 @@ status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app)
2727
};
2828

2929
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";
31-
// (use \"git restore --staged <file>...\" to unstage)\n
32-
// (use \"git reset HEAD <file>...\" to unstage)\n";
33-
// const std::string ignored_header = "Ignored files:\n (use \"git add -f <file>...\" to include in what will be committed)\n";
34-
const std::string notstagged_header = "Changes not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n";
35-
// (use \"git restore <file>...\" to discard changes in working directory)\n
36-
// (use \"git checkout -- <file>...\" to discard changes in working directory)\n"
37-
const std::string nothingtocommit_msg = "No changes added to commit (use \"git add\" and/or \"git commit -a\")";
38-
const std::string uptodate_msg = "Nothing to commit, working tree clean.";
39-
const std::string nothingtocommit_untrackedfiles_msg = "Nothing added to commit but untracked files present (use \"git add\" to track)";
40-
// no changes added to commit (use "git add" and/or "git commit -a")
41-
42-
struct status_messages
43-
{
44-
std::string short_mod;
45-
std::string long_mod;
46-
};
47-
48-
const std::map<git_status_t, status_messages> status_msg_map = //TODO : check spaces in short_mod
49-
{
50-
{ GIT_STATUS_CURRENT, {"", ""} },
51-
{ GIT_STATUS_INDEX_NEW, {"A ", "\tnew file:"} },
52-
{ GIT_STATUS_INDEX_MODIFIED, {"M ", "\tmodified:"} },
53-
{ GIT_STATUS_INDEX_DELETED, {"D ", "\tdeleted:"} },
54-
{ GIT_STATUS_INDEX_RENAMED, {"R ", "\trenamed:"} },
55-
{ GIT_STATUS_INDEX_TYPECHANGE, {"T ", "\ttypechange:"} },
56-
{ GIT_STATUS_WT_NEW, {"?? ", " "} },
57-
{ GIT_STATUS_WT_MODIFIED, {" M " , "\tmodified:"} },
58-
{ GIT_STATUS_WT_DELETED, {" D ", "\tdeleted:"} },
59-
{ GIT_STATUS_WT_TYPECHANGE, {" T ", "\ttypechange:"} },
60-
{ GIT_STATUS_WT_RENAMED, {" R ", "\trenamed:"} },
61-
{ GIT_STATUS_WT_UNREADABLE, {"", ""} },
62-
{ GIT_STATUS_IGNORED, {"!! ", ""} },
63-
{ GIT_STATUS_CONFLICTED, {"", ""} },
64-
};
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";
32+
const std::string notstagged_header = "Changes not staged for commit:\n";
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";
6537

6638
enum class output_format
6739
{
@@ -81,18 +53,11 @@ std::string get_print_status(git_status_t status, output_format of)
8153
std::string entry_status;
8254
if ((of == output_format::DEFAULT) || (of == output_format::LONG))
8355
{
84-
if (status == GIT_STATUS_WT_NEW)
85-
{
86-
entry_status = status_msg_map.at(status).long_mod + "\t";
87-
}
88-
else
89-
{
90-
entry_status = status_msg_map.at(status).long_mod + " ";
91-
}
56+
entry_status = get_status_msg(status).long_mod;
9257
}
9358
else if (of == output_format::SHORT)
9459
{
95-
entry_status = status_msg_map.at(status).short_mod;
60+
entry_status = get_status_msg(status).short_mod;
9661
}
9762
return entry_status;
9863
}
@@ -228,7 +193,12 @@ void status_subcommand::run()
228193
is_long = ((of == output_format::DEFAULT) || (of == output_format::LONG));
229194
if (is_long)
230195
{
231-
std::cout << "On branch " << branch_name << std::endl;
196+
std::cout << "On branch " << branch_name << "\n" << std::endl;
197+
198+
if (sl.has_unmerged_header())
199+
{
200+
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;
201+
}
232202
}
233203
else
234204
{
@@ -273,6 +243,21 @@ void status_subcommand::run()
273243
}
274244
}
275245

246+
// TODO: check if should be printed before "not stagged" files
247+
if (sl.has_unmerged_header())
248+
{
249+
stream_colour_fn colour = termcolor::red;
250+
if (is_long)
251+
{
252+
std::cout << unmerged_header;
253+
}
254+
print_not_tracked(get_entries_to_print(GIT_STATUS_CONFLICTED, sl, false, of), tracked_dir_set, untracked_dir_set, is_long, colour);
255+
if (is_long)
256+
{
257+
std::cout << std::endl;
258+
}
259+
}
260+
276261
if (sl.has_untracked_header())
277262
{
278263
stream_colour_fn colour = termcolor::red;
@@ -287,6 +272,12 @@ void status_subcommand::run()
287272
}
288273
}
289274

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

0 commit comments

Comments
 (0)