From cd009bda6bbd6586fa1cea7e81f4c1fcb30e417e Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 3 Jun 2025 10:15:58 +0800 Subject: [PATCH 01/44] ux: enable `Use monospace font only in text editor` by default Signed-off-by: leo --- src/ViewModels/Preferences.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 2698067e..df6d36bd 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -661,7 +661,7 @@ namespace SourceGit.ViewModels private string _themeOverrides = string.Empty; private string _defaultFontFamily = string.Empty; private string _monospaceFontFamily = string.Empty; - private bool _onlyUseMonoFontInEditor = false; + private bool _onlyUseMonoFontInEditor = true; private double _defaultFontSize = 13; private double _editorFontSize = 13; private int _editorTabWidth = 4; From bf43dd828a48322af1eb3a56d5f857475c3f8c7c Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 3 Jun 2025 12:34:49 +0800 Subject: [PATCH 02/44] ux: new style for ref's `Visibility in Graph` context menu item Signed-off-by: leo --- src/Resources/Styles.axaml | 17 ++++ src/ViewModels/FilterModeInGraph.cs | 62 +++++++++++++ src/ViewModels/Histories.cs | 127 +++++---------------------- src/Views/FilterModeInGraph.axaml | 31 +++++++ src/Views/FilterModeInGraph.axaml.cs | 12 +++ 5 files changed, 142 insertions(+), 107 deletions(-) create mode 100644 src/ViewModels/FilterModeInGraph.cs create mode 100644 src/Views/FilterModeInGraph.axaml create mode 100644 src/Views/FilterModeInGraph.axaml.cs diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 923ef22b..629c4354 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -843,6 +843,10 @@ + + + + @@ -949,6 +953,19 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LFSDiffView.axaml.cs b/src/Views/LFSDiffView.axaml.cs new file mode 100644 index 00000000..a16a6fd3 --- /dev/null +++ b/src/Views/LFSDiffView.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class LFSDiffView : UserControl + { + public LFSDiffView() + { + InitializeComponent(); + } + } +} From f716c5ee1e7019a3ced0c177e7c611e8d257b41c Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 4 Jun 2025 21:30:08 +0800 Subject: [PATCH 12/44] refactor: use existing `QueryFileContent` command Signed-off-by: leo --- src/Commands/SaveRevisionFile.cs | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/Commands/SaveRevisionFile.cs b/src/Commands/SaveRevisionFile.cs index 99e89093..550844ef 100644 --- a/src/Commands/SaveRevisionFile.cs +++ b/src/Commands/SaveRevisionFile.cs @@ -13,12 +13,8 @@ namespace SourceGit.Commands var isLFSFiltered = new IsLFSFiltered(repo, revision, file).Result(); if (isLFSFiltered) { - var tmpFile = saveTo + ".tmp"; - if (ExecCmd(repo, $"show {revision}:\"{file}\"", tmpFile)) - { - ExecCmd(repo, $"lfs smudge", saveTo, tmpFile); - } - File.Delete(tmpFile); + var pointerStream = QueryFileContent.Run(repo, revision, file); + ExecCmd(repo, $"lfs smudge", saveTo, pointerStream); } else { @@ -26,7 +22,7 @@ namespace SourceGit.Commands } } - private static bool ExecCmd(string repo, string args, string outputFile, string inputFile = null) + private static bool ExecCmd(string repo, string args, string outputFile, Stream input = null) { var starter = new ProcessStartInfo(); starter.WorkingDirectory = repo; @@ -45,21 +41,8 @@ namespace SourceGit.Commands { var proc = new Process() { StartInfo = starter }; proc.Start(); - - if (inputFile != null) - { - using (StreamReader sr = new StreamReader(inputFile)) - { - while (true) - { - var line = sr.ReadLine(); - if (line == null) - break; - proc.StandardInput.WriteLine(line); - } - } - } - + if (input != null) + proc.StandardInput.Write(new StreamReader(input).ReadToEnd()); proc.StandardOutput.BaseStream.CopyTo(sw); proc.WaitForExit(); var rs = proc.ExitCode == 0; From eebadd67a1e4f0507af3fb045b76b116493c2ba8 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 5 Jun 2025 09:18:19 +0800 Subject: [PATCH 13/44] feature: remember the last active tab index in lfs-image diff view Signed-off-by: leo --- src/ViewModels/Preferences.cs | 7 +++++++ src/Views/DiffView.axaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index df6d36bd..76305655 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -261,6 +261,12 @@ namespace SourceGit.ViewModels set => SetProperty(ref _useBlockNavigationInDiffView, value); } + public int LFSImageDiffActiveIdx + { + get => _lfsImageDiffActiveIdx; + set => SetProperty(ref _lfsImageDiffActiveIdx, value); + } + public Models.ChangeViewMode UnstagedChangeViewMode { get => _unstagedChangeViewMode; @@ -687,6 +693,7 @@ namespace SourceGit.ViewModels private bool _showHiddenSymbolsInDiffView = false; private bool _useFullTextDiff = false; private bool _useBlockNavigationInDiffView = false; + private int _lfsImageDiffActiveIdx = 0; private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List; private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List; diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index 50a3de62..c12c8a62 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -282,7 +282,7 @@ - + + + + + + + + + + + + + + + + + + + + From 75c32c1a0111626fa88f72bd5872810683be80b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= Date: Tue, 3 Jun 2025 10:25:46 +0200 Subject: [PATCH 15/44] typo: corrected spelling error in `App.GetLauncher()` method --- src/App.axaml.cs | 2 +- src/ViewModels/Blame.cs | 2 +- src/ViewModels/BranchCompare.cs | 2 +- src/ViewModels/Clone.cs | 2 +- src/ViewModels/Repository.cs | 6 +++--- src/ViewModels/RepositoryNode.cs | 6 +++--- src/ViewModels/RevisionCompare.cs | 2 +- src/ViewModels/Welcome.cs | 16 ++++++++-------- src/Views/WelcomeToolbar.axaml.cs | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/App.axaml.cs b/src/App.axaml.cs index 411f5cfb..b5868ca1 100644 --- a/src/App.axaml.cs +++ b/src/App.axaml.cs @@ -338,7 +338,7 @@ namespace SourceGit return null; } - public static ViewModels.Launcher GetLauncer() + public static ViewModels.Launcher GetLauncher() { return Current is App app ? app._launcher : null; } diff --git a/src/ViewModels/Blame.cs b/src/ViewModels/Blame.cs index c04842e5..d3558670 100644 --- a/src/ViewModels/Blame.cs +++ b/src/ViewModels/Blame.cs @@ -45,7 +45,7 @@ namespace SourceGit.ViewModels public void NavigateToCommit(string commitSHA) { - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); if (launcher == null) return; diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/BranchCompare.cs index 4edb978c..f56cfd76 100644 --- a/src/ViewModels/BranchCompare.cs +++ b/src/ViewModels/BranchCompare.cs @@ -86,7 +86,7 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); if (launcher == null) return; diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index 032551a2..94a74893 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -150,7 +150,7 @@ namespace SourceGit.ViewModels CallUIThread(() => { var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, null, true); - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); var page = null as LauncherPage; foreach (var one in launcher.Pages) { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index fcc4a853..0d22677c 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1401,7 +1401,7 @@ namespace SourceGit.ViewModels }; } - App.GetLauncer().OpenRepositoryInTab(node, null); + App.GetLauncher().OpenRepositoryInTab(node, null); } public void AddWorktree() @@ -1430,7 +1430,7 @@ namespace SourceGit.ViewModels }; } - App.GetLauncer()?.OpenRepositoryInTab(node, null); + App.GetLauncher()?.OpenRepositoryInTab(node, null); } public List GetPreferedOpenAIServices() @@ -2588,7 +2588,7 @@ namespace SourceGit.ViewModels private LauncherPage GetOwnerPage() { - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); if (launcher == null) return null; diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index cd79c06e..f609879b 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -70,14 +70,14 @@ namespace SourceGit.ViewModels public void Edit() { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new EditRepositoryNode(this); } public void AddSubFolder() { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new CreateGroup(this); } @@ -98,7 +98,7 @@ namespace SourceGit.ViewModels public void Delete() { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new DeleteRepositoryNode(this); } diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index 1dad7593..39400aa3 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -100,7 +100,7 @@ namespace SourceGit.ViewModels public void NavigateTo(string commitSHA) { - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); if (launcher == null) return; diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 95f7f010..069dcf38 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -110,7 +110,7 @@ namespace SourceGit.ViewModels var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(repoRoot, parent, bMoveExistedNode); Refresh(); - var launcher = App.GetLauncer(); + var launcher = App.GetLauncher(); launcher?.OpenRepositoryInTab(node, launcher.ActivePage); } @@ -122,7 +122,7 @@ namespace SourceGit.ViewModels return; } - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new Init(activePage.Node.Id, path, parent, reason); } @@ -135,7 +135,7 @@ namespace SourceGit.ViewModels return; } - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new Clone(activePage.Node.Id); } @@ -163,7 +163,7 @@ namespace SourceGit.ViewModels return; } - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.StartPopup(new ScanRepositories(defaultCloneDir)); } @@ -175,7 +175,7 @@ namespace SourceGit.ViewModels public void AddRootNode() { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new CreateGroup(null); } @@ -197,7 +197,7 @@ namespace SourceGit.ViewModels openAll.Icon = App.CreateMenuIcon("Icons.Folder.Open"); openAll.Click += (_, e) => { - OpenAllInNode(App.GetLauncer(), node); + OpenAllInNode(App.GetLauncher(), node); e.Handled = true; }; @@ -212,7 +212,7 @@ namespace SourceGit.ViewModels open.Icon = App.CreateMenuIcon("Icons.Folder.Open"); open.Click += (_, e) => { - App.GetLauncer()?.OpenRepositoryInTab(node, null); + App.GetLauncher()?.OpenRepositoryInTab(node, null); e.Handled = true; }; @@ -267,7 +267,7 @@ namespace SourceGit.ViewModels move.Icon = App.CreateMenuIcon("Icons.MoveToAnotherGroup"); move.Click += (_, e) => { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage != null && activePage.CanCreatePopup()) activePage.Popup = new MoveRepositoryNode(node); diff --git a/src/Views/WelcomeToolbar.axaml.cs b/src/Views/WelcomeToolbar.axaml.cs index e2a130f8..fd08e901 100644 --- a/src/Views/WelcomeToolbar.axaml.cs +++ b/src/Views/WelcomeToolbar.axaml.cs @@ -16,7 +16,7 @@ namespace SourceGit.Views private async void OpenLocalRepository(object _1, RoutedEventArgs e) { - var activePage = App.GetLauncer().ActivePage; + var activePage = App.GetLauncher().ActivePage; if (activePage == null || !activePage.CanCreatePopup()) return; From 54c05ac35a6d1fd01e50f2eef5fa923d71146aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= Date: Tue, 3 Jun 2025 13:34:52 +0200 Subject: [PATCH 16/44] fix: remove trailing slash in paths, to avoid failing comparisons. This is needed since DirectoryInfo.Fullname (and .FullPath) will not "normalize" trailing slashes, so direct equality tests are error-prone. This fixes a bug in ScanRepositories.GetUnmanagedRepositories(), where not all Git repo folders were added. (Also, corrected a variable name from 'founded' to 'found'.) --- src/ViewModels/ScanRepositories.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index cec932eb..743fcf27 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -31,8 +31,8 @@ namespace SourceGit.ViewModels watch.Start(); var rootDir = new DirectoryInfo(RootDir); - var founded = new List(); - GetUnmanagedRepositories(rootDir, founded, new EnumerationOptions() + var found = new List(); + GetUnmanagedRepositories(rootDir, found, new EnumerationOptions() { AttributesToSkip = FileAttributes.Hidden | FileAttributes.System, IgnoreInaccessible = true, @@ -46,11 +46,11 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => { - var normalizedRoot = rootDir.FullName.Replace("\\", "/"); + var normalizedRoot = rootDir.FullName.Replace("\\", "/").TrimEnd('/'); - foreach (var f in founded) + foreach (var f in found) { - var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace("\\", "/"); + var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace("\\", "/").TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, null, false); @@ -93,7 +93,7 @@ namespace SourceGit.ViewModels CallUIThread(() => ProgressDescription = $"Scanning {subdir.FullName}..."); - var normalizedSelf = subdir.FullName.Replace("\\", "/"); + var normalizedSelf = subdir.FullName.Replace("\\", "/").TrimEnd('/'); if (_managed.Contains(normalizedSelf)) continue; @@ -103,7 +103,7 @@ namespace SourceGit.ViewModels var test = new Commands.QueryRepositoryRootPath(subdir.FullName).ReadToEnd(); if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { - var normalized = test.StdOut.Trim().Replace("\\", "/"); + var normalized = test.StdOut.Trim().Replace("\\", "/").TrimEnd('/'); if (!_managed.Contains(normalized)) outs.Add(new FoundRepository(normalized, false)); } From 88c38b41396fd007149c8231ad8d8dd38d24270b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= Date: Tue, 3 Jun 2025 16:45:00 +0200 Subject: [PATCH 17/44] enhance: unified all file-path normalization - use char-replace, trim trailing slash --- src/Models/Watcher.cs | 4 ++-- src/ViewModels/DiffContext.cs | 2 +- src/ViewModels/Launcher.cs | 2 +- src/ViewModels/Preferences.cs | 4 +--- src/ViewModels/Repository.cs | 6 +++--- src/ViewModels/RepositoryNode.cs | 2 +- src/ViewModels/ScanRepositories.cs | 8 ++++---- src/ViewModels/WorkingCopy.cs | 4 ++-- 8 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs index 928951ca..ccdc645f 100644 --- a/src/Models/Watcher.cs +++ b/src/Models/Watcher.cs @@ -157,7 +157,7 @@ namespace SourceGit.Models if (string.IsNullOrEmpty(e.Name)) return; - var name = e.Name.Replace("\\", "/"); + var name = e.Name.Replace('\\', '/').TrimEnd('/'); if (name.Contains("fsmonitor--daemon/", StringComparison.Ordinal) || name.EndsWith(".lock", StringComparison.Ordinal) || name.StartsWith("lfs/", StringComparison.Ordinal)) @@ -205,7 +205,7 @@ namespace SourceGit.Models if (string.IsNullOrEmpty(e.Name)) return; - var name = e.Name.Replace("\\", "/"); + var name = e.Name.Replace('\\', '/').TrimEnd('/'); if (name.Equals(".git", StringComparison.Ordinal) || name.StartsWith(".git/", StringComparison.Ordinal) || name.EndsWith("/.git", StringComparison.Ordinal)) diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 5258f164..828a2d59 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -128,7 +128,7 @@ namespace SourceGit.ViewModels if (count <= 3) { var submoduleDiff = new Models.SubmoduleDiff(); - var submoduleRoot = $"{_repo}/{_option.Path}".Replace("\\", "/"); + var submoduleRoot = $"{_repo}/{_option.Path}".Replace('\\', '/').TrimEnd('/'); isSubmodule = true; for (int i = 1; i < count; i++) { diff --git a/src/ViewModels/Launcher.cs b/src/ViewModels/Launcher.cs index 4c0714df..004b6a0a 100644 --- a/src/ViewModels/Launcher.cs +++ b/src/ViewModels/Launcher.cs @@ -421,7 +421,7 @@ namespace SourceGit.ViewModels foreach (var page in Pages) { - var id = page.Node.Id.Replace("\\", "/"); + var id = page.Node.Id.Replace('\\', '/').TrimEnd('/'); if (id == pageId) { page.Notifications.Add(notification); diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 2fcbb0cf..31bc7bdb 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -455,9 +455,7 @@ namespace SourceGit.ViewModels public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode) { - var normalized = repo.Replace('\\', '/'); - if (normalized.EndsWith("/")) - normalized = normalized.TrimEnd('/'); + var normalized = repo.Replace('\\', '/').TrimEnd('/'); var node = FindNodeRecursive(normalized, RepositoryNodes); if (node == null) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 0d22677c..e20669e1 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -30,7 +30,7 @@ namespace SourceGit.ViewModels { if (value != null) { - var normalized = value.Replace('\\', '/'); + var normalized = value.Replace('\\', '/').TrimEnd('/'); SetProperty(ref _fullpath, normalized); } else @@ -499,7 +499,7 @@ namespace SourceGit.ViewModels { // For worktrees, we need to watch the $GIT_COMMON_DIR instead of the $GIT_DIR. var gitDirForWatcher = _gitDir; - if (_gitDir.Replace("\\", "/").IndexOf("/worktrees/", StringComparison.Ordinal) > 0) + if (_gitDir.Replace('\\', '/').IndexOf("/worktrees/", StringComparison.Ordinal) > 0) { var commonDir = new Commands.QueryGitCommonDir(_fullpath).Result(); if (!string.IsNullOrEmpty(commonDir)) @@ -1387,7 +1387,7 @@ namespace SourceGit.ViewModels return; var root = Path.GetFullPath(Path.Combine(_fullpath, submodule)); - var normalizedPath = root.Replace("\\", "/"); + var normalizedPath = root.Replace('\\', '/').TrimEnd('/'); var node = Preferences.Instance.FindNode(normalizedPath); if (node == null) diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index f609879b..c65d1dbd 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -13,7 +13,7 @@ namespace SourceGit.ViewModels get => _id; set { - var normalized = value.Replace('\\', '/'); + var normalized = value.Replace('\\', '/').TrimEnd('/'); SetProperty(ref _id, normalized); } } diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 743fcf27..833a1402 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -46,11 +46,11 @@ namespace SourceGit.ViewModels Dispatcher.UIThread.Invoke(() => { - var normalizedRoot = rootDir.FullName.Replace("\\", "/").TrimEnd('/'); + var normalizedRoot = rootDir.FullName.Replace('\\', '/').TrimEnd('/'); foreach (var f in found) { - var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace("\\", "/").TrimEnd('/'); + var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, null, false); @@ -93,7 +93,7 @@ namespace SourceGit.ViewModels CallUIThread(() => ProgressDescription = $"Scanning {subdir.FullName}..."); - var normalizedSelf = subdir.FullName.Replace("\\", "/").TrimEnd('/'); + var normalizedSelf = subdir.FullName.Replace('\\', '/').TrimEnd('/'); if (_managed.Contains(normalizedSelf)) continue; @@ -103,7 +103,7 @@ namespace SourceGit.ViewModels var test = new Commands.QueryRepositoryRootPath(subdir.FullName).ReadToEnd(); if (test.IsSuccess && !string.IsNullOrEmpty(test.StdOut)) { - var normalized = test.StdOut.Trim().Replace("\\", "/").TrimEnd('/'); + var normalized = test.StdOut.Trim().Replace('\\', '/').TrimEnd('/'); if (!_managed.Contains(normalized)) outs.Add(new FoundRepository(normalized, false)); } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 6469b564..ac6d9486 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -780,7 +780,7 @@ namespace SourceGit.ViewModels byParentFolder.IsVisible = !isRooted; byParentFolder.Click += (_, e) => { - var dir = Path.GetDirectoryName(change.Path)!.Replace("\\", "/"); + var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); Commands.GitIgnore.Add(_repo.FullPath, dir + "/"); e.Handled = true; }; @@ -802,7 +802,7 @@ namespace SourceGit.ViewModels byExtensionInSameFolder.IsVisible = !isRooted; byExtensionInSameFolder.Click += (_, e) => { - var dir = Path.GetDirectoryName(change.Path)!.Replace("\\", "/"); + var dir = Path.GetDirectoryName(change.Path)!.Replace('\\', '/').TrimEnd('/'); Commands.GitIgnore.Add(_repo.FullPath, $"{dir}/*{extension}"); e.Handled = true; }; From b969ac161a99c15b3ec69b90f0ce70a0dcd28c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= Date: Tue, 3 Jun 2025 20:52:30 +0200 Subject: [PATCH 18/44] enhance: unify sorting of `RepositoryNode` tree, unconditional sort & save after rescan --- src/ViewModels/Preferences.cs | 37 +++++++++++++++++++----------- src/ViewModels/ScanRepositories.cs | 13 +++++------ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 31bc7bdb..ccb30a0d 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -436,16 +436,26 @@ namespace SourceGit.ViewModels { var collection = to == null ? RepositoryNodes : to.SubNodes; collection.Add(node); - collection.Sort((l, r) => + SortNodes(collection); + + if (save) + Save(); + } + + public void SortNodes(List collection) + { + collection?.Sort((l, r) => { if (l.IsRepository != r.IsRepository) return l.IsRepository ? 1 : -1; return string.Compare(l.Name, r.Name, StringComparison.Ordinal); }); + } - if (save) - Save(); + public void SortAllNodes() + { + SortNodesRecursive(RepositoryNodes); } public RepositoryNode FindNode(string id) @@ -503,22 +513,14 @@ namespace SourceGit.ViewModels public void SortByRenamedNode(RepositoryNode node) { var container = FindNodeContainer(node, RepositoryNodes); - container?.Sort((l, r) => - { - if (l.IsRepository != r.IsRepository) - return l.IsRepository ? 1 : -1; - - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - }); + SortNodes(container); Save(); } - public void AutoRemoveInvalidNode() + public bool AutoRemoveInvalidNode() { - var changed = RemoveInvalidRepositoriesRecursive(RepositoryNodes); - if (changed) - Save(); + return RemoveInvalidRepositoriesRecursive(RepositoryNodes); } public void Save() @@ -588,6 +590,13 @@ namespace SourceGit.ViewModels } } + private void SortNodesRecursive(List collection) + { + SortNodes(collection); + foreach (var node in collection) + SortNodesRecursive(node.SubNodes); + } + private RepositoryNode FindNodeRecursive(string id, List collection) { foreach (var node in collection) diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 833a1402..2618519b 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -64,6 +64,11 @@ namespace SourceGit.ViewModels } Preferences.Instance.AutoRemoveInvalidNode(); + + // Sort & Save unconditionally after a complete rescan. + Preferences.Instance.SortAllNodes(); + Preferences.Instance.Save(); + Welcome.Instance.Refresh(); }); @@ -151,13 +156,7 @@ namespace SourceGit.ViewModels IsExpanded = true, }; collection.Add(added); - collection.Sort((l, r) => - { - if (l.IsRepository != r.IsRepository) - return l.IsRepository ? 1 : -1; - - return string.Compare(l.Name, r.Name, StringComparison.Ordinal); - }); + Preferences.Instance.SortNodes(collection); return added; } From 464fe745803c8264a112707394fa599326037a6e Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 5 Jun 2025 21:27:19 +0800 Subject: [PATCH 19/44] code_review: commit b969ac161a99c15b3ec69b90f0ce70a0dcd28c4a - The return code of `AutoRemoveInvalidNode` is never used - It's not necessary to sort all nodes after re-scan default clone dir. Because `FindOrAddNodeByRepositoryPath` makes sure added node is ordered - Add a new parameter `save` to `FindOrAddNodeByRepositoryPath` method, and disable it while scanning. Instead, we will save it after scan finished. Signed-off-by: leo --- src/ViewModels/Preferences.cs | 16 +++++----------- src/ViewModels/ScanRepositories.cs | 7 ++----- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index ccb30a0d..e41e046e 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -453,17 +453,12 @@ namespace SourceGit.ViewModels }); } - public void SortAllNodes() - { - SortNodesRecursive(RepositoryNodes); - } - public RepositoryNode FindNode(string id) { return FindNodeRecursive(id, RepositoryNodes); } - public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode) + public RepositoryNode FindOrAddNodeByRepositoryPath(string repo, RepositoryNode parent, bool shouldMoveNode, bool save = true) { var normalized = repo.Replace('\\', '/').TrimEnd('/'); @@ -478,11 +473,11 @@ namespace SourceGit.ViewModels IsRepository = true, }; - AddNode(node, parent, true); + AddNode(node, parent, save); } else if (shouldMoveNode) { - MoveNode(node, parent, true); + MoveNode(node, parent, save); } return node; @@ -514,13 +509,12 @@ namespace SourceGit.ViewModels { var container = FindNodeContainer(node, RepositoryNodes); SortNodes(container); - Save(); } - public bool AutoRemoveInvalidNode() + public void AutoRemoveInvalidNode() { - return RemoveInvalidRepositoriesRecursive(RepositoryNodes); + RemoveInvalidRepositoriesRecursive(RepositoryNodes); } public void Save() diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 2618519b..f97066b1 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -53,20 +53,17 @@ namespace SourceGit.ViewModels var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, null, false); + Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, null, false, false); } else if (parent.StartsWith(normalizedRoot, StringComparison.Ordinal)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); - Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, group, false); + Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, group, false, false); } } Preferences.Instance.AutoRemoveInvalidNode(); - - // Sort & Save unconditionally after a complete rescan. - Preferences.Instance.SortAllNodes(); Preferences.Instance.Save(); Welcome.Instance.Refresh(); From 406ace9e79bc9c29b9da9c6f2237b05f340d5f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=B6ran=20W?= Date: Tue, 3 Jun 2025 21:25:07 +0200 Subject: [PATCH 20/44] enhance: activate `TabsDropdownItem` on `Tapped` instead of `DoubleTapped` Signed-off-by: leo --- src/Views/LauncherTabBar.axaml | 2 +- src/Views/LauncherTabBar.axaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index a56da2b0..f078a598 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -211,7 +211,7 @@ - + Date: Tue, 3 Jun 2025 22:20:04 +0200 Subject: [PATCH 21/44] fix: prevent exception on repo with no commits/branches Signed-off-by: leo --- src/ViewModels/Repository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index e20669e1..2b2771ec 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1120,7 +1120,8 @@ namespace SourceGit.ViewModels if (_workingCopy != null) _workingCopy.HasRemotes = remotes.Count > 0; - GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !CurrentBranch.TrackStatus.IsVisible); + var hasPendingPullOrPush = CurrentBranch?.TrackStatus.IsVisible ?? false; + GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !hasPendingPullOrPush); }); } From f003f67129e0d0d2ca92caf6928088fb7e9e9e24 Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 5 Jun 2025 21:54:09 +0800 Subject: [PATCH 22/44] fix: should use `file.SHA` instead of `_commit.SHA` to query submodule's commit Signed-off-by: leo --- src/ViewModels/CommitDetail.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 10ee336a..2fe85653 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -245,9 +245,9 @@ namespace SourceGit.ViewModels case Models.ObjectType.Commit: Task.Run(() => { - var submoduleRoot = Path.Combine(_repo.FullPath, file.Path); - var commit = new Commands.QuerySingleCommit(submoduleRoot, _commit.SHA).Result(); - var message = commit != null ? new Commands.QueryCommitFullMessage(submoduleRoot, _commit.SHA).Result() : null; + var submoduleRoot = Path.Combine(_repo.FullPath, file.Path).Replace('\\', '/').Trim('/'); + var commit = new Commands.QuerySingleCommit(submoduleRoot, file.SHA).Result(); + var message = commit != null ? new Commands.QueryCommitFullMessage(submoduleRoot, file.SHA).Result() : null; var module = new Models.RevisionSubmodule() { Commit = commit ?? new Models.Commit() { SHA = _commit.SHA }, From ac55bed812282a6b31b213fcf04b6b950f5cb12a Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 10:07:58 +0800 Subject: [PATCH 23/44] enhance: revision file viewer - show current file path - add a toggle button to use global syntax highlighting setting (sometimes TextMateSharp will crash this app) Signed-off-by: leo --- src/ViewModels/CommitDetail.cs | 12 ++++++- src/Views/RevisionFileContentViewer.axaml | 1 + src/Views/RevisionFileContentViewer.axaml.cs | 34 ++++++++++++++++---- src/Views/RevisionFileTreeView.axaml.cs | 13 +++----- src/Views/RevisionFiles.axaml | 28 +++++++++++++++- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 2fe85653..02db9b4f 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -105,10 +105,16 @@ namespace SourceGit.ViewModels } } + public string ViewRevisionFilePath + { + get => _viewRevisionFilePath; + private set => SetProperty(ref _viewRevisionFilePath, value); + } + public object ViewRevisionFileContent { get => _viewRevisionFileContent; - set => SetProperty(ref _viewRevisionFileContent, value); + private set => SetProperty(ref _viewRevisionFileContent, value); } public string RevisionFileSearchFilter @@ -189,10 +195,13 @@ namespace SourceGit.ViewModels { if (file == null) { + ViewRevisionFilePath = string.Empty; ViewRevisionFileContent = null; return; } + ViewRevisionFilePath = file.Path; + switch (file.Type) { case Models.ObjectType.Blob: @@ -893,6 +902,7 @@ namespace SourceGit.ViewModels private List _selectedChanges = null; private string _searchChangeFilter = string.Empty; private DiffContext _diffContext = null; + private string _viewRevisionFilePath = string.Empty; private object _viewRevisionFileContent = null; private CancellationTokenSource _cancellationSource = null; private bool _requestingRevisionFiles = false; diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 8eaef3a3..422e9169 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -23,6 +23,7 @@ diff --git a/src/Views/RevisionFileContentViewer.axaml.cs b/src/Views/RevisionFileContentViewer.axaml.cs index 16f4fc83..59f2d33d 100644 --- a/src/Views/RevisionFileContentViewer.axaml.cs +++ b/src/Views/RevisionFileContentViewer.axaml.cs @@ -24,6 +24,15 @@ namespace SourceGit.Views set => SetValue(TabWidthProperty, value); } + public static readonly StyledProperty UseSyntaxHighlightingProperty = + AvaloniaProperty.Register(nameof(UseSyntaxHighlighting)); + + public bool UseSyntaxHighlighting + { + get => GetValue(UseSyntaxHighlightingProperty); + set => SetValue(UseSyntaxHighlightingProperty, value); + } + protected override Type StyleKeyOverride => typeof(TextEditor); public RevisionTextFileView() : base(new TextArea(), new TextDocument()) @@ -72,8 +81,8 @@ namespace SourceGit.Views if (DataContext is Models.RevisionTextFile source) { - UpdateTextMate(); Text = source.Content; + Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); } else { @@ -86,9 +95,9 @@ namespace SourceGit.Views base.OnPropertyChanged(change); if (change.Property == TabWidthProperty) - { Options.IndentationSize = TabWidth; - } + else if (change.Property == UseSyntaxHighlightingProperty) + UpdateTextMate(); } private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) @@ -124,11 +133,22 @@ namespace SourceGit.Views private void UpdateTextMate() { - if (_textMate == null) - _textMate = Models.TextMateHelper.CreateForEditor(this); + if (UseSyntaxHighlighting) + { + if (_textMate == null) + _textMate = Models.TextMateHelper.CreateForEditor(this); - if (DataContext is Models.RevisionTextFile file) - Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); + if (DataContext is Models.RevisionTextFile file) + Models.TextMateHelper.SetGrammarByFileName(_textMate, file.FileName); + } + else if (_textMate != null) + { + _textMate.Dispose(); + _textMate = null; + GC.Collect(); + + TextArea.TextView.Redraw(); + } } private TextMate.Installation _textMate = null; diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs index b671851c..410f747e 100644 --- a/src/Views/RevisionFileTreeView.axaml.cs +++ b/src/Views/RevisionFileTreeView.axaml.cs @@ -313,16 +313,13 @@ namespace SourceGit.Views private void OnRowsSelectionChanged(object sender, SelectionChangedEventArgs _) { - if (_disableSelectionChangingEvent) + if (_disableSelectionChangingEvent || DataContext is not ViewModels.CommitDetail vm) return; - if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode node } && DataContext is ViewModels.CommitDetail vm) - { - if (!node.IsFolder) - vm.ViewRevisionFile(node.Backend); - else - vm.ViewRevisionFile(null); - } + if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode { IsFolder: false } node }) + vm.ViewRevisionFile(node.Backend); + else + vm.ViewRevisionFile(null); } private List GetChildrenOfTreeNode(ViewModels.RevisionFileTreeNode node) diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index 6575dc66..cfc20198 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -113,7 +113,33 @@ - + + + + + + + + + + + + + + From 3bb20868fcc978814656baa663b8fa4c9caf0ab4 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 10:45:18 +0800 Subject: [PATCH 24/44] refactor: remove unnecessary sort by name (descending) for tags (#1393) Signed-off-by: leo --- src/Models/Tag.cs | 3 +-- src/Resources/Locales/de_DE.axaml | 3 +-- src/Resources/Locales/en_US.axaml | 3 +-- src/Resources/Locales/es_ES.axaml | 3 +-- src/Resources/Locales/fr_FR.axaml | 3 +-- src/Resources/Locales/it_IT.axaml | 3 +-- src/Resources/Locales/ja_JP.axaml | 2 +- src/Resources/Locales/ru_RU.axaml | 3 +-- src/Resources/Locales/ta_IN.axaml | 3 +-- src/Resources/Locales/uk_UA.axaml | 3 +-- src/Resources/Locales/zh_CN.axaml | 3 +-- src/Resources/Locales/zh_TW.axaml | 3 +-- src/ViewModels/Repository.cs | 30 ++++++++---------------------- 13 files changed, 20 insertions(+), 45 deletions(-) diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 20678530..87944637 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -5,8 +5,7 @@ namespace SourceGit.Models public enum TagSortMode { CreatorDate = 0, - NameInAscending, - NameInDescending, + Name, } public class Tag : ObservableObject diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index b7874216..89a0ae1b 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -624,8 +624,7 @@ TAGS NEUER TAG Nach Erstellungsdatum - Nach Namen (Aufsteigend) - Nach Namen (Absteigend) + Nach Namen Sortiere Öffne im Terminal Verwende relative Zeitangaben in Verlauf diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 765b6f8c..8ed2770d 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -638,8 +638,7 @@ TAGS New Tag By Creator Date - By Name (Ascending) - By Name (Descending) + By Name Sort Open in Terminal Use relative time in histories diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index c1687f9c..a1879bde 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -641,8 +641,7 @@ ETIQUETAS NUEVA ETIQUETA Por Fecha de Creación - Por Nombre (Ascendiente) - Por Nombre (Descendiente) + Por Nombre Ordenar Abrir en Terminal Usar tiempo relativo en las historias diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 24dd365e..52ec8d6d 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -602,8 +602,7 @@ TAGS NOUVEAU TAG Par date de créateur - Par nom (Croissant) - Par nom (Décroissant) + Par nom Trier Ouvrir dans un terminal Utiliser le temps relatif dans les historiques diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 1484201c..9488d8d9 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -628,8 +628,7 @@ TAG NUOVO TAG Per data di creazione - Per nome (ascendente) - Per nome (discendente) + Per nome Ordina Apri nel Terminale Usa tempo relativo nello storico diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index db395d86..80372cb4 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -600,7 +600,7 @@ タグ 新しいタグを作成 作成者日時 - 名前 (昇順) + 名前 ソート ターミナルで開く 履歴に相対時間を使用 diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 537a3deb..0aeb60b5 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -639,8 +639,7 @@ МЕТКИ НОВАЯ МЕТКА По дате создания - По имени (по возрастанию) - По имени (по убыванию) + По имени Сортировать Открыть в терминале Использовать относительное время в историях diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 7f3542c3..ec558c5d 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -601,8 +601,7 @@ குறிசொற்கள் புதிய குறிசொல் படைப்பாளர் தேதியின்படி - பெயர் (ஏறுவரிசை) மூலம் - பெயர் (இறகுவரிசை) மூலம் + பெயர் மூலம் வரிசைப்படுத்து முனையத்தில் திற வரலாறுகளில் உறவு நேரத்தைப் பயன்படுத்து diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index 62842054..714374ce 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -606,8 +606,7 @@ ТЕГИ НОВИЙ ТЕГ За датою створення - За назвою (за зростанням) - За назвою (за спаданням) + За назвою Сортувати Відкрити в терміналі Використовувати відносний час в історії diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index c2a44bbf..628a097a 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -642,8 +642,7 @@ 标签列表 新建标签 按创建时间 - 按名称(升序) - 按名称(降序) + 按名称 排序 在终端中打开 在提交列表中使用相对时间 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index a4e6074b..e2261005 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -642,8 +642,7 @@ 標籤列表 新增標籤 依建立時間 - 依名稱升序 - 依名稱降序 + 依名稱 排序 在終端機中開啟 在提交列表中使用相對時間 diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 2b2771ec..3dbe8cfc 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2442,30 +2442,19 @@ namespace SourceGit.ViewModels ev.Handled = true; }; - var byNameAsc = new MenuItem(); - byNameAsc.Header = App.Text("Repository.Tags.OrderByNameAsc"); - if (mode == Models.TagSortMode.NameInAscending) - byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); - byNameAsc.Click += (_, ev) => + var byName = new MenuItem(); + byName.Header = App.Text("Repository.Tags.OrderByName"); + if (mode == Models.TagSortMode.Name) + byName.Icon = App.CreateMenuIcon("Icons.Check"); + byName.Click += (_, ev) => { - changeMode(Models.TagSortMode.NameInAscending); - ev.Handled = true; - }; - - var byNameDes = new MenuItem(); - byNameDes.Header = App.Text("Repository.Tags.OrderByNameDes"); - if (mode == Models.TagSortMode.NameInDescending) - byNameDes.Icon = App.CreateMenuIcon("Icons.Check"); - byNameDes.Click += (_, ev) => - { - changeMode(Models.TagSortMode.NameInDescending); + changeMode(Models.TagSortMode.Name); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(byCreatorDate); - menu.Items.Add(byNameAsc); - menu.Items.Add(byNameDes); + menu.Items.Add(byName); return menu; } @@ -2638,11 +2627,8 @@ namespace SourceGit.ViewModels case Models.TagSortMode.CreatorDate: _tags.Sort((l, r) => r.CreatorDate.CompareTo(l.CreatorDate)); break; - case Models.TagSortMode.NameInAscending: - _tags.Sort((l, r) => Models.NumericSort.Compare(l.Name, r.Name)); - break; default: - _tags.Sort((l, r) => Models.NumericSort.Compare(r.Name, l.Name)); + _tags.Sort((l, r) => Models.NumericSort.Compare(l.Name, r.Name)); break; } From 08665e45c101c1e3209b5a31c1772d8811b702db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 6 Jun 2025 02:45:35 +0000 Subject: [PATCH 25/44] doc: Update translation status and sort locale files --- TRANSLATION.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 3916531d..78d1e843 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -52,7 +52,7 @@ This document shows the translation status of each locale file in the repository -### ![fr__FR](https://img.shields.io/badge/fr__FR-92.27%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-92.26%25-yellow)
Missing keys in fr_FR.axaml @@ -149,7 +149,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-91.90%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-92.01%25-yellow)
Missing keys in ja_JP.axaml @@ -196,7 +196,6 @@ This document shows the translation status of each locale file in the repository - Text.Repository.FilterCommits - Text.Repository.Search.ByContent - Text.Repository.ShowSubmodulesAsTree -- Text.Repository.Tags.OrderByNameDes - Text.Repository.ViewLogs - Text.Repository.Visit - Text.ResetWithoutCheckout @@ -222,7 +221,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-83.92%25-yellow) +### ![pt__BR](https://img.shields.io/badge/pt__BR-84.02%25-yellow)
Missing keys in pt_BR.axaml @@ -320,8 +319,7 @@ This document shows the translation status of each locale file in the repository - Text.Repository.ShowSubmodulesAsTree - Text.Repository.Skip - Text.Repository.Tags.OrderByCreatorDate -- Text.Repository.Tags.OrderByNameAsc -- Text.Repository.Tags.OrderByNameDes +- Text.Repository.Tags.OrderByName - Text.Repository.Tags.Sort - Text.Repository.UseRelativeTimeInHistories - Text.Repository.ViewLogs @@ -371,7 +369,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-92.14%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-92.13%25-yellow)
Missing keys in ta_IN.axaml @@ -442,7 +440,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-93.39%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-93.38%25-yellow)
Missing keys in uk_UA.axaml From f63fe8637b6998fe5665ba3ff15dcf82836a1395 Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 11:22:30 +0800 Subject: [PATCH 26/44] feature: use different icon for sort mode (#1393) Signed-off-by: leo --- src/Resources/Icons.axaml | 3 ++- src/ViewModels/Repository.cs | 24 ++++++++++++++++++++++++ src/Views/Repository.axaml | 15 ++++++++++++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index 9da3a51e..5cf08130 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -89,7 +89,8 @@ M299 811 299 725 384 725 384 811 299 811M469 811 469 725 555 725 555 811 469 811M640 811 640 725 725 725 725 811 640 811M299 640 299 555 384 555 384 640 299 640M469 640 469 555 555 555 555 640 469 640M640 640 640 555 725 555 725 640 640 640M299 469 299 384 384 384 384 469 299 469M469 469 469 384 555 384 555 469 469 469M640 469 640 384 725 384 725 469 640 469M299 299 299 213 384 213 384 299 299 299M469 299 469 213 555 213 555 299 469 299M640 299 640 213 725 213 725 299 640 299Z M64 363l0 204 265 0L329 460c0-11 6-18 14-20C349 437 355 437 362 441c93 60 226 149 226 149 33 22 34 60 0 82 0 0-133 89-226 149-14 9-32-3-32-18l-1-110L64 693l0 117c0 41 34 75 75 75l746 0c41 0 75-34 75-74L960 364c0-0 0-1 0-1L64 363zM64 214l0 75 650 0-33-80c-16-38-62-69-103-69l-440 0C97 139 64 173 64 214z M683 409v204L1024 308 683 0v191c-413 0-427 526-427 526c117-229 203-307 427-307zm85 492H102V327h153s38-63 114-122H51c-28 0-51 27-51 61v697c0 34 23 61 51 61h768c28 0 51-27 51-61V614l-102 100v187z - M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z + M841 627A43 43 0 00811 555h-299v85h196l-183 183A43 43 0 00555 896h299v-85h-196l183-183zM299 170H213v512H85l171 171 171-171H299zM725 128h-85c-18 0-34 11-40 28l-117 313h91L606 384h154l32 85h91l-117-313A43 43 0 00725 128zm-88 171 32-85h26l32 85h-90z + M0 512M1024 512M512 0M512 1024M279 128l3 0c9 1 18 5 25 12l171 171a43 43 0 01-60 60L320 274V853a43 43 0 01-39 43L277 896a43 43 0 01-43-43V274l-98 98a43 43 0 01-58 2l-3-2a43 43 0 010-60l171-171c8-8 18-12 28-12h3zM640 132c210 0 380 170 380 380 0 210-170 380-380 380a380 380 0 01-206-61 43 43 0 0146-72A294 294 0 00640 807c163 0 295-132 295-295 0-163-132-295-295-295-25 0-49 3-73 9a43 43 0 11-21-83A381 381 0 01640 132zm0 178a43 43 0 0143 43v117l125 0a43 43 0 0143 39l0 3a43 43 0 01-43 43h-167a43 43 0 01-43-43V352a43 43 0 0143-43z M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z m186 532 287 0 0 287c0 11 9 20 20 20s20-9 20-20l0-287 287 0c11 0 20-9 20-20s-9-20-20-20l-287 0 0-287c0-11-9-20-20-20s-20 9-20 20l0 287-287 0c-11 0-20 9-20 20s9 20 20 20z diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 3dbe8cfc..0a384ec7 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -434,6 +434,21 @@ namespace SourceGit.ViewModels } } + public bool IsSortingLocalBranchByName + { + get => _settings.LocalBranchSortMode == Models.BranchSortMode.Name; + } + + public bool IsSortingRemoteBranchByName + { + get => _settings.RemoteBranchSortMode == Models.BranchSortMode.Name; + } + + public bool IsSortingTagsByName + { + get => _settings.TagSortMode == Models.TagSortMode.Name; + } + public InProgressContext InProgressContext { get => _workingCopy?.InProgressContext; @@ -2381,9 +2396,15 @@ namespace SourceGit.ViewModels var changeMode = new Action(m => { if (local) + { _settings.LocalBranchSortMode = m; + OnPropertyChanged(nameof(IsSortingLocalBranchByName)); + } else + { _settings.RemoteBranchSortMode = m; + OnPropertyChanged(nameof(IsSortingRemoteBranchByName)); + } var builder = BuildBranchTree(_branches, _remotes); LocalBranchTrees = builder.Locals; @@ -2415,6 +2436,7 @@ namespace SourceGit.ViewModels }; var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; menu.Items.Add(byNameAsc); menu.Items.Add(byCommitterDate); return menu; @@ -2428,6 +2450,7 @@ namespace SourceGit.ViewModels if (_settings.TagSortMode != m) { _settings.TagSortMode = m; + OnPropertyChanged(nameof(IsSortingTagsByName)); VisibleTags = BuildVisibleTags(); } }); @@ -2453,6 +2476,7 @@ namespace SourceGit.ViewModels }; var menu = new ContextMenu(); + menu.Placement = PlacementMode.BottomEdgeAlignedLeft; menu.Items.Add(byCreatorDate); menu.Items.Add(byName); return menu; diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 21c1b780..0a196d99 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -229,7 +229,10 @@ Margin="8,0,0,0" Click="OnOpenSortLocalBranchMenu" ToolTip.Tip="{DynamicResource Text.Repository.BranchSort}"> - + + + + From a2ca071f082518862df5dd8fc6aa326b9245ce92 Mon Sep 17 00:00:00 2001 From: Henrik Andersson <138666275+henrik-andersson-sus@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:44:40 +0200 Subject: [PATCH 29/44] feature: `.dds` image support (#1392) * Added Pfim as 3rdparty lib * Added support for parsing showing dds and tga images using Pfim --------- Co-authored-by: Snimax --- src/Models/ImageDecoder.cs | 1 + src/SourceGit.csproj | 1 + src/ViewModels/ImageSource.cs | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/src/Models/ImageDecoder.cs b/src/Models/ImageDecoder.cs index 3a758882..ce3a44c1 100644 --- a/src/Models/ImageDecoder.cs +++ b/src/Models/ImageDecoder.cs @@ -4,5 +4,6 @@ { None = 0, Builtin, + Pfim } } diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 3fe21b1a..62ec6255 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -50,6 +50,7 @@ + diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index ec6b6a8d..2be09809 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -1,5 +1,7 @@ using System.IO; +using System.Runtime.InteropServices; using Avalonia.Media.Imaging; +using Pfim; namespace SourceGit.ViewModels { @@ -27,6 +29,9 @@ namespace SourceGit.ViewModels case ".png": case ".webp": return Models.ImageDecoder.Builtin; + case ".tga": + case ".dds": + return Models.ImageDecoder.Pfim; default: return Models.ImageDecoder.None; } @@ -70,9 +75,93 @@ namespace SourceGit.ViewModels // Just ignore. } } + else if (decoder == Models.ImageDecoder.Pfim) + { + return new ImageSource(LoadWithPfim(stream), size); + } } return new ImageSource(null, 0); } + + private static Bitmap LoadWithPfim(Stream stream) + { + var image = Pfim.Pfimage.FromStream(stream); + byte[] data; + int stride; + if (image.Format == ImageFormat.Rgba32) + { + data = image.Data; + stride = image.Stride; + } + else + { + int pixels = image.Width * image.Height; + data = new byte[pixels * 4]; + stride = image.Width * 4; + + switch (image.Format) + { + case ImageFormat.Rgba16: + case ImageFormat.R5g5b5a1: + { + for (int i = 0; i < pixels; i++) + { + data[i * 4 + 0] = image.Data[i * 4 + 2]; // B + data[i * 4 + 1] = image.Data[i * 4 + 1]; // G + data[i * 4 + 2] = image.Data[i * 4 + 0]; // R + data[i * 4 + 3] = image.Data[i * 4 + 3]; // A + } + } + break; + case ImageFormat.R5g5b5: + case ImageFormat.R5g6b5: + case ImageFormat.Rgb24: + { + for (int i = 0; i < pixels; i++) + { + data[i * 4 + 0] = image.Data[i * 3 + 2]; // B + data[i * 4 + 1] = image.Data[i * 3 + 1]; // G + data[i * 4 + 2] = image.Data[i * 3 + 0]; // R + data[i * 4 + 3] = 255; // A + } + } + break; + case ImageFormat.Rgb8: + { + for (int i = 0; i < pixels; i++) + { + var color = image.Data[i]; + data[i * 4 + 0] = color; + data[i * 4 + 1] = color; + data[i * 4 + 2] = color; + data[i * 4 + 3] = 255; + } + } + break; + default: + return null; + } + } + + // Pin the array and pass the pointer to Bitmap + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); + var bitmap = new Bitmap( + Avalonia.Platform.PixelFormat.Bgra8888, + Avalonia.Platform.AlphaFormat.Unpremul, + ptr, + new Avalonia.PixelSize(image.Width, image.Height), + new Avalonia.Vector(96, 96), + stride); + return bitmap; + } + finally + { + handle.Free(); + } + } } } From 8db033be991836c7be7eb8373a8c8a651bbd9f3a Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 18:21:04 +0800 Subject: [PATCH 30/44] code_review: PR #1392 - fix the issue that not all channel takes 8 bits - if `PixelFormatTranscoder.Transcode` supports the same pixel formats, let it converts pixels automatically Signed-off-by: leo --- src/ViewModels/ImageSource.cs | 174 +++++++++++++++++----------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index 2be09809..4993e2cf 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -1,6 +1,11 @@ -using System.IO; +using System; +using System.IO; using System.Runtime.InteropServices; + +using Avalonia; using Avalonia.Media.Imaging; +using Avalonia.Platform; + using Pfim; namespace SourceGit.ViewModels @@ -64,103 +69,98 @@ namespace SourceGit.ViewModels if (size > 0) { if (decoder == Models.ImageDecoder.Builtin) - { - try - { - var bitmap = new Bitmap(stream); - return new ImageSource(bitmap, size); - } - catch - { - // Just ignore. - } - } + return DecodeWithAvalonia(stream, size); else if (decoder == Models.ImageDecoder.Pfim) - { - return new ImageSource(LoadWithPfim(stream), size); - } + return DecodeWithPfim(stream, size); } return new ImageSource(null, 0); } - private static Bitmap LoadWithPfim(Stream stream) + private static ImageSource DecodeWithAvalonia(Stream stream, long size) { - var image = Pfim.Pfimage.FromStream(stream); - byte[] data; - int stride; - if (image.Format == ImageFormat.Rgba32) - { - data = image.Data; - stride = image.Stride; - } - else - { - int pixels = image.Width * image.Height; - data = new byte[pixels * 4]; - stride = image.Width * 4; - - switch (image.Format) - { - case ImageFormat.Rgba16: - case ImageFormat.R5g5b5a1: - { - for (int i = 0; i < pixels; i++) - { - data[i * 4 + 0] = image.Data[i * 4 + 2]; // B - data[i * 4 + 1] = image.Data[i * 4 + 1]; // G - data[i * 4 + 2] = image.Data[i * 4 + 0]; // R - data[i * 4 + 3] = image.Data[i * 4 + 3]; // A - } - } - break; - case ImageFormat.R5g5b5: - case ImageFormat.R5g6b5: - case ImageFormat.Rgb24: - { - for (int i = 0; i < pixels; i++) - { - data[i * 4 + 0] = image.Data[i * 3 + 2]; // B - data[i * 4 + 1] = image.Data[i * 3 + 1]; // G - data[i * 4 + 2] = image.Data[i * 3 + 0]; // R - data[i * 4 + 3] = 255; // A - } - } - break; - case ImageFormat.Rgb8: - { - for (int i = 0; i < pixels; i++) - { - var color = image.Data[i]; - data[i * 4 + 0] = color; - data[i * 4 + 1] = color; - data[i * 4 + 2] = color; - data[i * 4 + 3] = 255; - } - } - break; - default: - return null; - } - } - - // Pin the array and pass the pointer to Bitmap - var handle = GCHandle.Alloc(data, GCHandleType.Pinned); try { - var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); - var bitmap = new Bitmap( - Avalonia.Platform.PixelFormat.Bgra8888, - Avalonia.Platform.AlphaFormat.Unpremul, - ptr, - new Avalonia.PixelSize(image.Width, image.Height), - new Avalonia.Vector(96, 96), - stride); - return bitmap; + var bitmap = new Bitmap(stream); + return new ImageSource(bitmap, size); } - finally + catch { - handle.Free(); + return new ImageSource(null, 0); + } + } + + private static ImageSource DecodeWithPfim(Stream stream, long size) + { + using (var pfiImage = Pfimage.FromStream(stream)) + { + try + { + var data = pfiImage.Data; + var stride = pfiImage.Stride; + + var pixelFormat = PixelFormats.Rgba8888; + var alphaFormat = AlphaFormat.Opaque; + switch (pfiImage.Format) + { + case ImageFormat.Rgb8: + pixelFormat = PixelFormats.Gray8; + break; + case ImageFormat.R5g6b5: + pixelFormat = PixelFormats.Rgb565; + break; + case ImageFormat.Rgba16: + var pixels = pfiImage.DataLen / 2; + var newSize = pfiImage.DataLen * 2; + data = new byte[newSize]; + stride = 4 * pfiImage.Width; + for (int i = 0; i < pixels; i++) + { + var rg = pfiImage.Data[i * 2]; + var ba = pfiImage.Data[i * 2 + 1]; + data[i * 4 + 0] = (byte)Math.Round((rg >> 4) / 15.0 * 255); + data[i * 4 + 1] = (byte)Math.Round((rg & 0xF) / 15.0 * 255); + data[i * 4 + 2] = (byte)Math.Round((ba >> 4) / 15.0 * 255); + data[i * 4 + 3] = (byte)Math.Round((ba & 0xF) / 15.0 * 255); + } + alphaFormat = AlphaFormat.Premul; + break; + case ImageFormat.R5g5b5a1: + var pixels2 = pfiImage.DataLen / 2; + var newSize2 = pfiImage.DataLen * 2; + data = new byte[newSize2]; + stride = 4 * pfiImage.Width; + for (int i = 0; i < pixels2; i++) + { + var v = (int)pfiImage.Data[i * 2] << 8 + pfiImage.Data[i * 2 + 1]; + data[i * 4 + 0] = (byte)Math.Round(((v & 0b1111100000000000) >> 11) / 31.0 * 255); + data[i * 4 + 1] = (byte)Math.Round(((v & 0b11111000000) >> 6) / 31.0 * 255); + data[i * 4 + 2] = (byte)Math.Round(((v & 0b111110) >> 1) / 31.0 * 255); + data[i * 4 + 3] = (byte)((v & 1) == 1 ? 255 : 0); + } + alphaFormat = AlphaFormat.Premul; + break; + case ImageFormat.Rgb24: + pixelFormat = PixelFormats.Rgb24; + break; + case ImageFormat.Rgba32: + pixelFormat = PixelFormat.Rgba8888; + alphaFormat = AlphaFormat.Premul; + break; + default: + return new ImageSource(null, 0); + } + + var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(pfiImage.Data, 0); + var pixelSize = new PixelSize(pfiImage.Width, pfiImage.Height); + var dpi = new Vector(96, 96); + var bitmap = new Bitmap(pixelFormat, alphaFormat, ptr, pixelSize, dpi, stride); + return new ImageSource(bitmap, size); + } + catch + { + return new ImageSource(null, 0); + } } } } From 47012e29dc4076cee2e01b3a1111ef88bcb5269d Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 18:47:36 +0800 Subject: [PATCH 31/44] fix: file extensions are case-insensitive Signed-off-by: leo --- src/ViewModels/ImageSource.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index 4993e2cf..be8ffc0f 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Runtime.InteropServices; @@ -23,7 +24,7 @@ namespace SourceGit.ViewModels public static Models.ImageDecoder GetDecoder(string file) { - var ext = Path.GetExtension(file) ?? ".invalid_img"; + var ext = (Path.GetExtension(file) ?? ".invalid_img").ToLower(CultureInfo.CurrentCulture); switch (ext) { @@ -92,9 +93,9 @@ namespace SourceGit.ViewModels private static ImageSource DecodeWithPfim(Stream stream, long size) { - using (var pfiImage = Pfimage.FromStream(stream)) + try { - try + using (var pfiImage = Pfimage.FromStream(stream)) { var data = pfiImage.Data; var stride = pfiImage.Stride; @@ -136,7 +137,7 @@ namespace SourceGit.ViewModels data[i * 4 + 0] = (byte)Math.Round(((v & 0b1111100000000000) >> 11) / 31.0 * 255); data[i * 4 + 1] = (byte)Math.Round(((v & 0b11111000000) >> 6) / 31.0 * 255); data[i * 4 + 2] = (byte)Math.Round(((v & 0b111110) >> 1) / 31.0 * 255); - data[i * 4 + 3] = (byte)((v & 1) == 1 ? 255 : 0); + data[i * 4 + 3] = (byte)((v & 1) == 1 ? 255 : 0); } alphaFormat = AlphaFormat.Premul; break; @@ -157,10 +158,10 @@ namespace SourceGit.ViewModels var bitmap = new Bitmap(pixelFormat, alphaFormat, ptr, pixelSize, dpi, stride); return new ImageSource(bitmap, size); } - catch - { - return new ImageSource(null, 0); - } + } + catch (Exception e) + { + return new ImageSource(null, 0); } } } From 203c50350e1a2296338500250cf887e63930b64c Mon Sep 17 00:00:00 2001 From: leo Date: Fri, 6 Jun 2025 20:50:37 +0800 Subject: [PATCH 32/44] fix: wrong pfim image format Signed-off-by: leo --- src/ViewModels/ImageSource.cs | 44 +++++++---------------------------- 1 file changed, 8 insertions(+), 36 deletions(-) diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index be8ffc0f..22740dc7 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -100,59 +100,31 @@ namespace SourceGit.ViewModels var data = pfiImage.Data; var stride = pfiImage.Stride; - var pixelFormat = PixelFormats.Rgba8888; + var pixelFormat = PixelFormats.Bgra8888; var alphaFormat = AlphaFormat.Opaque; switch (pfiImage.Format) { case ImageFormat.Rgb8: pixelFormat = PixelFormats.Gray8; break; - case ImageFormat.R5g6b5: - pixelFormat = PixelFormats.Rgb565; - break; - case ImageFormat.Rgba16: - var pixels = pfiImage.DataLen / 2; - var newSize = pfiImage.DataLen * 2; - data = new byte[newSize]; - stride = 4 * pfiImage.Width; - for (int i = 0; i < pixels; i++) - { - var rg = pfiImage.Data[i * 2]; - var ba = pfiImage.Data[i * 2 + 1]; - data[i * 4 + 0] = (byte)Math.Round((rg >> 4) / 15.0 * 255); - data[i * 4 + 1] = (byte)Math.Round((rg & 0xF) / 15.0 * 255); - data[i * 4 + 2] = (byte)Math.Round((ba >> 4) / 15.0 * 255); - data[i * 4 + 3] = (byte)Math.Round((ba & 0xF) / 15.0 * 255); - } - alphaFormat = AlphaFormat.Premul; - break; + case ImageFormat.R5g5b5: case ImageFormat.R5g5b5a1: - var pixels2 = pfiImage.DataLen / 2; - var newSize2 = pfiImage.DataLen * 2; - data = new byte[newSize2]; - stride = 4 * pfiImage.Width; - for (int i = 0; i < pixels2; i++) - { - var v = (int)pfiImage.Data[i * 2] << 8 + pfiImage.Data[i * 2 + 1]; - data[i * 4 + 0] = (byte)Math.Round(((v & 0b1111100000000000) >> 11) / 31.0 * 255); - data[i * 4 + 1] = (byte)Math.Round(((v & 0b11111000000) >> 6) / 31.0 * 255); - data[i * 4 + 2] = (byte)Math.Round(((v & 0b111110) >> 1) / 31.0 * 255); - data[i * 4 + 3] = (byte)((v & 1) == 1 ? 255 : 0); - } - alphaFormat = AlphaFormat.Premul; + pixelFormat = PixelFormats.Bgr555; + break; + case ImageFormat.R5g6b5: + pixelFormat = PixelFormats.Bgr565; break; case ImageFormat.Rgb24: - pixelFormat = PixelFormats.Rgb24; + pixelFormat = PixelFormats.Bgr24; break; case ImageFormat.Rgba32: - pixelFormat = PixelFormat.Rgba8888; alphaFormat = AlphaFormat.Premul; break; default: return new ImageSource(null, 0); } - var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(pfiImage.Data, 0); + var ptr = Marshal.UnsafeAddrOfPinnedArrayElement(data, 0); var pixelSize = new PixelSize(pfiImage.Width, pfiImage.Height); var dpi = new Vector(96, 96); var bitmap = new Bitmap(pixelFormat, alphaFormat, ptr, pixelSize, dpi, stride); From d323a2064ef02adc759129e3984d3907d0be694d Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 7 Jun 2025 12:00:16 +0800 Subject: [PATCH 33/44] feature: supports RGBA16 pixel format Signed-off-by: leo --- src/ViewModels/ImageSource.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/ViewModels/ImageSource.cs b/src/ViewModels/ImageSource.cs index 22740dc7..634fbefe 100644 --- a/src/ViewModels/ImageSource.cs +++ b/src/ViewModels/ImageSource.cs @@ -117,6 +117,21 @@ namespace SourceGit.ViewModels case ImageFormat.Rgb24: pixelFormat = PixelFormats.Bgr24; break; + case ImageFormat.Rgba16: + var pixels2 = pfiImage.DataLen / 2; + data = new byte[pixels2 * 4]; + stride = pfiImage.Width * 4; + for (var i = 0; i < pixels2; i++) + { + var src = BitConverter.ToUInt16(pfiImage.Data, i * 2); + data[i * 4 + 0] = (byte)Math.Round((src & 0x0F) / 15F * 255); // B + data[i * 4 + 1] = (byte)Math.Round(((src >> 4) & 0x0F) / 15F * 255); // G + data[i * 4 + 2] = (byte)Math.Round(((src >> 8) & 0x0F) / 15F * 255); // R + data[i * 4 + 3] = (byte)Math.Round(((src >> 12) & 0x0F) / 15F * 255); // A + } + + alphaFormat = AlphaFormat.Premul; + break; case ImageFormat.Rgba32: alphaFormat = AlphaFormat.Premul; break; @@ -131,7 +146,7 @@ namespace SourceGit.ViewModels return new ImageSource(bitmap, size); } } - catch (Exception e) + catch { return new ImageSource(null, 0); } From f830b68f6a7e50a8b92ed0e4541627b4e4082af0 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 7 Jun 2025 12:20:09 +0800 Subject: [PATCH 34/44] ux: change foreground for some labels Signed-off-by: leo --- src/Views/DiffView.axaml | 8 ++++---- src/Views/ImageDiffView.axaml | 6 +++--- src/Views/RevisionFileContentViewer.axaml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Views/DiffView.axaml b/src/Views/DiffView.axaml index c9291a6b..d1a3966d 100644 --- a/src/Views/DiffView.axaml +++ b/src/Views/DiffView.axaml @@ -213,7 +213,7 @@ - + @@ -240,7 +240,7 @@ - + @@ -274,7 +274,7 @@ - + @@ -290,7 +290,7 @@ - + diff --git a/src/Views/ImageDiffView.axaml b/src/Views/ImageDiffView.axaml index c3e0f772..d0b79f57 100644 --- a/src/Views/ImageDiffView.axaml +++ b/src/Views/ImageDiffView.axaml @@ -44,7 +44,7 @@ - + @@ -79,7 +79,7 @@ - + @@ -115,7 +115,7 @@ - + diff --git a/src/Views/RevisionFileContentViewer.axaml b/src/Views/RevisionFileContentViewer.axaml index 422e9169..7dc6d384 100644 --- a/src/Views/RevisionFileContentViewer.axaml +++ b/src/Views/RevisionFileContentViewer.axaml @@ -39,7 +39,7 @@ - + From 74f52fb26636509f79c0dc0bdb33a75acd1e31a6 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 7 Jun 2025 20:27:52 +0800 Subject: [PATCH 35/44] enhance: only show syntax-highlighting toggle if current revision content is a text file Signed-off-by: leo --- src/Converters/ObjectConverters.cs | 27 +++++++++++++++++++++++++++ src/Views/FileHistories.axaml | 16 +++++++++++++++- src/Views/RevisionFiles.axaml | 7 +++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/Converters/ObjectConverters.cs diff --git a/src/Converters/ObjectConverters.cs b/src/Converters/ObjectConverters.cs new file mode 100644 index 00000000..f7c57764 --- /dev/null +++ b/src/Converters/ObjectConverters.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class ObjectConverters + { + public class IsTypeOfConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null || parameter == null) + return false; + + return value.GetType().IsAssignableTo((Type)parameter); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + return new NotImplementedException(); + } + } + + public static readonly IsTypeOfConverter IsTypeOf = new IsTypeOfConverter(); + } +} diff --git a/src/Views/FileHistories.axaml b/src/Views/FileHistories.axaml index e7a5c072..be0c91a0 100644 --- a/src/Views/FileHistories.axaml +++ b/src/Views/FileHistories.axaml @@ -139,7 +139,7 @@ - + + + + + + + diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml index cfc20198..b4bd3354 100644 --- a/src/Views/RevisionFiles.axaml +++ b/src/Views/RevisionFiles.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" xmlns:c="using:SourceGit.Converters" @@ -133,6 +134,12 @@ Background="Transparent" IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=UseSyntaxHighlighting, Mode=TwoWay}" ToolTip.Tip="{DynamicResource Text.Diff.SyntaxHighlight}"> + + + From 2478d2953b3c652fc278f66694af8ecfe6904019 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 7 Jun 2025 20:42:45 +0800 Subject: [PATCH 36/44] code_style: remove unnecessary code in `DiffContext` Signed-off-by: leo --- src/ViewModels/DiffContext.cs | 39 ++++++++++++++--------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/ViewModels/DiffContext.cs b/src/ViewModels/DiffContext.cs index 828a2d59..9bb3c710 100644 --- a/src/ViewModels/DiffContext.cs +++ b/src/ViewModels/DiffContext.cs @@ -12,7 +12,7 @@ namespace SourceGit.ViewModels { public string Title { - get => _title; + get; } public bool IgnoreWhitespace @@ -68,9 +68,9 @@ namespace SourceGit.ViewModels } if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null") - _title = _option.Path; + Title = _option.Path; else - _title = $"{_option.OrgPath} → {_option.Path}"; + Title = $"{_option.OrgPath} → {_option.Path}"; LoadDiffContent(); } @@ -112,9 +112,9 @@ namespace SourceGit.ViewModels Task.Run(() => { var numLines = Preferences.Instance.UseFullTextDiff ? 999999999 : _unifiedLines; - var ignoreWS = Preferences.Instance.IgnoreWhitespaceChangesInDiff; - var latest = new Commands.Diff(_repo, _option, numLines, ignoreWS).Result(); - var info = new Info(_option, numLines, ignoreWS, latest); + var ignoreWhitespace = Preferences.Instance.IgnoreWhitespaceChangesInDiff; + var latest = new Commands.Diff(_repo, _option, numLines, ignoreWhitespace).Result(); + var info = new Info(_option, numLines, ignoreWhitespace, latest); if (_info != null && info.IsSame(_info)) return; @@ -239,30 +239,24 @@ namespace SourceGit.ViewModels private Models.RevisionSubmodule QuerySubmoduleRevision(string repo, string sha) { var commit = new Commands.QuerySingleCommit(repo, sha).Result(); - if (commit != null) - { - var body = new Commands.QueryCommitFullMessage(repo, sha).Result(); - return new Models.RevisionSubmodule() - { - Commit = commit, - FullMessage = new Models.CommitFullMessage { Message = body } - }; - } + if (commit == null) + return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } }; + var body = new Commands.QueryCommitFullMessage(repo, sha).Result(); return new Models.RevisionSubmodule() { - Commit = new Models.Commit() { SHA = sha }, - FullMessage = null, + Commit = commit, + FullMessage = new Models.CommitFullMessage { Message = body } }; } private class Info { - public string Argument { get; set; } - public int UnifiedLines { get; set; } - public bool IgnoreWhitespace { get; set; } - public string OldHash { get; set; } - public string NewHash { get; set; } + public string Argument { get; } + public int UnifiedLines { get; } + public bool IgnoreWhitespace { get; } + public string OldHash { get; } + public string NewHash { get; } public Info(Models.DiffOption option, int unifiedLines, bool ignoreWhitespace, Models.DiffResult result) { @@ -285,7 +279,6 @@ namespace SourceGit.ViewModels private readonly string _repo; private readonly Models.DiffOption _option = null; - private string _title; private string _fileModeChange = string.Empty; private int _unifiedLines = 4; private bool _isTextDiff = false; From ba4c0f0cd2596946b6d7356cda83f068e5043f00 Mon Sep 17 00:00:00 2001 From: leo Date: Sat, 7 Jun 2025 20:53:34 +0800 Subject: [PATCH 37/44] enhance: scroll to home when active revision file changed Signed-off-by: leo --- src/Views/RevisionFileContentViewer.axaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Views/RevisionFileContentViewer.axaml.cs b/src/Views/RevisionFileContentViewer.axaml.cs index 59f2d33d..4a6f69b0 100644 --- a/src/Views/RevisionFileContentViewer.axaml.cs +++ b/src/Views/RevisionFileContentViewer.axaml.cs @@ -83,6 +83,7 @@ namespace SourceGit.Views { Text = source.Content; Models.TextMateHelper.SetGrammarByFileName(_textMate, source.FileName); + ScrollToHome(); } else { From fe54d30b7010f7e10ebcc1f7b92dc032cb4a7bbb Mon Sep 17 00:00:00 2001 From: Sina Hinderks Date: Sun, 8 Jun 2025 02:47:03 +0200 Subject: [PATCH 38/44] refactor: collecting inlines for subjects (#1402) Instead of checking intersections of inline elements yourself before adding an inline element, the new class `InlineElementCollector` prevents intersections internally. Additionally the inline elements are sorted by the new class, so it's no longer necessary to do this after adding the inline elements. --- src/Models/Commit.cs | 2 +- src/Models/InlineElementCollector.cs | 85 ++++++++++++++++++++++++++++ src/Models/IssueTrackerRule.cs | 17 +----- src/ViewModels/CommitDetail.cs | 31 +--------- src/Views/CommitMessagePresenter.cs | 37 ++++++------ src/Views/CommitSubjectPresenter.cs | 16 +----- 6 files changed, 109 insertions(+), 79 deletions(-) create mode 100644 src/Models/InlineElementCollector.cs diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index ced7597e..6bf2655a 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -121,6 +121,6 @@ namespace SourceGit.Models public class CommitFullMessage { public string Message { get; set; } = string.Empty; - public List Inlines { get; set; } = []; + public InlineElementCollector Inlines { get; set; } = []; } } diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs new file mode 100644 index 00000000..b4b2e9e1 --- /dev/null +++ b/src/Models/InlineElementCollector.cs @@ -0,0 +1,85 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace SourceGit.Models +{ + public class InlineElementCollector : IEnumerable + { + private readonly List _implementation = []; + + public void Clear() + { + _implementation.Clear(); + + AssertInvariant(); + } + + public int Count => _implementation.Count; + + public void Add(InlineElement element) + { + + var index = FindIndex(element.Start); + if (!IsIntersection(index, element.Start, element.Length)) + _implementation.Insert(index, element); + + AssertInvariant(); + } + + [Conditional("DEBUG")] + private void AssertInvariant() + { + if (_implementation.Count == 0) + return; + + for (var index = 1; index < _implementation.Count; index++) + { + var prev = _implementation[index - 1]; + var curr = _implementation[index]; + + Debug.Assert(prev.Start + prev.Length <= curr.Start); + } + } + + public InlineElement Lookup(int position) + { + var index = FindIndex(position); + return IsIntersection(index, position, 1) + ? _implementation[index] + : null; + } + + private int FindIndex(int start) + { + var index = 0; + while (index < _implementation.Count && _implementation[index].Start <= start) + index++; + + return index; + } + + private bool IsIntersection(int index, int start, int length) + { + if (index > 0) + { + var predecessor = _implementation[index - 1]; + if (predecessor.Start + predecessor.Length >= start) + return true; + } + + if (index < _implementation.Count) + { + var successor = _implementation[index]; + if (start + length >= successor.Start) + return true; + } + + return false; + } + + public IEnumerator GetEnumerator() => _implementation.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index fe0fe8e0..30bce596 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; @@ -46,7 +45,7 @@ namespace SourceGit.Models set => SetProperty(ref _urlTemplate, value); } - public void Matches(List outs, string message) + public void Matches(InlineElementCollector outs, string message) { if (_regex == null || string.IsNullOrEmpty(_urlTemplate)) return; @@ -60,18 +59,6 @@ namespace SourceGit.Models var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var exist in outs) - { - if (exist.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) - continue; var link = _urlTemplate; for (var j = 1; j < match.Groups.Count; j++) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 02db9b4f..8cc18152 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -620,9 +620,9 @@ namespace SourceGit.ViewModels }); } - private List ParseInlinesInMessage(string message) + private Models.InlineElementCollector ParseInlinesInMessage(string message) { - var inlines = new List(); + var inlines = new Models.InlineElementCollector(); if (_repo.Settings.IssueTrackerRules is { Count: > 0 } rules) { foreach (var rule in rules) @@ -638,18 +638,6 @@ namespace SourceGit.ViewModels var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var link in inlines) - { - if (link.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) - continue; var url = message.Substring(start, len); if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) @@ -665,18 +653,6 @@ namespace SourceGit.ViewModels var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var link in inlines) - { - if (link.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) - continue; var sha = match.Groups[1].Value; var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result(); @@ -684,9 +660,6 @@ namespace SourceGit.ViewModels inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha)); } - if (inlines.Count > 0) - inlines.Sort((l, r) => l.Start - r.Start); - return inlines; } diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs index 0858640b..87433c45 100644 --- a/src/Views/CommitMessagePresenter.cs +++ b/src/Views/CommitMessagePresenter.cs @@ -95,26 +95,11 @@ namespace SourceGit.Views point = new Point(x, y); var pos = TextLayout.HitTestPoint(point).TextPosition; - foreach (var link in links) - { - if (!link.Intersect(pos, 1)) - continue; - if (link == _lastHover) - return; - - SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); - - _lastHover = link; - if (link.Type == Models.InlineElementType.Link) - ToolTip.SetTip(this, link.Link); - else - ProcessHoverCommitLink(link); - - return; - } - - ClearHoveredIssueLink(); + if (links.Lookup(pos) is { } link) + SetHoveredIssueLink(link); + else + ClearHoveredIssueLink(); } } @@ -291,6 +276,20 @@ namespace SourceGit.Views } } + private void SetHoveredIssueLink(Models.InlineElement link) + { + if (link == _lastHover) + return; + + SetCurrentValue(CursorProperty, Cursor.Parse("Hand")); + + _lastHover = link; + if (link.Type == Models.InlineElementType.Link) + ToolTip.SetTip(this, link.Link); + else + ProcessHoverCommitLink(link); + } + private void ClearHoveredIssueLink() { if (_lastHover != null) diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index 18902462..b860ec1d 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -167,19 +167,6 @@ namespace SourceGit.Views var start = match.Index; var len = match.Length; - var intersect = false; - foreach (var exist in _elements) - { - if (exist.Intersect(start, len)) - { - intersect = true; - break; - } - } - - if (intersect) - continue; - _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty)); } @@ -187,7 +174,6 @@ namespace SourceGit.Views foreach (var rule in rules) rule.Matches(_elements, subject); - _elements.Sort((l, r) => l.Start - r.Start); _needRebuildInlines = true; InvalidateVisual(); } @@ -364,7 +350,7 @@ namespace SourceGit.Views } } - private List _elements = []; + private Models.InlineElementCollector _elements = []; private List _inlines = []; private Models.InlineElement _lastHover = null; private bool _needRebuildInlines = false; From 84fb39f97a23eb15c42460d5df8d990674664804 Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 8 Jun 2025 11:09:20 +0800 Subject: [PATCH 39/44] code_review: PR #1402 - it's unnecessary to implement `IEnumerable` interface - we should check `IsIntersecting` before creating `InlineElement` to avoid unnecessary works suck as running `git cat-file -t ` - sort whold list after all elements have been added to avoid unnecessary memmove in `Insert` Signed-off-by: leo --- src/Models/Commit.cs | 8 +-- src/Models/InlineElement.cs | 13 ++-- src/Models/InlineElementCollector.cs | 97 +++++++--------------------- src/Models/IssueTrackerRule.cs | 2 + src/ViewModels/CommitDetail.cs | 5 ++ src/Views/CommitMessagePresenter.cs | 6 +- src/Views/CommitSubjectPresenter.cs | 9 ++- 7 files changed, 52 insertions(+), 88 deletions(-) diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 6bf2655a..865b3ac1 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -33,8 +33,8 @@ namespace SourceGit.Models public User Committer { get; set; } = User.Invalid; public ulong CommitterTime { get; set; } = 0; public string Subject { get; set; } = string.Empty; - public List Parents { get; set; } = new List(); - public List Decorators { get; set; } = new List(); + public List Parents { get; set; } = new(); + public List Decorators { get; set; } = new(); public bool HasDecorators => Decorators.Count > 0; public string AuthorTimeStr => DateTime.UnixEpoch.AddSeconds(AuthorTime).ToLocalTime().ToString(DateTimeFormat.Active.DateTime); @@ -49,7 +49,7 @@ namespace SourceGit.Models public int Color { get; set; } = 0; public double Opacity => IsMerged ? 1 : OpacityForNotMerged; public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular; - public Thickness Margin { get; set; } = new Thickness(0); + public Thickness Margin { get; set; } = new(0); public IBrush Brush => CommitGraph.Pens[Color].Brush; public void ParseDecorators(string data) @@ -121,6 +121,6 @@ namespace SourceGit.Models public class CommitFullMessage { public string Message { get; set; } = string.Empty; - public InlineElementCollector Inlines { get; set; } = []; + public InlineElementCollector Inlines { get; set; } = new(); } } diff --git a/src/Models/InlineElement.cs b/src/Models/InlineElement.cs index 53761403..ea7bcee8 100644 --- a/src/Models/InlineElement.cs +++ b/src/Models/InlineElement.cs @@ -2,8 +2,7 @@ { public enum InlineElementType { - None = 0, - Keyword, + Keyword = 0, Link, CommitSHA, Code, @@ -11,10 +10,10 @@ public class InlineElement { - public InlineElementType Type { get; set; } = InlineElementType.None; - public int Start { get; set; } = 0; - public int Length { get; set; } = 0; - public string Link { get; set; } = ""; + public InlineElementType Type { get; } + public int Start { get; } + public int Length { get; } + public string Link { get; } public InlineElement(InlineElementType type, int start, int length, string link) { @@ -24,7 +23,7 @@ Link = link; } - public bool Intersect(int start, int length) + public bool IsIntersecting(int start, int length) { if (start == Start) return true; diff --git a/src/Models/InlineElementCollector.cs b/src/Models/InlineElementCollector.cs index b4b2e9e1..d81aaf8d 100644 --- a/src/Models/InlineElementCollector.cs +++ b/src/Models/InlineElementCollector.cs @@ -1,85 +1,38 @@ -using System.Collections; using System.Collections.Generic; -using System.Diagnostics; namespace SourceGit.Models { - public class InlineElementCollector : IEnumerable + public class InlineElementCollector { - private readonly List _implementation = []; + public int Count => _implementation.Count; + public InlineElement this[int index] => _implementation[index]; + + public InlineElement Intersect(int start, int length) + { + foreach (var elem in _implementation) + { + if (elem.IsIntersecting(start, length)) + return elem; + } + + return null; + } + + public void Add(InlineElement element) + { + _implementation.Add(element); + } + + public void Sort() + { + _implementation.Sort((l, r) => l.Start.CompareTo(r.Start)); + } public void Clear() { _implementation.Clear(); - - AssertInvariant(); } - public int Count => _implementation.Count; - - public void Add(InlineElement element) - { - - var index = FindIndex(element.Start); - if (!IsIntersection(index, element.Start, element.Length)) - _implementation.Insert(index, element); - - AssertInvariant(); - } - - [Conditional("DEBUG")] - private void AssertInvariant() - { - if (_implementation.Count == 0) - return; - - for (var index = 1; index < _implementation.Count; index++) - { - var prev = _implementation[index - 1]; - var curr = _implementation[index]; - - Debug.Assert(prev.Start + prev.Length <= curr.Start); - } - } - - public InlineElement Lookup(int position) - { - var index = FindIndex(position); - return IsIntersection(index, position, 1) - ? _implementation[index] - : null; - } - - private int FindIndex(int start) - { - var index = 0; - while (index < _implementation.Count && _implementation[index].Start <= start) - index++; - - return index; - } - - private bool IsIntersection(int index, int start, int length) - { - if (index > 0) - { - var predecessor = _implementation[index - 1]; - if (predecessor.Start + predecessor.Length >= start) - return true; - } - - if (index < _implementation.Count) - { - var successor = _implementation[index]; - if (start + length >= successor.Start) - return true; - } - - return false; - } - - public IEnumerator GetEnumerator() => _implementation.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + private readonly List _implementation = []; } } diff --git a/src/Models/IssueTrackerRule.cs b/src/Models/IssueTrackerRule.cs index 30bce596..40c84b9e 100644 --- a/src/Models/IssueTrackerRule.cs +++ b/src/Models/IssueTrackerRule.cs @@ -59,6 +59,8 @@ namespace SourceGit.Models var start = match.Index; var len = match.Length; + if (outs.Intersect(start, len) != null) + continue; var link = _urlTemplate; for (var j = 1; j < match.Groups.Count; j++) diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 8cc18152..8d2ade09 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -638,6 +638,8 @@ namespace SourceGit.ViewModels var start = match.Index; var len = match.Length; + if (inlines.Intersect(start, len) != null) + continue; var url = message.Substring(start, len); if (Uri.IsWellFormedUriString(url, UriKind.Absolute)) @@ -653,6 +655,8 @@ namespace SourceGit.ViewModels var start = match.Index; var len = match.Length; + if (inlines.Intersect(start, len) != null) + continue; var sha = match.Groups[1].Value; var isCommitSHA = new Commands.IsCommitSHA(_repo.FullPath, sha).Result(); @@ -660,6 +664,7 @@ namespace SourceGit.ViewModels inlines.Add(new Models.InlineElement(Models.InlineElementType.CommitSHA, start, len, sha)); } + inlines.Sort(); return inlines; } diff --git a/src/Views/CommitMessagePresenter.cs b/src/Views/CommitMessagePresenter.cs index 87433c45..d7c7ee3f 100644 --- a/src/Views/CommitMessagePresenter.cs +++ b/src/Views/CommitMessagePresenter.cs @@ -48,8 +48,9 @@ namespace SourceGit.Views var inlines = new List(); var pos = 0; - foreach (var link in links) + for (var i = 0; i < links.Count; i++) { + var link = links[i]; if (link.Start > pos) inlines.Add(new Run(message.Substring(pos, link.Start - pos))); @@ -95,8 +96,7 @@ namespace SourceGit.Views point = new Point(x, y); var pos = TextLayout.HitTestPoint(point).TextPosition; - - if (links.Lookup(pos) is { } link) + if (links.Intersect(pos, 1) is { } link) SetHoveredIssueLink(link); else ClearHoveredIssueLink(); diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index b860ec1d..361c1184 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -167,6 +167,9 @@ namespace SourceGit.Views var start = match.Index; var len = match.Length; + if (_elements.Intersect(start, len) != null) + continue; + _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty)); } @@ -174,6 +177,7 @@ namespace SourceGit.Views foreach (var rule in rules) rule.Matches(_elements, subject); + _elements.Sort(); _needRebuildInlines = true; InvalidateVisual(); } @@ -247,8 +251,9 @@ namespace SourceGit.Views var codeTypeface = new Typeface(codeFontFamily, FontStyle.Normal, FontWeight); var pos = 0; var x = 0.0; - foreach (var elem in _elements) + for (var i = 0; i < _elements.Count; i++) { + var elem = _elements[i]; if (elem.Start > pos) { var normal = new FormattedText( @@ -350,7 +355,7 @@ namespace SourceGit.Views } } - private Models.InlineElementCollector _elements = []; + private Models.InlineElementCollector _elements = new(); private List _inlines = []; private Models.InlineElement _lastHover = null; private bool _needRebuildInlines = false; From a22c39519f1769e56be967cd4abaecf0ff570212 Mon Sep 17 00:00:00 2001 From: leo Date: Sun, 8 Jun 2025 11:54:54 +0800 Subject: [PATCH 40/44] code_style: remove unnecessary code Signed-off-by: leo --- src/ViewModels/ScanRepositories.cs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index f97066b1..21cd9bf8 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -31,7 +31,7 @@ namespace SourceGit.ViewModels watch.Start(); var rootDir = new DirectoryInfo(RootDir); - var found = new List(); + var found = new List(); GetUnmanagedRepositories(rootDir, found, new EnumerationOptions() { AttributesToSkip = FileAttributes.Hidden | FileAttributes.System, @@ -50,20 +50,21 @@ namespace SourceGit.ViewModels foreach (var f in found) { - var parent = new DirectoryInfo(f.Path).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); + var parent = new DirectoryInfo(f).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, null, false, false); + Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); } else if (parent.StartsWith(normalizedRoot, StringComparison.Ordinal)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); - Preferences.Instance.FindOrAddNodeByRepositoryPath(f.Path, group, false, false); + Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); } } Preferences.Instance.AutoRemoveInvalidNode(); + Preferences.Instance.SortNodes(Preferences.Instance.RepositoryNodes); Preferences.Instance.Save(); Welcome.Instance.Refresh(); @@ -84,7 +85,7 @@ namespace SourceGit.ViewModels } } - private void GetUnmanagedRepositories(DirectoryInfo dir, List outs, EnumerationOptions opts, int depth = 0) + private void GetUnmanagedRepositories(DirectoryInfo dir, List outs, EnumerationOptions opts, int depth = 0) { var subdirs = dir.GetDirectories("*", opts); foreach (var subdir in subdirs) @@ -107,7 +108,7 @@ namespace SourceGit.ViewModels { var normalized = test.StdOut.Trim().Replace('\\', '/').TrimEnd('/'); if (!_managed.Contains(normalized)) - outs.Add(new FoundRepository(normalized, false)); + outs.Add(normalized); } continue; @@ -116,7 +117,7 @@ namespace SourceGit.ViewModels var isBare = new Commands.IsBareRepository(subdir.FullName).Result(); if (isBare) { - outs.Add(new FoundRepository(normalizedSelf, true)); + outs.Add(normalizedSelf); continue; } @@ -158,12 +159,6 @@ namespace SourceGit.ViewModels return added; } - private record FoundRepository(string path, bool isBare) - { - public string Path { get; set; } = path; - public bool IsBare { get; set; } = isBare; - } - - private HashSet _managed = new HashSet(); + private HashSet _managed = new(); } } From d55f19586f6a9578c9ff26d5a7926a9119fc3025 Mon Sep 17 00:00:00 2001 From: Sina Hinderks Date: Mon, 9 Jun 2025 03:26:27 +0200 Subject: [PATCH 41/44] fix: issue tracker rule over keyword in subject (#1403) Some teams use issue tracker numbers in front of the commit message subject, followed by a colon. It was not possible to use an issue tracker rule in such cases, since the issue tracker number would be interpreted as a keyword due to the colon and therefore displayed in bold face instead of as a link into the issue tracker. --- src/Views/CommitSubjectPresenter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Views/CommitSubjectPresenter.cs b/src/Views/CommitSubjectPresenter.cs index 361c1184..bfeab34f 100644 --- a/src/Views/CommitSubjectPresenter.cs +++ b/src/Views/CommitSubjectPresenter.cs @@ -151,11 +151,15 @@ namespace SourceGit.Views return; } + var rules = IssueTrackerRules ?? []; + foreach (var rule in rules) + rule.Matches(_elements, subject); + var keywordMatch = REG_KEYWORD_FORMAT1().Match(subject); if (!keywordMatch.Success) keywordMatch = REG_KEYWORD_FORMAT2().Match(subject); - if (keywordMatch.Success) + if (keywordMatch.Success && _elements.Intersect(0, keywordMatch.Length) == null) _elements.Add(new Models.InlineElement(Models.InlineElementType.Keyword, 0, keywordMatch.Length, string.Empty)); var codeMatches = REG_INLINECODE_FORMAT().Matches(subject); @@ -173,10 +177,6 @@ namespace SourceGit.Views _elements.Add(new Models.InlineElement(Models.InlineElementType.Code, start, len, string.Empty)); } - var rules = IssueTrackerRules ?? []; - foreach (var rule in rules) - rule.Matches(_elements, subject); - _elements.Sort(); _needRebuildInlines = true; InvalidateVisual(); From a8541a780e235c60fde494c8a5a7593566c257d5 Mon Sep 17 00:00:00 2001 From: AquariusStar <48148723+AquariusStar@users.noreply.github.com> Date: Mon, 9 Jun 2025 04:27:10 +0300 Subject: [PATCH 42/44] localization: update translate Russian (#1404) --- src/Resources/Locales/ru_RU.axaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 80766130..a625df98 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -39,6 +39,7 @@ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ СПИСОК ПУСТ УДАЛИТЬ + Загрузить картинку... Обновить ДВОИЧНЫЙ ФАЙЛ НЕ ПОДДЕРЖИВАЕТСЯ!!! Раздвоить @@ -51,6 +52,7 @@ Расследование РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! Переключиться на ${0}$... + Сравнить с ${0}$ Сравнить с рабочим каталогом Копировать имя ветки Изменить действие @@ -592,6 +594,7 @@ Очистить (Сбор мусора и удаление) Запустить команду (git gc) для данного репозитория. Очистить всё + Очистить Настройка репозитория ПРОДОЛЖИТЬ Изменить действия @@ -783,6 +786,7 @@ ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ НЕТ ШАБЛОНОВ РЕВИЗИИ + Сбросить автора Щёлкните правой кнопкой мыши выбранный файл(ы) и разрешите конфликты. Завершение работы СФОРМИРОВАННЫЕ From a1e76e9bea938e4aea9a68675a55f5f68dc5faea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Jun 2025 01:27:28 +0000 Subject: [PATCH 43/44] doc: Update translation status and sort locale files --- TRANSLATION.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index 78d1e843..ba51b82c 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -357,17 +357,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.50%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.Avatar.Load -- Text.BranchCM.CompareWithCurrent -- Text.Repository.ClearStashes -- Text.WorkingCopy.ResetAuthor - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ta__IN](https://img.shields.io/badge/ta__IN-92.13%25-yellow) From 637e133f478d1c4f30b033cca50ec1ffb971bb32 Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 9 Jun 2025 09:30:36 +0800 Subject: [PATCH 44/44] version: Release 2025.21 Signed-off-by: leo --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 67a3d7e0..b89504d0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.20 \ No newline at end of file +2025.21 \ No newline at end of file