From 8d6481c69484ae22e40269e2f514273516277ece Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 20 May 2024 14:39:05 +0800 Subject: [PATCH 01/22] feature: supports to open a repository directly from commandline (#134) --- src/ViewModels/Launcher.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 7d0c9197..c2e57b49 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -32,7 +32,27 @@ namespace SourceGit.ViewModels Pages = new AvaloniaList(); AddNewTab(); - if (Preference.Instance.RestoreTabs) + var commandlines = Environment.GetCommandLineArgs(); + if (commandlines.Length == 2) + { + var path = commandlines[1].Replace('\\', '/'); + var root = new Commands.QueryRepositoryRootPath(path).Result(); + if (string.IsNullOrEmpty(root)) + { + Pages[0].Notifications.Add(new Models.Notification + { + IsError = true, + Message = $"Given path: '{commandlines[1]}' is NOT a valid repository!" + }); + return; + } + + var gitDir = new Commands.QueryGitDir(root).Result(); + var repo = Preference.AddRepository(root, gitDir); + var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); + OpenRepositoryInTab(node, null); + } + else if (Preference.Instance.RestoreTabs) { foreach (var id in Preference.Instance.OpenedTabs) { From 8d726656dc8592b7bfc46bfc106b6a132dcda51a Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 20 May 2024 15:15:08 +0800 Subject: [PATCH 02/22] feature: supports to open repository directly from commandline on Linux --- build/build.linux.sh | 0 build/resources/_common/usr/bin/sourcegit | 2 +- src/ViewModels/Launcher.cs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) mode change 100644 => 100755 build/build.linux.sh diff --git a/build/build.linux.sh b/build/build.linux.sh old mode 100644 new mode 100755 diff --git a/build/resources/_common/usr/bin/sourcegit b/build/resources/_common/usr/bin/sourcegit index f056d708..74670167 100644 --- a/build/resources/_common/usr/bin/sourcegit +++ b/build/resources/_common/usr/bin/sourcegit @@ -1,2 +1,2 @@ #!/bin/bash -exec /opt/sourcegit/sourcegit +exec /opt/sourcegit/sourcegit $1 diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index c2e57b49..bf3ff28d 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -35,14 +35,14 @@ namespace SourceGit.ViewModels var commandlines = Environment.GetCommandLineArgs(); if (commandlines.Length == 2) { - var path = commandlines[1].Replace('\\', '/'); + var path = commandlines[1]; var root = new Commands.QueryRepositoryRootPath(path).Result(); if (string.IsNullOrEmpty(root)) { Pages[0].Notifications.Add(new Models.Notification { IsError = true, - Message = $"Given path: '{commandlines[1]}' is NOT a valid repository!" + Message = $"Given path: '{path}' is NOT a valid repository!" }); return; } From faf2c39056c61f33cb2607214f891466a5c67c1e Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 20 May 2024 17:36:43 +0800 Subject: [PATCH 03/22] refactor: rewrite `Preference.FindOrAddNodeByRepositoryPath` * prevent moving node while opening repository directly from commandline * supports to set parent node while dropping folder to initialize repository --- src/ViewModels/Clone.cs | 2 +- src/ViewModels/Init.cs | 11 +++++++---- src/ViewModels/Launcher.cs | 2 +- src/ViewModels/Preference.cs | 4 ++-- src/ViewModels/Welcome.cs | 4 ++-- src/Views/Welcome.axaml.cs | 4 ++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 103f0505..1e9bcb63 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -113,7 +113,7 @@ namespace SourceGit.ViewModels CallUIThread(() => { var repo = Preference.AddRepository(path, Path.Combine(path, ".git")); - var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); + var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, true); _launcher.OpenRepositoryInTab(node, _page); }); diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index b2c48fc7..a4e27341 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -11,9 +11,11 @@ namespace SourceGit.ViewModels set => SetProperty(ref _targetPath, value); } - public Init(string path) + public Init(string path, RepositoryNode parent) { - TargetPath = path; + _targetPath = path; + _parentNode = parent; + View = new Views.Init() { DataContext = this }; } @@ -31,13 +33,14 @@ namespace SourceGit.ViewModels CallUIThread(() => { var repo = Preference.AddRepository(_targetPath, gitDir); - Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); + Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, _parentNode, true); }); return true; }); } - private string _targetPath; + private string _targetPath = string.Empty; + private RepositoryNode _parentNode = null; } } diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index bf3ff28d..5bdd3c18 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -49,7 +49,7 @@ namespace SourceGit.ViewModels var gitDir = new Commands.QueryGitDir(root).Result(); var repo = Preference.AddRepository(root, gitDir); - var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null); + var node = Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, null, false); OpenRepositoryInTab(node, null); } else if (Preference.Instance.RestoreTabs) diff --git a/src/ViewModels/Preference.cs b/src/ViewModels/Preference.cs index 85f26611..ee387e35 100644 --- a/src/ViewModels/Preference.cs +++ b/src/ViewModels/Preference.cs @@ -363,7 +363,7 @@ namespace SourceGit.ViewModels return FindNodeRecursive(id, _instance.RepositoryNodes); } - public static RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent) + public static RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode) { var node = FindNodeRecursive(repo, _instance.RepositoryNodes); if (node == null) @@ -378,7 +378,7 @@ namespace SourceGit.ViewModels AddNode(node, parent); } - else + else if (shouldMoveNode) { MoveNode(node, parent); } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index d4569d05..45bf7614 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -26,7 +26,7 @@ namespace SourceGit.ViewModels } } - public void InitRepository(string path) + public void InitRepository(string path, RepositoryNode parent) { if (!Preference.Instance.IsGitConfigured) { @@ -36,7 +36,7 @@ namespace SourceGit.ViewModels if (PopupHost.CanCreatePopup()) { - PopupHost.ShowPopup(new Init(path)); + PopupHost.ShowPopup(new Init(path, parent)); } } diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index f0e3a1fc..d57c50f8 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -248,7 +248,7 @@ namespace SourceGit.Views { Dispatcher.UIThread.Invoke(() => { - (DataContext as ViewModels.Welcome).InitRepository(path); + (DataContext as ViewModels.Welcome).InitRepository(path, parent); }); return; } @@ -257,7 +257,7 @@ namespace SourceGit.Views Dispatcher.UIThread.Invoke(() => { var repo = ViewModels.Preference.AddRepository(root, gitDir); - var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent); + var node = ViewModels.Preference.FindOrAddNodeByRepositoryPath(repo.FullPath, parent, true); launcher.OpenRepositoryInTab(node, page); }); }); From 5eea54dbf89023fbdfa5738ceb8bd6fdcd543f6c Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 May 2024 09:42:51 +0800 Subject: [PATCH 04/22] fix: wrong popup title for editing remote --- src/Views/EditRemote.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/EditRemote.axaml b/src/Views/EditRemote.axaml index d719b585..8a51a643 100644 --- a/src/Views/EditRemote.axaml +++ b/src/Views/EditRemote.axaml @@ -12,7 +12,7 @@ + Text="{DynamicResource Text.Remote.EditTitle}"/> Date: Tue, 21 May 2024 09:58:06 +0800 Subject: [PATCH 05/22] refactor: implementation to delete branches --- src/Commands/Branch.cs | 22 +++++++++++++++++++++- src/Commands/Push.cs | 25 ------------------------- src/ViewModels/DeleteBranch.cs | 9 ++++++--- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/Commands/Branch.cs b/src/Commands/Branch.cs index 21210238..660a5daa 100644 --- a/src/Commands/Branch.cs +++ b/src/Commands/Branch.cs @@ -36,7 +36,7 @@ return cmd.Exec(); } - public static bool Delete(string repo, string name) + public static bool DeleteLocal(string repo, string name) { var cmd = new Command(); cmd.WorkingDirectory = repo; @@ -44,5 +44,25 @@ cmd.Args = $"branch -D {name}"; return cmd.Exec(); } + + public static bool DeleteRemote(string repo, string remote, string name) + { + var cmd = new Command(); + cmd.WorkingDirectory = repo; + cmd.Context = repo; + + var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); + if (!string.IsNullOrEmpty(sshKey)) + { + cmd.Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; + } + else + { + cmd.Args = "-c credential.helper=manager "; + } + + cmd.Args += $"push {remote} --delete {name}"; + return cmd.Exec(); + } } } diff --git a/src/Commands/Push.cs b/src/Commands/Push.cs index b3e4814a..0aac37a5 100644 --- a/src/Commands/Push.cs +++ b/src/Commands/Push.cs @@ -33,31 +33,6 @@ namespace SourceGit.Commands Args += $"{remote} {local}:{remoteBranch}"; } - /// - /// Only used to delete a remote branch!!!!!! - /// - /// - /// - /// - public Push(string repo, string remote, string branch) - { - WorkingDirectory = repo; - Context = repo; - TraitErrorAsOutput = true; - - var sshKey = new Config(repo).Get($"remote.{remote}.sshkey"); - if (!string.IsNullOrEmpty(sshKey)) - { - Args = $"-c core.sshCommand=\"ssh -i '{sshKey}'\" "; - } - else - { - Args = "-c credential.helper=manager "; - } - - Args += $"push {remote} --delete {branch}"; - } - public Push(string repo, string remote, string tag, bool isDelete) { WorkingDirectory = repo; diff --git a/src/ViewModels/DeleteBranch.cs b/src/ViewModels/DeleteBranch.cs index b5151e97..52a04d93 100644 --- a/src/ViewModels/DeleteBranch.cs +++ b/src/ViewModels/DeleteBranch.cs @@ -52,14 +52,17 @@ namespace SourceGit.ViewModels { if (Target.IsLocal) { - Commands.Branch.Delete(_repo.FullPath, Target.Name); + Commands.Branch.DeleteLocal(_repo.FullPath, Target.Name); if (_alsoDeleteTrackingRemote && TrackingRemoteBranch != null) - new Commands.Push(_repo.FullPath, TrackingRemoteBranch.Remote, TrackingRemoteBranch.Name).Exec(); + { + SetProgressDescription("Deleting tracking remote branch..."); + Commands.Branch.DeleteRemote(_repo.FullPath, TrackingRemoteBranch.Remote, TrackingRemoteBranch.Name); + } } else { - new Commands.Push(_repo.FullPath, Target.Remote, Target.Name).Exec(); + Commands.Branch.DeleteRemote(_repo.FullPath, Target.Remote, Target.Name); } CallUIThread(() => _repo.SetWatcherEnabled(true)); From 2e58da7c2a24076913646db039dc93c83f260398 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 May 2024 10:54:19 +0800 Subject: [PATCH 06/22] ux: use `SystemAccentColor` and `SystemListLowColor` --- src/Resources/Styles.axaml | 22 +++++++++++----------- src/Resources/Themes.axaml | 11 ++--------- src/Views/About.axaml | 10 +++++----- src/Views/Launcher.axaml | 2 +- src/Views/SelfUpdate.axaml | 2 +- src/Views/Statistics.axaml | 2 +- 6 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index afac434c..b8bb4e6c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -133,12 +133,12 @@ @@ -897,7 +897,7 @@ From 0aea8224991a5ece975ee99b63d6ada9a2643915 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 May 2024 11:52:30 +0800 Subject: [PATCH 08/22] enhance: remote ssh private key validation --- src/ViewModels/AddRemote.cs | 34 ++++++++++++++++++++++++++-------- src/ViewModels/EditRemote.cs | 34 ++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index 6ec6f06c..84ca2326 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.IO; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -29,13 +30,18 @@ namespace SourceGit.ViewModels public bool UseSSH { get => _useSSH; - set => SetProperty(ref _useSSH, value); + set + { + if (SetProperty(ref _useSSH, value)) + ValidateProperty(_sshkey, nameof(SSHKey)); + } } + [CustomValidation(typeof(AddRemote), nameof(ValidateSSHKey))] public string SSHKey { - get; - set; + get => _sshkey; + set => SetProperty(ref _sshkey, value, true); } public AddRemote(Repository repo) @@ -71,6 +77,20 @@ namespace SourceGit.ViewModels return ValidationResult.Success; } + public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext ctx) + { + if (ctx.ObjectInstance is AddRemote add && add._useSSH) + { + if (string.IsNullOrEmpty(sshkey)) + return new ValidationResult("SSH private key is required"); + + if (!File.Exists(sshkey)) + return new ValidationResult("Given SSH private key can NOT be found!"); + } + + return ValidationResult.Success; + } + public override Task Sure() { _repo.SetWatcherEnabled(false); @@ -84,11 +104,8 @@ namespace SourceGit.ViewModels SetProgressDescription("Fetching from added remote ..."); new Commands.Fetch(_repo.FullPath, _name, true, SetProgressDescription).Exec(); - if (_useSSH) - { - SetProgressDescription("Post processing ..."); - new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey); - } + SetProgressDescription("Post processing ..."); + new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); } CallUIThread(() => { @@ -103,5 +120,6 @@ namespace SourceGit.ViewModels private string _name = string.Empty; private string _url = string.Empty; private bool _useSSH = false; + private string _sshkey = string.Empty; } } diff --git a/src/ViewModels/EditRemote.cs b/src/ViewModels/EditRemote.cs index 91cf8749..5fd22d26 100644 --- a/src/ViewModels/EditRemote.cs +++ b/src/ViewModels/EditRemote.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.IO; using System.Threading.Tasks; namespace SourceGit.ViewModels @@ -29,13 +30,18 @@ namespace SourceGit.ViewModels public bool UseSSH { get => _useSSH; - set => SetProperty(ref _useSSH, value); + set + { + if (SetProperty(ref _useSSH, value)) + ValidateProperty(_sshkey, nameof(SSHKey)); + } } + [CustomValidation(typeof(EditRemote), nameof(ValidateSSHKey))] public string SSHKey { - get; - set; + get => _sshkey; + set => SetProperty(ref _sshkey, value, true); } public EditRemote(Repository repo, Models.Remote remote) @@ -85,6 +91,20 @@ namespace SourceGit.ViewModels return ValidationResult.Success; } + public static ValidationResult ValidateSSHKey(string sshkey, ValidationContext ctx) + { + if (ctx.ObjectInstance is EditRemote edit && edit.UseSSH) + { + if (string.IsNullOrEmpty(sshkey)) + return new ValidationResult("SSH private key is required"); + + if (!File.Exists(sshkey)) + return new ValidationResult("Given SSH private key can NOT be found!"); + } + + return ValidationResult.Success; + } + public override Task Sure() { _repo.SetWatcherEnabled(false); @@ -106,11 +126,8 @@ namespace SourceGit.ViewModels _remote.URL = _url; } - if (_useSSH) - { - SetProgressDescription("Post processing ..."); - new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", SSHKey); - } + SetProgressDescription("Post processing ..."); + new Commands.Config(_repo.FullPath).Set($"remote.{_name}.sshkey", _useSSH ? SSHKey : null); CallUIThread(() => _repo.SetWatcherEnabled(true)); return true; @@ -122,5 +139,6 @@ namespace SourceGit.ViewModels private string _name = string.Empty; private string _url = string.Empty; private bool _useSSH = false; + private string _sshkey = string.Empty; } } From 717e6b4fafbfe7dc803624458b0fd33f4eda704f Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 May 2024 14:35:14 +0800 Subject: [PATCH 09/22] enhance: a more adaptable regular expression for remote URLs (#136) --- src/Models/Remote.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Models/Remote.cs b/src/Models/Remote.cs index e3eaf36d..c1cce340 100644 --- a/src/Models/Remote.cs +++ b/src/Models/Remote.cs @@ -6,9 +6,9 @@ namespace SourceGit.Models { [GeneratedRegex(@"^http[s]?://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_HTTPS(); - [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^[\w\-]+@[\w\.\-]+(\:[0-9]+)?:[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_SSH1(); - [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-]+/[\w\-\.]+\.git$")] + [GeneratedRegex(@"^ssh://([\w\-]+@)?[\w\.\-]+(\:[0-9]+)?/[\w\-/]+/[\w\-\.]+\.git$")] private static partial Regex REG_SSH2(); private static readonly Regex[] URL_FORMATS = [ From 82b00b357b545e34bd84862ceb9cd0fc20fcddf5 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 May 2024 09:55:41 +0800 Subject: [PATCH 10/22] ux: change delay time to show/hide scrollbar (#139) --- src/Resources/Styles.axaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 898ccd1a..0e66f016 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -13,6 +13,9 @@ 12 + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/DeleteMultipleBranches.axaml.cs b/src/Views/DeleteMultipleBranches.axaml.cs new file mode 100644 index 00000000..7b3552d5 --- /dev/null +++ b/src/Views/DeleteMultipleBranches.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class DeleteMultipleBranches : UserControl + { + public DeleteMultipleBranches() + { + InitializeComponent(); + } + } +} + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 6b62a0b1..765210ae 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -109,7 +109,7 @@ - + @@ -119,14 +119,14 @@ - + - + @@ -142,7 +142,7 @@ - + @@ -198,19 +198,37 @@ + + + + - + @@ -250,20 +268,38 @@ + + + + - + @@ -294,7 +330,9 @@ + + + + + + + + + + @@ -364,6 +419,7 @@ + + + + + + + + + + @@ -390,7 +464,7 @@ - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 9bea2cc8..1468517b 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia; @@ -6,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -56,6 +58,24 @@ namespace SourceGit.Views public partial class Repository : UserControl { + public static readonly StyledProperty RefereshLocalBranchSelectionTokenProperty = + AvaloniaProperty.Register(nameof(RefereshLocalBranchSelectionToken), 0); + + public ulong RefereshLocalBranchSelectionToken + { + get => GetValue(RefereshLocalBranchSelectionTokenProperty); + set => SetValue(RefereshLocalBranchSelectionTokenProperty, value); + } + + public static readonly StyledProperty RefereshRemoteBranchSelectionTokenProperty = + AvaloniaProperty.Register(nameof(RefereshRemoteBranchSelectionToken), 0); + + public ulong RefereshRemoteBranchSelectionToken + { + get => GetValue(RefereshRemoteBranchSelectionTokenProperty); + set => SetValue(RefereshRemoteBranchSelectionTokenProperty, value); + } + public Repository() { InitializeComponent(); @@ -71,34 +91,21 @@ namespace SourceGit.Views } } - private void OnLocalBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnRemoteBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnTagDataGridLostFocus(object sender, RoutedEventArgs e) - { - if (sender is DataGrid datagrid) - datagrid.SelectedItem = null; - } - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender is TreeView tree && tree.SelectedItem != null) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var next = RefereshLocalBranchSelectionToken + 1; + SetCurrentValue(RefereshLocalBranchSelectionTokenProperty, next); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node.IsBranch && DataContext is ViewModels.Repository repo) + repo.NavigateToCommit((node.Backend as Models.Branch).Head); } } } @@ -108,11 +115,16 @@ namespace SourceGit.Views if (sender is TreeView tree && tree.SelectedItem != null) { localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var next = RefereshRemoteBranchSelectionToken + 1; + SetCurrentValue(RefereshRemoteBranchSelectionTokenProperty, next); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node.IsBranch && DataContext is ViewModels.Repository repo) + repo.NavigateToCommit((node.Backend as Models.Branch).Head); } } } @@ -121,11 +133,12 @@ namespace SourceGit.Views { if (sender is DataGrid datagrid && datagrid.SelectedItem != null) { + localBranchTree.UnselectAll(); + remoteBranchTree.UnselectAll(); + var tag = datagrid.SelectedItem as Models.Tag; if (DataContext is ViewModels.Repository repo) - { repo.NavigateToCommit(tag.SHA); - } } } @@ -133,9 +146,7 @@ namespace SourceGit.Views { var grid = sender as Grid; if (e.Property == IsVisibleProperty && grid.IsVisible) - { txtSearchCommitsBox.Focus(); - } } private void OnSearchKeyDown(object sender, KeyEventArgs e) @@ -143,9 +154,8 @@ namespace SourceGit.Views if (e.Key == Key.Enter) { if (DataContext is ViewModels.Repository repo) - { repo.StartSearchCommits(); - } + e.Handled = true; } } @@ -171,9 +181,7 @@ namespace SourceGit.Views if (toggle.DataContext is ViewModels.BranchTreeNode node) { if (node.IsBranch) - { filter = (node.Backend as Models.Branch).FullName; - } } else if (toggle.DataContext is Models.Tag tag) { @@ -192,15 +200,38 @@ namespace SourceGit.Views private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node) + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; + + var branches = new List(); + foreach (var item in tree.SelectedItems) + CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); + + if (branches.Count == 1) { - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null) { - var menu = repo.CreateContextMenuForLocalBranch(node.Backend as Models.Branch); - grid.OpenContextMenu(menu); + var menu = repo.CreateContextMenuForLocalBranch(branches[0]); + item.OpenContextMenu(menu); } } + else if (branches.Count > 1 && branches.Find(x => x.IsCurrent) == null) + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, true); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + tree.OpenContextMenu(menu); + } e.Handled = true; } @@ -208,20 +239,55 @@ namespace SourceGit.Views private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node && DataContext is ViewModels.Repository repo) + if (tree.SelectedItems.Count == 1) { - if (node.IsRemote) + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node != null && node.IsRemote) { - var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); - grid.OpenContextMenu(menu); + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null && item.DataContext == node) + { + var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); + item.OpenContextMenu(menu); + } + + e.Handled = true; + return; } - else if (node.IsBranch) + } + + var branches = new List(); + foreach (var item in tree.SelectedItems) + CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); + + if (branches.Count == 1) + { + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null) { - var menu = repo.CreateContextMenuForRemoteBranch(node.Backend as Models.Branch); - grid.OpenContextMenu(menu); + var menu = repo.CreateContextMenuForRemoteBranch(branches[0]); + item.OpenContextMenu(menu); } } + else + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, false); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + tree.OpenContextMenu(menu); + } e.Handled = true; } @@ -304,5 +370,23 @@ namespace SourceGit.Views e.Handled = true; } } + + private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) + { + if (node == null || node.IsRemote) + return; + + if (node.IsFolder) + { + foreach (var child in node.Children) + CollectBranchesFromNode(outs, child); + } + else + { + var b = node.Backend as Models.Branch; + if (b != null && !outs.Contains(b)) + outs.Add(b); + } + } } } diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 1af7bc44..20978345 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -137,6 +137,9 @@ namespace SourceGit.Views var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > _editor.DiffData.Lines.Count) break; @@ -517,6 +520,9 @@ namespace SourceGit.Views var infos = _editor.IsOld ? _editor.DiffData.Old : _editor.DiffData.New; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > infos.Count) break; From 476f9265e177131a410d71008d351e5471d82071 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 25 May 2024 01:36:08 +0800 Subject: [PATCH 17/22] fix: at least two selected branches are required --- src/Views/Repository.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 1468517b..bd5a69d7 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -274,7 +274,7 @@ namespace SourceGit.Views item.OpenContextMenu(menu); } } - else + else if (branches.Count > 1) { var menu = new ContextMenu(); var deleteMulti = new MenuItem(); From 8e3a8f4c06e4482538431d30643e089348ab8a86 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 25 May 2024 11:15:07 +0800 Subject: [PATCH 18/22] optimize: new way to update corner radius of branch tree node to improve performance (#137) --- src/Converters/BranchTreeNodeConverters.cs | 99 ---------------- src/ViewModels/BranchTreeNode.cs | 45 +++++++- src/Views/DeleteMultipleBranches.axaml | 124 ++++++++++----------- src/Views/Repository.axaml | 20 +--- src/Views/Repository.axaml.cs | 48 ++++---- 5 files changed, 128 insertions(+), 208 deletions(-) delete mode 100644 src/Converters/BranchTreeNodeConverters.cs diff --git a/src/Converters/BranchTreeNodeConverters.cs b/src/Converters/BranchTreeNodeConverters.cs deleted file mode 100644 index 14f73fd7..00000000 --- a/src/Converters/BranchTreeNodeConverters.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Collections.Generic; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data.Converters; - -namespace SourceGit.Converters -{ - public static class BranchTreeNodeConverters - { - public static readonly CornerRadius DEFAULT = new CornerRadius(4); - - public static readonly FuncMultiValueConverter ToCornerRadius = - new FuncMultiValueConverter(v => - { - if (v == null) - return DEFAULT; - - var array = new List(); - array.AddRange(v); - if (array.Count != 2) - return DEFAULT; - - var item = array[1] as TreeViewItem; - if (item == null || !item.IsSelected) - return DEFAULT; - - var prev = GetPrevTreeViewItem(item); - var next = GetNextTreeViewItem(item, true); - - double top = 4, bottom = 4; - if (prev != null && prev.IsSelected) - top = 0; - if (next != null && next.IsSelected) - bottom = 0; - - return new CornerRadius(top, bottom); - }); - - private static TreeViewItem GetPrevTreeViewItem(TreeViewItem item) - { - if (item.Parent is TreeView tree) - { - var idx = tree.IndexFromContainer(item); - if (idx == 0) - return null; - - var prev = tree.ContainerFromIndex(idx - 1) as TreeViewItem; - if (prev != null && prev.IsExpanded && prev.ItemCount > 0) - return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem; - - return prev; - } - else if (item.Parent is TreeViewItem parentItem) - { - var idx = parentItem.IndexFromContainer(item); - if (idx == 0) - return parentItem; - - var prev = parentItem.ContainerFromIndex(idx - 1) as TreeViewItem; - if (prev != null && prev.IsExpanded && prev.ItemCount > 0) - return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem; - - return prev; - } - else - { - return null; - } - } - - private static TreeViewItem GetNextTreeViewItem(TreeViewItem item, bool intoSelf = false) - { - if (intoSelf && item.IsExpanded && item.ItemCount > 0) - return item.ContainerFromIndex(0) as TreeViewItem; - - if (item.Parent is TreeView tree) - { - var idx = tree.IndexFromContainer(item); - if (idx == tree.ItemCount - 1) - return null; - - return tree.ContainerFromIndex(idx + 1) as TreeViewItem; - } - else if (item.Parent is TreeViewItem parentItem) - { - var idx = parentItem.IndexFromContainer(item); - if (idx == parentItem.ItemCount - 1) - return GetNextTreeViewItem(parentItem); - - return parentItem.ContainerFromIndex(idx + 1) as TreeViewItem; - } - else - { - return null; - } - } - } -} diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index b6b88b5d..194b1a26 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; using System.IO; + +using Avalonia; using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; + namespace SourceGit.ViewModels { public enum BranchTreeNodeType @@ -12,8 +16,10 @@ namespace SourceGit.ViewModels Branch, } - public class BranchTreeNode + public class BranchTreeNode : ObservableObject { + public const double DEFAULT_CORNER = 4.0; + public string Name { get; set; } public BranchTreeNodeType Type { get; set; } public object Backend { get; set; } @@ -51,6 +57,43 @@ namespace SourceGit.ViewModels get => IsBranch && (Backend as Models.Branch).IsCurrent; } + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + + public CornerRadius CornerRadius + { + get => _cornerRadius; + set => SetProperty(ref _cornerRadius, value); + } + + public void UpdateCornerRadius(ref BranchTreeNode prev) + { + if (_isSelected && prev != null && prev.IsSelected) + { + var prevTop = prev.CornerRadius.TopLeft; + prev.CornerRadius = new CornerRadius(prevTop, 0); + CornerRadius = new CornerRadius(0, DEFAULT_CORNER); + } + else if (CornerRadius.TopLeft != DEFAULT_CORNER || + CornerRadius.BottomLeft != DEFAULT_CORNER) + { + CornerRadius = new CornerRadius(DEFAULT_CORNER); + } + + prev = this; + + if (!IsBranch && IsExpanded) + { + foreach (var child in Children) + child.UpdateCornerRadius(ref prev); + } + } + private bool _isSelected = false; + private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER); + public class Builder { public List Locals => _locals; diff --git a/src/Views/DeleteMultipleBranches.axaml b/src/Views/DeleteMultipleBranches.axaml index ca0282ef..75f28674 100644 --- a/src/Views/DeleteMultipleBranches.axaml +++ b/src/Views/DeleteMultipleBranches.axaml @@ -14,75 +14,69 @@ Classes="bold" Text="{DynamicResource Text.DeleteMultiBranch}" /> - - - - - - + + + + - + - + - - + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 765210ae..9df2b540 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -208,14 +208,8 @@