diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs index faaea4fe..e05cf0dc 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs @@ -495,6 +495,7 @@ public void InstallBouncyCastleTest([Values] InstallMode installMode) [TestCase("1.2.3-rc1+1234", "1.2.3-rc2")] [TestCase("1.2.3-rc1+1234", "1.2.3-rc2+1234")] [TestCase("1.0.0", "1.0.0.10")] + [TestCase("1.0.0-beta.9", "1.0.0-beta.10")] public void VersionComparison(string smallerVersion, string greaterVersion) { var localNugetPackageSource = new NugetPackageSourceLocal("test", "test"); diff --git a/src/NuGetForUnity/Editor/Models/NugetPackageIdentifier.cs b/src/NuGetForUnity/Editor/Models/NugetPackageIdentifier.cs index 98dcae80..82fa4a82 100644 --- a/src/NuGetForUnity/Editor/Models/NugetPackageIdentifier.cs +++ b/src/NuGetForUnity/Editor/Models/NugetPackageIdentifier.cs @@ -269,7 +269,7 @@ public override bool Equals(object obj) Justification = "We only edit the version / id before we use the hash (stroe it in a dictionary).")] public override int GetHashCode() { - return Id.GetHashCode() ^ PackageVersion.GetHashCode(); + return GetHashCodeOfId() ^ PackageVersion.GetHashCode(); } /// @@ -280,5 +280,14 @@ public override string ToString() { return $"{Id}.{Version}"; } + + private int GetHashCodeOfId() + { +#if UNITY_2021_2_OR_NEWER + return Id.GetHashCode(StringComparison.OrdinalIgnoreCase); +#else + return StringComparer.OrdinalIgnoreCase.GetHashCode(Id); +#endif + } } } diff --git a/src/NuGetForUnity/Editor/Models/NugetPackageVersion.cs b/src/NuGetForUnity/Editor/Models/NugetPackageVersion.cs index d68fd2a6..8bdad217 100644 --- a/src/NuGetForUnity/Editor/Models/NugetPackageVersion.cs +++ b/src/NuGetForUnity/Editor/Models/NugetPackageVersion.cs @@ -450,6 +450,9 @@ private readonly struct SemVer2Version private readonly int patch; + [CanBeNull] + private readonly string[] preReleaseLabels; + private readonly int revision; /// @@ -465,6 +468,7 @@ public SemVer2Version(bool dummy) minor = -1; patch = -1; revision = -1; + preReleaseLabels = null; } /// @@ -486,10 +490,12 @@ public SemVer2Version([CanBeNull] string version) } PreRelease = null; + preReleaseLabels = null; var preReleaseStartIndex = version.IndexOf('-'); if (preReleaseStartIndex > 0) { PreRelease = version.Substring(preReleaseStartIndex + 1); + preReleaseLabels = PreRelease.Split('.'); version = version.Substring(0, preReleaseStartIndex); } @@ -524,6 +530,7 @@ public SemVer2Version([CanBeNull] string version) buildMetadata = null; PreRelease = null; + preReleaseLabels = null; major = -1; minor = -1; patch = -1; @@ -566,9 +573,24 @@ public int Compare(in SemVer2Version other) if (revisionComparison == 0) { // if the build versions are equal, just return the prerelease version comparison - var prerelease = PreRelease ?? "\uFFFF"; - var otherPrerelease = other.PreRelease ?? "\uFFFF"; - var prereleaseComparison = string.Compare(prerelease, otherPrerelease, StringComparison.OrdinalIgnoreCase); + if (preReleaseLabels == null && other.preReleaseLabels == null) + { + // no pre-release and the rest is equal + return 0; + } + + if (preReleaseLabels != null && other.preReleaseLabels == null) + { + // pre-release versions are always after release versions + return -1; + } + + if (preReleaseLabels == null && other.preReleaseLabels != null) + { + return 1; + } + + var prereleaseComparison = ComparePreReleaseLabels(preReleaseLabels, other.preReleaseLabels); return prereleaseComparison; } @@ -587,9 +609,9 @@ public int Compare(in SemVer2Version other) // the major versions are different, so use them return majorComparison; } - catch (Exception) + catch (Exception exception) { - Debug.LogErrorFormat("Compare Error: {0} {1}", this, other); + Debug.LogErrorFormat("Error: {0} while comparing '{1}' with '{2}'.", exception, this, other); return -1; } } @@ -637,6 +659,66 @@ public string ToString(bool withBuildMetadata) return stringBuilder.ToString(); } + + /// + /// Compares sets of Pre-Release labels ( splitted by '.'). + /// + private static int ComparePreReleaseLabels([NotNull] string[] releaseLabels1, [NotNull] string[] releaseLabels2) + { + var result = 0; + + var count = Math.Max(releaseLabels1.Length, releaseLabels2.Length); + + for (var i = 0; i < count; i++) + { + var hasLabel1 = i < releaseLabels1.Length; + var hasLabel2 = i < releaseLabels2.Length; + + if (!hasLabel1 && hasLabel2) + { + return -1; + } + + if (hasLabel1 && !hasLabel2) + { + return 1; + } + + // compare the labels + result = ComparePreReleaseLabel(releaseLabels1[i], releaseLabels2[i]); + + if (result != 0) + { + return result; + } + } + + return result; + } + + /// + /// Pre-Release labels are compared as numbers if they are numeric, otherwise they will be compared as strings (case insensitive). + /// + private static int ComparePreReleaseLabel(string releaseLabel1, string releaseLabel2) + { + var label1IsNumeric = int.TryParse(releaseLabel1, out var releaseLabel1Number); + var label2IsNumeric = int.TryParse(releaseLabel2, out var releaseLabel2Number); + + if (label1IsNumeric && label2IsNumeric) + { + // if both are numeric compare them as numbers + return releaseLabel1Number.CompareTo(releaseLabel2Number); + } + + if (label1IsNumeric || label2IsNumeric) + { + // numeric labels come before alpha labels + return label1IsNumeric ? -1 : 1; + } + + // Everything will be compared case insensitively. + return string.Compare(releaseLabel1, releaseLabel2, StringComparison.OrdinalIgnoreCase); + } } } } diff --git a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceLocal.cs b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceLocal.cs index ea258685..5d994bfb 100644 --- a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceLocal.cs +++ b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceLocal.cs @@ -302,24 +302,26 @@ private List GetLocalPackages( } /// - /// Gets a list of available packages from a local source (not a web server) that are upgrades for the given list of installed packages. + /// Gets a list of available packages from a local source (not a web server) that are versions / upgrade or downgrade of the given list of installed + /// packages. /// /// The list of packages to use to find updates. /// True to include prerelease packages (alpha, beta, etc). - /// A list of all updates available. + /// A list of all updates / downgrades available. [NotNull] [ItemNotNull] private List GetLocalUpdates([NotNull] [ItemNotNull] IEnumerable packages, bool includePrerelease = false) { var updates = new List(); - foreach (var installedPackage in packages) + foreach (var packageToSearch in packages) { - var availablePackages = GetLocalPackages($"{installedPackage.Id}*", false, includePrerelease); + var availablePackages = GetLocalPackages($"{packageToSearch.Id}*", false, includePrerelease); foreach (var availablePackage in availablePackages) { - if (installedPackage.Id.Equals(availablePackage.Id, StringComparison.OrdinalIgnoreCase) && - installedPackage.CompareTo(availablePackage) < 0) + if (packageToSearch.Id.Equals(availablePackage.Id, StringComparison.OrdinalIgnoreCase)) { + // keep the manually installed state + availablePackage.IsManuallyInstalled = packageToSearch.IsManuallyInstalled; updates.Add(availablePackage); } } diff --git a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV2.cs b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV2.cs index e5f7caac..ff786fc9 100644 --- a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV2.cs +++ b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV2.cs @@ -294,6 +294,7 @@ public List GetUpdates( try { var newPackages = GetPackagesFromUrl(url); + CopyIsManuallyInstalled(newPackages, packagesCollection); updates.AddRange(newPackages); } catch (Exception e) @@ -522,5 +523,16 @@ private List GetUpdatesFallback( NugetLogger.LogVerbose("NugetPackageSource.GetUpdatesFallback took {0} ms", stopwatch.ElapsedMilliseconds); return updates; } + + private void CopyIsManuallyInstalled(List newPackages, ICollection packagesToUpdate) + { + foreach (var newPackage in newPackages) + { + newPackage.IsManuallyInstalled = + packagesToUpdate.FirstOrDefault(packageToUpdate => packageToUpdate.Id.Equals(newPackage.Id, StringComparison.OrdinalIgnoreCase)) + ?.IsManuallyInstalled ?? + false; + } + } } } diff --git a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV3.cs b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV3.cs index 3b2ffe9d..70cab4cf 100644 --- a/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV3.cs +++ b/src/NuGetForUnity/Editor/PackageSource/NugetPackageSourceV3.cs @@ -247,6 +247,7 @@ await ApiClient.SearchPackageAsync( .GetAwaiter() .GetResult(); + CopyIsManuallyInstalled(packagesFromServer, packagesToFetch); packagesFromServer.Sort(); return packagesFromServer; } @@ -313,5 +314,16 @@ private NugetApiClientV3 InitializeApiClient() ApiClientCache.Add(SavedPath, apiClient); return apiClient; } + + private void CopyIsManuallyInstalled(List newPackages, ICollection packagesToUpdate) + { + foreach (var newPackage in newPackages) + { + newPackage.IsManuallyInstalled = + packagesToUpdate.FirstOrDefault(packageToUpdate => packageToUpdate.Id.Equals(newPackage.Id, StringComparison.OrdinalIgnoreCase)) + ?.IsManuallyInstalled ?? + false; + } + } } } diff --git a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs index d9819962..2999ecd7 100644 --- a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs +++ b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs @@ -163,14 +163,37 @@ private List FilteredUpdatePackages { get { + IEnumerable result; if (string.IsNullOrWhiteSpace(updatesSearchTerm) || updatesSearchTerm == "Search") { - return updatePackages; + result = updatePackages; } - - return updatePackages.Where( + else + { + result = updatePackages.Where( package => package.Id.IndexOf(updatesSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0 || - package.Title?.IndexOf(updatesSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0) + package.Title?.IndexOf(updatesSearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); + } + + var installedPackages = InstalledPackagesManager.InstalledPackages; + + // filter not updatable / not downgradable packages + return result.Where( + package => + { + var installed = installedPackages.FirstOrDefault(p => p.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase)); + + if (installed == null || package.Versions.Count == 0) + { + // normally shouldn't happen but for now include it in the result + return true; + } + + // we do not want to show packages that have no updates if we're showing updates + // similarly, we do not show packages that are on the lowest possible version if we're showing downgrades + return (showDowngrades && installed.PackageVersion > package.Versions[package.Versions.Count - 1]) || + (!showDowngrades && installed.PackageVersion < package.Versions[0]); + }) .ToList(); } } @@ -429,6 +452,15 @@ private static GUIStyle GetFoldoutStyle() return cachedFoldoutStyle; } + private static void DrawNoDataAvailableInfo(string message) + { + EditorStyles.label.fontStyle = FontStyle.Bold; + EditorStyles.label.fontSize = 14; + EditorGUILayout.LabelField(message, GUILayout.Height(20)); + EditorStyles.label.fontSize = 10; + EditorStyles.label.fontStyle = FontStyle.Normal; + } + /// /// Called when enabling the window. /// @@ -569,15 +601,11 @@ private void DrawUpdates() var filteredUpdatePackages = FilteredUpdatePackages; if (filteredUpdatePackages.Count > 0) { - DrawPackages(filteredUpdatePackages, true); + DrawPackagesSplittedByManuallyInstalled(filteredUpdatePackages); } else { - EditorStyles.label.fontStyle = FontStyle.Bold; - EditorStyles.label.fontSize = 14; - EditorGUILayout.LabelField("There are no updates available!", GUILayout.Height(20)); - EditorStyles.label.fontSize = 10; - EditorStyles.label.fontStyle = FontStyle.Normal; + DrawNoDataAvailableInfo("There are no updates available!"); } EditorGUILayout.EndVertical(); @@ -598,36 +626,40 @@ private void DrawInstalled() var installedPackages = FilteredInstalledPackages; if (installedPackages.Count > 0) { - var headerStyle = GetHeaderStyle(); + DrawPackagesSplittedByManuallyInstalled(installedPackages); + } + else + { + DrawNoDataAvailableInfo("There are no packages installed!"); + } - EditorGUILayout.LabelField("Installed packages", headerStyle, GUILayout.Height(20)); - DrawPackages(installedPackages.TakeWhile(package => package.IsManuallyInstalled), true); + EditorGUILayout.EndVertical(); + EditorGUILayout.EndScrollView(); + } - var rectangle = EditorGUILayout.GetControlRect(true, 20f, headerStyle); - EditorGUI.LabelField(rectangle, string.Empty, headerStyle); + private void DrawPackagesSplittedByManuallyInstalled(List packages) + { + var headerStyle = GetHeaderStyle(); - showImplicitlyInstalled = EditorGUI.Foldout( - rectangle, - showImplicitlyInstalled, - "Implicitly installed packages", - true, - GetFoldoutStyle()); - if (showImplicitlyInstalled) - { - DrawPackages(installedPackages.SkipWhile(package => package.IsManuallyInstalled), true); - } + var rectangle = EditorGUILayout.GetControlRect(true, 20f, headerStyle); + EditorGUI.LabelField(rectangle, " Installed packages", headerStyle); + if (packages.Exists(package => package.IsManuallyInstalled)) + { + DrawPackages(packages.TakeWhile(package => package.IsManuallyInstalled), true); } else { - EditorStyles.label.fontStyle = FontStyle.Bold; - EditorStyles.label.fontSize = 14; - EditorGUILayout.LabelField("There are no packages installed!", GUILayout.Height(20)); - EditorStyles.label.fontSize = 10; - EditorStyles.label.fontStyle = FontStyle.Normal; + DrawNoDataAvailableInfo("There are no explicitly installed packages."); } - EditorGUILayout.EndVertical(); - EditorGUILayout.EndScrollView(); + rectangle = EditorGUILayout.GetControlRect(true, 20f, headerStyle); + EditorGUI.LabelField(rectangle, string.Empty, headerStyle); + + showImplicitlyInstalled = EditorGUI.Foldout(rectangle, showImplicitlyInstalled, "Implicitly installed packages", true, GetFoldoutStyle()); + if (showImplicitlyInstalled) + { + DrawPackages(packages.SkipWhile(package => package.IsManuallyInstalled), true); + } } /// @@ -891,17 +923,6 @@ private void DrawPackage(INugetPackage package, GUIStyle packageStyle, GUIStyle var installedPackages = InstalledPackagesManager.InstalledPackages; var installed = installedPackages.FirstOrDefault(p => p.Id.Equals(package.Id, StringComparison.OrdinalIgnoreCase)); - // if we are on the update tab, we do not want to show packages that have no updates if we're showing updates; similarly, we do not - // show packages that are on the lowest possible version if we're showing downgrades - if (currentTab == NugetWindowTab.UpdatesTab && - installed != null && - package.Versions.Count >= 1 && - ((showDowngrades && installed.PackageVersion <= package.Versions[package.Versions.Count - 1]) || - (!showDowngrades && installed.PackageVersion >= package.Versions[0]))) - { - return; - } - using (new EditorGUILayout.HorizontalScope()) { // The Unity GUI system (in the Editor) is terrible. This probably requires some explanation.