diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 6acb5a9c05..b8b614a3d0 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -32,7 +32,8 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane public const string ACTION_DELETE = "delete"; public const string ACTION_NEW_FILE = "new-file"; public const string ACTION_NEW_FOLDER = "new-folder"; - public const string ACTION_CHANGE_BRANCH = "change-branch"; + public const string ACTION_CHECKOUT_LOCAL_BRANCH = "checkout-local-branch"; + public const string ACTION_CHECKOUT_REMOTE_BRANCH = "checkout-remote-branch"; public const string ACTION_CLOSE_FOLDER = "close-folder"; public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; public const string ACTION_SET_ACTIVE_PROJECT = "set-active-project"; diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 90ba56c59a..c53be4d553 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -26,7 +26,8 @@ namespace Scratch.FolderManager { private static Icon added_icon; private static Icon modified_icon; - private SimpleAction change_branch_action; + private SimpleAction checkout_local_branch_action; + private SimpleAction checkout_remote_branch_action; public signal void closed (); @@ -68,7 +69,7 @@ namespace Scratch.FolderManager { ); } - change_branch_action.set_state (monitored_repo.branch_name); + checkout_local_branch_action.set_state (monitored_repo.branch_name); } } @@ -76,8 +77,13 @@ namespace Scratch.FolderManager { monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); notify["name"].connect (branch_or_name_changed); if (monitored_repo != null) { - change_branch_action = new SimpleAction.stateful ( - FileView.ACTION_CHANGE_BRANCH, + checkout_local_branch_action = new SimpleAction.stateful ( + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, + GLib.VariantType.STRING, + "" + ); + checkout_remote_branch_action = new SimpleAction.stateful ( + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, GLib.VariantType.STRING, "" ); @@ -86,7 +92,8 @@ namespace Scratch.FolderManager { monitored_repo.file_status_change.connect (() => update_item_status (null)); monitored_repo.update_status_map (); monitored_repo.branch_changed (); - change_branch_action.activate.connect (handle_change_branch_action); + checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action); + checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); } } @@ -315,20 +322,44 @@ namespace Scratch.FolderManager { protected GLib.MenuItem create_submenu_for_branch () { // Ensures that action for relevant project is being used - view.actions.add_action (change_branch_action); - - GLib.Menu branch_selection_menu = new GLib.Menu (); - foreach (unowned var branch_name in monitored_repo.get_local_branches ()) { - branch_selection_menu.append ( - branch_name, - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHANGE_BRANCH, - branch_name - ) - ); + view.actions.add_action (checkout_local_branch_action); + view.actions.add_action (checkout_remote_branch_action); + + unowned var local_branches = monitored_repo.get_local_branches (); + var local_branch_submenu = new Menu (); + var local_branch_menu = new Menu (); + if (local_branches.length () > 0) { + local_branch_submenu.append_submenu (_("Local"), local_branch_menu); + foreach (unowned var branch_name in local_branches) { + local_branch_menu.append ( + branch_name, + GLib.Action.print_detailed_name ( + FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, + branch_name + ) + ); + } } + unowned var remote_branches = monitored_repo.get_remote_branches (); + var remote_branch_submenu = new Menu (); + var remote_branch_menu = new Menu (); + if (remote_branches.length () > 0) { + remote_branch_submenu.append_submenu (_("Remote"), remote_branch_menu); + foreach (unowned var branch_name in remote_branches) { + remote_branch_menu.append ( + branch_name, + GLib.Action.print_detailed_name ( + FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, + branch_name + ) + ); + } + + + } + var new_branch_item = new GLib.MenuItem ( _("New Branch…"), GLib.Action.print_detailed_name ( @@ -351,15 +382,16 @@ namespace Scratch.FolderManager { bottom_section.append_item (new_branch_item); var menu = new GLib.Menu (); - menu.append_section (null, branch_selection_menu); + menu.append_section (null, local_branch_submenu); + menu.append_section (null, remote_branch_submenu); menu.append_section (null, bottom_section); var menu_item = new GLib.MenuItem.submenu (_("Branch"), menu); return menu_item; } - private void handle_change_branch_action (GLib.Variant? parameter) { - var branch_name = parameter.get_string (); + private void handle_checkout_local_branch_action (GLib.Variant? param) { + var branch_name = param != null ? param.get_string () : ""; try { monitored_repo.change_local_branch (branch_name); } catch (GLib.Error e) { @@ -367,6 +399,19 @@ namespace Scratch.FolderManager { } } + private void handle_checkout_remote_branch_action (GLib.Variant? param) { + var branch_name = param != null ? param.get_string () : ""; + if (branch_name == "") { + return; + } + + try { + monitored_repo.checkout_remote_branch (branch_name); + } catch (GLib.Error e) { + warning ("Failed to change branch to %s. %s", branch_name, e.message); + } + } + public void update_item_status (FolderItem? start_folder) { if (monitored_repo == null) { debug ("Ignore non-git folders"); diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 33606af089..9ba30c4224 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -29,6 +29,7 @@ namespace Scratch.Services { } public class MonitoredRepository : Object { + public const string ORIGIN_PREFIX = "origin/"; public Ggit.Repository git_repo { get; set construct; } public string branch_name { get { @@ -67,6 +68,8 @@ namespace Scratch.Services { // Need to use nullable status in order to pass Flatpak CI. private Gee.HashMap file_status_map; + private List remote_branch_ref_list; + public Gee.Set> non_current_entries { owned get { return file_status_map.entries; @@ -97,6 +100,8 @@ namespace Scratch.Services { Ggit.StatusShow.INDEX_AND_WORKDIR, null ); + + remote_branch_ref_list = new List (); } public MonitoredRepository (Ggit.Repository _git_repo) { @@ -168,6 +173,28 @@ namespace Scratch.Services { return branches; } + public unowned List get_remote_branches () { + unowned List branch_names = null; + try { + var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.REMOTE); + + foreach (Ggit.Ref branch_ref in branch_enumerator) { + var remote_name = branch_ref.get_shorthand (); + if (!remote_name.has_suffix ("HEAD") && + !has_local_branch_name (remote_name.substring (ORIGIN_PREFIX.length))) { + + branch_names.append (branch_ref.get_shorthand ()); + } + + remote_branch_ref_list.append (branch_ref); + } + } catch (Error e) { + warning ("Could not enumerate local branches %s", e.message); + } + + return branch_names; + } + public bool has_local_branch_name (string name) { try { git_repo.lookup_branch (name, Ggit.BranchType.LOCAL); @@ -186,11 +213,47 @@ namespace Scratch.Services { return true; } - public void change_local_branch (string new_branch_name) throws Error { + public void change_local_branch (string new_branch_name) throws Error + requires (!new_branch_name.has_prefix (ORIGIN_PREFIX)) { + var branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); checkout_branch (branch); } + public void checkout_remote_branch (string target_shorthand) throws Error + requires (target_shorthand.has_prefix (ORIGIN_PREFIX)) { + + Ggit.Ref? branch_ref; + //Assume list is up to date as this is called from context menu + unowned var list_pointer = remote_branch_ref_list.first (); + while (list_pointer.data != null && + list_pointer.data.get_shorthand () != target_shorthand) { + + list_pointer = list_pointer.next; + } + + branch_ref = list_pointer.data; + if (branch_ref == null) { + var dialog = new Granite.MessageDialog.with_image_from_icon_name ( + _("Remote Branch '%s' not found").printf (target_shorthand), + _("The requested branch was not found in any remote linked to this repository"), + "dialog-warning" + ) { + modal = true + }; + + dialog.response.connect (() => {dialog.destroy ();}); + dialog.present (); + return; + } + + var commit = branch_ref.lookup (); + var local_name = target_shorthand.substring (ORIGIN_PREFIX.length); + var local_branch = git_repo.create_branch (local_name, commit, NONE) as Ggit.Branch; + checkout_branch (local_branch); + local_branch.set_upstream (target_shorthand); + } + private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { var new_branch_name = ""; try { diff --git a/vapi/libgit2-glib-1.0.vapi b/vapi/libgit2-glib-1.0.vapi index 6ed3eca1be..add7762a71 100644 --- a/vapi/libgit2-glib-1.0.vapi +++ b/vapi/libgit2-glib-1.0.vapi @@ -77,6 +77,7 @@ namespace Ggit { public Ggit.Ref? get_upstream () throws GLib.Error; public bool is_head () throws GLib.Error; public Ggit.Branch? move (string new_branch_name, Ggit.CreateFlags flags) throws GLib.Error; + public void set_upstream (string upstream_branch_name) throws GLib.Error; } [CCode (cheader_filename = "libgit2-glib/ggit.h", ref_function = "ggit_branch_enumerator_ref", type_id = "ggit_branch_enumerator_get_type ()", unref_function = "ggit_branch_enumerator_unref")] [Compact]