1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2015 - Jesse van den Kieboom
5 *
6 * gitg is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * gitg is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with gitg. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20namespace Gitg
21{
22
23public class ActionSupport : Object
24{
25	// Do this to pull in config.h before glib.h (for gettext...)
26	private const string version = Gitg.Config.VERSION;
27
28	public GitgExt.Application? application { owned get; construct set; }
29	public GitgExt.RefActionInterface action_interface { owned get; construct set; }
30
31	public ActionSupport(GitgExt.Application application, GitgExt.RefActionInterface action_interface)
32	{
33		Object(application: application, action_interface: action_interface);
34	}
35
36	public async bool working_directory_dirty()
37	{
38		var options = new Ggit.StatusOptions(Ggit.StatusOption.EXCLUDE_SUBMODULES,
39		                                     Ggit.StatusShow.WORKDIR_ONLY,
40		                                     null);
41		var is_dirty = false;
42
43		yield Async.thread_try(() => {
44			application.repository.file_status_foreach(options, (path, flags) => {
45				is_dirty = true;
46				return -1;
47			});
48		});
49
50		return is_dirty;
51	}
52
53	public async bool save_stash(SimpleNotification notification, Gitg.Ref? head)
54	{
55		var committer = application.get_verified_committer();
56
57		if (committer == null)
58		{
59			return false;
60		}
61
62		try
63		{
64			yield Async.thread(() => {
65				// Try to stash changes
66				string message;
67
68				if (head != null)
69				{
70					var headname = head.parsed_name.shortname;
71
72					try
73					{
74						var head_commit = head.resolve().lookup() as Ggit.Commit;
75						var shortid = head_commit.get_id().to_string()[0:6];
76						var subject = head_commit.get_subject();
77
78						message = @"WIP on $(headname): $(shortid) $(subject)";
79					}
80					catch
81					{
82						message = @"WIP on $(headname)";
83					}
84				}
85				else
86				{
87					message = "WIP on HEAD";
88				}
89
90				application.repository.save_stash(committer, message, Ggit.StashFlags.DEFAULT);
91			});
92		}
93		catch (Error err)
94		{
95			notification.error(_("Failed to stash changes: %s").printf(err.message));
96			return false;
97		}
98
99		return true;
100	}
101
102	public bool reference_is_head(Gitg.Ref reference, ref Gitg.Ref? head)
103	{
104		var branch = reference as Ggit.Branch;
105		head = null;
106
107		if (branch == null)
108		{
109			return false;
110		}
111
112		try
113		{
114			if (!branch.is_head())
115			{
116				return false;
117			}
118
119			head = application.repository.lookup_reference("HEAD");
120		} catch {}
121
122		return head != null;
123	}
124
125	public async bool stash_if_needed(SimpleNotification notification, Gitg.Ref head)
126	{
127		// Offer to stash if there are any local changes
128		if ((yield working_directory_dirty()))
129		{
130			var q = new GitgExt.UserQuery.full(_("Unstaged changes"),
131			                                   _("You appear to have unstaged changes in your working directory. Would you like to stash the changes before the checkout?"),
132			                                   Gtk.MessageType.QUESTION,
133			                                   _("Cancel"), Gtk.ResponseType.CANCEL,
134			                                   _("Stash changes"), Gtk.ResponseType.OK);
135
136			if ((yield application.user_query_async(q)) != Gtk.ResponseType.OK)
137			{
138				notification.error(_("Failed with conflicts"));
139				return false;
140			}
141
142			if (!(yield save_stash(notification, head)))
143			{
144				return false;
145			}
146		}
147
148		return true;
149	}
150
151	public async bool checkout_conflicts(SimpleNotification notification, Gitg.Ref reference, Ggit.Index index, Gitg.Ref? head)
152	{
153		if (!(yield stash_if_needed(notification, head)))
154		{
155			return false;
156		}
157
158		if (head == null)
159		{
160			// Perform checkout of the local branch first
161			var checkout = new RefActionCheckout(application, action_interface, reference);
162
163			if (!(yield checkout.checkout()))
164			{
165				notification.error(_("Failed with conflicts"));
166				return false;
167			}
168		}
169
170		// Finally, checkout the conflicted index
171		try
172		{
173			yield Async.thread(() => {
174				var opts = new Ggit.CheckoutOptions();
175				opts.set_strategy(Ggit.CheckoutStrategy.SAFE);
176				application.repository.checkout_index(index, opts);
177			});
178		}
179		catch (Error err)
180		{
181			notification.error(_("Failed to checkout conflicts: %s").printf(err.message));
182			return false;
183		}
184
185		return true;
186	}
187
188	public async Ggit.OId? commit_index(SimpleNotification notification,
189	                                    Gitg.Ref           reference,
190	                                    Ggit.Index         index,
191	                                    owned Ggit.OId[]?  parents,
192	                                    Ggit.Signature?    author,
193	                                    string             message)
194	{
195		var committer = application.get_verified_committer();
196
197		if (committer == null)
198		{
199			notification.error(_("Failed to obtain author details"));
200			return null;
201		}
202
203		if (author == null)
204		{
205			author = committer;
206		}
207
208		var stage = application.repository.stage;
209
210		Gitg.Ref? head = null;
211		var ishead = reference_is_head(reference, ref head);
212
213		Ggit.OId? oid = null;
214		Ggit.Tree? head_tree = null;
215		Gitg.Commit? commit = null;
216
217		try
218		{
219			commit = reference.lookup() as Gitg.Commit;
220		}
221		catch (Error e)
222		{
223			notification.error(_("Failed to lookup commit: %s").printf(e.message));
224			return null;
225		}
226
227		if (ishead)
228		{
229			if (!(yield stash_if_needed(notification, head)))
230			{
231				return null;
232			}
233
234			head_tree = commit.get_tree();
235		}
236
237		if (parents == null)
238		{
239			parents = new Ggit.OId[] { commit.get_id() };
240		}
241
242		try
243		{
244			// TODO: not all hooks are being executed yet
245			oid = yield stage.commit_index(index,
246			                               ishead ? head : reference,
247			                               message,
248			                               author,
249			                               committer,
250			                               parents,
251			                               StageCommitOptions.NONE);
252		}
253		catch (Error e)
254		{
255			notification.error(_("Failed to create commit: %s").printf(e.message));
256			return null;
257		}
258
259		if (ishead)
260		{
261			try
262			{
263				yield Async.thread(() => {
264					var opts = new Ggit.CheckoutOptions();
265
266					opts.set_strategy(Ggit.CheckoutStrategy.SAFE);
267					opts.set_baseline(head_tree);
268
269					var newcommit = application.repository.lookup<Ggit.Commit>(oid);
270					var newtree = newcommit.get_tree();
271
272					application.repository.checkout_tree(newtree, opts);
273				});
274			}
275			catch (Error e)
276			{
277				notification.error(_("Failed to checkout index: %s").printf(e.message));
278				return null;
279			}
280		}
281
282		return oid;
283	}
284}
285
286}
287
288// ex:set ts=4 noet
289