1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2014 - 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 enum RemoteState
24{
25	DISCONNECTED,
26	CONNECTING,
27	CONNECTED,
28	TRANSFERRING
29}
30
31public errordomain RemoteError
32{
33	ALREADY_CONNECTED,
34	ALREADY_CONNECTING,
35	ALREADY_DISCONNECTED,
36	STILL_CONNECTING
37}
38
39public interface CredentialsProvider : Object
40{
41	public abstract Ggit.Cred? credentials(string url, string? username_from_url, Ggit.Credtype allowed_types) throws Error;
42}
43
44public class Remote : Ggit.Remote
45{
46	private class Callbacks : Ggit.RemoteCallbacks
47	{
48		private Remote d_remote;
49		private Ggit.RemoteCallbacks? d_proxy;
50
51		public delegate void TransferProgress(Ggit.TransferProgress stats);
52		private TransferProgress? d_transfer_progress;
53
54		public Callbacks(Remote remote, Ggit.RemoteCallbacks? proxy, owned TransferProgress? transfer_progress)
55		{
56			d_remote = remote;
57			d_proxy = proxy;
58			d_transfer_progress = (owned)transfer_progress;
59		}
60
61		protected override void progress(string message)
62		{
63			if (d_proxy != null)
64			{
65				d_proxy.progress(message);
66			}
67		}
68
69		protected override void transfer_progress(Ggit.TransferProgress stats)
70		{
71			if (d_transfer_progress != null)
72			{
73				d_transfer_progress(stats);
74			}
75
76			if (d_proxy != null)
77			{
78				d_proxy.transfer_progress(stats);
79			}
80		}
81
82		protected override void update_tips(string refname, Ggit.OId a, Ggit.OId b)
83		{
84			d_remote.tip_updated(refname, a, b);
85
86			if (d_proxy != null)
87			{
88				d_proxy.update_tips(refname, a, b);
89			}
90		}
91
92		protected override void completion(Ggit.RemoteCompletionType type)
93		{
94			if (d_proxy != null)
95			{
96				d_proxy.completion(type);
97			}
98		}
99
100		protected override Ggit.Cred? credentials(string url, string? username_from_url, Ggit.Credtype allowed_types) throws Error
101		{
102			Ggit.Cred? ret = null;
103
104			var provider = d_remote.credentials_provider;
105
106			if (provider != null)
107			{
108				ret = provider.credentials(url, username_from_url, allowed_types);
109			}
110
111			if (ret == null && d_proxy != null)
112			{
113				ret = d_proxy.credentials(url, username_from_url, allowed_types);
114			}
115
116			return ret;
117		}
118	}
119
120	private RemoteState d_state;
121	private string[]? d_fetch_specs;
122	private string[]? d_push_specs;
123	private uint d_reset_transfer_progress_timeout;
124	private double d_transfer_progress;
125
126	private Callbacks? d_callbacks;
127
128	public signal void tip_updated(string refname, Ggit.OId a, Ggit.OId b);
129
130	public override void dispose()
131	{
132		if (d_reset_transfer_progress_timeout != 0)
133		{
134			Source.remove(d_reset_transfer_progress_timeout);
135			d_reset_transfer_progress_timeout = 0;
136		}
137
138		base.dispose();
139	}
140
141	public double transfer_progress
142	{
143		get { return d_transfer_progress; }
144	}
145
146	public RemoteState state
147	{
148		get { return d_state; }
149		private set
150		{
151			if (d_state != value)
152			{
153				d_state = value;
154				notify_property("state");
155			}
156		}
157	}
158
159	private void do_reset_transfer_progress()
160	{
161		d_reset_transfer_progress_timeout = 0;
162		d_transfer_progress = 0.0;
163		notify_property("transfer-progress");
164	}
165
166	private void reset_transfer_progress(bool with_delay)
167	{
168		if (d_transfer_progress == 0)
169		{
170			return;
171		}
172
173		if (with_delay)
174		{
175			d_reset_transfer_progress_timeout = Timeout.add(500, () => {
176				do_reset_transfer_progress();
177				return false;
178			});
179		}
180		else if (d_reset_transfer_progress_timeout == 0)
181		{
182			do_reset_transfer_progress();
183		}
184	}
185
186	private void update_transfer_progress(Ggit.TransferProgress stats)
187	{
188		var total = stats.get_total_objects();
189		var received = stats.get_received_objects();
190		var indexed = stats.get_indexed_objects();
191
192		d_transfer_progress = (double)(received + indexed) / (double)(total + total);
193		notify_property("transfer-progress");
194
195		if (received == total && indexed == total)
196		{
197			reset_transfer_progress(true);
198		}
199	}
200
201	private void update_state(bool force_disconnect = false)
202	{
203		if (get_connected())
204		{
205			if (force_disconnect)
206			{
207				disconnect.begin((obj, res) => {
208					try
209					{
210						disconnect.end(res);
211					} catch {}
212				});
213			}
214			else
215			{
216				state = RemoteState.CONNECTED;
217			}
218		}
219		else
220		{
221			state = RemoteState.DISCONNECTED;
222		}
223	}
224
225	public new async void connect(Ggit.Direction direction, Ggit.RemoteCallbacks? callbacks = null) throws Error
226	{
227		if (get_connected())
228		{
229			if (state != RemoteState.CONNECTED)
230			{
231				state = RemoteState.CONNECTED;
232			}
233
234			throw new RemoteError.ALREADY_CONNECTED("already connected");
235		}
236		else if (state == RemoteState.CONNECTING)
237		{
238			throw new RemoteError.ALREADY_CONNECTING("already connecting");
239		}
240		else
241		{
242			reset_transfer_progress(false);
243		}
244
245		state = RemoteState.CONNECTING;
246
247		while (true)
248		{
249			try
250			{
251				d_callbacks = new Callbacks(this, callbacks, update_transfer_progress);
252
253				yield Async.thread(() => {
254					base.connect(direction, d_callbacks, null, null);
255				});
256			}
257			catch (Error e)
258			{
259				d_callbacks = null;
260
261				// NOTE: need to check the message for now in case of failed
262				// http or ssh auth. This is fragile and will likely break
263				// in future libgit2 releases. Please fix!
264				if (e.message == "Unexpected HTTP status code: 401" ||
265				    e.message == "error authenticating: Username/PublicKey combination invalid")
266				{
267					continue;
268				}
269				else
270				{
271					update_state();
272					throw e;
273				}
274			}
275
276			break;
277		}
278
279		update_state();
280	}
281
282	public new async void disconnect() throws Error
283	{
284		if (!get_connected())
285		{
286			if (state != RemoteState.DISCONNECTED)
287			{
288				state = RemoteState.DISCONNECTED;
289			}
290
291			throw new RemoteError.ALREADY_DISCONNECTED("already disconnected");
292		}
293
294		try
295		{
296			yield Async.thread(() => {
297				base.disconnect();
298			});
299		}
300		catch (Error e)
301		{
302			update_state();
303			reset_transfer_progress(true);
304
305			throw e;
306		}
307
308		update_state();
309		reset_transfer_progress(true);
310	}
311
312	private async void push_intern(string branch, Ggit.RemoteCallbacks? callbacks) throws Error
313	{
314		state = RemoteState.TRANSFERRING;
315		reset_transfer_progress(false);
316
317		try
318		{
319			yield Async.thread(() => {
320				var options = new Ggit.PushOptions();
321				if (d_callbacks == null) {
322					d_callbacks = new Callbacks(this, callbacks, update_transfer_progress);
323				}
324
325				options.set_remote_callbacks(d_callbacks);
326
327				string [] push_refs = { "refs/heads/%s:refs/heads/%s".printf(branch, branch) };
328
329				if (!base.push(push_refs, options))
330				  throw new Error(0,0,"push");
331			});
332		}
333		catch (Error e)
334		{
335			reset_transfer_progress(true);
336			throw e;
337		}
338
339		reset_transfer_progress(true);
340	}
341
342	private async void download_intern(string? message, Ggit.RemoteCallbacks? callbacks) throws Error
343	{
344		bool dis = false;
345
346		if (!get_connected())
347		{
348			dis = true;
349			yield connect(Ggit.Direction.FETCH, callbacks);
350		}
351
352		state = RemoteState.TRANSFERRING;
353		reset_transfer_progress(false);
354
355		try
356		{
357			yield Async.thread(() => {
358				var options = new Ggit.FetchOptions();
359				options.set_remote_callbacks(d_callbacks);
360
361				base.download(null, options);
362
363				if (message != null)
364				{
365					base.update_tips(d_callbacks, true, options.get_download_tags(), message);
366				}
367			});
368		}
369		catch (Error e)
370		{
371			update_state(dis);
372			reset_transfer_progress(true);
373			throw e;
374		}
375
376		update_state(dis);
377		reset_transfer_progress(true);
378	}
379
380	public new async void download(Ggit.RemoteCallbacks? callbacks = null) throws Error
381	{
382		yield download_intern(null, callbacks);
383	}
384
385	public new async void push(string branch, Ggit.RemoteCallbacks? callbacks = null) throws Error
386	{
387		yield push_intern(branch, callbacks);
388	}
389
390	public new async void fetch(string? message, Ggit.RemoteCallbacks? callbacks = null) throws Error
391	{
392		var msg = message;
393
394		if (msg == null)
395		{
396			var name = get_name();
397
398			if (name == null)
399			{
400				name = get_url();
401			}
402
403			if (name != null)
404			{
405				msg = "fetch: " + name;
406			}
407			else
408			{
409				msg = "";
410			}
411		}
412
413		yield download_intern(msg, callbacks);
414	}
415
416	public string[]? fetch_specs
417	{
418		owned get
419		{
420			if (d_fetch_specs != null)
421			{
422				return d_fetch_specs;
423			}
424
425			try
426			{
427				return get_fetch_specs();
428			}
429			catch (Error e)
430			{
431				return null;
432			}
433		}
434
435		set
436		{
437			d_fetch_specs = value;
438		}
439	}
440
441	public string[]? push_specs
442	{
443		owned get
444		{
445			if (d_push_specs != null)
446			{
447				return d_push_specs;
448			}
449
450			try
451			{
452				return get_push_specs();
453			}
454			catch (Error e)
455			{
456				return null;
457			}
458		}
459
460		set
461		{
462			d_push_specs = value;
463		}
464	}
465
466	public CredentialsProvider? credentials_provider
467	{
468		get; set;
469	}
470}
471
472}
473