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
811merge_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. \n Note 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.\n If 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.\n 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." );
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,7 +39,23 @@ annotated_commit_list_wrapper merge_subcommand::resolve_heads(const repository_w
3339 return annotated_commit_list_wrapper (std::move (commits_to_merge));
3440}
3541
36- void perform_fastforward (repository_wrapper& repo, const git_oid target_oid, int is_unborn)
42+ annotated_commit_list_wrapper resolve_mergeheads (const repository_wrapper& repo, const 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+
58+ 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;
3961
@@ -60,12 +82,13 @@ void perform_fastforward(repository_wrapper& repo, const git_oid target_oid, int
6082void merge_subcommand::create_merge_commit (
6183 repository_wrapper& repo,
6284 const index_wrapper& index,
85+ const std::vector<std::string>& branches_to_merge,
6386 const annotated_commit_list_wrapper& commits_to_merge,
6487 size_t num_commits_to_merge)
6588{
6689 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 ();
90+ auto merge_ref = repo.find_reference_dwim (branches_to_merge .front ());
91+ auto merge_commit = repo.resolve_local_ref (branches_to_merge .front ()).value ();
6992
7093 std::vector<commit_wrapper> parents_list;
7194 parents_list.reserve (num_commits_to_merge + 1 );
@@ -85,6 +108,7 @@ void merge_subcommand::create_merge_commit(
85108
86109 // TODO: add a prompt to edit the merge message
87110 std::string msg_target = merge_ref ? merge_ref->short_name () : git_oid_tostr_s (&(merge_commit.oid ()));
111+ msg_target = " \' " + msg_target + " \' " ;
88112 std::string msg = merge_ref ? " Merge branch " : " Merge commit " ;
89113 msg.append (msg_target);
90114
@@ -93,15 +117,159 @@ void merge_subcommand::create_merge_commit(
93117 repo.state_cleanup ();
94118}
95119
120+ // This function is used as a callback in git_repository_mergehead_foreach and therefore its type must be git_repository_mergehead_foreach_cb.
121+ int populate_list (const git_oid* oid, void * payload)
122+ {
123+ auto * l = reinterpret_cast <std::vector<git_oid>*>(payload);
124+ l->push_back (*oid);
125+ return 0 ;
126+ }
127+
96128void merge_subcommand::run ()
97129{
98130 auto directory = get_current_git_path ();
99131 auto bare = false ;
100132 auto repo = repository_wrapper::open (directory);
101-
102133 auto state = repo.state ();
103- if (state != GIT_REPOSITORY_STATE_NONE)
134+ index_wrapper index = repo.make_index ();
135+ stream_colour_fn yellow = termcolor::yellow;
136+
137+ if (state == GIT_REPOSITORY_STATE_MERGE)
104138 {
139+ if (m_abort)
140+ {
141+ // git merge --abort is equivalent to git reset --merge when MERGE_HEAD is present
142+ // unless MERGE_AUTOSTASH is also present in which case git merge --abort applies
143+ // the stash entry to the worktree whereas git reset --merge will save the stashed
144+ // changes in the stash list.
145+
146+ if (m_quit | m_continue)
147+ {
148+ std::cout << " fatal: --abort expects no arguments" << std::endl; // TODO: add the help info
149+ return ;
150+ }
151+
152+ std::cout << " Warning: 'merge --abort' is not implemented yet. A 'reset --hard HEAD' will be executed." << std::endl;
153+ std::cout << " Do you want to continue [y/N] ?" << std::endl;
154+ std::string answer;
155+ std::cin >> answer;
156+ if (answer == " y" )
157+ {
158+ repo.state_cleanup ();
159+ index.conflict_cleanup ();
160+
161+ git_checkout_options options;
162+ git_checkout_options_init (&options, GIT_CHECKOUT_OPTIONS_VERSION);
163+ auto head_ref = repo.head ();
164+ repo.reset (head_ref.peel <object_wrapper>(), GIT_RESET_HARD, options);
165+ }
166+ else
167+ {
168+ std::cout << " Abort." << std::endl; // maybe another message would be more clear?
169+ }
170+ return ;
171+ }
172+ else if (m_quit)
173+ {
174+ // Forget about the current merge in progress. Leave the index and the working tree as-is.
175+ // If MERGE_AUTOSTASH is present, the stash entry will be saved to the stash list.
176+ //
177+
178+ // if (m_continue)
179+ // {
180+ // std::cout << "fatal: --abort expects no arguments" << std::endl; // TODO: add the help info
181+ // return;
182+ // }
183+
184+ // problem: can't do a reset if the state is not cleaned up, but it shouldn't be.
185+ // Idem for the index and the conflicts.
186+
187+ // repo.state_cleanup();
188+ // index.conflict_cleanup();
189+
190+ // git_checkout_options options;
191+ // git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION);
192+ // auto head_ref = repo.head();
193+ // repo.reset(head_ref.peel<object_wrapper>(), GIT_RESET_SOFT, options);
194+
195+ std::cout << " merge --quit is not implemented yet." << std::endl;
196+ return ;
197+ }
198+ else if (m_continue)
199+ {
200+ auto sl = status_list_wrapper::status_list (repo);
201+ if (!sl.has_unmerged_header ())
202+ {
203+ // std::string commit_message = "Merge branch "; // how to get the name of the branch the merge was started on ?
204+ // auto author_committer_signatures = signature_wrapper::get_default_signature_from_env(repo);
205+ // repo.create_commit(author_committer_signatures, commit_message, std::nullopt);
206+
207+ std::vector<git_oid> oid_list;
208+ git_repository_mergehead_foreach (repo, populate_list, &oid_list);
209+
210+ annotated_commit_list_wrapper commits_to_merge = resolve_mergeheads (repo, oid_list);
211+ size_t num_commits_to_merge = commits_to_merge.size ();
212+
213+ std::vector<std::string> branches_to_merge_names;
214+ for (const auto & id:oid_list)
215+ {
216+ git_reference_iterator* iter;
217+ git_reference_iterator_new (&iter, repo);
218+ git_reference* ref;
219+ git_reference_next (&ref, iter);
220+ if (git_oid_equal (git_reference_target (ref), &id))
221+ {
222+ auto name = git_reference_name (ref);
223+ branches_to_merge_names.push_back (name);
224+ }
225+ git_reference_free (ref);
226+ }
227+
228+ create_merge_commit (repo, index, branches_to_merge_names, commits_to_merge, num_commits_to_merge);
229+ std::cout << " Merge made" << std::endl; // TODO: change the outpout to something like this: 3c22161 (HEAD -> master) Merge branch 'foregone'
230+
231+ repo.state_cleanup ();
232+ index.conflict_cleanup ();
233+ return ;
234+ }
235+ else
236+ {
237+ auto entry_status = get_status_msg (GIT_STATUS_CONFLICTED).short_mod ;
238+ const auto & entry_list = sl.get_entry_list (GIT_STATUS_CONFLICTED);
239+ for (auto * entry : entry_list)
240+ {
241+ git_diff_delta* diff_delta = entry->head_to_index ; // ou entry->index_to_workdir ???
242+ const char * old_path = diff_delta->old_file .path ;
243+ std::cout << entry_status << " \t " << old_path << std::endl;
244+ }
245+ std::cout << " error: Committing is not possible because you have unmerged files." << std::endl;
246+ }
247+ }
248+ else
249+ {
250+ std::cout << " error: Merging is not possible because you have unmerged files." << std::endl;
251+ }
252+ std::cout << yellow << " hint: Fix them up in the work tree, and then use 'git add/rm <file>'" << std::endl;
253+ std::cout << " hint: as appropriate to mark resolution and make a commit." << termcolor::reset << std::endl;
254+ std::cout << " fatal: Exiting because of an unresolved conflict." << std::endl;
255+ return ;
256+ }
257+ else
258+ {
259+ if (m_abort)
260+ {
261+ std::cout << " fatal: There is no merge to abort (MERGE_HEAD missing)." << std::endl;
262+ return ;
263+ }
264+ if (m_continue)
265+ {
266+ std::cout << " fatal: There is no merge in progress (MERGE_HEAD missing)." << std::endl;
267+ return ;
268+ }
269+ }
270+
271+ if (state != GIT_REPOSITORY_STATE_NONE) // Could this be a "else if before the "else" above ?
272+ {
105273 std::cout << " repository is in unexpected state " << state <<std::endl;
106274 }
107275
@@ -159,17 +327,13 @@ void merge_subcommand::run()
159327 &checkout_opts));
160328 }
161329
162- index_wrapper index = repo.make_index ();
163-
164- if (git_index_has_conflicts (index))
330+ if (index.has_conflict ())
165331 {
166- std::cout << " Conflict. To be implemented" << std::endl;
167- /* Handle conflicts */
168- // output_conflicts(index);
332+ index.output_conflicts ();
169333 }
170334 else if (!m_no_commit)
171335 {
172- create_merge_commit (repo, index, commits_to_merge, num_commits_to_merge);
173- printf ( " Merge made\n " ) ;
336+ create_merge_commit (repo, index, m_branches_to_merge , commits_to_merge, num_commits_to_merge);
337+ std::cout << " Merge made" << std::endl ;
174338 }
175339}
0 commit comments