From e45e37d3059d72c7cbff738f7237f98489f00c68 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 6 May 2025 18:15:11 +0800 Subject: [PATCH] feature: supports sort branches by committer date (#1192) Signed-off-by: leo --- src/Commands/QueryBranches.cs | 13 ++--- src/Models/Branch.cs | 7 +++ src/Models/RepositorySettings.cs | 12 +++++ src/Resources/Locales/en_US.axaml | 15 +++--- src/Resources/Locales/zh_CN.axaml | 3 ++ src/Resources/Locales/zh_TW.axaml | 3 ++ src/ViewModels/BranchTreeNode.cs | 90 +++++++++++++++++++++++++++++-- src/ViewModels/Repository.cs | 47 +++++++++++++++- src/Views/Repository.axaml | 25 +++++++-- src/Views/Repository.axaml.cs | 22 ++++++++ 10 files changed, 217 insertions(+), 20 deletions(-) diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index 39b77189..19514954 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -14,7 +14,7 @@ namespace SourceGit.Commands { WorkingDirectory = repo; Context = repo; - Args = "branch -l --all -v --format=\"%(refname)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; + Args = "branch -l --all -v --format=\"%(refname)%00%(committerdate:unix)%00%(objectname)%00%(HEAD)%00%(upstream)%00%(upstream:trackshort)\""; } public List Result() @@ -49,7 +49,7 @@ namespace SourceGit.Commands private Models.Branch ParseLine(string line) { var parts = line.Split('\0'); - if (parts.Length != 5) + if (parts.Length != 6) return null; var branch = new Models.Branch(); @@ -83,12 +83,13 @@ namespace SourceGit.Commands } branch.FullName = refName; - branch.Head = parts[1]; - branch.IsCurrent = parts[2] == "*"; - branch.Upstream = parts[3]; + branch.CommitterDate = ulong.Parse(parts[1]); + branch.Head = parts[2]; + branch.IsCurrent = parts[3] == "*"; + branch.Upstream = parts[4]; branch.IsUpstreamGone = false; - if (branch.IsLocal && !string.IsNullOrEmpty(parts[4]) && !parts[4].Equals("=", StringComparison.Ordinal)) + if (branch.IsLocal && !string.IsNullOrEmpty(parts[5]) && !parts[5].Equals("=", StringComparison.Ordinal)) branch.TrackStatus = new QueryTrackStatus(WorkingDirectory, branch.Name, branch.Upstream).Result(); else branch.TrackStatus = new Models.BranchTrackStatus(); diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index d0ac1990..7146da3f 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -23,10 +23,17 @@ namespace SourceGit.Models } } + public enum BranchSortMode + { + Name = 0, + CommitterDate, + } + public class Branch { public string Name { get; set; } public string FullName { get; set; } + public ulong CommitterDate { get; set; } public string Head { get; set; } public bool IsLocal { get; set; } public bool IsCurrent { get; set; } diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 3df463b3..fcd0099d 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -38,6 +38,18 @@ namespace SourceGit.Models set; } = false; + public BranchSortMode LocalBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + + public BranchSortMode RemoteBranchSortMode + { + get; + set; + } = BranchSortMode.Name; + public TagSortMode TagSortMode { get; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 0ebeeeb0..5294f566 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -572,6 +572,9 @@ Branch: ABORT Auto fetching changes from remotes... + Sort + By Committer Date + By Name Cleanup(GC & Prune) Run `git gc` command for this repository. Clear all @@ -602,7 +605,7 @@ Open in External Tools Refresh REMOTES - ADD REMOTE + Add Remote Search Commit Author Committer @@ -615,10 +618,10 @@ SKIP Statistics SUBMODULES - ADD SUBMODULE - UPDATE SUBMODULE + Add Submodule + Update Submodule TAGS - NEW TAG + New Tag By Creator Date By Name (Ascending) By Name (Descending) @@ -628,8 +631,8 @@ View Logs Visit '{0}' in Browser WORKTREES - ADD WORKTREE - PRUNE + Add Worktree + Prune Git Repository URL Reset Current Branch To Revision Reset Mode: diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 6bf7f5d1..1593dc2e 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -576,6 +576,9 @@ 分支 : 终止合并 自动拉取远端变更中... + 排序方式 + 按提交时间 + 按名称 清理本仓库(GC) 本操作将执行`git gc`命令。 清空过滤规则 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9ed35c8a..a2a039b2 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -576,6 +576,9 @@ 分支: 中止 自動提取遠端變更中... + 排序 + 依建立時間 + 依名稱升序 清理本存放庫 (GC) 本操作將執行 `git gc` 命令。 清空篩選規則 diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 0148844a..005d8b90 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -10,6 +10,7 @@ namespace SourceGit.ViewModels public string Name { get; private set; } = string.Empty; public string Path { get; private set; } = string.Empty; public object Backend { get; private set; } = null; + public ulong TimeToSort { get; private set; } = 0; public int Depth { get; set; } = 0; public bool IsSelected { get; set; } = false; public List Children { get; private set; } = new List(); @@ -62,6 +63,12 @@ namespace SourceGit.ViewModels public List Remotes => _remotes; public List InvalidExpandedNodes => _invalidExpandedNodes; + public Builder(Models.BranchSortMode localSortMode, Models.BranchSortMode remoteSortMode) + { + _localSortMode = localSortMode; + _remoteSortMode = remoteSortMode; + } + public void SetExpandedNodes(List expanded) { foreach (var node in expanded) @@ -72,6 +79,7 @@ namespace SourceGit.ViewModels { var folders = new Dictionary(); + var fakeRemoteTime = (ulong)remotes.Count; foreach (var remote in remotes) { var path = $"refs/remotes/{remote.Name}"; @@ -81,8 +89,10 @@ namespace SourceGit.ViewModels Path = path, Backend = remote, IsExpanded = bForceExpanded || _expanded.Contains(path), + TimeToSort = fakeRemoteTime, }; + fakeRemoteTime--; folders.Add(path, node); _remotes.Add(node); } @@ -108,8 +118,26 @@ namespace SourceGit.ViewModels } folders.Clear(); - SortNodes(_locals); - SortNodes(_remotes); + + if (_localSortMode == Models.BranchSortMode.Name) + { + SortNodesByName(_locals); + } + else + { + SetTimeToSortRecusive(_locals); + SortNodesByTime(_locals); + } + + if (_remoteSortMode == Models.BranchSortMode.Name) + { + SortNodesByName(_remotes); + } + else + { + SetTimeToSortRecusive(_remotes); + SortNodesByTime(_remotes); + } } private void MakeBranchNode(Models.Branch branch, List roots, Dictionary folders, string prefix, bool bForceExpanded) @@ -124,6 +152,7 @@ namespace SourceGit.ViewModels Path = fullpath, Backend = branch, IsExpanded = false, + TimeToSort = branch.CommitterDate, }); return; } @@ -175,10 +204,11 @@ namespace SourceGit.ViewModels Path = fullpath, Backend = branch, IsExpanded = false, + TimeToSort = branch.CommitterDate, }); } - private void SortNodes(List nodes) + private void SortNodesByName(List nodes) { nodes.Sort((l, r) => { @@ -192,9 +222,61 @@ namespace SourceGit.ViewModels }); foreach (var node in nodes) - SortNodes(node.Children); + SortNodesByName(node.Children); } + private void SortNodesByTime(List nodes) + { + nodes.Sort((l, r) => + { + if (l.Backend is Models.Branch { IsDetachedHead: true }) + return -1; + + if (l.Backend is Models.Branch) + { + if (r.Backend is Models.Branch) + return r.TimeToSort == l.TimeToSort ? Models.NumericSort.Compare(l.Name, r.Name) : r.TimeToSort.CompareTo(l.TimeToSort); + else + return 1; + } + + if (r.Backend is Models.Branch) + return -1; + + if (r.TimeToSort == l.TimeToSort) + return Models.NumericSort.Compare(l.Name, r.Name); + + return r.TimeToSort.CompareTo(l.TimeToSort); + }); + + foreach (var node in nodes) + SortNodesByTime(node.Children); + } + + private ulong SetTimeToSortRecusive(List nodes) + { + var recent = (ulong)0; + + foreach (var node in nodes) + { + if (node.Backend is Models.Branch) + { + recent = Math.Max(recent, node.TimeToSort); + continue; + } + + var time = SetTimeToSortRecusive(node.Children); + recent = Math.Max(recent, time); + + if (node.Backend is not Models.Remote) + node.TimeToSort = time; + } + + return recent; + } + + private readonly Models.BranchSortMode _localSortMode = Models.BranchSortMode.Name; + private readonly Models.BranchSortMode _remoteSortMode = Models.BranchSortMode.Name; private readonly List _locals = new List(); private readonly List _remotes = new List(); private readonly List _invalidExpandedNodes = new List(); diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index bf084fb0..54070cf5 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -2229,6 +2229,51 @@ namespace SourceGit.ViewModels return menu; } + public ContextMenu CreateContextMenuForBranchSortMode(bool local) + { + var mode = local ? _settings.LocalBranchSortMode : _settings.RemoteBranchSortMode; + var changeMode = new Action(m => + { + if (local) + _settings.LocalBranchSortMode = m; + else + _settings.RemoteBranchSortMode = m; + + var builder = BuildBranchTree(_branches, _remotes); + LocalBranchTrees = builder.Locals; + RemoteBranchTrees = builder.Remotes; + }); + + var byNameAsc = new MenuItem(); + byNameAsc.Header = App.Text("Repository.BranchSort.ByName"); + if (mode == Models.BranchSortMode.Name) + byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); + byNameAsc.Click += (_, ev) => + { + if (mode != Models.BranchSortMode.Name) + changeMode(Models.BranchSortMode.Name); + + ev.Handled = true; + }; + + var byCommitterDate = new MenuItem(); + byCommitterDate.Header = App.Text("Repository.BranchSort.ByCommitterDate"); + if (mode == Models.BranchSortMode.CommitterDate) + byCommitterDate.Icon = App.CreateMenuIcon("Icons.Check"); + byCommitterDate.Click += (_, ev) => + { + if (mode != Models.BranchSortMode.CommitterDate) + changeMode(Models.BranchSortMode.CommitterDate); + + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(byNameAsc); + menu.Items.Add(byCommitterDate); + return menu; + } + public ContextMenu CreateContextMenuForTagSortMode() { var mode = _settings.TagSortMode; @@ -2398,7 +2443,7 @@ namespace SourceGit.ViewModels private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) { - var builder = new BranchTreeNode.Builder(); + var builder = new BranchTreeNode.Builder(_settings.LocalBranchSortMode, _settings.RemoteBranchSortMode); if (string.IsNullOrEmpty(_filter)) { builder.SetExpandedNodes(_settings.ExpandedBranchNodesInSideBar); diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index a06432d4..2896a5ff 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -202,9 +202,20 @@ - + + + - + - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index a9f5bfee..3e30d161 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -403,6 +403,28 @@ namespace SourceGit.Views e.Handled = true; } + private void OnOpenSortLocalBranchMenu(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForBranchSortMode(true); + menu?.Open(button); + } + + e.Handled = true; + } + + private void OnOpenSortRemoteBranchMenu(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForBranchSortMode(false); + menu?.Open(button); + } + + e.Handled = true; + } + private void OnOpenSortTagMenu(object sender, RoutedEventArgs e) { if (sender is Button button && DataContext is ViewModels.Repository repo)