Browse Source

Switch to Hive.Versioning (part 3)

This marks old overloads obsolete and adds new overloads for Hive.Versioning.
pull/72/head
Anairkoen Schno 3 years ago
parent
commit
cec187f361
Signed by: DaNike GPG Key ID: BEFB74D5F3FC4387
7 changed files with 181 additions and 108 deletions
  1. +16
    -15
      IPA.Loader/JsonConverters/SemverRangeConverter.cs
  2. +19
    -17
      IPA.Loader/JsonConverters/SemverVersionConverter.cs
  3. +13
    -12
      IPA.Loader/Loader/PluginLoader.cs
  4. +4
    -3
      IPA.Loader/Loader/PluginManifest.cs
  5. +11
    -3
      IPA.Loader/Loader/PluginMetadata.cs
  6. +104
    -55
      IPA.Loader/Utilities/AlmostVersion.cs
  7. +14
    -3
      IPA.Loader/Utilities/Utils.cs

+ 16
- 15
IPA.Loader/JsonConverters/SemverRangeConverter.cs View File

@ -1,15 +1,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using SemVer;
namespace IPA.JsonConverters
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
internal class SemverRangeConverter : JsonConverter<Range>
{
public override Range ReadJson(JsonReader reader, Type objectType, Range existingValue, bool hasExistingValue, JsonSerializer serializer) => new Range(reader.Value as string);
public override void WriteJson(JsonWriter writer, Range value, JsonSerializer serializer) => writer.WriteValue(value.ToString());
}
}
#nullable enable
using System;
using Hive.Versioning;
using Newtonsoft.Json;
namespace IPA.JsonConverters
{
internal class SemverRangeConverter : JsonConverter<VersionRange?>
{
public override VersionRange? ReadJson(JsonReader reader, Type objectType, VersionRange? existingValue, bool hasExistingValue, JsonSerializer serializer)
=> reader.Value is string s && VersionRange.TryParse(s, out var range) ? range : existingValue;
public override void WriteJson(JsonWriter writer, VersionRange? value, JsonSerializer serializer)
=> writer.WriteValue(value?.ToString());
}
}

+ 19
- 17
IPA.Loader/JsonConverters/SemverVersionConverter.cs View File

@ -1,17 +1,19 @@
using System;
using Newtonsoft.Json;
using Version = SemVer.Version;
namespace IPA.JsonConverters
{
internal class SemverVersionConverter : JsonConverter<Version>
{
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => reader.Value == null ? null : new Version(reader.Value as string, true);
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer)
{
if (value == null) writer.WriteNull();
else writer.WriteValue(value.ToString());
}
}
}
#nullable enable
using System;
using Newtonsoft.Json;
using Version = Hive.Versioning.Version;
namespace IPA.JsonConverters
{
internal class SemverVersionConverter : JsonConverter<Version?>
{
public override Version? ReadJson(JsonReader reader, Type objectType, Version? existingValue, bool hasExistingValue, JsonSerializer serializer)
=> reader.Value is string s && Version.TryParse(s, out var version) ? version : existingValue;
public override void WriteJson(JsonWriter writer, Version? value, JsonSerializer serializer)
{
if (value == null) writer.WriteNull();
else writer.WriteValue(value.ToString());
}
}
}

+ 13
- 12
IPA.Loader/Loader/PluginLoader.cs View File

@ -18,6 +18,7 @@ using System.Diagnostics.CodeAnalysis;
using HarmonyLib;
using System.Diagnostics;
using IPA.AntiMalware;
using Hive.Versioning;
#if NET4
using Task = System.Threading.Tasks.Task;
using TaskEx = System.Threading.Tasks.Task;
@ -770,7 +771,7 @@ namespace IPA.Loader
Logger.loader.Debug(string.Join(", ", PluginsMetadata.StrJP()));
#endif
PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
PluginsMetadata.Sort((a, b) => b.HVersion.CompareTo(a.HVersion));
#if DEBUG
// print base resolution order
@ -830,20 +831,20 @@ namespace IPA.Loader
foreach (var (id, range) in meta.Manifest.Conflicts)
{
if (metadataCache.TryGetValue(id, out var plugin)
&& range.IsSatisfied(plugin.Meta.Version))
&& range.Matches(plugin.Meta.HVersion))
{
// make sure that there's a mutual dependency
var targetRange = new Range($"={meta.Version}", true);
var targetRange = VersionRange.ForVersion(meta.HVersion);
var targetConflicts = plugin.Meta.Manifest.Conflicts;
if (!targetConflicts.TryGetValue(meta.Id, out var realRange))
{
// there's not already a listed conflict
targetConflicts.Add(meta.Id, targetRange);
}
else if (!realRange.IsSatisfied(meta.Version))
else if (!realRange.Matches(meta.HVersion))
{
// there is already a listed conflict that isn't mutual
targetRange = new Range($"{realRange} || {targetRange}", true);
targetRange = realRange | targetRange;
targetConflicts[meta.Id] = targetRange;
}
}
@ -960,7 +961,7 @@ namespace IPA.Loader
if (id == SelfMeta.Id)
dependsOnSelf = true;
if (!TryResolveId(id, out var depMeta, out var depDisabled, out var depIgnored)
|| !range.IsSatisfied(depMeta.Version))
|| !range.Matches(depMeta.HVersion))
{
Logger.loader.Warn($"Dependency '{id}@{range}' for '{plugin.Id}' does not exist; ignoring '{plugin.Id}'");
ignoredPlugins.Add(plugin, new(Reason.Dependency)
@ -1033,19 +1034,19 @@ namespace IPA.Loader
}
// after we handle dependencies and loadafters, then check conflicts
foreach (var conflict in plugin.Manifest.Conflicts)
foreach (var (id, range) in plugin.Manifest.Conflicts)
{
Logger.loader.Trace($">- Checking conflict '{conflict.Key}' {conflict.Value}");
Logger.loader.Trace($">- Checking conflict '{id}' {range}");
// this lookup must be partial to prevent loadBefore/conflictsWith from creating a recursion loop
if (TryResolveId(conflict.Key, out var meta, out var conflDisabled, out var conflIgnored, partial: true)
&& conflict.Value.IsSatisfied(meta.Version)
if (TryResolveId(id, out var meta, out var conflDisabled, out var conflIgnored, partial: true)
&& range.Matches(meta.HVersion)
&& !conflIgnored && !conflDisabled) // the conflict is only *actually* a problem if it is both not ignored and not disabled
{
Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.Version}; ignoring '{plugin.Id}'");
Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.HVersion}; ignoring '{plugin.Id}'");
ignoredPlugins.Add(plugin, new(Reason.Conflict)
{
ReasonText = $"Conflicts with {meta.Id}@{meta.Version}",
ReasonText = $"Conflicts with {meta.Id}@{meta.HVersion}",
RelatedTo = meta
});
ignored = true;


+ 4
- 3
IPA.Loader/Loader/PluginManifest.cs View File

@ -1,4 +1,5 @@
#nullable enable
using Hive.Versioning;
using IPA.JsonConverters;
using IPA.Utilities;
using Newtonsoft.Json;
@ -7,7 +8,7 @@ using SemVer;
using System;
using System.Collections.Generic;
using AlmostVersionConverter = IPA.JsonConverters.AlmostVersionConverter;
using Version = SemVer.Version;
using Version = Hive.Versioning.Version;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
@ -36,10 +37,10 @@ namespace IPA.Loader
public string Author = null!;
[JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))]
public Dictionary<string, Range> Dependencies = new();
public Dictionary<string, VersionRange> Dependencies = new();
[JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))]
public Dictionary<string, Range> Conflicts = new();
public Dictionary<string, VersionRange> Conflicts = new();
[JsonProperty("features", Required = Required.DisallowNull), JsonConverter(typeof(FeaturesFieldConverter))]
public Dictionary<string, JObject> Features = new();


+ 11
- 3
IPA.Loader/Loader/PluginMetadata.cs View File

@ -7,7 +7,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Version = SemVer.Version;
using SVersion = SemVer.Version;
using Version = Hive.Versioning.Version;
#if NET3
using Net3_Proxy;
using Path = Net3_Proxy.Path;
@ -60,7 +61,14 @@ namespace IPA.Loader
/// The version of the plugin.
/// </summary>
/// <value>the version of the plugin</value>
public Version Version => manifest.Version;
[Obsolete("Use HVersion instead.")]
public SVersion Version => SVersion.ForHiveVersion(manifest.Version);
/// <summary>
/// The version of the plugin.
/// </summary>
/// <value>the version of the plugin</value>
public Version HVersion => manifest.Version;
/// <summary>
/// The file the plugin was loaded from.
@ -144,6 +152,6 @@ namespace IPA.Loader
/// Gets all of the metadata as a readable string.
/// </summary>
/// <returns>the readable printable metadata string</returns>
public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName ?? "", UnityGame.InstallPath)}'";
public override string ToString() => $"{Name}({Id}@{HVersion})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName ?? "", UnityGame.InstallPath)}'";
}
}

+ 104
- 55
IPA.Loader/Utilities/AlmostVersion.cs View File

@ -1,9 +1,11 @@
using IPA.Config.Data;
#nullable enable
using IPA.Config.Data;
using IPA.Config.Stores;
using IPA.Config.Stores.Converters;
using System;
using System.Collections.Generic;
using Version = SemVer.Version;
using SVersion = SemVer.Version;
using Version = Hive.Versioning.Version;
namespace IPA.Utilities
{
@ -11,7 +13,10 @@ namespace IPA.Utilities
/// A type that wraps <see cref="Version"/> so that the string of the version is stored when the string is
/// not a valid <see cref="Version"/>.
/// </summary>
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version>
public class AlmostVersion : IComparable<AlmostVersion>, IComparable<Version>,
#pragma warning disable CS0618 // Type or member is obsolete
IComparable<SVersion>
#pragma warning restore CS0618 // Type or member is obsolete
{
/// <summary>
/// Represents a storage type of either parsed <see cref="Version"/> object or raw <see cref="String"/>.
@ -19,7 +24,7 @@ namespace IPA.Utilities
public enum StoredAs
{
/// <summary>
/// The version was stored as a <see cref="Version"/>.
/// The version was stored as a <see cref="SVersion"/>.
/// </summary>
SemVer,
/// <summary>
@ -35,7 +40,7 @@ namespace IPA.Utilities
public AlmostVersion(string vertext)
{
if (!TryParseFrom(vertext, StoredAs.SemVer))
TryParseFrom(vertext, StoredAs.String);
_ = TryParseFrom(vertext, StoredAs.String);
}
/// <summary>
@ -46,7 +51,14 @@ namespace IPA.Utilities
{
SemverValue = ver;
StorageMode = StoredAs.SemVer;
}
}
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the <see cref="SVersion"/> provided in <paramref name="ver"/>.
/// </summary>
/// <param name="ver">the <see cref="SVersion"/> to store</param>
[Obsolete("Use Hive.Versioning.Version constructor instead.")]
public AlmostVersion(SVersion ver) : this(ver?.UnderlyingVersion ?? throw new ArgumentNullException(nameof(ver))) { }
/// <summary>
/// Creates an <see cref="AlmostVersion"/> from the version string in <paramref name="vertext"/> stored using
@ -68,26 +80,22 @@ namespace IPA.Utilities
/// <param name="copyMode">an <see cref="AlmostVersion"/> to copy the storage mode of</param>
public AlmostVersion(string vertext, AlmostVersion copyMode)
{
if (copyMode == null)
if (copyMode is null)
throw new ArgumentNullException(nameof(copyMode));
if (!TryParseFrom(vertext, copyMode.StorageMode))
TryParseFrom(vertext, StoredAs.String); // silently parse differently
_ = TryParseFrom(vertext, StoredAs.String); // silently parse differently
}
private bool TryParseFrom(string str, StoredAs mode)
{
if (mode == StoredAs.SemVer)
try
{
SemverValue = new Version(str, true);
StorageMode = StoredAs.SemVer;
return true;
}
catch
{
return false;
}
if (mode == StoredAs.SemVer)
{
StorageMode = StoredAs.SemVer;
var result = Version.TryParse(str, out var version);
SemverValue = version;
return result;
}
else
{
StringValue = str;
@ -100,13 +108,13 @@ namespace IPA.Utilities
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="string"/>.
/// </summary>
/// <value>the stored value as a <see cref="string"/>, or <see langword="null"/> if not stored as a string.</value>
public string StringValue { get; private set; } = null;
public string? StringValue { get; private set; }
/// <summary>
/// The value of the <see cref="AlmostVersion"/> if it was stored as a <see cref="Version"/>.
/// </summary>
/// <value>the stored value as a <see cref="Version"/>, or <see langword="null"/> if not stored as a version.</value>
public Version SemverValue { get; private set; } = null;
public Version? SemverValue { get; private set; }
/// <summary>
/// The way the value is stored, whether it be as a <see cref="Version"/> or a <see cref="string"/>.
@ -114,15 +122,14 @@ namespace IPA.Utilities
/// <value>the storage mode used to store this value</value>
public StoredAs StorageMode { get; private set; }
// can I just <inheritdoc /> this?
/// <summary>
/// Gets a string representation of the current version. If the value is stored as a string, this returns it. If it is
/// stored as a <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString"/>.
/// stored as a <see cref="Version"/>, it is equivalent to calling <see cref="Version.ToString()"/>.
/// </summary>
/// <returns>a string representation of the current version</returns>
/// <seealso cref="object.ToString"/>
public override string ToString() =>
StorageMode == StoredAs.SemVer ? SemverValue.ToString() : StringValue;
StorageMode == StoredAs.SemVer ? SemverValue!.ToString() : StringValue!;
/// <summary>
/// Compares <see langword="this"/> to the <see cref="AlmostVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>
@ -136,14 +143,11 @@ namespace IPA.Utilities
/// <seealso cref="CompareTo(Version)"/>
public int CompareTo(AlmostVersion other)
{
if (other == null) return -1;
if (StorageMode != other.StorageMode)
throw new InvalidOperationException("Cannot compare AlmostVersions with different stores!");
if (other is null) return 1;
if (StorageMode == StoredAs.SemVer)
return SemverValue.CompareTo(other.SemverValue);
else
return StringValue.CompareTo(other.StringValue);
return StorageMode == StoredAs.SemVer && other.StorageMode == StoredAs.SemVer
? SemverValue!.CompareTo(other.SemverValue!)
: string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
}
/// <summary>
@ -161,8 +165,21 @@ namespace IPA.Utilities
if (StorageMode != StoredAs.SemVer)
throw new InvalidOperationException("Cannot compare a SemVer version with an AlmostVersion stored as a string!");
return SemverValue.CompareTo(other);
}
return SemverValue!.CompareTo(other);
}
/// <summary>
/// Compares <see langword="this"/> to the <see cref="SVersion"/> in <paramref name="other"/> using <see cref="Version.CompareTo(Version)"/>.
/// </summary>
/// <remarks>
/// The storage method of <see langword="this"/> must be <see cref="StoredAs.SemVer"/>, else an <see cref="InvalidOperationException"/> will
/// be thrown.
/// </remarks>
/// <param name="other">the <see cref="SVersion"/> to compare to</param>
/// <returns>less than 0 if <paramref name="other"/> is considered bigger than <see langword="this"/>, 0 if equal, and greater than zero if smaller</returns>
/// <seealso cref="CompareTo(AlmostVersion)"/>
[Obsolete("Use the Hive.Versioning.Version overload instead.")]
public int CompareTo(SVersion other) => CompareTo(other.UnderlyingVersion);
/// <summary>
/// Performs a strict equality check between <see langword="this"/> and <paramref name="obj"/>.
@ -189,12 +206,12 @@ namespace IPA.Utilities
public override int GetHashCode()
{
var hashCode = -126402897;
hashCode = hashCode * -1521134295 + EqualityComparer<Version>.Default.GetHashCode(SemverValue);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(StringValue);
hashCode = hashCode * -1521134295 + StorageMode.GetHashCode();
hashCode = (hashCode * -1521134295) + EqualityComparer<Version?>.Default.GetHashCode(SemverValue);
hashCode = (hashCode * -1521134295) + EqualityComparer<string?>.Default.GetHashCode(StringValue);
hashCode = (hashCode * -1521134295) + StorageMode.GetHashCode();
return hashCode;
}
}
/// <summary>
/// Compares two versions, only taking into account the numeric part of the version if they are stored as <see cref="Version"/>s,
/// or strict equality if they are stored as <see cref="string"/>s.
@ -212,10 +229,9 @@ namespace IPA.Utilities
if (l is null && r is null) return true;
if (l is null || r is null) return false;
if (l.StorageMode != r.StorageMode) return false;
if (l.StorageMode == StoredAs.SemVer)
return Utils.VersionCompareNoPrerelease(l.SemverValue, r.SemverValue) == 0;
else
return l.StringValue == r.StringValue;
return l.StorageMode == StoredAs.SemVer
? Utils.VersionCompareNoPrerelease(l.SemverValue!, r.SemverValue!) == 0
: l.StringValue == r.StringValue;
}
/// <summary>
@ -225,24 +241,57 @@ namespace IPA.Utilities
/// <param name="r">the second value to compare</param>
/// <returns><see langword="true"/> if they are not mostly equal, <see langword="false"/> otherwise</returns>
/// <seealso cref="operator ==(AlmostVersion, AlmostVersion)"/>
public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r);
// implicitly convertible from Version
public static bool operator!=(AlmostVersion l, AlmostVersion r) => !(l == r);
// implicitly convertible from Version
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CA2225 // Operator overloads have named alternates
/// <summary>
/// Implicitly converts a <see cref="Version"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(Version)"/>.
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>.
/// </summary>
/// <param name="ver">the <see cref="Version"/> to convert</param>
/// <seealso cref="AlmostVersion(Version)"/>
public static implicit operator AlmostVersion(Version ver) => new AlmostVersion(ver);
// implicitly convertible to Version
/// <param name="ver">the <see cref="SVersion"/> to convert</param>
/// <seealso cref="AlmostVersion(SVersion)"/>
[Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")]
public static implicit operator AlmostVersion?(SVersion? ver) => ver is null ? null : new(ver);
// implicitly convertible to Version
/// <summary>
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>.
/// If not applicable, returns <see langword="null"/>
/// </summary>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param>
/// <seealso cref="SemverValue"/>
[Obsolete("Use Hive.Versioning.Version instead of SemVer.Version")]
public static implicit operator SVersion?(AlmostVersion? av) => av?.SemverValue is not null ? SVersion.ForHiveVersion(av.SemverValue) : null;
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Implicitly converts a <see cref="SVersion"/> to <see cref="AlmostVersion"/> using <see cref="AlmostVersion(SVersion)"/>.
/// </summary>
/// <param name="ver">the <see cref="SVersion"/> to convert</param>
/// <seealso cref="AlmostVersion(SVersion)"/>
public static implicit operator AlmostVersion?(Version? ver) => ver is null ? null : new(ver);
// implicitly convertible to Version
/// <summary>
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="Version"/>, if applicable, using <see cref="SemverValue"/>.
/// Implicitly converts an <see cref="AlmostVersion"/> to <see cref="SVersion"/>, if applicable, using <see cref="SemverValue"/>.
/// If not applicable, returns <see langword="null"/>
/// </summary>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="Version"/></param>
/// <param name="av">the <see cref="AlmostVersion"/> to convert to a <see cref="SVersion"/></param>
/// <seealso cref="SemverValue"/>
public static implicit operator Version(AlmostVersion av) => av?.SemverValue;
public static implicit operator Version?(AlmostVersion av) => av?.SemverValue;
#pragma warning restore CA2225 // Operator overloads have named alternates
public static bool operator <(AlmostVersion left, AlmostVersion right)
=> left is null ? right is not null : left.CompareTo(right) < 0;
public static bool operator <=(AlmostVersion left, AlmostVersion right)
=> left is null || left.CompareTo(right) <= 0;
public static bool operator >(AlmostVersion left, AlmostVersion right)
=> left is not null && left.CompareTo(right) > 0;
public static bool operator >=(AlmostVersion left, AlmostVersion right)
=> left is null ? right is null : left.CompareTo(right) >= 0;
}
/// <summary>
@ -257,7 +306,7 @@ namespace IPA.Utilities
/// <param name="parent">the owner of the new object</param>
/// <returns></returns>
public override AlmostVersion FromValue(Value value, object parent)
=> new AlmostVersion(Converter<string>.Default.FromValue(value, parent));
=> new(Converter<string>.Default.FromValue(value, parent));
/// <summary>
/// Converts an <see cref="AlmostVersion"/> to a <see cref="Text"/> node.
/// </summary>


+ 14
- 3
IPA.Loader/Utilities/Utils.cs View File

@ -8,6 +8,7 @@ using Mono.Cecil;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
using Version = Hive.Versioning.Version;
#if NET3
using File = Net3_Proxy.File;
#endif
@ -193,16 +194,26 @@ namespace IPA.Utilities
/// <param name="l">the left value</param>
/// <param name="r">the right value</param>
/// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
[Obsolete("Use Hive.Versioning.Version overload instead.")]
public static int VersionCompareNoPrerelease(SemVer.Version l, SemVer.Version r)
=> VersionCompareNoPrerelease(l?.UnderlyingVersion!, r?.UnderlyingVersion!);
/// <summary>
/// Compares a pair of <see cref="Version"/>s ignoring both the prerelease and build fields.
/// </summary>
/// <param name="l">the left value</param>
/// <param name="r">the right value</param>
/// <returns>&lt; 0 if l is less than r, 0 if they are equal in the numeric portion, or &gt; 0 if l is greater than r</returns>
public static int VersionCompareNoPrerelease(Version l, Version r)
{
if (l is null) throw new ArgumentNullException(nameof(l));
if (r is null) throw new ArgumentNullException(nameof(r));
var cmpVal = l.Major - r.Major;
var cmpVal = l.Major.CompareTo(r.Major);
if (cmpVal != 0) return cmpVal;
cmpVal = l.Minor - r.Minor;
cmpVal = l.Minor.CompareTo(r.Minor);
if (cmpVal != 0) return cmpVal;
cmpVal = l.Patch - r.Patch;
cmpVal = l.Patch.CompareTo(r.Patch);
return cmpVal;
}


Loading…
Cancel
Save