From dae9c48e322653f92f7934ca3f235d33b9b8a766 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 22 Jan 2025 12:35:03 +0000 Subject: [PATCH 01/16] Add remote branch submenu to context menu --- src/FolderManager/ProjectFolderItem.vala | 18 ++++++++-- src/Services/MonitoredRepository.vala | 44 ++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 9998db6be3..3d1db8947b 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -316,6 +316,19 @@ namespace Scratch.FolderManager { ); } + var remote_branch_submenu = new Menu (); + var remote_branch_menu = new Menu (); + remote_branch_submenu.append_submenu (_("Remote Branches"), remote_branch_menu); + + foreach (unowned var branch_name in monitored_repo.get_remote_branches ()) { + remote_branch_menu.append ( + branch_name, + GLib.Action.print_detailed_name ( + FileView.ACTION_PREFIX + FileView.ACTION_CHANGE_BRANCH, + branch_name + ) + ); + } var new_branch_item = new GLib.MenuItem ( _("New Branch…"), @@ -340,14 +353,15 @@ namespace Scratch.FolderManager { var menu = new GLib.Menu (); menu.append_section (null, branch_selection_menu); + 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_change_branch_action (GLib.Variant? param) { + var branch_name = param != null ? param.get_string () : ""; try { monitored_repo.change_branch (branch_name); } catch (GLib.Error e) { diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 0bf8d8c451..e93dc108de 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -30,6 +30,8 @@ namespace Scratch.Services { public class MonitoredRepository : Object { public Ggit.Repository git_repo { get; set construct; } + public Ggit.Remote? remote_origin { get; set construct; } + public Ggit.Repository? remote_origin_repo { get; set construct; } public string branch_name { get { return _branch_name; @@ -127,6 +129,13 @@ namespace Scratch.Services { warning ("An error occurred setting up a file monitor on the gitignore file: %s", e.message); } } + + remote_origin = git_repo.lookup_remote ("origin"); + if (remote_origin != null) { + remote_origin_repo = remote_origin.get_owner (); + } else { + warning ("No remote"); + } } ~MonitoredRepository () { @@ -168,6 +177,26 @@ namespace Scratch.Services { return branches; } + public unowned List get_remote_branches () { + unowned List branches = null; + var offset = "origin/".length; + 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 (offset))) { + branches.append (branch_ref.get_shorthand ()); + } + + } + } catch (Error e) { + warning ("Could not enumerate branches %s", e.message); + } + + return branches; + } + public bool has_local_branch_name (string name) { try { git_repo.lookup_branch (name, Ggit.BranchType.LOCAL); @@ -187,13 +216,24 @@ namespace Scratch.Services { } public void change_branch (string new_branch_name) throws Error { - var branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + Ggit.Ref? branch; + if (new_branch_name.contains ("refs/remote")) { + warning ("Looking up remote branch %s", new_branch_name); + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.REMOTE); + } else { + warning ("Looking up local branch %s", new_branch_name); + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + } + + if (branch == null) { + throw new IOError.NOT_FOUND ("Branch %s not found in this repository".printf (new_branch_name)); + } + git_repo.set_head (((Ggit.Ref)branch).get_name ()); var options = new Ggit.CheckoutOptions () { //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) strategy = Ggit.CheckoutStrategy.FORCE }; - git_repo.checkout_head (options); branch_name = new_branch_name; From 21d9840a7cde056945419697c08a4c39d7debb45 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 22 Jan 2025 13:21:19 +0000 Subject: [PATCH 02/16] Checkout remote branch --- src/FolderManager/FileView.vala | 1 + src/FolderManager/ProjectFolderItem.vala | 25 +++++++++++++++-- src/Services/MonitoredRepository.vala | 35 ++++++++++++++++++------ 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index c9efd85bbf..7531958587 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -33,6 +33,7 @@ public class Scratch.FolderManager.FileView : Code.Widgets.SourceList, Code.Pane 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_REMOTE_BRANCH = "checkout-remote-branch"; public const string ACTION_CLOSE_FOLDER = "close-folder"; public const string ACTION_CLOSE_OTHER_FOLDERS = "close-other-folders"; diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 3d1db8947b..c509519e6b 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -27,6 +27,7 @@ namespace Scratch.FolderManager { private static Icon added_icon; private static Icon modified_icon; private SimpleAction change_branch_action; + private SimpleAction checkout_remote_branch_action; public signal void closed (); @@ -81,12 +82,18 @@ namespace Scratch.FolderManager { 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 (); change_branch_action.activate.connect (handle_change_branch_action); + checkout_remote_branch_action.activate.connect (handle_checkout_remote_branch_action); } } @@ -304,13 +311,14 @@ 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); + view.actions.add_action (checkout_remote_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, + FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, branch_name ) ); @@ -324,7 +332,7 @@ namespace Scratch.FolderManager { remote_branch_menu.append ( branch_name, GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHANGE_BRANCH, + FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, branch_name ) ); @@ -369,6 +377,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 e93dc108de..a90e2601f0 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 Ggit.Remote? remote_origin { get; set construct; } public Ggit.Repository? remote_origin_repo { get; set construct; } @@ -217,22 +218,40 @@ namespace Scratch.Services { public void change_branch (string new_branch_name) throws Error { Ggit.Ref? branch; - if (new_branch_name.contains ("refs/remote")) { - warning ("Looking up remote branch %s", new_branch_name); - branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.REMOTE); - } else { - warning ("Looking up local branch %s", new_branch_name); - branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + assert (!new_branch_name.has_prefix (ORIGIN_PREFIX)); + warning ("Looking up local branch %s", new_branch_name); + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); + + if (branch == null) { + throw new IOError.NOT_FOUND ("Local Branch %s not found".printf (new_branch_name)); } + git_repo.set_head (((Ggit.Ref)branch).get_name ()); + var options = new Ggit.CheckoutOptions () { + //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) + strategy = Ggit.CheckoutStrategy.SAFE + }; + git_repo.checkout_head (options); + + branch_name = new_branch_name; + } + public void checkout_remote_branch (string new_branch_name) throws Error { + Ggit.Ref? branch; + assert (new_branch_name.has_prefix (ORIGIN_PREFIX)); + branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.REMOTE); + if (branch == null) { - throw new IOError.NOT_FOUND ("Branch %s not found in this repository".printf (new_branch_name)); + throw new IOError.NOT_FOUND ("Remote Branch %s not found".printf (new_branch_name)); } + var fetch_opts = new Ggit.FetchOptions (); + remote_origin.download ({new_branch_name}, fetch_opts); + var local_name = new_branch_name.substring (ORIGIN_PREFIX.length); + branch = git_repo.lookup_branch (local_name, Ggit.BranchType.LOCAL); git_repo.set_head (((Ggit.Ref)branch).get_name ()); var options = new Ggit.CheckoutOptions () { //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) - strategy = Ggit.CheckoutStrategy.FORCE + strategy = Ggit.CheckoutStrategy.SAFE }; git_repo.checkout_head (options); From 962fcf7758ffcf1494a24c8170014a37fbe4cb37 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Jan 2025 14:57:49 +0000 Subject: [PATCH 03/16] Update local libgit2-glib-1.0.vapi --- vapi/libgit2-glib-1.0.vapi | 1 + 1 file changed, 1 insertion(+) 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] From 1007710604fcbb214c84a5d676458d2c8deb9f6a Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Jan 2025 14:58:31 +0000 Subject: [PATCH 04/16] Correct action name --- src/FolderManager/ProjectFolderItem.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index c509519e6b..0a73d20c2f 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -318,7 +318,7 @@ namespace Scratch.FolderManager { branch_selection_menu.append ( branch_name, GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, + FileView.ACTION_PREFIX + FileView.ACTION_CHANGE_BRANCH, branch_name ) ); From e04d3c5e687eaf061beefa9af9325b4b40d8c977 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Jan 2025 14:58:51 +0000 Subject: [PATCH 05/16] Get remote branch checkout working --- src/FolderManager/ProjectFolderItem.vala | 2 +- src/Services/MonitoredRepository.vala | 74 +++++++++++------------- 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 0a73d20c2f..5c8915ced3 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -371,7 +371,7 @@ namespace Scratch.FolderManager { private void handle_change_branch_action (GLib.Variant? param) { var branch_name = param != null ? param.get_string () : ""; try { - monitored_repo.change_branch (branch_name); + monitored_repo.change_local_branch (branch_name); } catch (GLib.Error e) { warning ("Failed to change branch to %s. %s", branch_name, e.message); } diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index a90e2601f0..422794d393 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -31,8 +31,6 @@ namespace Scratch.Services { public class MonitoredRepository : Object { public const string ORIGIN_PREFIX = "origin/"; public Ggit.Repository git_repo { get; set construct; } - public Ggit.Remote? remote_origin { get; set construct; } - public Ggit.Repository? remote_origin_repo { get; set construct; } public string branch_name { get { return _branch_name; @@ -70,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; @@ -100,6 +100,8 @@ namespace Scratch.Services { Ggit.StatusShow.INDEX_AND_WORKDIR, null ); + + remote_branch_ref_list = new List (); } public MonitoredRepository (Ggit.Repository _git_repo) { @@ -130,13 +132,6 @@ namespace Scratch.Services { warning ("An error occurred setting up a file monitor on the gitignore file: %s", e.message); } } - - remote_origin = git_repo.lookup_remote ("origin"); - if (remote_origin != null) { - remote_origin_repo = remote_origin.get_owner (); - } else { - warning ("No remote"); - } } ~MonitoredRepository () { @@ -179,23 +174,26 @@ namespace Scratch.Services { } public unowned List get_remote_branches () { - unowned List branches = null; + unowned List branch_names = null; var offset = "origin/".length; 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 (offset))) { - branches.append (branch_ref.get_shorthand ()); + if (!remote_name.has_suffix ("HEAD") && + !has_local_branch_name (remote_name.substring (offset))) { + + branch_names.append (branch_ref.get_shorthand ()); } + remote_branch_ref_list.append (branch_ref); } } catch (Error e) { - warning ("Could not enumerate branches %s", e.message); + warning ("Could not enumerate local branches %s", e.message); } - return branches; + return branch_names; } public bool has_local_branch_name (string name) { @@ -216,46 +214,44 @@ namespace Scratch.Services { return true; } - public void change_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)) { + + //TODO Check current head has no uncommitted changes. Ggit.Ref? branch; - assert (!new_branch_name.has_prefix (ORIGIN_PREFIX)); - warning ("Looking up local branch %s", new_branch_name); branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); - if (branch == null) { - throw new IOError.NOT_FOUND ("Local Branch %s not found".printf (new_branch_name)); - } - git_repo.set_head (((Ggit.Ref)branch).get_name ()); var options = new Ggit.CheckoutOptions () { - //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) strategy = Ggit.CheckoutStrategy.SAFE }; git_repo.checkout_head (options); branch_name = new_branch_name; } - public void checkout_remote_branch (string new_branch_name) throws Error { - Ggit.Ref? branch; - assert (new_branch_name.has_prefix (ORIGIN_PREFIX)); - branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.REMOTE); - if (branch == null) { - throw new IOError.NOT_FOUND ("Remote Branch %s not found".printf (new_branch_name)); + 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; } - var fetch_opts = new Ggit.FetchOptions (); - remote_origin.download ({new_branch_name}, fetch_opts); - var local_name = new_branch_name.substring (ORIGIN_PREFIX.length); - branch = git_repo.lookup_branch (local_name, Ggit.BranchType.LOCAL); - git_repo.set_head (((Ggit.Ref)branch).get_name ()); - var options = new Ggit.CheckoutOptions () { - //Ensure documents match checked out branch (deal with potential conflicts/losses beforehand) - strategy = Ggit.CheckoutStrategy.SAFE - }; - git_repo.checkout_head (options); + branch_ref = list_pointer.data; + if (branch_ref == null) { + //TODO Warn user + return; + } - branch_name = new_branch_name; + 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; + local_branch.set_upstream (local_branch.get_name ()); } public void create_new_branch (string name) throws Error { From 089190f74a99c357d53b2a4eafcfb6acec1cfa4d Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Jan 2025 15:07:30 +0000 Subject: [PATCH 06/16] Simplify --- src/Services/MonitoredRepository.vala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 422794d393..c59df49edf 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -175,14 +175,13 @@ namespace Scratch.Services { public unowned List get_remote_branches () { unowned List branch_names = null; - var offset = "origin/".length; 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 (offset))) { + !has_local_branch_name (remote_name.substring (ORIGIN_PREFIX.length))) { branch_names.append (branch_ref.get_shorthand ()); } From e1a12d6ef909f2c9fa709071a3bb3eb0fe859d19 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 23 Jan 2025 15:54:33 +0000 Subject: [PATCH 07/16] Do not show Remote Branch option when no remote branches --- src/FolderManager/ProjectFolderItem.vala | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 5c8915ced3..a02b909298 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -324,18 +324,24 @@ namespace Scratch.FolderManager { ); } + unowned var remote_branches = monitored_repo.get_remote_branches (); var remote_branch_submenu = new Menu (); var remote_branch_menu = new Menu (); - remote_branch_submenu.append_submenu (_("Remote Branches"), remote_branch_menu); + if (remote_branches.length () > 0) { + remote_branch_submenu.append_submenu (_("Remote Branches"), 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 + ) + ); + } - foreach (unowned var branch_name in monitored_repo.get_remote_branches ()) { - remote_branch_menu.append ( - branch_name, - GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_REMOTE_BRANCH, - branch_name - ) - ); + + } else { + warning ("No remote branches"); } var new_branch_item = new GLib.MenuItem ( From df09e257d394fac7ab3d26b7739a5ca037d482a6 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 25 Jan 2025 13:43:56 +0000 Subject: [PATCH 08/16] Revert to CheckoutStrategy.FORCE --- src/Services/MonitoredRepository.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index c59df49edf..18e5abfce8 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -216,13 +216,15 @@ namespace Scratch.Services { public void change_local_branch (string new_branch_name) throws Error requires (!new_branch_name.has_prefix (ORIGIN_PREFIX)) { - //TODO Check current head has no uncommitted changes. + //TODO Check current head has no uncommitted changes.since we FORCE the checkout. Ggit.Ref? branch; branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); git_repo.set_head (((Ggit.Ref)branch).get_name ()); + // This will force all changes in current branch to be overwritten even if uncommitted. + // Using SAFE results in unwanted diff files in the staging area even if there are no uncommitted changes var options = new Ggit.CheckoutOptions () { - strategy = Ggit.CheckoutStrategy.SAFE + strategy = Ggit.CheckoutStrategy.FORCE }; git_repo.checkout_head (options); From 55b39ac8b01b5d486262b64face8b3153cbd5d4e Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 25 Jan 2025 14:52:04 +0000 Subject: [PATCH 09/16] Actually checkout remote branch as head; DRY --- src/Services/MonitoredRepository.vala | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 18e5abfce8..62027f8114 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -216,19 +216,8 @@ namespace Scratch.Services { public void change_local_branch (string new_branch_name) throws Error requires (!new_branch_name.has_prefix (ORIGIN_PREFIX)) { - //TODO Check current head has no uncommitted changes.since we FORCE the checkout. - Ggit.Ref? branch; - branch = git_repo.lookup_branch (new_branch_name, Ggit.BranchType.LOCAL); - - git_repo.set_head (((Ggit.Ref)branch).get_name ()); - // This will force all changes in current branch to be overwritten even if uncommitted. - // Using SAFE results in unwanted diff files in the staging area even if there are no uncommitted changes - var options = new Ggit.CheckoutOptions () { - strategy = Ggit.CheckoutStrategy.FORCE - }; - git_repo.checkout_head (options); - - branch_name = new_branch_name; + 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 @@ -253,6 +242,20 @@ namespace Scratch.Services { var local_name = target_shorthand.substring (ORIGIN_PREFIX.length); var local_branch = git_repo.create_branch (local_name, commit, NONE) as Ggit.Branch; local_branch.set_upstream (local_branch.get_name ()); + checkout_branch (local_branch); + } + + private void checkout_branch (Ggit.Branch new_head_branch) { + //TODO Check current head has no uncommitted changes.since we FORCE the checkout. + git_repo.set_head (((Ggit.Ref)new_head_branch).get_name ()); + // This will force all changes in current branch to be overwritten even if uncommitted. + // Using SAFE results in unwanted diff files in the staging area even if there are no uncommitted changes + var options = new Ggit.CheckoutOptions () { + strategy = Ggit.CheckoutStrategy.FORCE + }; + git_repo.checkout_head (options); + + branch_name = new_head_branch.get_name (); } public void create_new_branch (string name) throws Error { From 167185a0f115d5c1ad4944fc447032a15329ec05 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 25 Jan 2025 15:35:31 +0000 Subject: [PATCH 10/16] Warn if remote branch not found --- src/Services/MonitoredRepository.vala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 62027f8114..927a8b6186 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -234,7 +234,16 @@ namespace Scratch.Services { branch_ref = list_pointer.data; if (branch_ref == null) { - //TODO Warn user + 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; } From bae164c29961f2127c9d1091ef3c8f29d1511f48 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 25 Jan 2025 16:33:04 +0000 Subject: [PATCH 11/16] Confirm overwriting uncommitted changes when checking out branch --- ...verwriteUncommittedConfirmationDialog.vala | 43 +++++++++++++++++++ src/Services/MonitoredRepository.vala | 20 ++++++++- src/meson.build | 1 + 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/Dialogs/OverwriteUncommittedConfirmationDialog.vala diff --git a/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala b/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala new file mode 100644 index 0000000000..efe7c638cb --- /dev/null +++ b/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala @@ -0,0 +1,43 @@ +/* +* Copyright 2025 elementary, Inc. (https://elementary.io) +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License version 3 as published by the Free Software Foundation. +* +* This program 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 Lesser General Public +* License along with this program; if not, write to the +* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +* Boston, MA 02110-1301 USA +*/ + +public class Scratch.Dialogs.OverwriteUncommittedConfirmationDialog : Granite.MessageDialog { + + public string branch_name { get; construct; } + public OverwriteUncommittedConfirmationDialog (Gtk.Window parent, string new_branch_name) { + Object ( + buttons: Gtk.ButtonsType.NONE, + transient_for: parent, + branch_name: new_branch_name + ); + } + + construct { + modal = true; + image_icon = new ThemedIcon ("dialog-stop"); + + primary_text = _("There are uncommitted changes in the current branch"); + ///TRANSLATORS '%s' is a placeholder for the name of the branch to be checked out + secondary_text = _("Uncommitted changes will be permanently lost if '%s' is checked out now.\n\nIt is recommended that uncommitted changes are stashed, committed, or reverted before proceeding ").printf (branch_name); + + var proceed_button = (Gtk.Button) add_button (_("Checkout and Overwrite"), Gtk.ResponseType.ACCEPT); + proceed_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + var cancel_button = add_button (_("Do not Checkout"), Gtk.ResponseType.REJECT); + cancel_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + } +} diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 927a8b6186..48e0995444 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -254,8 +254,13 @@ namespace Scratch.Services { checkout_branch (local_branch); } - private void checkout_branch (Ggit.Branch new_head_branch) { + private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { //TODO Check current head has no uncommitted changes.since we FORCE the checkout. + if (confirm && has_uncommitted) { + confirm_checkout_branch (new_head_branch); + return; + } + git_repo.set_head (((Ggit.Ref)new_head_branch).get_name ()); // This will force all changes in current branch to be overwritten even if uncommitted. // Using SAFE results in unwanted diff files in the staging area even if there are no uncommitted changes @@ -267,6 +272,19 @@ namespace Scratch.Services { branch_name = new_head_branch.get_name (); } + private void confirm_checkout_branch (Ggit.Branch new_head_branch) { + var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); + var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog (parent, new_head_branch.get_name ()); + dialog.response.connect ((res) => { + dialog.destroy (); + if (res == Gtk.ResponseType.ACCEPT) { + checkout_branch (new_head_branch, false); + } + }); + + dialog.present (); + } + 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); diff --git a/src/meson.build b/src/meson.build index 08497a1100..a62845ccd0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,7 @@ code_files = files( 'Utils.vala', 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', + 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/File.vala', From 38f37a44eedf8e2e50693431d40e84b64b3ddf50 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sun, 26 Jan 2025 09:05:04 +0000 Subject: [PATCH 12/16] Update POTFILES --- po/POTFILES | 1 + 1 file changed, 1 insertion(+) diff --git a/po/POTFILES b/po/POTFILES index 5b5c700065..1225a73ef7 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -5,6 +5,7 @@ src/Dialogs/GlobalSearchDialog.vala src/Dialogs/NewBranchDialog.vala src/Dialogs/PreferencesDialog.vala src/Dialogs/RestoreConfirmationDialog.vala +src/Dialogs/OverwriteUncommittedConfirmationDialog.vala src/FolderManager/File.vala src/FolderManager/FileItem.vala src/FolderManager/FileView.vala From 2fd5fbcf9b429b750cf5410e82bab6ddf3fe7693 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 29 Jan 2025 11:57:14 +0000 Subject: [PATCH 13/16] Split out confirmation dialog into another PR --- po/POTFILES | 1 - ...verwriteUncommittedConfirmationDialog.vala | 43 ------------------- src/Services/MonitoredRepository.vala | 19 -------- src/meson.build | 1 - 4 files changed, 64 deletions(-) delete mode 100644 src/Dialogs/OverwriteUncommittedConfirmationDialog.vala diff --git a/po/POTFILES b/po/POTFILES index 1225a73ef7..5b5c700065 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -5,7 +5,6 @@ src/Dialogs/GlobalSearchDialog.vala src/Dialogs/NewBranchDialog.vala src/Dialogs/PreferencesDialog.vala src/Dialogs/RestoreConfirmationDialog.vala -src/Dialogs/OverwriteUncommittedConfirmationDialog.vala src/FolderManager/File.vala src/FolderManager/FileItem.vala src/FolderManager/FileView.vala diff --git a/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala b/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala deleted file mode 100644 index efe7c638cb..0000000000 --- a/src/Dialogs/OverwriteUncommittedConfirmationDialog.vala +++ /dev/null @@ -1,43 +0,0 @@ -/* -* Copyright 2025 elementary, Inc. (https://elementary.io) -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License version 3 as published by the Free Software Foundation. -* -* This program 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 Lesser General Public -* License along with this program; if not, write to the -* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, -* Boston, MA 02110-1301 USA -*/ - -public class Scratch.Dialogs.OverwriteUncommittedConfirmationDialog : Granite.MessageDialog { - - public string branch_name { get; construct; } - public OverwriteUncommittedConfirmationDialog (Gtk.Window parent, string new_branch_name) { - Object ( - buttons: Gtk.ButtonsType.NONE, - transient_for: parent, - branch_name: new_branch_name - ); - } - - construct { - modal = true; - image_icon = new ThemedIcon ("dialog-stop"); - - primary_text = _("There are uncommitted changes in the current branch"); - ///TRANSLATORS '%s' is a placeholder for the name of the branch to be checked out - secondary_text = _("Uncommitted changes will be permanently lost if '%s' is checked out now.\n\nIt is recommended that uncommitted changes are stashed, committed, or reverted before proceeding ").printf (branch_name); - - var proceed_button = (Gtk.Button) add_button (_("Checkout and Overwrite"), Gtk.ResponseType.ACCEPT); - proceed_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); - var cancel_button = add_button (_("Do not Checkout"), Gtk.ResponseType.REJECT); - cancel_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); - } -} diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index 48e0995444..208a8fc204 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -255,12 +255,6 @@ namespace Scratch.Services { } private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) { - //TODO Check current head has no uncommitted changes.since we FORCE the checkout. - if (confirm && has_uncommitted) { - confirm_checkout_branch (new_head_branch); - return; - } - git_repo.set_head (((Ggit.Ref)new_head_branch).get_name ()); // This will force all changes in current branch to be overwritten even if uncommitted. // Using SAFE results in unwanted diff files in the staging area even if there are no uncommitted changes @@ -272,19 +266,6 @@ namespace Scratch.Services { branch_name = new_head_branch.get_name (); } - private void confirm_checkout_branch (Ggit.Branch new_head_branch) { - var parent = ((Gtk.Application)(GLib.Application.get_default ())).get_active_window (); - var dialog = new Scratch.Dialogs.OverwriteUncommittedConfirmationDialog (parent, new_head_branch.get_name ()); - dialog.response.connect ((res) => { - dialog.destroy (); - if (res == Gtk.ResponseType.ACCEPT) { - checkout_branch (new_head_branch, false); - } - }); - - dialog.present (); - } - 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); diff --git a/src/meson.build b/src/meson.build index a62845ccd0..08497a1100 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,7 +20,6 @@ code_files = files( 'Utils.vala', 'Dialogs/PreferencesDialog.vala', 'Dialogs/RestoreConfirmationDialog.vala', - 'Dialogs/OverwriteUncommittedConfirmationDialog.vala', 'Dialogs/GlobalSearchDialog.vala', 'Dialogs/NewBranchDialog.vala', 'FolderManager/File.vala', From e9630cd466b3175b7c9a0cb75f45ad4d77dfd10f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 29 Jan 2025 12:05:25 +0000 Subject: [PATCH 14/16] Construct local branch menu like remote one --- src/FolderManager/ProjectFolderItem.vala | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index a02b909298..27870f46b8 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -313,22 +313,28 @@ namespace Scratch.FolderManager { view.actions.add_action (change_branch_action); view.actions.add_action (checkout_remote_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 - ) - ); + 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_CHANGE_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 Branches"), remote_branch_menu); + remote_branch_submenu.append_submenu (_("Remote"), remote_branch_menu); foreach (unowned var branch_name in remote_branches) { remote_branch_menu.append ( branch_name, @@ -340,8 +346,6 @@ namespace Scratch.FolderManager { } - } else { - warning ("No remote branches"); } var new_branch_item = new GLib.MenuItem ( @@ -366,7 +370,7 @@ 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); From dd443a9d69834e5e21ab69e75546bce199324e8f Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Wed, 29 Jan 2025 12:08:35 +0000 Subject: [PATCH 15/16] Consistent action names --- src/FolderManager/FileView.vala | 2 +- src/FolderManager/ProjectFolderItem.vala | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 7531958587..950aa1ba03 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -32,7 +32,7 @@ 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"; diff --git a/src/FolderManager/ProjectFolderItem.vala b/src/FolderManager/ProjectFolderItem.vala index 27870f46b8..a5b4cb0e90 100644 --- a/src/FolderManager/ProjectFolderItem.vala +++ b/src/FolderManager/ProjectFolderItem.vala @@ -26,7 +26,7 @@ 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 (); @@ -69,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); } } @@ -77,8 +77,8 @@ 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, "" ); @@ -92,7 +92,7 @@ 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); } } @@ -310,7 +310,7 @@ 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); + 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 (); @@ -322,7 +322,7 @@ namespace Scratch.FolderManager { local_branch_menu.append ( branch_name, GLib.Action.print_detailed_name ( - FileView.ACTION_PREFIX + FileView.ACTION_CHANGE_BRANCH, + FileView.ACTION_PREFIX + FileView.ACTION_CHECKOUT_LOCAL_BRANCH, branch_name ) ); @@ -378,7 +378,7 @@ namespace Scratch.FolderManager { return menu_item; } - private void handle_change_branch_action (GLib.Variant? param) { + 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); From 23721c53fcd9029b5ec3f95f2c9f4c73d1d08004 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Mon, 3 Feb 2025 14:22:39 +0000 Subject: [PATCH 16/16] Fix set upstream --- src/Services/MonitoredRepository.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/MonitoredRepository.vala b/src/Services/MonitoredRepository.vala index bbac1db9e2..5d935b4e9b 100644 --- a/src/Services/MonitoredRepository.vala +++ b/src/Services/MonitoredRepository.vala @@ -250,8 +250,8 @@ namespace Scratch.Services { 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; - local_branch.set_upstream (local_branch.get_name ()); checkout_branch (local_branch); + local_branch.set_upstream (target_shorthand); } private void checkout_branch (Ggit.Branch new_head_branch, bool confirm = true) throws Error {