diff --git a/src/Dialogs/BranchActions/BranchActionDialog.vala b/src/Dialogs/BranchActions/BranchActionDialog.vala new file mode 100644 index 000000000..b264484b7 --- /dev/null +++ b/src/Dialogs/BranchActions/BranchActionDialog.vala @@ -0,0 +1,101 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +public enum Scratch.BranchAction { + CHECKOUT, + COMMIT, + PUSH, + PULL, + MERGE, + DELETE, + CREATE +} + +public interface Scratch.BranchActionPage : Gtk.Widget { + public abstract BranchAction action { get; } + public abstract Ggit.Ref? branch_ref { get; } + public abstract string new_branch_name { get; } + public virtual void focus_start_widget () {} +} + +public class Scratch.Dialogs.BranchActionDialog : Granite.MessageDialog { + public signal void page_activated (); + + public BranchAction action { + get { + return ((BranchActionPage)stack.get_visible_child ()).action; + } + } + + public Ggit.Ref branch_ref { + get { + return ((BranchActionPage)stack.get_visible_child ()).branch_ref; + } + } + + public string new_branch_name { + get { + return ((BranchActionPage)stack.get_visible_child ()).new_branch_name; + } + } + + public bool can_apply { get; set; default = false; } + public FolderManager.ProjectFolderItem project { get; construct; } + + private Gtk.Stack stack; + + public BranchActionDialog (FolderManager.ProjectFolderItem project) { + Object ( + project: project + ); + } + + construct { + transient_for = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + add_button (_("Cancel"), Gtk.ResponseType.CANCEL); + if (project.is_git_repo) { + primary_text = _("Perform branch action on project '%s'").printf ( + project.file.file.get_basename () + ); + primary_label.can_focus = false; + image_icon = new ThemedIcon ("git"); + var apply_button = add_button (_("Apply"), Gtk.ResponseType.APPLY); + bind_property ("can-apply", apply_button, "sensitive", SYNC_CREATE); + stack = new Gtk.Stack (); + var checkout_page = new BranchCheckoutPage (this); + var create_page = new BranchCreatePage (this); + stack.add_titled (checkout_page, BranchAction.CHECKOUT.to_string (), _("Checkout")); + stack.add_titled (create_page, BranchAction.CREATE.to_string (), _("New")); + + var sidebar = new Gtk.StackSidebar () { + stack = stack + }; + + var content_box = new Gtk.Box (HORIZONTAL, 12); + content_box.add (sidebar); + content_box.add (stack); + + custom_bin.add (content_box); + custom_bin.show_all (); + } else { + primary_text = _("'%s' is not a git repository").printf ( + project.file.file.get_basename () + ); + secondary_text = _("Unable to perform branch actions"); + image_icon = new ThemedIcon ("dialog-error"); + } + + realize.connect (() => { + ((BranchActionPage)stack.get_visible_child ()).focus_start_widget (); + }); + + page_activated.connect (() => { + if (can_apply) { + response (Gtk.ResponseType.APPLY); + } + }); + } +} diff --git a/src/Dialogs/BranchActions/BranchCheckoutPage.vala b/src/Dialogs/BranchActions/BranchCheckoutPage.vala new file mode 100644 index 000000000..b68e1be18 --- /dev/null +++ b/src/Dialogs/BranchActions/BranchCheckoutPage.vala @@ -0,0 +1,48 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ + +public class Scratch.Dialogs.BranchCheckoutPage : Gtk.Box, BranchActionPage { + public BranchAction action { + get { + return BranchAction.CHECKOUT; + } + } + + public Ggit.Ref? branch_ref { + get { + return list_box.get_selected_row ().bref; + } + } + + public string new_branch_name { + get { + return ""; + } + } + + public BranchActionDialog dialog { get; construct; } + + private BranchListBox list_box; + + public BranchCheckoutPage (BranchActionDialog dialog) { + Object ( + dialog: dialog + ); + } + + construct { + list_box = new BranchListBox (dialog, true); + add (list_box); + list_box.branch_changed.connect ((text) => { + dialog.can_apply = dialog.project.has_branch_name (text, null); + }); + } + + public override void focus_start_widget () { + list_box.grab_focus (); + } +} diff --git a/src/Dialogs/BranchActions/BranchCreatePage.vala b/src/Dialogs/BranchActions/BranchCreatePage.vala new file mode 100644 index 000000000..675b7d802 --- /dev/null +++ b/src/Dialogs/BranchActions/BranchCreatePage.vala @@ -0,0 +1,71 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ + +public class Scratch.Dialogs.BranchCreatePage : Gtk.Box, BranchActionPage { + public BranchAction action { + get { + return BranchAction.CREATE; + } + } + + public Ggit.Ref? branch_ref { + get { + return null; + } + } + + public string new_branch_name { + get { + return new_branch_name_entry.text; + } + } + + public BranchActionDialog dialog { get; construct; } + + private Granite.ValidatedEntry new_branch_name_entry; + + public BranchCreatePage (BranchActionDialog dialog) { + Object ( + dialog: dialog + ); + } + + construct { + orientation = VERTICAL; + vexpand = false; + hexpand = true; + margin_start = 24; + spacing = 12; + valign = CENTER; + var label = new Granite.HeaderLabel (_("Name of branch to create")); + new_branch_name_entry = new Granite.ValidatedEntry () { + activates_default = true, + placeholder_text = _("Enter new branch name") + }; + + add (label); + add (new_branch_name_entry); + + new_branch_name_entry.bind_property ("is-valid", dialog, "can-apply"); + + new_branch_name_entry.changed.connect (() => { + unowned var new_name = new_branch_name_entry.text; + if (!dialog.project.is_valid_new_branch_name (new_name)) { + new_branch_name_entry.is_valid = false; + return; + } + + if (dialog.project.has_local_branch_name (new_name)) { + new_branch_name_entry.is_valid = false; + return; + } + + //Do we need to check remote branches as well? + new_branch_name_entry.is_valid = true; + }); + } +} diff --git a/src/Dialogs/BranchActions/BranchListBox.vala b/src/Dialogs/BranchActions/BranchListBox.vala new file mode 100644 index 000000000..dc36072ce --- /dev/null +++ b/src/Dialogs/BranchActions/BranchListBox.vala @@ -0,0 +1,151 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +private class Scratch.Dialogs.BranchListBox : Gtk.Bin { + public signal void branch_changed (string branch_name); + public string text { + get { + return search_entry.text; + } + } + + public bool show_remotes { get; construct;} + public BranchActionDialog dialog { get; construct;} + + private Gtk.ListBox list_box; + private Gtk.SearchEntry search_entry; + private Gtk.Label local_header; + private Gtk.Label remote_header; + private Gtk.Label recent_header; + + public BranchListBox (BranchActionDialog dialog, bool show_remotes) { + Object ( + dialog: dialog, + show_remotes: show_remotes + ); + } + + construct { + list_box = new Gtk.ListBox () { + activate_on_single_click = false + }; + var scrolled_window = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = NEVER, + vscrollbar_policy = AUTOMATIC, + min_content_height = 200, + vexpand = true + }; + scrolled_window.child = list_box; + search_entry = new Gtk.SearchEntry () { + placeholder_text = _("Enter search term") + }; + var box = new Gtk.Box (VERTICAL, 6); + box.add (search_entry); + box.add (scrolled_window); + child = box; + + recent_header = new Granite.HeaderLabel (_("Recent Branches")); + local_header = new Granite.HeaderLabel (_("Local Branches")); + remote_header = new Granite.HeaderLabel (_("Remote Branches")); + var branch_refs = dialog.project.get_all_branch_refs (); + + foreach (var branch_ref in branch_refs) { + if (branch_ref.is_branch () || show_remotes) { + var row = new BranchNameRow (branch_ref); + if (dialog.project.is_recent_ref (branch_ref)) { + row.is_recent = true; + } + list_box.add (row); + } + } + list_box.set_sort_func (listbox_sort_func); + list_box.set_header_func (listbox_header_func); + list_box.row_selected.connect ((listboxrow) => { + //We want cursor to end up after the inserted text + search_entry.text = ((BranchNameRow)(listboxrow)).branch_name; + search_entry.grab_focus_without_selecting (); + search_entry.move_cursor (DISPLAY_LINE_ENDS, 1, false); + }); + list_box.row_activated.connect ((listboxrow) => { + dialog.page_activated (); + }); + list_box.set_filter_func ((listboxrow) => { + return (((BranchNameRow)(listboxrow)).branch_name.contains (search_entry.text)); + }); + search_entry.changed.connect (() => { + list_box.invalidate_filter (); + // recent_header.unparent (); + // local_header.unparent (); + // remote_header.unparent (); + // list_box.invalidate_headers (); + branch_changed (text); + }); + search_entry.activate.connect (() => { + dialog.page_activated (); + }); + } + + public BranchNameRow? get_selected_row () { + int index = 0; + var row = list_box.get_row_at_index (index); + while (row != null && + ((BranchNameRow)row).branch_name != search_entry.text) { + + row = list_box.get_row_at_index (++index); + } + + return (BranchNameRow)row; + } + + + private int listbox_sort_func (Gtk.ListBoxRow rowa, Gtk.ListBoxRow rowb) { + var a = (BranchNameRow)(rowa); + var b = (BranchNameRow)(rowb); + + if (a.is_recent && !b.is_recent) { + return -1; + } else if (b.is_recent && !a.is_recent) { + return 1; + } + + if (a.is_remote && !b.is_remote) { + return 1; + } else if (b.is_remote && !a.is_remote) { + return -1; + } + + return (a.branch_name.collate (b.branch_name)); + } + + private void listbox_header_func (Gtk.ListBoxRow row, Gtk.ListBoxRow? row_before) { + var a = (BranchNameRow)row; + var b = (BranchNameRow?)row_before; + a.set_header (null); + if (b == null) { + if (a.is_recent) { + a.set_header (recent_header); + } else if (!a.is_remote) { + a.set_header (local_header); + } else { + a.set_header (remote_header); + } + } else if (b.is_recent) { + if (!a.is_remote) { + a.set_header (local_header); + } else if (a.is_remote) { + a.set_header (remote_header); + } + } else if (!b.is_remote) { + if (a.is_remote) { + a.set_header (remote_header); + } + } + } + + public new void grab_focus () { + search_entry.grab_focus (); + } +} diff --git a/src/Dialogs/BranchActions/BranchNameRow.vala b/src/Dialogs/BranchActions/BranchNameRow.vala new file mode 100644 index 000000000..8e6f66f4e --- /dev/null +++ b/src/Dialogs/BranchActions/BranchNameRow.vala @@ -0,0 +1,35 @@ +/* + * Copyright 2025 elementary, Inc. + * SPDX-License-Identifier: GPL-3.0-or-later +* +* Authored by: Jeremy Wootten +*/ +private class Scratch.Dialogs.BranchNameRow : Gtk.ListBoxRow { + public Ggit.Ref bref { get; construct; } + public bool is_remote { get; private set; } + public bool is_recent { get; set; default = false; } + + public string branch_name { + get { + return label.label; + } + } + + private Gtk.Label label; + + public BranchNameRow (Ggit.Ref bref) { + Object ( + bref: bref + ); + } + + construct { + is_remote = !bref.is_branch (); + label = new Gtk.Label (bref.get_shorthand ()) { + halign = START, + margin_start = 24 + }; + + child = label; + } +} diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 2bb5035b3..4921bbfe4 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -32,8 +32,6 @@ 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_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"; @@ -133,14 +131,20 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane return; } + set_active_project (path); + } + + private ProjectFolderItem? set_active_project (string path) { var folder_root = find_path (root, path) as ProjectFolderItem; if (folder_root == null) { - return; + return null; } git_manager.active_project_path = path; write_settings (); + + return folder_root; } public async void restore_saved_state () { @@ -209,6 +213,52 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } } + public void branch_actions (string path) { + // Must only carry out branch actions on active project so switch if necessary. + //TODO Warn before switching active project? + var active_project = set_active_project (path); + if (active_project == null || !active_project.is_git_repo) { + Gdk.beep (); + return; + } + + var dialog = new Dialogs.BranchActionDialog (active_project); + dialog.response.connect ((res) => { + if (res == Gtk.ResponseType.APPLY) { + perform_branch_action (dialog); + } + + dialog.destroy (); + }); + + dialog.present (); + } + + private void perform_branch_action ( + Scratch.Dialogs.BranchActionDialog dialog + ) { + switch (dialog.action) { + case CHECKOUT: + dialog.project.checkout_branch_ref (dialog.branch_ref); + break; + case COMMIT: + break; + case PUSH: + break; + case PULL: + break; + case MERGE: + break; + case DELETE: + break; + case CREATE: + dialog.project.new_branch (dialog.new_branch_name); + break; + default: + assert_not_reached (); + } + } + private unowned Code.Widgets.SourceList.Item? find_path ( Code.Widgets.SourceList.ExpandableItem list, string path, @@ -288,26 +338,6 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane } } - public void new_branch (string active_project_path) { - unowned var active_project = (ProjectFolderItem)(find_path (root, active_project_path)); - if (active_project == null || !active_project.is_git_repo) { - Gdk.beep (); - return; - } - - string? branch_name = null; - var dialog = new Dialogs.NewBranchDialog (active_project); - dialog.show_all (); - if (dialog.run () == Gtk.ResponseType.APPLY) { - branch_name = dialog.new_branch_name; - } - - dialog.destroy (); - if (branch_name != null) { - active_project.new_branch (branch_name); - } - } - public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { plugins.hook_folder_item_change (source, dest, event); } diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 78c2ecdae..5b139abb6 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -26,8 +26,8 @@ namespace Scratch.FolderManager { private static Icon added_icon; private static Icon modified_icon; - private SimpleAction checkout_local_branch_action; - private SimpleAction checkout_remote_branch_action; + // private SimpleAction checkout_local_branch_action; + // private SimpleAction checkout_remote_branch_action; public signal void closed (); @@ -69,7 +69,7 @@ namespace Scratch.FolderManager { ); } - checkout_local_branch_action.set_state (monitored_repo.branch_name); + // checkout_local_branch_action.set_state (monitored_repo.branch_name); } } @@ -77,23 +77,11 @@ namespace Scratch.FolderManager { monitored_repo = Scratch.Services.GitManager.get_instance ().add_project (this); notify["name"].connect (branch_or_name_changed); if (monitored_repo != null) { - 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, - "" - ); monitored_repo.branch_changed.connect (branch_or_name_changed); monitored_repo.ignored_changed.connect ((deprioritize_git_ignored)); monitored_repo.file_status_change.connect (() => update_item_status (null)); monitored_repo.update_status_map (); monitored_repo.branch_changed (); - checkout_local_branch_action.activate.connect (handle_checkout_local_branch_action); - checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); } } @@ -179,7 +167,14 @@ namespace Scratch.FolderManager { var folder_actions_section = new GLib.Menu (); folder_actions_section.append_item (create_submenu_for_new ()); if (monitored_repo != null) { - folder_actions_section.append_item (create_submenu_for_branch ()); + var branch_action_item = new MenuItem ( + _("Branch Actions…"), + GLib.Action.print_detailed_name ( + MainWindow.ACTION_PREFIX + MainWindow.ACTION_BRANCH_ACTIONS, + new Variant.string (file.path) + ) + ); + folder_actions_section.append_item (branch_action_item); } var close_folder_item = new GLib.MenuItem ( @@ -308,98 +303,6 @@ namespace Scratch.FolderManager { return menu; } - protected GLib.MenuItem create_submenu_for_branch () { - // Ensures that action for relevant project is being used - 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 ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - file.path - ) - ); - - new_branch_item.set_attribute_value ( - "accel", - Utils.get_accel_for_action ( - GLib.Action.print_detailed_name ( - MainWindow.ACTION_PREFIX + MainWindow.ACTION_NEW_BRANCH, - "" - ) - ) - ); - - GLib.Menu bottom_section = new GLib.Menu (); - bottom_section.append_item (new_branch_item); - - var menu = new GLib.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_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) { - warning ("Failed to change branch to %s. %s", branch_name, e.message); - } - } - - 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"); @@ -459,6 +362,29 @@ namespace Scratch.FolderManager { }); } + public bool checkout_branch_ref (Ggit.Ref branch_ref) { + if (branch_ref.is_branch ()) { + try { + var branch_name = ((Ggit.Branch)branch_ref).get_name (); + if (branch_name != null) { + return monitored_repo.change_local_branch (branch_name); + } else { + return false; + } + } catch (Error e) { + warning ("Failed to get branch name from ref. %s", e.message); + return false; + } + } else { + var target_shorthand = branch_ref.get_shorthand (); + if (target_shorthand != null) { + return monitored_repo.checkout_remote_branch (target_shorthand); + } else { + return false; + } + } + } + public void new_branch (string branch_name) { try { if (monitored_repo.head_is_branch) { @@ -483,14 +409,36 @@ namespace Scratch.FolderManager { } } - public unowned List get_branch_names () { - return is_git_repo ? monitored_repo.get_local_branches () : null; + public bool is_recent_ref (Ggit.Ref bref) { + return monitored_repo.is_recent_ref (bref); + } + + public Gee.ArrayList get_all_branch_refs () requires (is_git_repo) { + return monitored_repo.get_all_branch_refs (); } public bool has_local_branch_name (string name) { return is_git_repo ? monitored_repo.has_local_branch_name (name) : false; } + public bool has_remote_branch_name (string name) { + return is_git_repo ? monitored_repo.has_remote_branch_name (name) : false; + } + + public bool has_branch_name (string name, out bool found_is_remote) { + found_is_remote = false; + // var is_remote = false; + var found = false; + if (!has_local_branch_name (name)) { + found = has_remote_branch_name (name); + found_is_remote = found; + } else { + found = true; + } + + return found; + } + public string get_current_branch_name () { return is_git_repo ? monitored_repo.branch_name : ""; } diff --git a/src/MainWindow.vala b/src/MainWindow.vala index bcf37aeb6..16c6112da 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -113,7 +113,7 @@ namespace Scratch { public const string ACTION_NEXT_TAB = "action-next-tab"; public const string ACTION_PREVIOUS_TAB = "action-previous-tab"; public const string ACTION_CLEAR_LINES = "action-clear-lines"; - public const string ACTION_NEW_BRANCH = "action-new-branch"; + public const string ACTION_BRANCH_ACTIONS = "action-branch-actions"; public const string ACTION_CLOSE_TAB = "action-close-tab"; public const string ACTION_CLOSE_TABS_TO_RIGHT = "action-close-tabs-to-right"; public const string ACTION_CLOSE_OTHER_TABS = "action-close-other-tabs"; @@ -173,7 +173,7 @@ namespace Scratch { { ACTION_NEXT_TAB, action_next_tab }, { ACTION_PREVIOUS_TAB, action_previous_tab }, { ACTION_CLEAR_LINES, action_clear_lines }, - { ACTION_NEW_BRANCH, action_new_branch, "s" }, + { ACTION_BRANCH_ACTIONS, action_branch_actions, "s" }, { ACTION_ADD_MARK, action_add_mark}, { ACTION_PREVIOUS_MARK, action_previous_mark}, { ACTION_NEXT_MARK, action_next_mark}, @@ -245,7 +245,7 @@ namespace Scratch { action_accelerators.set (ACTION_PREVIOUS_TAB, "Tab"); action_accelerators.set (ACTION_PREVIOUS_TAB, "Page_Up"); action_accelerators.set (ACTION_CLEAR_LINES, "K"); //Geany - action_accelerators.set (ACTION_NEW_BRANCH + "::", "B"); + action_accelerators.set (ACTION_BRANCH_ACTIONS + "::", "B"); action_accelerators.set (ACTION_ADD_MARK, "equal"); action_accelerators.set (ACTION_PREVIOUS_MARK, "Left"); action_accelerators.set (ACTION_NEXT_MARK, "Right"); @@ -1484,8 +1484,8 @@ namespace Scratch { doc.source_view.clear_selected_lines (); } - private void action_new_branch (SimpleAction action, Variant? param) { - folder_manager_view.new_branch (get_target_path_for_actions (param)); + private void action_branch_actions (SimpleAction action, Variant? param) { + folder_manager_view.branch_actions (get_target_path_for_actions (param)); } private void action_previous_mark () { diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index e48b874c0..1b5ed620c 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -68,7 +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; + private Gee.ArrayList all_branch_refs; + private Gee.HashSet recently_used_branches; public Gee.Set> non_current_entries { owned get { @@ -101,7 +102,16 @@ namespace Scratch.Services { null ); - remote_branch_ref_list = new List (); + all_branch_refs = new Gee.ArrayList (); + recently_used_branches = new Gee.HashSet ( + (v) => { + return v.get_name ().hash (); + }, + + (va, vb) => { + return va.get_name () == vb.get_name (); + } + ); } public MonitoredRepository (Ggit.Repository _git_repo) { @@ -157,50 +167,32 @@ namespace Scratch.Services { return ""; } - //Returns an alphabetically sorted list of local branch names - public unowned List get_local_branches () { - unowned List branches = null; + public Gee.ArrayList get_all_branch_refs () { + all_branch_refs = new Gee.ArrayList (); try { var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.LOCAL); foreach (Ggit.Ref branch_ref in branch_enumerator) { - if (branch_ref is Ggit.Branch) { - branches.insert_sorted ( - ((Ggit.Branch)branch_ref).get_name (), - string.collate - ); - } + all_branch_refs.add (branch_ref); } - } catch (Error e) { - warning ("Could not enumerate branches %s", e.message); - } - - return branches; - } - - //Returns an alphabetically sorted list of remote branch names - public unowned List get_remote_branches () { - unowned List branch_names = null; - try { - var branch_enumerator = git_repo.enumerate_branches (Ggit.BranchType.REMOTE); + 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.insert_sorted ( - branch_ref.get_shorthand (), - string.collate - ); + all_branch_refs.add (branch_ref); } - - remote_branch_ref_list.append (branch_ref); } } catch (Error e) { warning ("Could not enumerate local branches %s", e.message); } - return branch_names; + return all_branch_refs; + } + + public bool is_recent_ref (Ggit.Ref bref) { + return bref.is_branch () && recently_used_branches.contains (bref); } public bool has_local_branch_name (string name) { @@ -212,6 +204,15 @@ namespace Scratch.Services { return false; } + public bool has_remote_branch_name (string name) { + try { + git_repo.lookup_branch (name, Ggit.BranchType.REMOTE); + return true; + } catch (Error e) {} + + return false; + } + public bool is_valid_new_local_branch_name (string new_name) { if (!Ggit.Ref.is_valid_name ("refs/heads/" + new_name) || has_local_branch_name (new_name) ) { @@ -221,26 +222,33 @@ namespace Scratch.Services { return true; } - public void change_local_branch (string new_branch_name) throws Error + public bool change_local_branch (string new_branch_name) requires (!new_branch_name.has_prefix (ORIGIN_PREFIX)) { - var branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); - checkout_branch (branch); + Ggit.Branch branch; + try { + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + } catch (Error e) { + warning ("failed to lookup branch %s", new_branch_name); + return false; + } + + return checkout_branch (branch); } - public void checkout_remote_branch (string target_shorthand) throws Error + public bool checkout_remote_branch (string target_shorthand) requires (target_shorthand.has_prefix (ORIGIN_PREFIX)) { - Ggit.Ref? branch_ref; + Ggit.Ref? branch_ref = null; //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; + foreach (var bref in all_branch_refs) { + // Can assume that all brefs in this list have a non-null shorthand + if (bref.get_shorthand () == target_shorthand) { + branch_ref = bref; + break; + } } - 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), @@ -252,26 +260,74 @@ namespace Scratch.Services { dialog.response.connect (() => {dialog.destroy ();}); dialog.present (); - return; + return false; + } + + Ggit.Object commit; + try { + commit = branch_ref.lookup (); + } catch (Error e) { + warning ("Failed to lookup commit. %s", e.message); + return false; } - 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); + Ggit.Branch local_branch; + try { + local_branch = git_repo.create_branch (local_name, commit, NONE); + } catch (Error e) { + warning ("Failed to create branch. %s", e.message); + return false; + } + + if (checkout_branch (local_branch)) { + try { + local_branch.set_upstream (target_shorthand); + return true; + } catch (Error e) { + warning ("Failed to set upstream. %s", e.message); + } + } + + return false; } - private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { - var new_branch_name = ""; - try { - new_branch_name = new_head_branch.get_name (); - if (confirm && has_uncommitted) { - confirm_checkout_branch (new_head_branch); - return; + private bool checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { + var new_branch_name = ((Ggit.Ref) new_head_branch).get_name (); + if (new_branch_name == null) { + return false; + } + + if (confirm && has_uncommitted) { + var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + // var new_branch_name = new_head_branch.get_name (); + string project_diff; + try { + project_diff = get_project_diff (); + } catch (Error e) { + warning ("Failed to get project diff. %s", e.message); + return false; } - git_repo.set_head (((Ggit.Ref) new_head_branch).get_name ()); + var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog ( + parent, + new_branch_name, + project_diff + ); + dialog.response.connect ((res) => { + dialog.destroy (); + if (res == Gtk.ResponseType.ACCEPT) { + checkout_branch (new_head_branch, false); + } + }); + + dialog.present (); + // confirm_checkout_branch (new_head_branch); + return false; + } + + try { + git_repo.set_head (new_branch_name); var options = new Ggit.CheckoutOptions () { //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) strategy = Ggit.CheckoutStrategy.FORCE @@ -279,6 +335,8 @@ namespace Scratch.Services { git_repo.checkout_head (options); branch_name = new_branch_name; + //TODO limit recent to ? branches and persist + recently_used_branches.add ((Ggit.Ref)new_head_branch); } catch (Error e) { var dialog = new Granite.MessageDialog.with_image_from_icon_name ( _("An error occurred while checking out the requested branch"), @@ -286,33 +344,24 @@ namespace Scratch.Services { "dialog-warning" ); - dialog.run (); - dialog.destroy (); + dialog.response.connect (dialog.destroy); + dialog.present (); + return false; } - } - private void confirm_checkout_branch (Ggit.Branch new_head_branch) throws Error { - var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); - var new_branch_name = new_head_branch.get_name (); - var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog ( - parent, - new_branch_name, - get_project_diff () - ); - dialog.response.connect ((res) => { - dialog.destroy (); - if (res == Gtk.ResponseType.ACCEPT) { - checkout_branch (new_head_branch, false); - } - }); - - dialog.present (); + return true; } - public void create_new_branch (string name) throws Error { - Ggit.Object git_object = git_repo.get_head ().lookup (); - var new_branch = git_repo.create_branch (name, git_object, Ggit.CreateFlags.NONE); - git_repo.set_head (((Ggit.Ref)new_branch).get_name ()); + public bool create_new_branch (string name) { + try { + Ggit.Object git_object = git_repo.get_head ().lookup (); + var new_branch = git_repo.create_branch (name, git_object, Ggit.CreateFlags.NONE); + git_repo.set_head (((Ggit.Ref)new_branch).get_name ()); + } catch (Error e) { + warning ("Failed to create new branch. %s", e.message); + } + + return true; } private bool do_update = false; diff --git a/src/meson.build b/src/meson.build index b5a5aca59..9e69442a2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -24,7 +24,12 @@ code_files = files( 'Dialogs/CloneRepositoryDialog.vala', 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', - 'Dialogs/NewBranchDialog.vala', + # 'Dialogs/NewBranchDialog.vala', + 'Dialogs/BranchActions/BranchActionDialog.vala', + 'Dialogs/BranchActions/BranchCheckoutPage.vala', + 'Dialogs/BranchActions/BranchCreatePage.vala', + 'Dialogs/BranchActions/BranchListBox.vala', + 'Dialogs/BranchActions/BranchNameRow.vala', 'FolderManager/File.vala', 'FolderManager/FileItem.vala', 'FolderManager/FileView.vala',