1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2013 - 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 interface StageStatusItem : Object
24{
25	public abstract string path { owned get; }
26
27	public abstract bool is_staged { get; }
28	public abstract bool is_unstaged { get; }
29	public abstract bool is_untracked { get; }
30
31	public abstract string? icon_name { owned get; }
32}
33
34public class StageStatusFile : Object, StageStatusItem
35{
36	private string d_path;
37	private Ggit.StatusFlags d_flags;
38
39	private static Ggit.StatusFlags s_index_flags =
40		  Ggit.StatusFlags.INDEX_NEW
41		| Ggit.StatusFlags.INDEX_MODIFIED
42		| Ggit.StatusFlags.INDEX_DELETED
43		| Ggit.StatusFlags.INDEX_RENAMED
44		| Ggit.StatusFlags.INDEX_TYPECHANGE;
45
46	private static Ggit.StatusFlags s_work_flags =
47		  Ggit.StatusFlags.WORKING_TREE_MODIFIED
48		| Ggit.StatusFlags.WORKING_TREE_DELETED
49		| Ggit.StatusFlags.WORKING_TREE_TYPECHANGE;
50
51	private static Ggit.StatusFlags s_untracked_flags =
52		  Ggit.StatusFlags.WORKING_TREE_NEW;
53
54	public StageStatusFile(string path, Ggit.StatusFlags flags)
55	{
56		d_path = path;
57		d_flags = flags;
58	}
59
60	public string path
61	{
62		owned get { return d_path; }
63	}
64
65	public bool is_staged
66	{
67		get { return (d_flags & s_index_flags) != 0; }
68	}
69
70	public bool is_unstaged
71	{
72		get { return (d_flags & s_work_flags) != 0; }
73	}
74
75	public bool is_untracked
76	{
77		get { return (d_flags & s_untracked_flags) != 0; }
78	}
79
80	public Ggit.StatusFlags flags
81	{
82		get { return d_flags; }
83	}
84
85	private string? icon_for_status(Ggit.StatusFlags status)
86	{
87		if ((status & (Ggit.StatusFlags.INDEX_NEW |
88			           Ggit.StatusFlags.WORKING_TREE_NEW)) != 0)
89		{
90			return "list-add-symbolic";
91		}
92		else if ((status & (Ggit.StatusFlags.INDEX_MODIFIED |
93			                Ggit.StatusFlags.INDEX_RENAMED |
94			                Ggit.StatusFlags.INDEX_TYPECHANGE |
95			                Ggit.StatusFlags.WORKING_TREE_MODIFIED |
96			                Ggit.StatusFlags.WORKING_TREE_TYPECHANGE)) != 0)
97		{
98			return "text-editor-symbolic";
99		}
100		else if ((status & (Ggit.StatusFlags.INDEX_DELETED |
101			                Ggit.StatusFlags.WORKING_TREE_DELETED)) != 0)
102		{
103			return "edit-delete-symbolic";
104		}
105
106		return null;
107	}
108
109	public string? icon_name
110	{
111		owned get { return icon_for_status(d_flags); }
112	}
113}
114
115public class StageStatusSubmodule : Object, StageStatusItem
116{
117	private Ggit.Submodule d_submodule;
118	private string d_path;
119	private Ggit.SubmoduleStatus d_flags;
120
121	private static Ggit.SubmoduleStatus s_index_flags =
122		  Ggit.SubmoduleStatus.INDEX_ADDED
123		| Ggit.SubmoduleStatus.INDEX_DELETED
124		| Ggit.SubmoduleStatus.INDEX_MODIFIED;
125
126	private static Ggit.SubmoduleStatus s_work_flags =
127		  Ggit.SubmoduleStatus.WD_ADDED
128		| Ggit.SubmoduleStatus.WD_DELETED
129		| Ggit.SubmoduleStatus.WD_MODIFIED;
130
131	private static Ggit.SubmoduleStatus s_untracked_flags =
132		  Ggit.SubmoduleStatus.IN_WD;
133
134	private static Ggit.SubmoduleStatus s_tracked_flags =
135		  Ggit.SubmoduleStatus.IN_HEAD
136		| Ggit.SubmoduleStatus.IN_INDEX;
137
138	private static Ggit.SubmoduleStatus s_dirty_flags =
139		  Ggit.SubmoduleStatus.WD_INDEX_MODIFIED
140		| Ggit.SubmoduleStatus.WD_WD_MODIFIED;
141
142	public StageStatusSubmodule(Ggit.Submodule submodule)
143	{
144		d_submodule = submodule;
145		d_path = submodule.get_path();
146
147		var repository = submodule.get_owner();
148
149		try
150		{
151			d_flags = repository.get_submodule_status(submodule.get_name(),
152			                                          Ggit.SubmoduleIgnore.UNTRACKED);
153		} catch {}
154	}
155
156	public Ggit.Submodule submodule
157	{
158		get { return d_submodule; }
159	}
160
161	public string path
162	{
163		owned get { return d_path; }
164	}
165
166	public bool is_staged
167	{
168		get { return (d_flags & s_index_flags) != 0; }
169	}
170
171	public bool is_unstaged
172	{
173		get { return !is_untracked && (d_flags & s_work_flags) != 0; }
174	}
175
176	public bool is_untracked
177	{
178		get
179		{
180			return    (d_flags & s_untracked_flags) != 0
181			       && (d_flags & s_tracked_flags) == 0;
182		}
183	}
184
185	public bool is_dirty
186	{
187		get { return (d_flags & s_dirty_flags) != 0; }
188	}
189
190	public Ggit.SubmoduleStatus flags
191	{
192		get { return d_flags; }
193	}
194
195	public string? icon_name {
196		owned get { return "folder-remote-symbolic"; }
197	}
198}
199
200public class StageStatusEnumerator : Object
201{
202	private Repository d_repository;
203	private Thread<void *> d_thread;
204	private StageStatusItem[] d_items;
205	private int d_offset;
206	private int d_callback_num;
207	private Cancellable d_cancellable;
208	private SourceFunc d_callback;
209	private Ggit.StatusOptions? d_options;
210	private Gee.HashSet<string> d_ignored_submodules;
211
212	private static Regex s_ignore_regex;
213
214	static construct
215	{
216		try
217		{
218			s_ignore_regex = new Regex("submodule\\.(.*)\\.gitgignore");
219		}
220		catch (Error e)
221		{
222			stderr.printf(@"Failed to compile stage status enumerator regex: $(e.message)\n");
223		}
224	}
225
226	internal StageStatusEnumerator(Repository repository,
227	                               Ggit.StatusOptions? options = null)
228	{
229		d_repository = repository;
230		d_options = options;
231
232		d_items = new StageStatusItem[100];
233		d_items.length = 0;
234		d_cancellable = new Cancellable();
235
236		try
237		{
238			d_ignored_submodules = new Gee.HashSet<string>();
239
240			repository.get_config().snapshot().match_foreach(s_ignore_regex, (match, val) => {
241				if (val != "true")
242				{
243					return 0;
244				}
245
246				d_ignored_submodules.add(match.fetch(1));
247				return 0;
248			});
249		} catch {}
250
251		try
252		{
253			d_thread = new Thread<void *>.try("gitg-status-enumerator", run_status);
254		} catch {}
255	}
256
257	public void cancel()
258	{
259		lock (d_items)
260		{
261			if (d_cancellable != null)
262			{
263				d_cancellable.cancel();
264			}
265		}
266
267		if (d_thread != null)
268		{
269			d_thread.join();
270			d_thread = null;
271		}
272	}
273
274	private delegate void AddItem(StageStatusItem item);
275
276	private void *run_status()
277	{
278		AddItem add = (item) => {
279			lock (d_items)
280			{
281				d_items += item;
282
283				if (d_callback != null && d_callback_num != -1 && d_items.length >= d_callback_num)
284				{
285					var cb = (owned)d_callback;
286					d_callback = null;
287
288					Idle.add((owned)cb);
289				}
290			}
291		};
292
293		var submodule_paths = new Gee.HashSet<string>();
294
295		// Due to a bug in libgit2, submodule iteration crashes when performed
296		// on a bare repository
297		if (!d_repository.is_bare)
298		{
299			try
300			{
301				d_repository.submodule_foreach((submodule, name) => {
302					submodule_paths.add(submodule.get_path());
303
304					if (!d_ignored_submodules.contains(name))
305					{
306						try
307						{
308							add(new StageStatusSubmodule(d_repository.lookup_submodule(name)));
309						} catch {}
310					}
311
312					return d_cancellable.is_cancelled() ? 1 : 0;
313				});
314			} catch {}
315		}
316
317		try
318		{
319			d_repository.file_status_foreach(d_options, (path, flags) => {
320				if (!submodule_paths.contains(path))
321				{
322					add(new StageStatusFile(path, flags));
323				}
324
325				return d_cancellable.is_cancelled() ? 1 : 0;
326			});
327		} catch {}
328
329		lock (d_items)
330		{
331			d_cancellable = null;
332
333			if (d_callback != null && d_callback_num == -1)
334			{
335				var cb = (owned)d_callback;
336				d_callback = null;
337
338				Idle.add((owned)cb);
339			}
340		}
341
342		return null;
343	}
344
345	private StageStatusItem[] fill_items(int num)
346	{
347		int n = 0;
348
349		if (num == -1)
350		{
351			num = d_items.length - d_offset;
352		}
353
354		StageStatusItem[] ret = new StageStatusItem[int.min(num, d_items.length - d_offset)];
355		ret.length = 0;
356
357		// d_items is already locked here, so it's safe to access
358		while (d_offset < d_items.length)
359		{
360			if (n == num)
361			{
362				break;
363			}
364
365			ret += d_items[d_offset];
366			d_offset++;
367
368			++n;
369		}
370
371		return ret;
372	}
373
374	public async StageStatusItem[] next_items(int num)
375	{
376		SourceFunc callback = next_items.callback;
377		StageStatusItem[] ret;
378
379		lock (d_items)
380		{
381			if (d_cancellable == null)
382			{
383				// Already finished
384				return fill_items(num);
385			}
386			else
387			{
388				d_callback = (owned)callback;
389				d_callback_num = num;
390			}
391		}
392
393		yield;
394
395		lock (d_items)
396		{
397			ret = fill_items(num);
398		}
399
400		if (ret.length != num)
401		{
402			cancel();
403		}
404
405		return ret;
406	}
407}
408
409}
410
411// ex:set ts=4 noet
412