From d777b14b895185da73eefef8a06830c42d2b5f26 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 13 Nov 2024 21:45:28 +0800 Subject: [PATCH] refactor: rewrite the histories filter function to supports both `include` and `exclude` modes (#690) Signed-off-by: leo --- src/Models/Filter.cs | 60 +++++ src/Models/RepositorySettings.cs | 182 ++++++++++++++- src/Models/Tag.cs | 21 +- src/Resources/Styles.axaml | 29 --- src/ViewModels/BranchTreeNode.cs | 59 ++--- src/ViewModels/Checkout.cs | 2 +- src/ViewModels/CreateBranch.cs | 2 +- src/ViewModels/RenameBranch.cs | 16 +- src/ViewModels/Repository.cs | 121 ++-------- src/ViewModels/TagCollection.cs | 10 - src/Views/BranchTree.axaml | 17 +- src/Views/BranchTree.axaml.cs | 39 ---- src/Views/FilterModeSwitchButton.axaml | 41 ++++ src/Views/FilterModeSwitchButton.axaml.cs | 256 ++++++++++++++++++++++ src/Views/Repository.axaml | 26 ++- src/Views/TagsView.axaml | 34 ++- src/Views/TagsView.axaml.cs | 17 -- 17 files changed, 662 insertions(+), 270 deletions(-) create mode 100644 src/Models/Filter.cs create mode 100644 src/Views/FilterModeSwitchButton.axaml create mode 100644 src/Views/FilterModeSwitchButton.axaml.cs diff --git a/src/Models/Filter.cs b/src/Models/Filter.cs new file mode 100644 index 00000000..8ffd27c7 --- /dev/null +++ b/src/Models/Filter.cs @@ -0,0 +1,60 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models +{ + public enum FilterType + { + LocalBranch = 0, + LocalBranchFolder, + RemoteBranch, + RemoteBranchFolder, + Tag, + } + + public enum FilterMode + { + None = 0, + Included, + Excluded, + } + + public class Filter : ObservableObject + { + public string Pattern + { + get => _pattern; + set => SetProperty(ref _pattern, value); + } + + public FilterType Type + { + get; + set; + } = FilterType.LocalBranch; + + public FilterMode Mode + { + get => _mode; + set => SetProperty(ref _mode, value); + } + + public bool IsBranch + { + get => Type != FilterType.Tag; + } + + public Filter() + { + } + + public Filter(string pattern, FilterType type, FilterMode mode) + { + _pattern = pattern; + _mode = mode; + Type = type; + } + + private string _pattern = string.Empty; + private FilterMode _mode = FilterMode.None; + } +} diff --git a/src/Models/RepositorySettings.cs b/src/Models/RepositorySettings.cs index 77c58ee7..f0156bc2 100644 --- a/src/Models/RepositorySettings.cs +++ b/src/Models/RepositorySettings.cs @@ -1,4 +1,9 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using System.Text; + +using Avalonia.Collections; +using Avalonia.Threading; namespace SourceGit.Models { @@ -76,11 +81,11 @@ namespace SourceGit.Models set; } = true; - public AvaloniaList Filters + public AvaloniaList HistoriesFilters { get; set; - } = new AvaloniaList(); + } = new AvaloniaList(); public AvaloniaList CommitTemplates { @@ -148,6 +153,177 @@ namespace SourceGit.Models set; } = "---"; + public FilterMode GetHistoriesFilterMode(string pattern, FilterType type) + { + foreach (var filter in HistoriesFilters) + { + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + return filter.Mode; + } + + return FilterMode.None; + } + + public bool UpdateHistoriesFilter(string pattern, FilterType type, FilterMode mode) + { + for (int i = 0; i < HistoriesFilters.Count; i++) + { + var filter = HistoriesFilters[i]; + if (filter.Type != type) + continue; + + if (filter.Pattern.Equals(pattern, StringComparison.Ordinal)) + { + if (mode == FilterMode.None) + { + HistoriesFilters.RemoveAt(i); + return true; + } + + if (mode != filter.Mode) + { + filter.Mode = mode; + return true; + } + } + } + + if (mode != FilterMode.None) + { + HistoriesFilters.Add(new Filter(pattern, type, mode)); + return true; + } + + return false; + } + + public string BuildHistoriesFilter() + { + var builder = new StringBuilder(); + + var excludedBranches = new List(); + var excludedRemotes = new List(); + var excludedTags = new List(); + var includedBranches = new List(); + var includedRemotes = new List(); + var includedTags = new List(); + foreach (var filter in HistoriesFilters) + { + if (filter.Type == FilterType.LocalBranch) + { + var name = filter.Pattern.Substring(11); + var b = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedBranches.Add(b); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add(b); + } + else if (filter.Type == FilterType.LocalBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedBranches.Add($"{filter.Pattern.Substring(11)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedBranches.Add($"{filter.Pattern.Substring(11)}/*"); + } + else if (filter.Type == FilterType.RemoteBranch) + { + var name = filter.Pattern.Substring(13); + var r = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedRemotes.Add(r); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add(r); + } + else if (filter.Type == FilterType.RemoteBranchFolder) + { + if (filter.Mode == FilterMode.Included) + includedRemotes.Add($"{filter.Pattern.Substring(13)}/*"); + else if (filter.Mode == FilterMode.Excluded) + excludedRemotes.Add($"{filter.Pattern.Substring(13)}/*"); + } + else if (filter.Type == FilterType.Tag) + { + var name = filter.Pattern; + var t = $"{name.Substring(0, name.Length - 1)}[{name[^1]}]"; + + if (filter.Mode == FilterMode.Included) + includedTags.Add(t); + else if (filter.Mode == FilterMode.Excluded) + excludedTags.Add(t); + } + } + + foreach (var b in excludedBranches) + { + builder.Append("--exclude="); + builder.Append(b); + builder.Append(' '); + } + + if (includedBranches.Count > 0) + { + foreach (var b in includedBranches) + { + builder.Append("--branches="); + builder.Append(b); + builder.Append(' '); + } + } + else if (excludedBranches.Count > 0) + { + builder.Append("--branches "); + } + + foreach (var r in excludedRemotes) + { + builder.Append("--exclude="); + builder.Append(r); + builder.Append(' '); + } + + if (includedRemotes.Count > 0) + { + foreach (var r in includedRemotes) + { + builder.Append("--remotes="); + builder.Append(r); + builder.Append(' '); + } + } + else if (excludedRemotes.Count > 0) + { + builder.Append("--remotes "); + } + + foreach (var t in excludedTags) + { + builder.Append("--exclude="); + builder.Append(t); + builder.Append(' '); + } + + if (includedTags.Count > 0) + { + foreach (var t in includedTags) + { + builder.Append("--tags="); + builder.Append(t); + builder.Append(' '); + } + } + else if (excludedTags.Count > 0) + { + builder.Append("--tags "); + } + + return builder.ToString(); + } + public void PushCommitMessage(string message) { var existIdx = CommitMessages.IndexOf(message); diff --git a/src/Models/Tag.cs b/src/Models/Tag.cs index 2ec9e093..2e8f2c8e 100644 --- a/src/Models/Tag.cs +++ b/src/Models/Tag.cs @@ -1,10 +1,19 @@ -namespace SourceGit.Models +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.Models { - public class Tag + public class Tag : ObservableObject { - public string Name { get; set; } - public string SHA { get; set; } - public string Message { get; set; } - public bool IsFiltered { get; set; } + public string Name { get; set; } = string.Empty; + public string SHA { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + + public FilterMode FilterMode + { + get => _filterMode; + set => SetProperty(ref _filterMode, value); + } + + private FilterMode _filterMode = FilterMode.None; } } diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 88aca6b0..8fdfaa3c 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -1038,35 +1038,6 @@ - - - - + + @@ -44,7 +48,7 @@ @@ -67,15 +71,8 @@ Foreground="{DynamicResource Brush.BadgeFG}" Background="{DynamicResource Brush.Badge}"/> - - + + diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs index e96b2594..92c2b043 100644 --- a/src/Views/BranchTree.axaml.cs +++ b/src/Views/BranchTree.axaml.cs @@ -428,28 +428,6 @@ namespace SourceGit.Views } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo && - sender is ToggleButton toggle && - toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch } node) - { - bool filtered = toggle.IsChecked == true; - List filters = [branch.FullName]; - if (branch.IsLocal && !string.IsNullOrEmpty(branch.Upstream)) - { - filters.Add(branch.Upstream); - - node.IsFiltered = filtered; - UpdateUpstreamFilterState(repo.RemoteBranchTrees, branch.Upstream, filtered); - } - - repo.UpdateFilters(filters, filtered); - } - - e.Handled = true; - } - private void MakeRows(List rows, List nodes, int depth) { foreach (var node in nodes) @@ -477,23 +455,6 @@ namespace SourceGit.Views CollectBranchesInNode(outs, sub); } - private bool UpdateUpstreamFilterState(List collection, string upstream, bool isFiltered) - { - foreach (var node in collection) - { - if (node.Backend is Models.Branch b && b.FullName == upstream) - { - node.IsFiltered = isFiltered; - return true; - } - - if (node.Backend is Models.Remote r && upstream.StartsWith($"refs/remotes/{r.Name}/", StringComparison.Ordinal)) - return UpdateUpstreamFilterState(node.Children, upstream, isFiltered); - } - - return false; - } - private bool _disableSelectionChangingEvent = false; } } diff --git a/src/Views/FilterModeSwitchButton.axaml b/src/Views/FilterModeSwitchButton.axaml new file mode 100644 index 00000000..5b6d5341 --- /dev/null +++ b/src/Views/FilterModeSwitchButton.axaml @@ -0,0 +1,41 @@ + + + diff --git a/src/Views/FilterModeSwitchButton.axaml.cs b/src/Views/FilterModeSwitchButton.axaml.cs new file mode 100644 index 00000000..7bcf3100 --- /dev/null +++ b/src/Views/FilterModeSwitchButton.axaml.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public partial class FilterModeSwitchButton : UserControl + { + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + public Models.FilterMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + public static readonly StyledProperty IsNoneVisibleProperty = + AvaloniaProperty.Register(nameof(IsNoneVisible)); + + public bool IsNoneVisible + { + get => GetValue(IsNoneVisibleProperty); + set => SetValue(IsNoneVisibleProperty, value); + } + + public static readonly StyledProperty IsContextMenuOpeningProperty = + AvaloniaProperty.Register(nameof(IsContextMenuOpening)); + + public bool IsContextMenuOpening + { + get => GetValue(IsContextMenuOpeningProperty); + set => SetValue(IsContextMenuOpeningProperty, value); + } + + public FilterModeSwitchButton() + { + InitializeComponent(); + } + + private void OnChangeFilterModeButtonClicked(object sender, RoutedEventArgs e) + { + var repoView = this.FindAncestorOfType(); + if (repoView == null) + return; + + var repo = repoView.DataContext as ViewModels.Repository; + if (repo == null) + return; + + var button = sender as Button; + if (button == null) + return; + + if (DataContext is Models.Tag tag) + { + var mode = tag.FilterMode; + + var none = new MenuItem(); + none.Icon = App.CreateMenuIcon("Icons.Eye"); + none.Header = "Default"; + none.IsEnabled = mode != Models.FilterMode.None; + none.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.None); + ev.Handled = true; + }; + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = "Filter"; + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.Included); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = "Hide"; + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + UpdateTagFilterMode(repo, tag, Models.FilterMode.Excluded); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(none); + menu.Items.Add(include); + menu.Items.Add(exclude); + + if (mode == Models.FilterMode.None) + { + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } + + menu.Open(button); + } + else if (DataContext is ViewModels.BranchTreeNode node) + { + var mode = node.FilterMode; + + var none = new MenuItem(); + none.Icon = App.CreateMenuIcon("Icons.Eye"); + none.Header = "Default"; + none.IsEnabled = mode != Models.FilterMode.None; + none.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.None); + ev.Handled = true; + }; + + var include = new MenuItem(); + include.Icon = App.CreateMenuIcon("Icons.Filter"); + include.Header = "Filter"; + include.IsEnabled = mode != Models.FilterMode.Included; + include.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.Included); + ev.Handled = true; + }; + + var exclude = new MenuItem(); + exclude.Icon = App.CreateMenuIcon("Icons.EyeClose"); + exclude.Header = "Hide"; + exclude.IsEnabled = mode != Models.FilterMode.Excluded; + exclude.Click += (_, ev) => + { + UpdateBranchFilterMode(repo, node, Models.FilterMode.Excluded); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(none); + menu.Items.Add(include); + menu.Items.Add(exclude); + + if (mode == Models.FilterMode.None) + { + IsContextMenuOpening = true; + menu.Closed += (_, _) => IsContextMenuOpening = false; + } + + menu.Open(button); + } + + e.Handled = true; + } + + private void UpdateTagFilterMode(ViewModels.Repository repo, Models.Tag tag, Models.FilterMode mode) + { + var changed = repo.Settings.UpdateHistoriesFilter(tag.Name, Models.FilterType.Tag, mode); + if (changed) + { + tag.FilterMode = mode; + Task.Run(repo.RefreshCommits); + } + } + + private void UpdateBranchFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, Models.FilterMode mode) + { + var isLocal = node.Path.StartsWith("refs/heads/", StringComparison.Ordinal); + var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; + var tree = isLocal ? repo.LocalBranchTrees : repo.RemoteBranchTrees; + + if (node.Backend is Models.Branch branch) + { + var changed = repo.Settings.UpdateHistoriesFilter(node.Path, type, mode); + if (!changed) + return; + + node.FilterMode = mode; + } + else + { + var changed = repo.Settings.UpdateHistoriesFilter(node.Path, isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder, mode); + if (!changed) + return; + + node.FilterMode = mode; + ResetChildrenBranchNodeFilterMode(repo, node, isLocal); + } + + var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; + var cur = node; + do + { + var lastSepIdx = cur.Path.LastIndexOf('/'); + if (lastSepIdx <= 0) + break; + + var parentPath = cur.Path.Substring(0, lastSepIdx); + var parent = FindParentNode(tree, parentPath); + if (parent == null) + break; + + repo.Settings.UpdateHistoriesFilter(parent.Path, parentType, Models.FilterMode.None); + parent.FilterMode = Models.FilterMode.None; + cur = parent; + } while (true); + + Task.Run(repo.RefreshCommits); + } + + private void ResetChildrenBranchNodeFilterMode(ViewModels.Repository repo, ViewModels.BranchTreeNode node, bool isLocal) + { + foreach (var child in node.Children) + { + child.FilterMode = Models.FilterMode.None; + + if (child.IsBranch) + { + var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; + repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None); + } + else + { + var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; + repo.Settings.UpdateHistoriesFilter(child.Path, type, Models.FilterMode.None); + ResetChildrenBranchNodeFilterMode(repo, child, isLocal); + } + } + } + + private ViewModels.BranchTreeNode FindParentNode(List nodes, string parent) + { + foreach (var node in nodes) + { + if (node.IsBranch) + continue; + + if (node.Path.Equals(parent, StringComparison.Ordinal)) + return node; + + if (parent.StartsWith(node.Path, StringComparison.Ordinal)) + { + var founded = FindParentNode(node.Children, parent); + if (founded != null) + return founded; + } + } + + return null; + } + } +} + + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index eab0d28a..6ff0a3ab 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -577,14 +577,14 @@ - + - + @@ -592,10 +592,24 @@ - - - - + + + + + + + + + + + + diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml index b50ef481..78165f2c 100644 --- a/src/Views/TagsView.axaml +++ b/src/Views/TagsView.axaml @@ -12,6 +12,10 @@ + + @@ -43,15 +47,14 @@ Classes="primary" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="8,0,0,0"/> - - + + + + + + + + @@ -66,7 +69,7 @@ SelectionChanged="OnRowSelectionChanged"> - @@ -78,15 +81,10 @@ + Margin="8,0,0,0" + TextTrimming="CharacterEllipsis"/> - + diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs index bde5ae32..c83cfd28 100644 --- a/src/Views/TagsView.axaml.cs +++ b/src/Views/TagsView.axaml.cs @@ -247,23 +247,6 @@ namespace SourceGit.Views } } - private void OnToggleFilterClicked(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) - { - var target = null as Models.Tag; - if (toggle.DataContext is ViewModels.TagTreeNode node) - target = node.Tag; - else if (toggle.DataContext is Models.Tag tag) - target = tag; - - if (target != null) - repo.UpdateFilters([target.Name], toggle.IsChecked == true); - } - - e.Handled = true; - } - private void MakeTreeRows(List rows, List nodes) { foreach (var node in nodes)