1/*
2 * This file is part of gitg
3 *
4 * Copyright (C) 2012 - 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 GitgHistory
21{
22	public enum DefaultSelection
23	{
24		CURRENT_BRANCH,
25		ALL_BRANCHES,
26		ALL_COMMITS
27	}
28
29	/* The main history view. This view shows the equivalent of git log, but
30	 * in a nice way with lanes, merges, ref labels etc.
31	 */
32	public class Activity : Object, GitgExt.UIElement, GitgExt.Activity, GitgExt.Searchable, GitgExt.History
33	{
34		// Do this to pull in config.h before glib.h (for gettext...)
35		private const string version = Gitg.Config.VERSION;
36
37		public GitgExt.Application? application { owned get; construct set; }
38
39		private Gitg.CommitModel? d_commit_list_model;
40
41		private Gee.HashSet<Ggit.OId> d_selected;
42		private Ggit.OId? d_scroll_to;
43		private float d_scroll_y;
44		private ulong d_insertsig;
45		private Settings d_settings;
46		private uint d_walker_update_idle_id;
47		private ulong d_refs_list_selection_id;
48		private ulong d_refs_list_changed_id;
49		private ulong d_externally_changed_id;
50		private ulong d_commits_changed_id;
51
52		private Gitg.WhenMapped? d_reload_when_mapped;
53
54		private Paned d_main;
55		private Gitg.PopupMenu d_refs_list_popup;
56		private Gitg.PopupMenu d_commit_list_popup;
57
58		private string[] d_mainline;
59		private bool d_ignore_external;
60
61		private Gitg.UIElements<GitgExt.HistoryPanel> d_panels;
62
63		public Activity(GitgExt.Application application)
64		{
65			Object(application: application);
66		}
67
68		public string id
69		{
70			owned get { return "/org/gnome/gitg/Activities/History"; }
71		}
72
73		private Gitg.Repository d_repository;
74
75		public Gitg.Repository repository
76		{
77			get
78			{
79				return d_repository;
80			}
81
82			set
83			{
84				if (d_repository != value)
85				{
86					d_repository = value;
87					reload();
88				}
89			}
90		}
91
92		public void foreach_selected(GitgExt.ForeachCommitSelectionFunc func)
93		{
94			bool breakit = false;
95
96			d_main.commit_list_view.get_selection().selected_foreach((model, path, iter) => {
97				if (!breakit)
98				{
99					var c = d_commit_list_model.commit_from_iter(iter);
100
101					if (c != null)
102					{
103						breakit = !func(c);
104					}
105				}
106			});
107		}
108
109		public void select(Gitg.Commit commit)
110		{
111			var model = (Gitg.CommitModel)d_main.commit_list_view.model;
112			var path = model.path_from_commit(commit);
113
114			if (path != null)
115			{
116				var sel = d_main.commit_list_view.get_selection();
117				sel.select_path(path);
118
119				d_main.commit_list_view.scroll_to_cell(path, null, true, 0.5f, 0);
120			}
121			else
122			{
123				stderr.printf("Failed to lookup tree path for commit '%s'\n", commit.get_id().to_string());
124			}
125		}
126
127		construct
128		{
129			d_settings = new Settings(Gitg.Config.APPLICATION_ID + ".preferences.history");
130
131			d_settings.changed["topological-order"].connect((s, k) => {
132				update_sort_mode();
133			});
134
135			d_settings.changed["mainline-head"].connect((s, k) => {
136				update_walker();
137			});
138
139			d_settings.changed["show-upstream-with-branch"].connect((s, k) => {
140				update_walker();
141			});
142
143			d_selected = new Gee.HashSet<Ggit.OId>((Gee.HashDataFunc<Ggit.OId>)Ggit.OId.hash,
144			                                       (Gee.EqualDataFunc<Ggit.OId>)Ggit.OId.equal);
145
146			d_commit_list_model = new Gitg.CommitModel(application.repository);
147			d_commit_list_model.started.connect(on_commit_model_started);
148			d_commit_list_model.finished.connect(on_commit_model_finished);
149
150			update_sort_mode();
151
152			d_repository = application.repository;
153
154			application.bind_property("repository", this,
155			                          "repository", BindingFlags.DEFAULT);
156
157			reload_mainline();
158
159			d_externally_changed_id = application.repository_changed_externally.connect(repository_changed_externally);
160			d_commits_changed_id = application.repository_commits_changed.connect(repository_commits_changed);
161		}
162
163		private void repository_changed_externally(GitgExt.ExternalChangeHint hint)
164		{
165			if (d_main != null && (hint & GitgExt.ExternalChangeHint.REFS) != 0  && !d_ignore_external)
166			{
167				reload_when_mapped();
168			}
169
170			d_ignore_external = false;
171		}
172
173		private void repository_commits_changed()
174		{
175			if (d_main != null)
176			{
177				d_ignore_external = true;
178				reload_when_mapped();
179			}
180		}
181
182		private void reload_when_mapped()
183		{
184			if (d_main != null)
185			{
186				d_reload_when_mapped = new Gitg.WhenMapped(d_main);
187
188				d_reload_when_mapped.update(() => {
189					reload();
190				}, this);
191			}
192		}
193
194		public override void dispose()
195		{
196			if (d_refs_list_selection_id != 0)
197			{
198				d_main.refs_list.disconnect(d_refs_list_selection_id);
199				d_refs_list_selection_id = 0;
200			}
201
202			if (d_refs_list_changed_id != 0)
203			{
204				d_main.refs_list.disconnect(d_refs_list_changed_id);
205				d_refs_list_changed_id = 0;
206			}
207
208			if (d_walker_update_idle_id != 0)
209			{
210				Source.remove(d_walker_update_idle_id);
211				d_walker_update_idle_id = 0;
212			}
213
214			if (d_externally_changed_id != 0)
215			{
216				application.disconnect(d_externally_changed_id);
217				d_externally_changed_id = 0;
218			}
219
220			if (d_commits_changed_id != 0)
221			{
222				application.disconnect(d_commits_changed_id);
223				d_commits_changed_id = 0;
224			}
225
226			d_commit_list_model.repository = null;
227			base.dispose();
228		}
229
230		private void update_sort_mode()
231		{
232			if (d_settings.get_boolean("topological-order"))
233			{
234				d_commit_list_model.sort_mode = Ggit.SortMode.TOPOLOGICAL;
235			}
236			else
237			{
238				d_commit_list_model.sort_mode = Ggit.SortMode.TIME | Ggit.SortMode.TOPOLOGICAL;
239			}
240		}
241
242		private void on_commit_model_started(Gitg.CommitModel model)
243		{
244			if (d_insertsig == 0)
245			{
246				d_insertsig = d_commit_list_model.row_inserted.connect(on_row_inserted_select);
247			}
248		}
249
250		private void on_row_inserted_select(Gtk.TreeModel model, Gtk.TreePath path, Gtk.TreeIter iter)
251		{
252			var commit = d_commit_list_model.commit_from_path(path);
253
254			var sel = d_main.commit_list_view.get_selection();
255
256			if (d_selected.size == 0 || d_selected.remove(commit.get_id()))
257			{
258				sel.select_path(path);
259
260				if (commit.get_id().equal(d_scroll_to))
261				{
262					d_main.commit_list_view.scroll_to_cell(path,
263					                                       null,
264					                                       true,
265					                                       d_scroll_y,
266					                                       0);
267
268					d_scroll_to = null;
269				}
270			}
271
272			if (d_selected.size == 0 || (sel.count_selected_rows() != 0 &&
273			                             (sel.mode == Gtk.SelectionMode.SINGLE ||
274			                              sel.mode == Gtk.SelectionMode.BROWSE)))
275			{
276				d_selected.clear();
277
278				d_commit_list_model.disconnect(d_insertsig);
279				d_insertsig = 0;
280			}
281		}
282
283		private void scroll_into_view()
284		{
285			if (d_main == null)
286			{
287				return;
288			}
289
290			var sel = d_main.commit_list_view.get_selection();
291
292			Gtk.TreeModel m;
293			var rows = sel.get_selected_rows(out m);
294
295			if (rows == null)
296			{
297				return;
298			}
299
300			var row = rows.data;
301
302			Gtk.TreePath startp;
303			Gtk.TreePath endp;
304
305			if (d_main.commit_list_view.get_visible_range(out startp, out endp))
306			{
307				if (row.compare(startp) < 0 || row.compare(endp) > 0)
308				{
309					d_main.commit_list_view.scroll_to_cell(row, null, true, 0, 0);
310				}
311			}
312		}
313
314		private void on_commit_model_finished(Gitg.CommitModel model)
315		{
316			if (d_insertsig != 0)
317			{
318				d_commit_list_model.disconnect(d_insertsig);
319				d_insertsig = 0;
320			}
321
322			scroll_into_view();
323		}
324
325		private void on_commit_model_begin_clear(Gitg.CommitModel model)
326		{
327			d_main.commit_list_view.model = null;
328		}
329
330		private void on_commit_model_end_clear(Gitg.CommitModel model)
331		{
332			d_main.commit_list_view.model = d_commit_list_model;
333		}
334
335		public bool available
336		{
337			get { return true; }
338		}
339
340		public string display_name
341		{
342			owned get { return _("History"); }
343		}
344
345		public string description
346		{
347			owned get { return _("Examine the history of the repository"); }
348		}
349
350		public string? icon
351		{
352			owned get { return "view-list-symbolic"; }
353		}
354
355		public Gtk.Widget? widget
356		{
357			owned get
358			{
359				if (d_main == null)
360				{
361					build_ui();
362				}
363
364				return d_main;
365			}
366		}
367
368		public bool is_default_for(string action)
369		{
370			return (action == "" || action == "history");
371		}
372
373		public bool enabled
374		{
375			get { return true; }
376		}
377
378		public int negotiate_order(GitgExt.UIElement other)
379		{
380			return -1;
381		}
382
383		private void store_changed_mainline()
384		{
385			var repo = application.repository;
386
387			if (repo == null)
388			{
389				return;
390			}
391
392			Ggit.Config config;
393
394			try
395			{
396				config = repo.get_config();
397			} catch { return; }
398
399			store_mainline(config, string.joinv(",", d_mainline));
400		}
401
402		private void store_mainline(Ggit.Config? config, string mainline)
403		{
404			if (config != null)
405			{
406				try
407				{
408					config.set_string("gitg.mainline", mainline);
409				}
410				catch (Error e)
411				{
412					stderr.printf("Failed to set gitg.mainline: %s\n", e.message);
413				}
414			}
415		}
416
417		private void reload_mainline()
418		{
419			d_reload_when_mapped = null;
420
421			var uniq = new Gee.HashSet<string>();
422
423			d_mainline = new string[0];
424
425			var repository = application.repository;
426
427			if (repository == null)
428			{
429				return;
430			}
431
432			Ggit.Config? config = null;
433			var ref_names = new string[0];
434
435			try
436			{
437				config = repository.get_config();
438				ref_names = config.snapshot().get_string("gitg.mainline").split(",");
439			}
440			catch
441			{
442				ref_names = new string[] {"refs/heads/master"};
443			}
444
445			foreach (var name in ref_names)
446			{
447				Gitg.Ref r;
448
449				try
450				{
451					r = repository.lookup_reference(name);
452				}
453				catch (Error e)
454				{
455					stderr.printf("Failed to lookup reference (%s): %s\n", name, e.message);
456					continue;
457				}
458
459				var id = id_for_ref(r);
460
461				if (id != null && uniq.add(name))
462				{
463					d_mainline += name;
464				}
465			}
466
467			store_mainline(config, string.joinv(",", d_mainline));
468		}
469
470		public RefsList refs_list
471		{
472			get { return d_main.refs_list; }
473		}
474
475		private void reload()
476		{
477			if (d_walker_update_idle_id != 0)
478			{
479				Source.remove(d_walker_update_idle_id);
480				d_walker_update_idle_id = 0;
481			}
482
483			var view = d_main.commit_list_view;
484
485			double vadj = d_main.refs_list.get_adjustment().get_value();
486
487			reload_mainline();
488
489			d_selected.clear();
490
491			d_scroll_to = null;
492
493			Gtk.TreePath startp, endp;
494
495			var isvis = view.get_visible_range(out startp, out endp);
496
497			view.get_selection().selected_foreach((model, path, iter) => {
498				var c = d_commit_list_model.commit_from_iter(iter);
499
500				if (c != null)
501				{
502					d_selected.add(c.get_id());
503
504					if (d_scroll_to == null &&
505					    (!isvis || startp.compare(path) <= 0 && endp.compare(path) >= 0))
506					{
507						if (isvis)
508						{
509							Gdk.Rectangle rect;
510							Gdk.Rectangle visrect;
511
512							view.get_cell_area(path, null, out rect);
513							view.get_visible_rect(out visrect);
514
515							int x, y;
516
517							view.convert_tree_to_bin_window_coords(visrect.x,
518							                                       visrect.y,
519							                                       out x,
520							                                       out y);
521
522							// + 2 seems to work correctly here, but this is probably
523							// something related to a border or padding of the
524							// treeview (i.e. theme related)
525							d_scroll_y = (float)(rect.y + rect.height / 2.0 - y + 2) / (float)visrect.height;
526						}
527						else
528						{
529							d_scroll_y = 0.5f;
530						}
531
532						d_scroll_to = c.get_id();
533					}
534				}
535			});
536
537			// Clears the commit model
538			d_commit_list_model.repository = repository;
539
540			// Reloads branches, tags, etc.
541			d_main.refs_list.repository = repository;
542
543			ulong sid = 0;
544
545			sid = d_main.refs_list.size_allocate.connect((a) => {
546				d_main.refs_list.get_adjustment().set_value(vadj);
547
548				if (sid != 0)
549				{
550					d_main.refs_list.disconnect(sid);
551				}
552			});
553		}
554
555		private void build_ui()
556		{
557			d_main = new Paned();
558
559			d_main.refs_list.remote_lookup = application.remote_lookup;
560
561			d_main.commit_list_view.model = d_commit_list_model;
562
563			d_main.commit_list_view.get_selection().changed.connect((sel) => {
564				selection_changed();
565
566				// Set primary selection to sha1 of first selected commit
567				var clip = ((Gtk.Widget)application).get_clipboard(Gdk.SELECTION_PRIMARY);
568
569				foreach_selected((commit) => {
570					clip.set_text(commit.get_id().to_string(), -1);
571					return false;
572				});
573			});
574
575			var engine = Gitg.PluginsEngine.get_default();
576
577			var extset = new Peas.ExtensionSet(engine,
578			                                   typeof(GitgExt.HistoryPanel),
579			                                   "history",
580			                                   this,
581			                                   "application",
582			                                   application);
583
584			d_panels = new Gitg.UIElements<GitgExt.HistoryPanel>(extset,
585			                                                     d_main.stack_panel);
586
587			d_refs_list_popup = new Gitg.PopupMenu(d_main.refs_list);
588			d_refs_list_popup.populate_menu.connect(on_refs_list_populate_menu);
589
590			d_refs_list_selection_id = d_main.refs_list.notify["selection"].connect(update_walker_idle);
591			d_refs_list_changed_id = d_main.refs_list.changed.connect(update_walker_idle);
592
593			d_commit_list_popup = new Gitg.PopupMenu(d_main.commit_list_view);
594			d_commit_list_popup.populate_menu.connect(on_commit_list_populate_menu);
595			d_commit_list_popup.request_menu_position.connect(on_commit_list_request_menu_position);
596
597			application.bind_property("repository", d_main.refs_list,
598			                          "repository",
599			                          BindingFlags.DEFAULT |
600			                          BindingFlags.SYNC_CREATE);
601
602			d_main.commit_list_view.set_search_equal_func(search_filter_func);
603
604			d_commit_list_model.begin_clear.connect(on_commit_model_begin_clear);
605			d_commit_list_model.end_clear.connect(on_commit_model_end_clear);
606		}
607
608		private void update_walker_idle()
609		{
610			if (d_repository == null)
611			{
612				return;
613			}
614
615			if (d_walker_update_idle_id == 0)
616			{
617				d_walker_update_idle_id = Idle.add(() => {
618					d_walker_update_idle_id = 0;
619					update_walker();
620					return false;
621				});
622			}
623		}
624
625		private Gtk.Menu? popup_on_ref(Gdk.EventButton? event)
626		{
627			int cell_x;
628			int cell_y;
629			int cell_w;
630			Gtk.TreePath path;
631			Gtk.TreeViewColumn column;
632
633			if (event == null)
634			{
635				return null;
636			}
637
638			if (!d_main.commit_list_view.get_path_at_pos((int)event.x,
639			                                             (int)event.y,
640			                                             out path,
641			                                             out column,
642			                                             out cell_x,
643			                                             out cell_y))
644			{
645				return null;
646			}
647
648			var cell = d_main.commit_list_view.find_cell_at_pos(column, path, cell_x, out cell_w) as Gitg.CellRendererLanes;
649
650			if (cell == null)
651			{
652				return null;
653			}
654
655			var reference = cell.get_ref_at_pos(d_main.commit_list_view, cell_x, cell_w, null);
656
657			if (reference == null)
658			{
659				return null;
660			}
661
662			return popup_menu_for_ref(reference);
663		}
664
665		private Gtk.Menu? on_commit_list_populate_menu(Gdk.EventButton? event)
666		{
667			var ret = popup_on_ref(event);
668
669			if (ret == null)
670			{
671				ret = popup_menu_for_commit(event);
672			}
673
674			// event is most likely null.
675			if (ret == null)
676			{
677				ret = popup_menu_for_selection();
678			}
679
680			return ret;
681		}
682
683		private Gdk.Rectangle? on_commit_list_request_menu_position()
684		{
685			var selection = d_main.commit_list_view.get_selection();
686
687			Gtk.TreeModel model;
688			Gtk.TreeIter iter;
689
690			if (!selection.get_selected(out model, out iter))
691			{
692				return null;
693			}
694
695			var path = model.get_path(iter);
696
697			Gdk.Rectangle rect = { 0 };
698
699			d_main.commit_list_view.get_cell_area(path, null, out rect);
700			d_main.commit_list_view.convert_bin_window_to_widget_coords(rect.x, rect.y,
701			                                                            out rect.x, out rect.y);
702
703			return rect;
704		}
705
706		private void add_ref_action(Gee.LinkedList<GitgExt.RefAction> actions,
707		                            GitgExt.RefAction?                action)
708		{
709			if (action != null && action.available)
710			{
711				actions.add(action);
712			}
713		}
714
715		private Gtk.Menu? populate_menu_for_commit(Gitg.Commit commit)
716		{
717			var af = new ActionInterface(application, d_main.refs_list);
718
719			af.updated.connect(() => {
720				d_ignore_external = true;
721			});
722
723			var actions = new Gee.LinkedList<GitgExt.CommitAction>();
724
725			add_commit_action(actions,
726			                  new Gitg.CommitActionCreateBranch(application,
727			                                                    af,
728			                                                    commit));
729
730			add_commit_action(actions,
731			                  new Gitg.CommitActionCreateTag(application,
732			                                                 af,
733			                                                 commit));
734
735			add_commit_action(actions,
736			                  new Gitg.CommitActionCreatePatch(application,
737			                                                   af,
738			                                                   commit));
739
740			add_commit_action(actions,
741			                  new Gitg.CommitActionCherryPick(application,
742			                                                  af,
743			                                                  commit));
744
745			var exts = new Peas.ExtensionSet(Gitg.PluginsEngine.get_default(),
746			                                 typeof(GitgExt.CommitAction),
747			                                 "application",
748			                                 application,
749			                                 "action_interface",
750			                                 af,
751			                                 "commit",
752			                                 commit);
753
754			exts.foreach((extset, info, extension) => {
755				add_commit_action(actions, extension as GitgExt.CommitAction);
756			});
757
758			if (actions.size == 0)
759			{
760				return null;
761			}
762
763			Gtk.Menu menu = new Gtk.Menu();
764
765			foreach (var ac in actions)
766			{
767				ac.populate_menu(menu);
768			}
769
770			// To keep actions alive as long as the menu is alive
771			menu.set_data("gitg-ext-actions", actions);
772
773			return menu;
774		}
775
776		private Gtk.Menu? popup_menu_for_selection()
777		{
778			var selection = d_main.commit_list_view.get_selection();
779
780			Gtk.TreeIter iter;
781
782			if (!selection.get_selected(null, out iter))
783			{
784				return null;
785			}
786
787			var commit = d_commit_list_model.commit_from_iter(iter);
788
789			if (commit == null)
790			{
791				return null;
792			}
793
794			return populate_menu_for_commit(commit);
795		}
796
797		private Gtk.Menu? popup_menu_for_commit(Gdk.EventButton? event)
798		{
799			int cell_x;
800			int cell_y;
801			Gtk.TreePath path;
802			Gtk.TreeViewColumn column;
803
804			if (event == null)
805			{
806				return null;
807			}
808
809			if (!d_main.commit_list_view.get_path_at_pos((int)event.x,
810			                                             (int)event.y,
811			                                             out path,
812			                                             out column,
813			                                             out cell_x,
814			                                             out cell_y))
815			{
816				return null;
817			}
818
819			var commit = d_commit_list_model.commit_from_path(path);
820
821			if (commit == null)
822			{
823				return null;
824			}
825
826			d_main.commit_list_view.get_selection().select_path(path);
827
828			return populate_menu_for_commit(commit);
829		}
830
831		private Gtk.Menu? popup_menu_for_ref(Gitg.Ref reference)
832		{
833			var actions = new Gee.LinkedList<GitgExt.RefAction?>();
834
835			var af = new ActionInterface(application, d_main.refs_list);
836
837			af.updated.connect(() => {
838				d_ignore_external = true;
839			});
840
841			add_ref_action(actions, new Gitg.RefActionCheckout(application, af, reference));
842			add_ref_action(actions, new Gitg.RefActionRename(application, af, reference));
843			add_ref_action(actions, new Gitg.RefActionDelete(application, af, reference));
844			add_ref_action(actions, new Gitg.RefActionCopyName(application, af, reference));
845
846			var fetch = new Gitg.RefActionFetch(application, af, reference);
847
848			if (fetch.available)
849			{
850				actions.add(null);
851			}
852
853			add_ref_action(actions, fetch);
854
855			var push = new Gitg.RefActionPush(application, af, reference);
856
857			if (push.available)
858			{
859				actions.add(null);
860			}
861
862			add_ref_action(actions, push);
863
864			var merge = new Gitg.RefActionMerge(application, af, reference);
865
866			if (merge.available)
867			{
868				actions.add(null);
869				add_ref_action(actions, merge);
870			}
871
872			var exts = new Peas.ExtensionSet(Gitg.PluginsEngine.get_default(),
873			                                 typeof(GitgExt.RefAction),
874			                                 "application",
875			                                 application,
876			                                 "action_interface",
877			                                 af,
878			                                 "reference",
879			                                 reference);
880
881			var addedsep = false;
882
883			exts.foreach((extset, info, extension) => {
884				if (!addedsep)
885				{
886					actions.add(null);
887					addedsep = true;
888				}
889
890				add_ref_action(actions, extension as GitgExt.RefAction);
891			});
892
893			if (actions.is_empty)
894			{
895				return null;
896			}
897
898			Gtk.Menu menu = new Gtk.Menu();
899
900			foreach (var ac in actions)
901			{
902				if (ac != null)
903				{
904					ac.populate_menu(menu);
905				}
906				else
907				{
908					var sep = new Gtk.SeparatorMenuItem();
909					sep.show();
910					menu.append(sep);
911				}
912			}
913
914			var sep = new Gtk.SeparatorMenuItem();
915			sep.show();
916			menu.append(sep);
917
918			var item = new Gtk.CheckMenuItem.with_label(_("Mainline"));
919			int pos = 0;
920
921			foreach (var ml in d_mainline)
922			{
923				if (ml == reference.get_name())
924				{
925					item.active = true;
926					break;
927				}
928
929				++pos;
930			}
931
932			item.activate.connect(() => {
933				if (item.active)
934				{
935					d_mainline += reference.get_name();
936				}
937				else
938				{
939					var nml = new string[d_mainline.length - 1];
940					nml.length = 0;
941
942					for (var i = 0; i < d_mainline.length; i++)
943					{
944						if (i != pos)
945						{
946							nml += d_mainline[i];
947						}
948					}
949
950					d_mainline = nml;
951				}
952
953				store_changed_mainline();
954				update_walker();
955			});
956
957			item.show();
958			menu.append(item);
959
960			// To keep actions alive as long as the menu is alive
961			menu.set_data("gitg-ext-actions", actions);
962			return menu;
963		}
964
965		private Gtk.Menu? on_refs_list_populate_menu(Gdk.EventButton? event)
966		{
967			if (event != null)
968			{
969				var row = d_main.refs_list.get_row_at_y((int)event.y);
970				d_main.refs_list.select_row(row);
971			}
972
973			var references = d_main.refs_list.selection;
974
975			if (references.is_empty || references.first() != references.last())
976			{
977				return null;
978			}
979
980			return popup_menu_for_ref(references.first());
981		}
982
983		private Ggit.OId? id_for_ref(Ggit.Ref r)
984		{
985			Ggit.OId? id = null;
986
987			try
988			{
989				var resolved = r.resolve();
990
991				if (resolved.is_tag())
992				{
993					var t = application.repository.lookup<Ggit.Tag>(resolved.get_target());
994
995					id = t.get_target_id();
996				}
997				else
998				{
999					id = resolved.get_target();
1000				}
1001			}
1002			catch {}
1003
1004			return id;
1005		}
1006
1007		private void update_walker()
1008		{
1009			d_selected.clear();
1010
1011			var include = new Gee.HashSet<Ggit.OId>((Gee.HashDataFunc)Ggit.OId.hash,
1012			                                        (Gee.EqualDataFunc)Ggit.OId.equal);
1013
1014			var isall = d_main.refs_list.is_all;
1015			var isheader = d_main.refs_list.is_header;
1016
1017			var perm_uniq = new Gee.HashSet<Ggit.OId>((Gee.HashDataFunc)Ggit.OId.hash,
1018			                                          (Gee.EqualDataFunc)Ggit.OId.equal);
1019
1020			var permanent = new Ggit.OId[0];
1021
1022			if (application.repository != null)
1023			{
1024				foreach (var ml in d_mainline)
1025				{
1026					Ggit.OId id;
1027
1028					try
1029					{
1030						id = id_for_ref(application.repository.lookup_reference(ml));
1031					} catch { continue; }
1032
1033					if (id != null && perm_uniq.add(id))
1034					{
1035						permanent += id;
1036					}
1037				}
1038
1039				if (d_settings.get_boolean("mainline-head"))
1040				{
1041					try
1042					{
1043						var head = id_for_ref(application.repository.get_head());
1044
1045						if (head != null && perm_uniq.add(head))
1046						{
1047							permanent += head;
1048						}
1049					} catch {}
1050				}
1051			}
1052
1053			var show_upstream_with_branch = d_settings.get_boolean("show-upstream-with-branch");
1054
1055			foreach (var r in d_main.refs_list.selection)
1056			{
1057				var id = id_for_ref(r);
1058
1059				if (id != null)
1060				{
1061					include.add(id);
1062
1063					if (!isall)
1064					{
1065						d_selected.add(id);
1066
1067						if (!isheader && perm_uniq.add(id))
1068						{
1069							permanent += id;
1070						}
1071					}
1072
1073					if (show_upstream_with_branch && r.is_branch())
1074					{
1075						var branch = r as Gitg.Branch;
1076
1077						try
1078						{
1079							var upid = id_for_ref(branch.get_upstream());
1080
1081							if (upid != null)
1082							{
1083								include.add(upid);
1084							}
1085						} catch {}
1086					}
1087				}
1088			}
1089
1090			d_commit_list_model.set_permanent_lanes(permanent);
1091			d_commit_list_model.set_include(include.to_array());
1092			d_commit_list_model.reload();
1093		}
1094
1095		public bool search_available
1096		{
1097			get { return true; }
1098		}
1099
1100		private void add_commit_action(Gee.LinkedList<GitgExt.CommitAction> actions,
1101		                               GitgExt.CommitAction?                action)
1102		{
1103			if (action != null && action.available)
1104			{
1105				actions.add(action);
1106			}
1107		}
1108
1109		private string normalize(string s)
1110		{
1111			return s.normalize(-1, NormalizeMode.ALL).casefold();
1112		}
1113
1114		private bool search_filter_func(Gtk.TreeModel model, int column, string key, Gtk.TreeIter iter)
1115		{
1116			var c = d_commit_list_model.commit_from_iter(iter);
1117
1118			if (c.get_id().has_prefix(key))
1119			{
1120				return false;
1121			}
1122
1123			var nkey = normalize(key);
1124			var subject = normalize(c.get_subject());
1125
1126			if (subject.contains(nkey))
1127			{
1128				return false;
1129			}
1130
1131			var message = normalize(c.get_message());
1132
1133			if (message.contains(nkey))
1134			{
1135				return false;
1136			}
1137
1138			return true;
1139		}
1140
1141		public Gtk.Entry? search_entry
1142		{
1143			set
1144			{
1145				d_main.commit_list_view.set_search_entry(value);
1146
1147				if (value != null)
1148				{
1149					d_main.commit_list_view.set_search_column(0);
1150				}
1151				else
1152				{
1153					d_main.commit_list_view.set_search_column(-1);
1154				}
1155			}
1156		}
1157
1158		public string search_text { owned get; set; default = ""; }
1159		public bool search_visible { get; set; }
1160	}
1161}
1162
1163// ex: ts=4 noet
1164