Commit 881cb12a authored by Jesse van den Kieboom's avatar Jesse van den Kieboom

Implement cherry picking of single commits

parent 0f81278a
......@@ -63,6 +63,7 @@ gitg_gitg_VALASOURCES = \
gitg/gitg-commit-action-create-branch.vala \
gitg/gitg-commit-action-create-patch.vala \
gitg/gitg-commit-action-create-tag.vala \
gitg/gitg-commit-action-cherry-pick.vala \
gitg/gitg-create-branch-dialog.vala \
gitg/gitg-create-tag-dialog.vala \
gitg/gitg-dash-view.vala \
......
......@@ -35,7 +35,9 @@ public class ActionSupport : Object
public async bool working_directory_dirty()
{
var options = new Ggit.StatusOptions(0, Ggit.StatusShow.WORKDIR_ONLY, null);
var options = new Ggit.StatusOptions(Ggit.StatusOption.EXCLUDE_SUBMODULES,
Ggit.StatusShow.WORKDIR_ONLY,
null);
var is_dirty = false;
yield Async.thread_try(() => {
......@@ -133,7 +135,7 @@ public class ActionSupport : Object
if ((yield application.user_query_async(q)) != Gtk.ResponseType.OK)
{
notification.error(_("Merge failed with conflicts"));
notification.error(_("Failed with conflicts"));
return false;
}
......@@ -146,7 +148,7 @@ public class ActionSupport : Object
return true;
}
public async bool checkout_conflicts(SimpleNotification notification, Gitg.Ref reference, Ggit.Index index, Gitg.Ref source, Gitg.Ref? head)
public async bool checkout_conflicts(SimpleNotification notification, Gitg.Ref reference, Ggit.Index index, Gitg.Ref? head)
{
if (!(yield stash_if_needed(notification, head)))
{
......@@ -182,6 +184,103 @@ public class ActionSupport : Object
return true;
}
public async Ggit.OId? commit_index(SimpleNotification notification,
Gitg.Ref reference,
Ggit.Index index,
owned Ggit.OId[]? parents,
Ggit.Signature? author,
string message)
{
var committer = application.get_verified_committer();
if (committer == null)
{
notification.error(_("Failed to obtain author details"));
return null;
}
if (author == null)
{
author = committer;
}
var stage = application.repository.stage;
Gitg.Ref? head = null;
var ishead = reference_is_head(reference, ref head);
Ggit.OId? oid = null;
Ggit.Tree? head_tree = null;
Gitg.Commit? commit = null;
try
{
commit = reference.lookup() as Gitg.Commit;
}
catch (Error e)
{
notification.error(_("Failed to lookup commit: %s").printf(e.message));
return null;
}
if (ishead)
{
if (!(yield stash_if_needed(notification, head)))
{
return null;
}
head_tree = commit.get_tree();
}
if (parents == null)
{
parents = new Ggit.OId[] { commit.get_id() };
}
try
{
// TODO: not all hooks are being executed yet
oid = yield stage.commit_index(index,
ishead ? head : reference,
message,
author,
committer,
parents,
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 newcommit = application.repository.lookup<Ggit.Commit>(oid);
var newtree = newcommit.get_tree();
application.repository.checkout_tree(newtree, opts);
});
}
catch (Error e)
{
notification.error(_("Failed to checkout index: %s").printf(e.message));
return null;
}
}
return oid;
}
}
}
......
/*
* 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 CommitActionCherryPick : GitgExt.UIElement, GitgExt.Action, GitgExt.CommitAction, 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.Commit commit { get; construct set; }
private Gitg.Ref[]? d_destinations;
private ActionSupport d_support;
public CommitActionCherryPick(GitgExt.Application application,
GitgExt.RefActionInterface action_interface,
Gitg.Commit commit)
{
Object(application: application,
action_interface: action_interface,
commit: commit);
d_support = new ActionSupport(application, action_interface);
}
public string id
{
owned get { return "/org/gnome/gitg/commit-actions/cherry-pick"; }
}
public string display_name
{
owned get { return _("Cherry pick onto"); }
}
public string description
{
owned get { return _("Cherry pick this commit onto a branch"); }
}
public bool available
{
get { return true; }
}
public bool enabled
{
get
{
if (commit.get_parents().get_size() > 1)
{
return false;
}
ensure_destinations();
return d_destinations.length != 0;
}
}
private void ensure_destinations()
{
if (d_destinations != null)
{
return;
}
d_destinations = new Gitg.Ref[0];
foreach (var r in action_interface.references)
{
if (r.is_branch())
{
try
{
var c = r.lookup() as Ggit.Commit;
if (!c.get_id().equal(commit.get_id()))
{
d_destinations += r;
}
} catch {}
}
}
}
private async Ggit.Index? create_index(SimpleNotification notification, Gitg.Ref destination)
{
Gitg.Commit? theirs = null;
string theirs_name = destination.parsed_name.shortname;
try
{
theirs = destination.lookup() as Gitg.Commit;
}
catch (Error e)
{
notification.error(_("Failed to lookup the commit for branch %s: %s").printf(@"'$theirs_name'", e.message));
return null;
}
var merge_options = new Ggit.MergeOptions();
Ggit.Index? index = null;
try
{
yield Async.thread(() => {
index = application.repository.cherry_pick_commit(commit, theirs, 0, merge_options);
});
}
catch (Error e)
{
notification.error(_("Failed to cherry-pick the commit: %s").printf(e.message));
return null;
}
return index;
}
private async bool checkout_conflicts(SimpleNotification notification, Ggit.Index index, Gitg.Ref destination)
{
var ours_name = commit.get_id().to_string()[0:6];
var theirs_name = destination.parsed_name.shortname;
notification.message = _("Cherry pick has conflicts");
Gitg.Ref? head = null;
var ishead = d_support.reference_is_head(destination, ref head);
string message;
if (ishead)
{
message = _("The cherry pick of %s onto %s has caused conflicts, would you like to checkout branch %s with the cherry pick to your working directory to resolve the conflicts?").printf(@"'$ours_name'", @"'$theirs_name'", @"'$theirs_name'");
}
else
{
message = _("The cherry-pick of %s onto %s has caused conflicts, would you like to checkout the cherry pick to your working directory to resolve the conflicts?").printf(@"'$ours_name'", @"'$theirs_name'");
}
var q = new GitgExt.UserQuery.full(_("Cherry pick 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(_("Cherry pick failed with conflicts"));
return false;
}
if (!(yield d_support.checkout_conflicts(notification, destination, index, head)))
{
return false;
}
write_cherry_pick_state_files();
notification.success(_("Cherry pick finished with conflicts in working directory"));
return true;
}
private void write_cherry_pick_state_files()
{
var wd = application.repository.get_location().get_path();
try
{
FileUtils.set_contents(Path.build_filename(wd, "CHERRY_PICK_HEAD"), "%s\n".printf(commit.get_id().to_string()));
} catch {}
}
public async void cherry_pick(Gitg.Ref destination)
{
var id = commit.get_id();
var shortid = id.to_string()[0:6];
var name = destination.parsed_name.shortname;
var notification = new SimpleNotification(_("Cherry pick %s onto %s").printf(@"'$shortid'", @"'$name'"));
application.notifications.add(notification);
var index = yield create_index(notification, destination);
if (index == null)
{
return;
}
if (index.has_conflicts())
{
yield checkout_conflicts(notification, index, destination);
return;
}
var oid = yield d_support.commit_index(notification,
destination,
index,
null,
commit.get_author(),
commit.get_message());
if (oid != null) {
notification.success(_("Successfully cherry picked"));
}
}
private void activate_destination(Gitg.Ref destination)
{
cherry_pick.begin(destination, (obj, res) => {
cherry_pick.end(res);
});
}
public void populate_menu(Gtk.Menu menu)
{
if (!available)
{
return;
}
ensure_destinations();
if (!enabled)
{
return;
}
var item = new Gtk.MenuItem.with_label(display_name);
item.tooltip_text = description;
item.show();
var submenu = new Gtk.Menu();
submenu.show();
foreach (var dest in d_destinations)
{
var name = dest.parsed_name.shortname;
var subitem = new Gtk.MenuItem.with_label(name);
subitem.tooltip_text = _("Cherry pick onto %s").printf(@"'$name'");
subitem.show();
subitem.activate.connect(() => {
activate_destination(dest);
});
submenu.append(subitem);
}
item.submenu = submenu;
menu.append(item);
}
public void activate()
{
}
}
}
// ex:set ts=4 noet
......@@ -200,7 +200,7 @@ class RefActionMerge : GitgExt.UIElement, GitgExt.Action, GitgExt.RefAction, Obj
return false;
}
if (!(yield d_support.checkout_conflicts(notification, reference, index, source, head)))
if (!(yield d_support.checkout_conflicts(notification, reference, index, head)))
{
return false;
}
......@@ -255,14 +255,6 @@ class RefActionMerge : GitgExt.UIElement, GitgExt.Action, GitgExt.RefAction, Obj
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)
......@@ -274,73 +266,18 @@ class RefActionMerge : GitgExt.UIElement, GitgExt.Action, GitgExt.RefAction, Obj
msg = @"Merge branch '$theirs_name'";
}
var stage = application.repository.stage;
Gitg.Ref? head = null;
var ishead = d_support.reference_is_head(reference, ref head);
Ggit.OId? oid = null;
Ggit.Tree? head_tree = null;
if (ishead)
{
if (!(yield d_support.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;
}
var oid = yield d_support.commit_index(notification,
reference,
index,
new Ggit.OId[] { ours.get_id(), theirs.get_id() },
null,
msg);
if (ishead)
if (oid != null)
{
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'"));
}
notification.success(_("Successfully merged %s into %s").printf(@"'$theirs_name'", @"'$ours_name'"));
return oid;
}
......
......@@ -675,6 +675,11 @@ namespace GitgHistory
af,
commit));
add_commit_action(actions,
new Gitg.CommitActionCherryPick(application,
af,
commit));
var exts = new Peas.ExtensionSet(Gitg.PluginsEngine.get_default(),
typeof(GitgExt.CommitAction),
"application",
......
......@@ -49,12 +49,14 @@ TESTS_GITG_TEST_GITG_COPIED_SOURCES = \
tests/gitg/support-repository.vala \
tests/gitg/gitg-ref-action-checkout.vala \
tests/gitg/gitg-ref-action-merge.vala \
tests/gitg/gitg-commit-action-cherry-pick.vala \
tests/gitg/gitg-action-support.vala
tests_gitg_test_gitg_SOURCES = \
tests/gitg/main.vala \
tests/gitg/test-checkout-ref.vala \
tests/gitg/test-merge-ref.vala \
tests/gitg/test-cherry-pick-commit.vala \
tests/gitg/simple-notification-mock.vala \
tests/gitg/application-mock.vala \
tests/gitg/notifications-mock.vala \
......
......@@ -24,7 +24,8 @@ class Gitg.Test.Runner
var m = new Gitg.Test.Main(args);
m.add(new CheckoutRef(),
new MergeRef());
new MergeRef(),
new CherryPickCommit());
m.run();
}
......
/*
* 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.CherryPickCommit : Application
{
private Gitg.Branch ours;
private Gitg.Branch theirs;
private Gitg.Commit theirs_commit;
private Gitg.Branch master;
private Gitg.Commit master_commit;
private Gitg.Branch not_master;
private RefActionInterface action_interface;
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");
theirs_commit = theirs.lookup() as Gitg.Commit;
checkout_branch("master");
not_master = create_branch("not_master");
master = lookup_branch("master");
master_commit = master.lookup() as Gitg.Commit;
action_interface = new RefActionInterface(this);
}
protected virtual signal void test_cherry_pick_simple()
{
var loop = new MainLoop();
var action = new Gitg.CommitActionCherryPick(this, action_interface, theirs_commit);
action.cherry_pick.begin(master, (obj, res) => {
action.cherry_pick.end(res);
loop.quit();
});
loop.run();
assert_inteq(simple_notifications.size, 1);
assert_streq(simple_notifications[0].title, "Cherry pick '72af7c' onto 'master'");
assert_streq(simple_notifications[0].message, "Successfully cherry picked");
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 commit = lookup_commit("master");
assert_streq(commit.get_message(), "commit c");
assert_streq(commit.get_id().to_string(), "87aef9f8f4320a9d997d194614d175254c24adc7");
assert_inteq((int)commit.get_author().get_time().to_unix(), 2);
assert_inteq((int)commit.get_committer().get_time().to_unix(), 3);
}
protected virtual signal void test_cherry_pick_not_head()
{
var loop = new MainLoop();
var action = new Gitg.CommitActionCherryPick(this, action_interface, theirs_commit);
action.cherry_pick.begin(not_master, (obj, res) => {
action.cherry_pick.end(res);
loop.quit();
});
loop.run();
assert_inteq(simple_notifications.size, 1);
assert_streq(simple_notifications[0].title, "Cherry pick '72af7c' onto 'not_master'");
assert_streq(simple_notifications[0].message, "Successfully cherry picked");
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"));
var commit = lookup_commit("not_master");
assert_streq(commit.get_message(), "commit c");
assert_streq(commit.get_id().to_string(), "87aef9f8f4320a9d997d194614d175254c24adc7");
assert_inteq((int)commit.get_author().get_time().to_unix(), 2);
assert_inteq((int)commit.get_committer().get_time().to_unix(), 3);
}
protected virtual signal void test_cherry_pick_not_head_would_have_conflicted()
{
var loop = new MainLoop();
commit("c", "c file other content\n");
var action = new Gitg.CommitActionCherryPick(this, action_interface, theirs_commit);
action.cherry_pick.begin(not_master, (obj, res) => {
action.cherry_pick.end(res);
loop.quit();
});
loop.run();
assert_inteq(simple_notifications.size, 1);
assert_streq(simple_notifications[0].title, "Cherry pick '72af7c' onto 'not_master'");
assert_streq(simple_notifications[0].message, "Successfully cherry picked");
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");
var commit = lookup_commit("not_master");
assert_streq(commit.get_message(), "commit c");
assert_streq(commit.get_id().to_string(), "e9e99c25e6061b42b6d48d143e028d1806f85745");
assert_inteq((int)commit.get_author().get_time().to_unix(), 2);
assert_inteq((int)commit.get_committer().get_time().to_unix(), 4);
}
protected virtual signal void test_cherry_pick_theirs_conflicts_no_checkout()
{
var loop = new MainLoop();
commit("c", "c file other content\n");
master = lookup_branch("master");
var action = new Gitg.CommitActionCherryPick(this, action_interface, theirs_commit);