[gitg] Add basic merging support



commit 8eb1e3ed662c4bc9c05b74965f8919b1aeabf438
Author: Jesse van den Kieboom <jessevdk gnome org>
Date:   Fri Aug 14 00:55:43 2015 +0200

    Add basic merging support

 gitg/Makefile.am                         |    1 +
 gitg/gitg-ref-action-merge.vala          |  708 ++++++++++++++++++++++++++++++
 gitg/history/gitg-history.vala           |   39 ++-
 libgitg/gitg-stage.vala                  |   19 +-
 tests/gitg/Makefile.am                   |    4 +-
 tests/gitg/application-mock.vala         |   44 ++-
 tests/gitg/main.vala                     |    3 +-
 tests/gitg/simple-notification-mock.vala |    2 +-
 tests/gitg/test-merge-ref.vala           |  482 ++++++++++++++++++++
 tests/support/gitg-assert.h              |   13 +-
 tests/support/gitg-assert.vapi           |    1 +
 tests/support/repository.vala            |   40 ++-
 12 files changed, 1337 insertions(+), 19 deletions(-)
---
diff --git a/gitg/Makefile.am b/gitg/Makefile.am
index 4be7d14..d3d70e8 100644
--- a/gitg/Makefile.am
+++ b/gitg/Makefile.am
@@ -75,6 +75,7 @@ gitg_gitg_VALASOURCES =                                               \
        gitg/gitg-ref-action-copy-name.vala                     \
        gitg/gitg-ref-action-delete.vala                        \
        gitg/gitg-ref-action-fetch.vala                         \
+       gitg/gitg-ref-action-merge.vala                         \
        gitg/gitg-ref-action-rename.vala                        \
        gitg/gitg-remote-manager.vala                           \
        gitg/gitg-remote-notification.vala                      \
diff --git a/gitg/gitg-ref-action-merge.vala b/gitg/gitg-ref-action-merge.vala
new file mode 100644
index 0000000..346fa82
--- /dev/null
+++ b/gitg/gitg-ref-action-merge.vala
@@ -0,0 +1,708 @@
+/*
+ * This file is part of gitg
+ *
+ * Copyright (C) 2015 - Jesse van den Kieboom
+ *
+ * gitg is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gitg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with gitg. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Gitg
+{
+
+class RefActionMerge : GitgExt.UIElement, GitgExt.Action, GitgExt.RefAction, Object
+{
+       // Do this to pull in config.h before glib.h (for gettext...)
+       private const string version = Gitg.Config.VERSION;
+
+       public GitgExt.Application? application { owned get; construct set; }
+       public GitgExt.RefActionInterface action_interface { get; construct set; }
+       public Gitg.Ref reference { get; construct set; }
+
+       private struct RemoteSource
+       {
+               public string name;
+               public Gitg.Ref[] sources;
+       }
+
+       private bool d_has_sourced;
+       private Gitg.Ref? d_upstream;
+       private Gitg.Ref[]? d_local_sources;
+       private RemoteSource[]? d_remote_sources;
+       private Gitg.Ref[]? d_tag_sources;
+
+       public RefActionMerge(GitgExt.Application        application,
+                             GitgExt.RefActionInterface action_interface,
+                             Gitg.Ref                   reference)
+       {
+               Object(application:      application,
+                      action_interface: action_interface,
+                      reference:        reference);
+       }
+
+       public string id
+       {
+               owned get { return "/org/gnome/gitg/ref-actions/merge"; }
+       }
+
+       public string display_name
+       {
+               owned get { return _("Merge into %s").printf(reference.parsed_name.shortname); }
+       }
+
+       public string description
+       {
+               // TODO
+               owned get { return _("Merge another branch into branch 
%s").printf(reference.parsed_name.shortname); }
+       }
+
+       public bool available
+       {
+               get
+               {
+                       return reference.is_branch();
+               }
+       }
+
+       public bool enabled
+       {
+               get
+               {
+                       ensure_sources();
+
+                       return d_upstream != null ||
+                              d_local_sources.length != 0 ||
+                              d_remote_sources.length != 0 ||
+                              d_tag_sources.length != 0;
+               }
+       }
+
+       private async Ggit.Index create_merge_index(SimpleNotification notification, Ggit.Commit ours, 
Ggit.Commit theirs)
+       {
+               Ggit.Index? index = null;
+
+               yield Async.thread_try(() => {
+                       var options = new Ggit.MergeOptions();
+
+                       try
+                       {
+                               index = application.repository.merge_commits(ours, theirs, options);
+                       }
+                       catch (Error e)
+                       {
+                               notification.error(_("Failed to merge commits: %s").printf(e.message));
+                               return;
+                       }
+               });
+
+               return index;
+       }
+
+       private async bool working_directory_dirty()
+       {
+               var options = new Ggit.StatusOptions(0, Ggit.StatusShow.WORKDIR_ONLY, null);
+               var is_dirty = false;
+
+               yield Async.thread_try(() => {
+                       application.repository.file_status_foreach(options, (path, flags) => {
+                               is_dirty = true;
+                               return -1;
+                       });
+               });
+
+               return is_dirty;
+       }
+
+       private async bool save_stash(SimpleNotification notification, Gitg.Ref? head)
+       {
+               var committer = application.get_verified_committer();
+
+               if (committer == null)
+               {
+                       return false;
+               }
+
+               try
+               {
+                       yield Async.thread(() => {
+                               // Try to stash changes
+                               string message;
+
+                               if (head != null)
+                               {
+                                       var headname = head.parsed_name.shortname;
+
+                                       try
+                                       {
+                                               var head_commit = head.resolve().lookup() as Ggit.Commit;
+                                               var shortid = head_commit.get_id().to_string()[0:6];
+                                               var subject = head_commit.get_subject();
+
+                                               message = @"WIP on $(headname): $(shortid) $(subject)";
+                                       }
+                                       catch
+                                       {
+                                               message = @"WIP on $(headname)";
+                                       }
+                               }
+                               else
+                               {
+                                       message = "WIP on HEAD";
+                               }
+
+                               application.repository.save_stash(committer, message, 
Ggit.StashFlags.DEFAULT);
+                       });
+               }
+               catch (Error err)
+               {
+                       notification.error(_("Failed to stash changes: %s").printf(err.message));
+                       return false;
+               }
+
+               return true;
+       }
+
+       private bool reference_is_head(ref Gitg.Ref? head)
+       {
+               var branch = reference as Ggit.Branch;
+               head = null;
+
+               if (branch == null)
+               {
+                       return false;
+               }
+
+               try
+               {
+                       if (!branch.is_head())
+                       {
+                               return false;
+                       }
+
+                       head = application.repository.lookup_reference("HEAD");
+               } catch {}
+
+               return head != null;
+       }
+
+       private async bool stash_if_needed(SimpleNotification notification, Gitg.Ref head)
+       {
+               // Offer to stash if there are any local changes
+               if ((yield working_directory_dirty()))
+               {
+                       var q = new GitgExt.UserQuery.full(_("Unstaged changes"),
+                                                          _("You appear to have unstaged changes in your 
working directory. Would you like to stash the changes before the checkout?"),
+                                                          Gtk.MessageType.QUESTION,
+                                                          _("Cancel"), Gtk.ResponseType.CANCEL,
+                                                          _("Stash changes"), Gtk.ResponseType.OK);
+
+                       if ((yield application.user_query_async(q)) != Gtk.ResponseType.OK)
+                       {
+                               notification.error(_("Merge failed with conflicts"));
+                               return false;
+                       }
+
+                       if (!(yield save_stash(notification, head)))
+                       {
+                               return false;
+                       }
+               }
+
+               return true;
+       }
+
+       private async bool checkout_conflicts(SimpleNotification notification, Ggit.Index index, Gitg.Ref 
source)
+       {
+               var ours_name = reference.parsed_name.shortname;
+               var theirs_name = source.parsed_name.shortname;
+
+               notification.message = _("Merge has conflicts");
+
+               Gitg.Ref? head = null;
+               var ishead = reference_is_head(ref head);
+
+               string message;
+
+               if (ishead)
+               {
+                       message = _("The merge of %s into %s has caused conflicts, would you like to checkout 
branch %s with the merge to your working directory to resolve the conflicts?").printf(@"'$theirs_name'", 
@"'$ours_name'", @"'$ours_name'");
+               }
+               else
+               {
+                       message = _("The merge of %s into %s has caused conflicts, would you like to checkout 
the merge to your working directory to resolve the conflicts?").printf(@"'$theirs_name'", @"'$ours_name'");
+               }
+
+               var q = new GitgExt.UserQuery.full(_("Merge has conflicts"),
+                                                  message,
+                                                  Gtk.MessageType.QUESTION,
+                                                  _("Cancel"), Gtk.ResponseType.CANCEL,
+                                                  _("Checkout"), Gtk.ResponseType.OK);
+
+               if ((yield application.user_query_async(q)) != Gtk.ResponseType.OK)
+               {
+                       notification.error(_("Merge failed with conflicts"));
+                       return false;
+               }
+
+               if (!(yield stash_if_needed(notification, head)))
+               {
+                       return false;
+               }
+
+               if (!ishead)
+               {
+                       // Perform checkout of the local branch first
+                       var checkout = new RefActionCheckout(application, action_interface, reference);
+
+                       if (!(yield checkout.checkout()))
+                       {
+                               notification.error(_("Merge failed with conflicts"));
+                               return false;
+                       }
+               }
+
+               // Finally, checkout the conflicted index
+               try
+               {
+                       yield Async.thread(() => {
+                               var opts = new Ggit.CheckoutOptions();
+                               opts.set_strategy(Ggit.CheckoutStrategy.SAFE);
+                               application.repository.checkout_index(index, opts);
+                       });
+               }
+               catch (Error err)
+               {
+                       notification.error(_("Failed to checkout conflicts: %s").printf(err.message));
+                       return false;
+               }
+
+               // Write the merge state files
+               var wd = application.repository.get_location().get_path();
+
+               try
+               {
+                       var dest_oid = reference.resolve().get_target();
+
+                       FileUtils.set_contents(Path.build_filename(wd, "ORIG_HEAD"), 
"%s\n".printf(dest_oid.to_string()));
+               } catch {}
+
+               try
+               {
+                       var source_oid = source.resolve().get_target();
+
+                       FileUtils.set_contents(Path.build_filename(wd, "MERGE_HEAD"), 
"%s\n".printf(source_oid.to_string()));
+               } catch {}
+
+               try
+               {
+                       FileUtils.set_contents(Path.build_filename(wd, "MERGE_MODE"), "no-ff\n");
+               } catch {}
+
+               try
+               {
+                       string msg;
+
+                       if (source.parsed_name.rtype == RefType.REMOTE)
+                       {
+                               msg = @"Merge remote branch '$theirs_name'";
+                       }
+                       else
+                       {
+                               msg = @"Merge branch '$theirs_name'";
+                       }
+
+                       msg += "\n\nConflicts:\n";
+
+                       var entries = index.get_entries();
+                       var seen = new Gee.HashSet<string>();
+
+                       for (var i = 0; i < entries.size(); i++)
+                       {
+                               var entry = entries.get_by_index(i);
+                               var p = entry.get_path();
+
+                               if (entry.is_conflict() && !seen.contains(p))
+                               {
+                                       msg += "\t%s\n".printf(p);
+                                       seen.add(p);
+                               }
+                       }
+
+                       FileUtils.set_contents(Path.build_filename(wd, "MERGE_MSG"), msg);
+               } catch {}
+
+               notification.success(_("Finished merge with conflicts in working directory"));
+               return true;
+       }
+
+       public async Ggit.OId? merge(Gitg.Ref source)
+       {
+               Ggit.Commit ours;
+               Ggit.Commit theirs;
+
+               var ours_name = reference.parsed_name.shortname;
+               var theirs_name = source.parsed_name.shortname;
+
+               var notification = new SimpleNotification(_("Merge %s into %s").printf(@"'$theirs_name'", 
@"'$ours_name'"));
+               application.notifications.add(notification);
+
+               try
+               {
+                       ours = reference.resolve().lookup() as Ggit.Commit;
+               }
+               catch (Error e)
+               {
+                       notification.error(_("Failed to lookup our commit: %s").printf(e.message));
+                       return null;
+               }
+
+               try
+               {
+                       theirs = source.resolve().lookup() as Ggit.Commit;
+               }
+               catch (Error e)
+               {
+                       notification.error(_("Failed to lookup their commit: %s").printf(e.message));
+                       return null;
+               }
+
+               var index = yield create_merge_index(notification, ours, theirs);
+
+               if (index == null)
+               {
+                       return null;
+               }
+
+               if (index.has_conflicts())
+               {
+                       yield checkout_conflicts(notification, index, source);
+                       return null;
+               }
+
+               var committer = application.get_verified_committer();
+
+               if (committer == null)
+               {
+                       notification.error(_("Failed to obtain author details"));
+                       return null;
+               }
+
+               string msg;
+
+               if (source.parsed_name.rtype == RefType.REMOTE)
+               {
+                       msg = @"Merge remote branch '$theirs_name'";
+               }
+               else
+               {
+                       msg = @"Merge branch '$theirs_name'";
+               }
+
+               var stage = application.repository.stage;
+
+               Gitg.Ref? head = null;
+               var ishead = reference_is_head(ref head);
+
+               Ggit.OId? oid = null;
+               Ggit.Tree? head_tree = null;
+
+               if (ishead)
+               {
+                       if (!(yield stash_if_needed(notification, head)))
+                       {
+                               return null;
+                       }
+
+                       try
+                       {
+                               head_tree = (reference.lookup() as Ggit.Commit).get_tree();
+                       }
+                       catch (Error e)
+                       {
+                               notification.error(_("Failed to obtain HEAD tree: %s").printf(e.message));
+                               return null;
+                       }
+               }
+
+               try
+               {
+                       // TODO: not all hooks are being executed yet
+                       oid = yield stage.commit_index(index,
+                                                      ishead ? head : reference,
+                                                      msg,
+                                                      committer,
+                                                      committer,
+                                                      new Ggit.OId[] { ours.get_id(), theirs.get_id() },
+                                                      StageCommitOptions.NONE);
+               }
+               catch (Error e)
+               {
+                       notification.error(_("Failed to create commit: %s").printf(e.message));
+                       return null;
+               }
+
+               if (ishead)
+               {
+                       try
+                       {
+                               yield Async.thread(() => {
+                                       var opts = new Ggit.CheckoutOptions();
+
+                                       opts.set_strategy(Ggit.CheckoutStrategy.SAFE);
+                                       opts.set_baseline(head_tree);
+
+                                       var commit = application.repository.lookup<Ggit.Commit>(oid);
+                                       var tree = commit.get_tree();
+
+                                       application.repository.checkout_tree(tree, opts);
+                               });
+                       }
+                       catch (Error e)
+                       {
+                               notification.error(_("Failed to checkout index: %s").printf(e.message));
+                               return null;
+                       }
+               }
+
+               notification.success(_("Successfully merged %s into %s").printf(@"'$theirs_name'", 
@"'$ours_name'"));
+               return oid;
+       }
+
+       public void activate_source(Gitg.Ref source)
+       {
+               merge.begin(source, (obj, res) => {
+                       merge.end(res);
+               });
+       }
+
+       private Gitg.Ref? upstream_reference()
+       {
+               var branch = reference as Ggit.Branch;
+
+               if (branch != null)
+               {
+                       try
+                       {
+                               return branch.get_upstream() as Gitg.Ref;
+                       } catch {}
+               }
+
+               return null;
+       }
+
+       private void add_merge_source(Gtk.Menu submenu, Gitg.Ref? source)
+       {
+               if (source == null)
+               {
+                       var sep = new Gtk.SeparatorMenuItem();
+                       sep.show();
+                       submenu.append(sep);
+                       return;
+               }
+
+               var name = source.parsed_name.shortname;
+               var item = new Gtk.MenuItem.with_label(name);
+
+               item.show();
+               item.tooltip_text = _("Merge %s into branch %s").printf(@"'$name'", 
@"'$(reference.parsed_name.shortname)'");
+
+               item.activate.connect(() => {
+                       activate_source(source);
+               });
+
+               submenu.append(item);
+       }
+
+       private void ensure_sources()
+       {
+               if (d_has_sourced)
+               {
+                       return;
+               }
+
+               d_has_sourced = true;
+
+               if (!available)
+               {
+                       return;
+               }
+
+               // Allow merging from remotes and other local branches, offer
+               // to merge upstream first.
+               d_upstream = upstream_reference();
+
+               d_local_sources = new Gitg.Ref[0];
+               d_remote_sources = new RemoteSource[0];
+               d_tag_sources = new Gitg.Ref[0];
+
+               Ggit.OId? target_oid = null;
+
+               try
+               {
+                       target_oid = reference.resolve().get_target();
+               } catch {}
+
+               string? last_remote = null;
+
+               foreach (var r in action_interface.references)
+               {
+                       if (d_upstream != null && r.get_name() == d_upstream.get_name())
+                       {
+                               continue;
+                       }
+
+                       // Filter out things where merging is a noop
+                       if (target_oid != null)
+                       {
+                               Ggit.OId? oid = null;
+
+                               try
+                               {
+                                       oid = r.resolve().get_target();
+                               } catch {}
+
+                               if (oid != null && oid.equal(target_oid))
+                               {
+                                       continue;
+                               }
+                       }
+
+                       if (r.is_branch())
+                       {
+                               d_local_sources += r;
+                       }
+                       else if (r.is_tag())
+                       {
+                               d_tag_sources += r;
+                       }
+                       else if (r.parsed_name.rtype == RefType.REMOTE)
+                       {
+                               var remote_name = r.parsed_name.remote_name;
+
+                               if (remote_name != last_remote)
+                               {
+                                       var source = RemoteSource() {
+                                               name = remote_name,
+                                               sources = new Gitg.Ref[] { r }
+                                       };
+
+                                       d_remote_sources += source;
+                               }
+                               else
+                               {
+                                       d_remote_sources[d_remote_sources.length - 1].sources += r;
+                               }
+
+                               last_remote = remote_name;
+                       }
+               }
+       }
+
+       public void populate_menu(Gtk.Menu menu)
+       {
+               if (!available)
+               {
+                       return;
+               }
+
+               var item = new Gtk.MenuItem.with_label(display_name);
+               item.tooltip_text = description;
+
+               if (enabled)
+               {
+                       var submenu = new Gtk.Menu();
+                       submenu.show();
+
+                       if (d_upstream != null)
+                       {
+                               add_merge_source(submenu, d_upstream);
+                       }
+
+                       if (d_local_sources.length != 0)
+                       {
+                               if (d_upstream != null)
+                               {
+                                       // Add a separator
+                                       add_merge_source(submenu, null);
+                               }
+
+                               foreach (var source in d_local_sources)
+                               {
+                                       add_merge_source(submenu, source);
+                               }
+                       }
+
+                       if (d_remote_sources.length != 0)
+                       {
+                               if (d_local_sources.length != 0 || d_upstream != null)
+                               {
+                                       // Add a separator
+                                       add_merge_source(submenu, null);
+                               }
+
+                               foreach (var remote in d_remote_sources)
+                               {
+                                       var subitem = new Gtk.MenuItem.with_label(remote.name);
+                                       subitem.show();
+
+                                       var subsubmenu = new Gtk.Menu();
+                                       subsubmenu.show();
+
+                                       foreach (var source in remote.sources)
+                                       {
+                                               add_merge_source(subsubmenu, source);
+                                       }
+
+                                       subitem.submenu = subsubmenu;
+                                       submenu.append(subitem);
+                               }
+                       }
+
+                       if (d_tag_sources.length != 0)
+                       {
+                               if (d_remote_sources.length != 0 || d_local_sources.length != 0 || d_upstream 
!= null)
+                               {
+                                       // Add a separator
+                                       add_merge_source(submenu, null);
+                               }
+
+                               var subitem = new Gtk.MenuItem.with_label(_("Tags"));
+                               subitem.show();
+
+                               var subsubmenu = new Gtk.Menu();
+                               subsubmenu.show();
+
+                               foreach (var source in d_tag_sources)
+                               {
+                                       add_merge_source(subsubmenu, source);
+                               }
+
+                               subitem.submenu = subsubmenu;
+                               submenu.append(subitem);
+                       }
+
+                       item.submenu = submenu;
+               }
+               else
+               {
+                       item.sensitive = false;
+               }
+
+               item.show();
+               menu.append(item);
+       }
+}
+
+}
+
+// ex:set ts=4 noet
diff --git a/gitg/history/gitg-history.vala b/gitg/history/gitg-history.vala
index 205f543..a85b6cb 100644
--- a/gitg/history/gitg-history.vala
+++ b/gitg/history/gitg-history.vala
@@ -708,7 +708,7 @@ namespace GitgHistory
 
                private Gtk.Menu? popup_menu_for_ref(Gitg.Ref reference)
                {
-                       var actions = new Gee.LinkedList<GitgExt.RefAction>();
+                       var actions = new Gee.LinkedList<GitgExt.RefAction?>();
 
                        var af = new ActionInterface(application, d_main.refs_list);
 
@@ -720,7 +720,23 @@ namespace GitgHistory
                        add_ref_action(actions, new Gitg.RefActionRename(application, af, reference));
                        add_ref_action(actions, new Gitg.RefActionDelete(application, af, reference));
                        add_ref_action(actions, new Gitg.RefActionCopyName(application, af, reference));
-                       add_ref_action(actions, new Gitg.RefActionFetch(application, af, reference));
+
+                       var fetch = new Gitg.RefActionFetch(application, af, reference);
+
+                       if (fetch.available)
+                       {
+                               actions.add(null);
+                       }
+
+                       add_ref_action(actions, fetch);
+
+                       var merge = new Gitg.RefActionMerge(application, af, reference);
+
+                       if (merge.available)
+                       {
+                               actions.add(null);
+                               add_ref_action(actions, merge);
+                       }
 
                        var exts = new Peas.ExtensionSet(Gitg.PluginsEngine.get_default(),
                                                         typeof(GitgExt.RefAction),
@@ -731,7 +747,15 @@ namespace GitgHistory
                                                         "reference",
                                                         reference);
 
+                       var addedsep = false;
+
                        exts.foreach((extset, info, extension) => {
+                               if (!addedsep)
+                               {
+                                       actions.add(null);
+                                       addedsep = true;
+                               }
+
                                add_ref_action(actions, extension as GitgExt.RefAction);
                        });
 
@@ -744,7 +768,16 @@ namespace GitgHistory
 
                        foreach (var ac in actions)
                        {
-                               ac.populate_menu(menu);
+                               if (ac != null)
+                               {
+                                       ac.populate_menu(menu);
+                               }
+                               else
+                               {
+                                       var sep = new Gtk.SeparatorMenuItem();
+                                       sep.show();
+                                       menu.append(sep);
+                               }
                        }
 
                        var sep = new Gtk.SeparatorMenuItem();
diff --git a/libgitg/gitg-stage.vala b/libgitg/gitg-stage.vala
index 08b38d2..49955a0 100644
--- a/libgitg/gitg-stage.vala
+++ b/libgitg/gitg-stage.vala
@@ -375,6 +375,23 @@ public class Stage : Object
                                            Ggit.OId[]?        parents,
                                            StageCommitOptions options) throws Error
        {
+               Ggit.OId? treeoid = null;
+
+               yield Async.thread(() => {
+                       treeoid = index.write_tree_to(d_repository);
+               });
+
+               return yield commit_tree(treeoid, reference, message, author, committer, parents, options);
+       }
+
+       public async Ggit.OId? commit_tree(Ggit.OId           treeoid,
+                                          Ggit.Ref           reference,
+                                          string             message,
+                                          Ggit.Signature     author,
+                                          Ggit.Signature     committer,
+                                          Ggit.OId[]?        parents,
+                                          StageCommitOptions options) throws Error
+       {
                Ggit.OId? ret = null;
 
                yield Async.thread(() => {
@@ -400,8 +417,6 @@ public class Stage : Object
                                emsg = commit_msg_hook(emsg, author, committer);
                        }
 
-                       var treeoid = index.write_tree_to(d_repository);
-
                        Ggit.OId? refoid = null;
 
                        try
diff --git a/tests/gitg/Makefile.am b/tests/gitg/Makefile.am
index 46ef18f..4b8b6f4 100644
--- a/tests/gitg/Makefile.am
+++ b/tests/gitg/Makefile.am
@@ -47,11 +47,13 @@ TESTS_GITG_TEST_GITG_COPIED_SOURCES =                       \
        tests/gitg/support-test.vala                    \
        tests/gitg/support-main.vala                    \
        tests/gitg/support-repository.vala              \
-       tests/gitg/gitg-ref-action-checkout.vala
+       tests/gitg/gitg-ref-action-checkout.vala        \
+       tests/gitg/gitg-ref-action-merge.vala
 
 tests_gitg_test_gitg_SOURCES =                         \
        tests/gitg/main.vala                            \
        tests/gitg/test-checkout-ref.vala               \
+       tests/gitg/test-merge-ref.vala                  \
        tests/gitg/simple-notification-mock.vala        \
        tests/gitg/application-mock.vala                \
        tests/gitg/notifications-mock.vala              \
diff --git a/tests/gitg/application-mock.vala b/tests/gitg/application-mock.vala
index a9c53c4..b8265e2 100644
--- a/tests/gitg/application-mock.vala
+++ b/tests/gitg/application-mock.vala
@@ -21,6 +21,13 @@ using Gitg.Test.Assert;
 
 class Gitg.Test.Application : Gitg.Test.Repository, GitgExt.Application
 {
+       class ExpectedUserQuery : Object
+       {
+               public GitgExt.UserQuery query;
+               public Gtk.ResponseType response;
+       }
+
+       private Gee.ArrayQueue<ExpectedUserQuery> d_expected_queries;
        private Notifications d_notifications;
 
        public Application()
@@ -33,6 +40,7 @@ class Gitg.Test.Application : Gitg.Test.Repository, GitgExt.Application
                base.set_up();
 
                d_notifications = new Notifications();
+               d_expected_queries = new Gee.ArrayQueue<ExpectedUserQuery>();
        }
 
        public Gitg.Repository? repository
@@ -64,13 +72,47 @@ class Gitg.Test.Application : Gitg.Test.Repository, GitgExt.Application
        public GitgExt.Activity? get_activity_by_id(string id) { return null; }
        public GitgExt.Activity? set_activity_by_id(string id) { return null; }
 
+       protected Application expect_user_query(GitgExt.UserQuery query, Gtk.ResponseType response)
+       {
+               d_expected_queries.add(new ExpectedUserQuery() {
+                       query = query,
+                       response = response
+               });
+
+               return this;
+       }
+
+       private Gtk.ResponseType user_query_respond(GitgExt.UserQuery query)
+       {
+               assert_true(d_expected_queries.size > 0);
+
+               var expected = d_expected_queries.poll();
+
+               assert_streq(expected.query.title, query.title);
+               assert_streq(expected.query.message, query.message);
+               assert_inteq(expected.query.message_type, query.message_type);
+               assert_inteq(expected.query.default_response, query.default_response);
+               assert_booleq(expected.query.default_is_destructive, query.default_is_destructive);
+               assert_booleq(expected.query.message_use_markup, query.message_use_markup);
+               assert_inteq(expected.query.responses.length, query.responses.length);
+
+               for (var i = 0; i < expected.query.responses.length; i++)
+               {
+                       assert_inteq(expected.query.responses[i].response_type, 
query.responses[i].response_type);
+                       assert_streq(expected.query.responses[i].text, query.responses[i].text);
+               }
+
+               return expected.response;
+       }
+
        public void user_query(GitgExt.UserQuery query)
        {
+               query.response(user_query_respond(query));
        }
 
        public async Gtk.ResponseType user_query_async(GitgExt.UserQuery query)
        {
-               return Gtk.ResponseType.CLOSE;
+               return user_query_respond(query);
        }
 
        public void show_infobar(string          primary_msg,
diff --git a/tests/gitg/main.vala b/tests/gitg/main.vala
index 9673d50..77a3498 100644
--- a/tests/gitg/main.vala
+++ b/tests/gitg/main.vala
@@ -23,7 +23,8 @@ class Gitg.Test.Runner
        {
                var m = new Gitg.Test.Main(args);
 
-               m.add(new CheckoutRef());
+               m.add(new CheckoutRef(),
+                     new MergeRef());
 
                m.run();
        }
diff --git a/tests/gitg/simple-notification-mock.vala b/tests/gitg/simple-notification-mock.vala
index 8416451..f516fcc 100644
--- a/tests/gitg/simple-notification-mock.vala
+++ b/tests/gitg/simple-notification-mock.vala
@@ -30,7 +30,7 @@ public class SimpleNotification : Object, GitgExt.Notification
        }
 
        public signal void cancel();
-       public Status status;
+       public Status status { get; set; }
 
        public string title { get; set; }
        public string message { get; set; }
diff --git a/tests/gitg/test-merge-ref.vala b/tests/gitg/test-merge-ref.vala
new file mode 100644
index 0000000..006c99d
--- /dev/null
+++ b/tests/gitg/test-merge-ref.vala
@@ -0,0 +1,482 @@
+/*
+ * This file is part of gitg
+ *
+ * Copyright (C) 2015 - Jesse van den Kieboom
+ *
+ * gitg is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gitg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with gitg. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Gitg.Test.Assert;
+
+class Gitg.Test.MergeRef : Application
+{
+       private Gitg.Branch ours;
+       private Gitg.Branch theirs;
+       private Gitg.Branch master;
+       private Gitg.Branch not_master;
+
+       private RefActionInterface action_interface;
+       private RefActionCheckout action;
+
+       protected override void set_up()
+       {
+               base.set_up();
+
+               commit("a", "a file\n");
+               create_branch("theirs");
+
+               commit("b", "b file\n");
+
+               checkout_branch("theirs");
+               commit("c", "c file\n");
+
+               theirs = lookup_branch("theirs");
+
+               checkout_branch("master");
+               not_master = create_branch("not_master");
+
+               master = lookup_branch("master");
+
+               action_interface = new RefActionInterface(this);
+       }
+
+       private void assert_merged(Ggit.OId ours, Ggit.OId theirs, string name)
+       {
+               var now_commit = lookup_commit(name);
+
+               var parents = now_commit.get_parents();
+               assert_uinteq(parents.size, 2);
+
+               assert_streq(parents[0].get_id().to_string(), ours.to_string());
+               assert_streq(parents[1].get_id().to_string(), theirs.to_string());
+       }
+
+       protected virtual signal void test_merge_simple()
+       {
+               var loop = new MainLoop();
+               var action = new Gitg.RefActionMerge(this, action_interface, master);
+
+               var ours_oid = lookup_commit("master").get_id();
+               var theirs_oid = theirs.get_target();
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'master'");
+               assert_streq(simple_notifications[0].message, "Successfully merged 'theirs' into 'master'");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "c file\n");
+
+               assert_merged(ours_oid, theirs_oid, "master");
+       }
+
+       protected virtual signal void test_merge_not_head()
+       {
+               var loop = new MainLoop();
+               var action = new Gitg.RefActionMerge(this, action_interface, not_master);
+
+               var ours_oid = not_master.get_target();
+               var theirs_oid = theirs.get_target();
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'not_master'");
+               assert_streq(simple_notifications[0].message, "Successfully merged 'theirs' into 
'not_master'");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_true(!file_exists("c"));
+
+               assert_merged(ours_oid, theirs_oid, "not_master");
+       }
+
+       protected virtual signal void test_merge_not_head_would_have_conflicted()
+       {
+               var loop = new MainLoop();
+
+               commit("c", "c file other content\n");
+
+               var ours_oid = not_master.get_target();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, not_master);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'not_master'");
+               assert_streq(simple_notifications[0].message, "Successfully merged 'theirs' into 
'not_master'");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "c file other content\n");
+
+               assert_merged(ours_oid, theirs_oid, "not_master");
+       }
+
+       protected virtual signal void test_merge_theirs_conflicts_no_checkout()
+       {
+               var loop = new MainLoop();
+
+               commit("c", "c file other content\n");
+
+               var ours_oid = lookup_commit("master").get_id();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Merge has conflicts",
+                                                           "The merge of 'theirs' into 'master' has caused 
conflicts, would you like to checkout branch 'master' with the merge to your working directory to resolve the 
conflicts?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Checkout",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.CANCEL);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'master'");
+               assert_streq(simple_notifications[0].message, "Merge failed with conflicts");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.ERROR);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "c file other content\n");
+       }
+
+       protected virtual signal void test_merge_theirs_conflicts_checkout()
+       {
+               var loop = new MainLoop();
+
+               commit("c", "c file other content\n");
+
+               var ours_oid = lookup_commit("master").get_id();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Merge has conflicts",
+                                                            "The merge of 'theirs' into 'master' has caused 
conflicts, would you like to checkout branch 'master' with the merge to your working directory to resolve the 
conflicts?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Checkout",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'master'");
+               assert_streq(simple_notifications[0].message, "Finished merge with conflicts in working 
directory");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "<<<<<<< ours\nc file other content\n=======\nc file\n>>>>>>> 
theirs\n");
+
+               assert_file_contents(".git/ORIG_HEAD", "e1219dd5fbcf8fb5b17bbd3db7a9fa88e98d6651\n");
+               assert_file_contents(".git/MERGE_HEAD", "72af7ccf47852d832b06c7244de8ae9ded639024\n");
+               assert_file_contents(".git/MERGE_MODE", "no-ff\n");
+               assert_file_contents(".git/MERGE_MSG", "Merge branch 'theirs'\n\nConflicts:\n\tc\n");
+       }
+
+       protected virtual signal void test_merge_theirs_dirty_stash()
+       {
+               var loop = new MainLoop();
+
+               write_file("b", "b file other content\n");
+
+               var ours_oid = lookup_commit("master").get_id();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Unstaged changes",
+                                                            "You appear to have unstaged changes in your 
working directory. Would you like to stash the changes before the checkout?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Stash changes",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'master'");
+               assert_streq(simple_notifications[0].message, "Successfully merged 'theirs' into 'master'");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "c file\n");
+
+               var messages = new string[0];
+               var oids = new Ggit.OId[0];
+
+               d_repository.stash_foreach((index, message, oid) => {
+                       messages += message;
+                       oids += oid;
+
+                       return 0;
+               });
+
+               assert_inteq(messages.length, 1);
+               assert_streq(messages[0], "On master: WIP on HEAD: 50ac9b commit b");
+               assert_streq(oids[0].to_string(), "aaf63a72d8c0d5799ccfcf1623daef228968382f");
+       }
+
+       protected virtual signal void test_merge_theirs_not_master_conflicts_checkout()
+       {
+               var loop = new MainLoop();
+
+               checkout_branch("not_master");
+               commit("c", "c file other content\n");
+               not_master = lookup_branch("not_master");
+               checkout_branch("master");
+
+               var ours_oid = not_master.get_target();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, not_master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Merge has conflicts",
+                                                            "The merge of 'theirs' into 'not_master' has 
caused conflicts, would you like to checkout the merge to your working directory to resolve the conflicts?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Checkout",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               expect_user_query(new GitgExt.UserQuery.full("Unstaged changes",
+                                                            "You appear to have unstaged changes in your 
working directory. Would you like to stash the changes before the checkout?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Stash changes",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 2);
+
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'not_master'");
+               assert_streq(simple_notifications[0].message, "Finished merge with conflicts in working 
directory");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_streq(simple_notifications[1].title, "Checkout 'not_master'");
+               assert_streq(simple_notifications[1].message, "Successfully checked out branch to working 
directory");
+               assert_inteq(simple_notifications[1].status, SimpleNotification.Status.SUCCESS);
+
+               assert_streq(lookup_branch("HEAD").get_name(), "refs/heads/not_master");
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "<<<<<<< ours\nc file other content\n=======\nc file\n>>>>>>> 
theirs\n");
+
+               assert_file_contents(".git/ORIG_HEAD", "e1219dd5fbcf8fb5b17bbd3db7a9fa88e98d6651\n");
+               assert_file_contents(".git/MERGE_HEAD", "72af7ccf47852d832b06c7244de8ae9ded639024\n");
+               assert_file_contents(".git/MERGE_MODE", "no-ff\n");
+               assert_file_contents(".git/MERGE_MSG", "Merge branch 'theirs'\n\nConflicts:\n\tc\n");
+       }
+
+       protected virtual signal void test_merge_theirs_not_master_conflicts_checkout_dirty()
+       {
+               var loop = new MainLoop();
+
+               checkout_branch("not_master");
+               commit("c", "c file other content\n");
+               not_master = lookup_branch("not_master");
+               checkout_branch("master");
+
+               write_file("b", "b file other content\n");
+
+               var ours_oid = not_master.get_target();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, not_master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Merge has conflicts",
+                                                            "The merge of 'theirs' into 'not_master' has 
caused conflicts, would you like to checkout the merge to your working directory to resolve the conflicts?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Checkout",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               expect_user_query(new GitgExt.UserQuery.full("Unstaged changes",
+                                                            "You appear to have unstaged changes in your 
working directory. Would you like to stash the changes before the checkout?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Stash changes",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 2);
+
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'not_master'");
+               assert_streq(simple_notifications[0].message, "Finished merge with conflicts in working 
directory");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.SUCCESS);
+
+               assert_streq(simple_notifications[1].title, "Checkout 'not_master'");
+               assert_streq(simple_notifications[1].message, "Successfully checked out branch to working 
directory");
+               assert_inteq(simple_notifications[1].status, SimpleNotification.Status.SUCCESS);
+
+               assert_streq(lookup_branch("HEAD").get_name(), "refs/heads/not_master");
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file\n");
+               assert_file_contents("c", "<<<<<<< ours\nc file other content\n=======\nc file\n>>>>>>> 
theirs\n");
+
+               assert_file_contents(".git/ORIG_HEAD", "e1219dd5fbcf8fb5b17bbd3db7a9fa88e98d6651\n");
+               assert_file_contents(".git/MERGE_HEAD", "72af7ccf47852d832b06c7244de8ae9ded639024\n");
+               assert_file_contents(".git/MERGE_MODE", "no-ff\n");
+               assert_file_contents(".git/MERGE_MSG", "Merge branch 'theirs'\n\nConflicts:\n\tc\n");
+
+               var messages = new string[0];
+               var oids = new Ggit.OId[0];
+
+               d_repository.stash_foreach((index, message, oid) => {
+                       messages += message;
+                       oids += oid;
+
+                       return 0;
+               });
+
+               assert_inteq(messages.length, 1);
+               assert_streq(messages[0], "On master: WIP on HEAD");
+               assert_streq(oids[0].to_string(), "147b7b7b6ad2f9c90f4c93f3bfda78c78ec2dcde");
+       }
+
+       protected virtual signal void test_merge_theirs_not_master_conflicts_checkout_dirty_no_stash()
+       {
+               var loop = new MainLoop();
+
+               checkout_branch("not_master");
+               commit("c", "c file other content\n");
+               not_master = lookup_branch("not_master");
+               checkout_branch("master");
+
+               write_file("b", "b file other content\n");
+
+               var ours_oid = not_master.get_target();
+               var theirs_oid = theirs.get_target();
+
+               var action = new Gitg.RefActionMerge(this, action_interface, not_master);
+
+               expect_user_query(new GitgExt.UserQuery.full("Merge has conflicts",
+                                                            "The merge of 'theirs' into 'not_master' has 
caused conflicts, would you like to checkout the merge to your working directory to resolve the conflicts?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Checkout",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.OK);
+
+               expect_user_query(new GitgExt.UserQuery.full("Unstaged changes",
+                                                            "You appear to have unstaged changes in your 
working directory. Would you like to stash the changes before the checkout?",
+                                                            Gtk.MessageType.QUESTION,
+                                                            "Cancel",
+                                                            Gtk.ResponseType.CANCEL,
+                                                            "Stash changes",
+                                                            Gtk.ResponseType.OK),
+                                      Gtk.ResponseType.CANCEL);
+
+               action.merge.begin(theirs, (obj, res) => {
+                       action.merge.end(res);
+                       loop.quit();
+               });
+
+               loop.run();
+
+               assert_inteq(simple_notifications.size, 1);
+
+               assert_streq(simple_notifications[0].title, "Merge 'theirs' into 'not_master'");
+               assert_streq(simple_notifications[0].message, "Merge failed with conflicts");
+               assert_inteq(simple_notifications[0].status, SimpleNotification.Status.ERROR);
+
+               assert_streq(lookup_branch("HEAD").get_name(), "refs/heads/master");
+
+               assert_file_contents("a", "a file\n");
+               assert_file_contents("b", "b file other content\n");
+               assert(!file_exists("c"));
+
+               assert(!file_exists(".git/ORIG_HEAD"));
+               assert(!file_exists(".git/MERGE_HEAD"));
+               assert(!file_exists(".git/MERGE_MODE"));
+               assert(!file_exists(".git/MERGE_MSG"));
+
+               d_repository.stash_foreach((index, message, oid) => {
+                       assert(false);
+                       return 0;
+               });
+       }
+}
+
+// ex:set ts=4 noet
diff --git a/tests/support/gitg-assert.h b/tests/support/gitg-assert.h
index 561e70c..37ff302 100644
--- a/tests/support/gitg-assert.h
+++ b/tests/support/gitg-assert.h
@@ -23,14 +23,15 @@
 #include <glib.h>
 
 #define gitg_test_assert_assert_no_error(error) g_assert_no_error(error)
-#define gitg_test_assert_assert_streq(a, b) g_assert_cmpstr(a, ==, b)
-#define gitg_test_assert_assert_inteq(a, b) g_assert_cmpint(a, ==, b)
-#define gitg_test_assert_assert_uinteq(a, b) g_assert_cmpuint(a, ==, b)
-#define gitg_test_assert_assert_floateq(a, b) g_assert_cmpfloat(a, ==, b)
+#define gitg_test_assert_assert_streq(a, b) g_assert_cmpstr((a), ==, (b))
+#define gitg_test_assert_assert_inteq(a, b) g_assert_cmpint((a), ==, (b))
+#define gitg_test_assert_assert_booleq(a, b) g_assert_cmpuint((guint)(a), ==, (guint)(b))
+#define gitg_test_assert_assert_uinteq(a, b) g_assert_cmpuint((a), ==, (b))
+#define gitg_test_assert_assert_floateq(a, b) g_assert_cmpfloat((a), ==, (b))
 #define gitg_test_assert_assert_datetime(a, b) \
-       g_assert_cmpstr (g_date_time_format (a, "%F %T %z"), \
+       g_assert_cmpstr (g_date_time_format ((a), "%F %T %z"), \
                         ==, \
-                        g_date_time_format (b, "%F %T %z") \
+                        g_date_time_format ((b), "%F %T %z") \
 )
 
 #endif /* __GITG_ASSERT_H__ */
diff --git a/tests/support/gitg-assert.vapi b/tests/support/gitg-assert.vapi
index bca04c4..e6bfebe 100644
--- a/tests/support/gitg-assert.vapi
+++ b/tests/support/gitg-assert.vapi
@@ -4,6 +4,7 @@ namespace Gitg.Test.Assert
        public static void assert_no_error(GLib.Error e);
        public static void assert_streq(string a, string b);
        public static void assert_inteq(int a, int b);
+       public static void assert_booleq(bool a, bool b);
        public static void assert_uinteq(uint a, uint b);
        public static void assert_floateq(float a, float b);
        public static void assert_datetime(GLib.DateTime a, GLib.DateTime b);
diff --git a/tests/support/repository.vala b/tests/support/repository.vala
index 3271cec..f4bcee3 100644
--- a/tests/support/repository.vala
+++ b/tests/support/repository.vala
@@ -397,10 +397,8 @@ class Gitg.Test.Repository : Gitg.Test.Test
                                        remove_recursively(c);
                                }
                        }
-                       else
-                       {
-                               f.delete();
-                       }
+
+                       f.delete();
                }
                catch (Error e)
                {
@@ -408,6 +406,40 @@ class Gitg.Test.Repository : Gitg.Test.Test
                }
        }
 
+       protected Gitg.Branch? lookup_branch(string name)
+       {
+               try
+               {
+                       var ret = d_repository.lookup_reference_dwim(name) as Gitg.Branch;
+                       assert_nonnull(ret);
+
+                       return ret;
+               }
+               catch (Error e)
+               {
+                       assert_no_error(e);
+               }
+
+               return null;
+       }
+
+       protected Gitg.Commit? lookup_commit(string name)
+       {
+               try
+               {
+                       var ret = lookup_branch(name).lookup() as Gitg.Commit;
+                       assert_nonnull(ret);
+
+                       return ret;
+               }
+               catch (Error e)
+               {
+                       assert_no_error(e);
+               }
+
+               return null;
+       }
+
        protected override void tear_down()
        {
                if (d_repository == null)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]