From 6c255cca7fdb6624b2f16032f95540fc8e1974c6 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 15 Jul 2026 16:55:03 -0400 Subject: [PATCH] Add configurable skip level for minor updates --- src/Languages/lang_en.json | 2 + .../DialogPages/InstallOptionsViewModel.cs | 8 ++++ .../DialogPages/InstallOptionsControl.axaml | 28 ++++++++----- .../IPackage.cs | 8 ++-- .../UpgradablePackagesLoader.cs | 5 ++- .../Packages/InvalidImportedPackage.cs | 2 +- .../Packages/Package.cs | 23 +++++++---- .../InstallOptions.cs | 11 +++++ .../UpgradablePackagesLoaderTests.cs | 41 +++++++++++++++++++ 9 files changed, 105 insertions(+), 23 deletions(-) diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json index 7599eb6899..37e25622bf 100644 --- a/src/Languages/lang_en.json +++ b/src/Languages/lang_en.json @@ -142,6 +142,8 @@ "Skip hash check": "Skip hash check", "Uninstall previous versions when updated": "Uninstall previous versions when updated", "Skip minor updates for this package": "Skip minor updates for this package", + "Ignore changes after the first": "Ignore changes after the first", + "version numbers": "version numbers", "Automatically update this package": "Automatically update this package", "{0} installation options": "{0} installation options", "Latest": "Latest", diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs index 51b86141f2..d04678f6b9 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/InstallOptionsViewModel.cs @@ -84,6 +84,8 @@ public partial class InstallOptionsViewModel : ObservableObject public string SkipHashCheckBox_Content { get; } = CoreTools.Translate("Skip hash check"); public string UninstallPrevCheckBox_Content { get; } = CoreTools.Translate("Uninstall previous versions when updated"); public string SkipMinorCheckBox_Content { get; } = CoreTools.Translate("Skip minor updates for this package"); + public string SkipMinorLevelPrefix_Content { get; } = CoreTools.Translate("Ignore changes after the first"); + public string SkipMinorLevelSuffix_Content { get; } = CoreTools.Translate("version numbers"); public string AutoUpdateCheckBox_Content { get; } = CoreTools.Translate("Automatically update this package"); public string IgnoreUpdatesCheckBox_Content { get; } = CoreTools.Translate("Ignore future updates for this package"); @@ -152,6 +154,9 @@ partial void OnSelectedProfileChanged(string? value) [ObservableProperty] private string? _selectedVersion; [ObservableProperty] private bool _skipMinorChecked; + // Index 0 => keep first 1 number significant (level 2), 1 => first 2 (level 3), 2 => first 3 (level 4). + public ObservableCollection SkipMinorLevelOptions { get; } = ["1", "2", "3"]; + [ObservableProperty] private int _skipMinorLevelIndex; [ObservableProperty] private bool _autoUpdateChecked; [ObservableProperty] private bool _ignoreUpdatesChecked; @@ -267,6 +272,7 @@ public InstallOptionsViewModel(IPackage package, OperationType operation, Instal SkipHashEnabled = caps.CanSkipIntegrityChecks; UninstallPrevChecked = options.UninstallPreviousVersionsOnUpdate; SkipMinorChecked = options.SkipMinorUpdates; + SkipMinorLevelIndex = options.SkipMinorUpdatesLevel - 2; // level is validated to 2..4 by the getter AutoUpdateChecked = options.AutoUpdatePackage; // Version @@ -515,6 +521,7 @@ private InstallOptions SnapshotOptions() o.UninstallPreviousVersionsOnUpdate = UninstallPrevChecked; o.AutoUpdatePackage = AutoUpdateChecked; o.SkipMinorUpdates = SkipMinorChecked; + o.SkipMinorUpdatesLevel = SkipMinorLevelIndex + 2; o.OverridesNextLevelOpts = !FollowGlobal; var ver = SelectedVersion ?? ""; @@ -550,6 +557,7 @@ private void ApplyToOptions() _options.UninstallPreviousVersionsOnUpdate = s.UninstallPreviousVersionsOnUpdate; _options.AutoUpdatePackage = s.AutoUpdatePackage; _options.SkipMinorUpdates = s.SkipMinorUpdates; + _options.SkipMinorUpdatesLevel = s.SkipMinorUpdatesLevel; _options.OverridesNextLevelOpts = s.OverridesNextLevelOpts; _options.PreRelease = s.PreRelease; _options.Version = s.Version; diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsControl.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsControl.axaml index d3a3ac5ce6..67bcc51bfd 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsControl.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsControl.axaml @@ -145,20 +145,28 @@ - + - + automation:AutomationProperties.Name="{Binding IgnoreUpdatesCheckBox_Content}"/> - + automation:AutomationProperties.Name="{Binding AutoUpdateCheckBox_Content}"/> + + + + + + + diff --git a/src/UniGetUI.PackageEngine.Interfaces/IPackage.cs b/src/UniGetUI.PackageEngine.Interfaces/IPackage.cs index d6ecd5e58e..5ff85d4125 100644 --- a/src/UniGetUI.PackageEngine.Interfaces/IPackage.cs +++ b/src/UniGetUI.PackageEngine.Interfaces/IPackage.cs @@ -151,10 +151,12 @@ public interface IPackage : INotifyPropertyChanged, IEquatable public Task GetInstallerFileName(); /// - /// Checks whether a new update of this package is a minor update or not (0.0.x) + /// Checks whether a new update of this package is a minor update or not, according to + /// — the 1-based version-number position (2=Minor, 3=Patch, 4=Remainder) + /// from which a change is still considered minor. /// - /// False if the update is a major update or the update doesn't exist, true if it's a minor update - public bool IsUpdateMinor(); + /// False if the most significant changed component is more significant than or the update doesn't exist, true otherwise + public bool IsUpdateMinor(int level = InstallOptions.DefaultSkipMinorLevel); /// /// Gets the applicable install options for this package diff --git a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs index 70ef99b327..3cc6dd79fb 100644 --- a/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs +++ b/src/UniGetUI.PackageEngine.PackageLoader/UpgradablePackagesLoader.cs @@ -39,10 +39,11 @@ protected override async Task IsPackageValid(IPackage package) if (package.NewerVersionIsInstalled()) return false; - if (package.IsUpdateMinor() && (await package.GetInstallOptions()).SkipMinorUpdates) + var installOptions = await package.GetInstallOptions(); + if (installOptions.SkipMinorUpdates && package.IsUpdateMinor(installOptions.SkipMinorUpdatesLevel)) { Logger.Info( - $"Ignoring package {package.Id} because it is a minor update ({package.VersionString} -> {package.NewVersionString}) and SkipMinorUpdates is set to true." + $"Ignoring package {package.Id} because it is a minor update ({package.VersionString} -> {package.NewVersionString}) below skip level {installOptions.SkipMinorUpdatesLevel} and SkipMinorUpdates is set to true." ); return false; } diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/InvalidImportedPackage.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/InvalidImportedPackage.cs index 77f773a5bd..5d11e48bbc 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/InvalidImportedPackage.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/InvalidImportedPackage.cs @@ -199,7 +199,7 @@ public bool NewerVersionIsInstalled() return Task.FromResult(""); } - public bool IsUpdateMinor() + public bool IsUpdateMinor(int level = InstallOptions.DefaultSkipMinorLevel) { return false; } diff --git a/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs b/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs index b3dffe9362..0f9b3ebbac 100644 --- a/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs +++ b/src/UniGetUI.PackageEngine.PackageManagerClasses/Packages/Package.cs @@ -324,17 +324,26 @@ public virtual bool NewerVersionIsInstalled() } } - public virtual bool IsUpdateMinor() + // 1-based position of the most significant version component that changed + // (1=Major, 2=Minor, 3=Patch, 4=Remainder), or 0 if identical/unparseable. + private int HighestChangedVersionComponent() + { + if (NormalizedVersion == CoreTools.Version.Null || NormalizedNewVersion == CoreTools.Version.Null) + return 0; + if (NormalizedVersion.Major != NormalizedNewVersion.Major) return 1; + if (NormalizedVersion.Minor != NormalizedNewVersion.Minor) return 2; + if (NormalizedVersion.Patch != NormalizedNewVersion.Patch) return 3; + if (NormalizedVersion.Remainder != NormalizedNewVersion.Remainder) return 4; + return 0; + } + + public virtual bool IsUpdateMinor(int level = InstallOptions.DefaultSkipMinorLevel) { if (!IsUpgradable) return false; - return NormalizedVersion.Major == NormalizedNewVersion.Major - && NormalizedVersion.Minor == NormalizedNewVersion.Minor - && ( - NormalizedVersion.Patch != NormalizedNewVersion.Patch - || NormalizedVersion.Remainder != NormalizedNewVersion.Remainder - ); + int changed = HighestChangedVersionComponent(); + return changed >= level; } public virtual Task GetInstallOptions() => diff --git a/src/UniGetUI.PackageEngine.Serializable/InstallOptions.cs b/src/UniGetUI.PackageEngine.Serializable/InstallOptions.cs index a39b27525e..cdc484f8f2 100644 --- a/src/UniGetUI.PackageEngine.Serializable/InstallOptions.cs +++ b/src/UniGetUI.PackageEngine.Serializable/InstallOptions.cs @@ -12,6 +12,7 @@ public class InstallOptions : SerializableComponent private const string AS_ADMIN = "RunAsAdministrator"; private const string PRERELEASE = "PreRelease"; private const string SKIP_MINOR = "SkipMinorUpdates"; + private const string SKIP_MINOR_LEVEL = "SkipMinorUpdatesLevel"; private const string REMOVE_DATA_UNINST = "RemoveDataOnUninstall"; private const string CLEAR_PREV_VER = "UninstallPreviousVersionsOnUpdate"; private const string ABORT_PRE_INST_FAIL = "AbortOnPreInstallFail"; @@ -65,6 +66,7 @@ public class InstallOptions : SerializableComponent POST_UPD_CMD, PRE_UNINST_CMD, POST_UNINST_CMD, + SKIP_MINOR_LEVEL, ]; public readonly IReadOnlyList _listKeys = @@ -104,6 +106,15 @@ public bool SkipMinorUpdates get => _boolVal[SKIP_MINOR]; set => _boolVal[SKIP_MINOR] = value; } + + // The version-number position (1=Major, 2=Minor, 3=Patch, 4=Remainder) from which a change is + // still considered "minor" and therefore skippable. Default 3 reproduces the legacy behaviour. + public const int DefaultSkipMinorLevel = 3; + public int SkipMinorUpdatesLevel + { + get => int.TryParse(_strVal[SKIP_MINOR_LEVEL], out int v) && v is >= 2 and <= 4 ? v : DefaultSkipMinorLevel; + set => _strVal[SKIP_MINOR_LEVEL] = value == DefaultSkipMinorLevel ? "" : value.ToString(); + } public bool RemoveDataOnUninstall { get => _boolVal[REMOVE_DATA_UNINST]; diff --git a/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs b/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs index f1aa6816e0..974610d54a 100644 --- a/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/UpgradablePackagesLoaderTests.cs @@ -66,6 +66,47 @@ public async Task EvaluatePackageAsync_SkipsMinorUpdatesWhenConfigured() Assert.False(await loader.EvaluatePackageAsync(package)); } + [Fact] + public async Task EvaluatePackageAsync_RespectsSkipMinorLevel() + { + var manager = new PackageManagerBuilder().Build(); + _ = new InstalledPackagesLoader([manager]); + _ = new DiscoverablePackagesLoader([manager]); + var loader = new TestUpgradablePackagesLoader([manager]); + + // A 3rd-position (patch) change on a four-part version. + var package = new PackageBuilder() + .WithManager(manager) + .WithId("Contoso.FourPart") + .WithVersion("3.4.5.6") + .WithNewVersion("3.4.6.6") + .Build(); + + // Level 4 => only 4th-position changes count as minor, so this patch change must NOT be skipped. + InstallOptionsFactory.SaveForPackage( + new InstallOptions + { + OverridesNextLevelOpts = true, + SkipMinorUpdates = true, + SkipMinorUpdatesLevel = 4, + }, + package + ); + Assert.True(await loader.EvaluatePackageAsync(package)); + + // Default level (3) => a 3rd-position change is minor and must be skipped. + InstallOptionsFactory.SaveForPackage( + new InstallOptions + { + OverridesNextLevelOpts = true, + SkipMinorUpdates = true, + SkipMinorUpdatesLevel = InstallOptions.DefaultSkipMinorLevel, + }, + package + ); + Assert.False(await loader.EvaluatePackageAsync(package)); + } + [Fact] public async Task EvaluatePackageAsync_SkipsIgnoredAndSupersededPackages() {