diff --git a/src/shared/Core.Tests/GitTests.cs b/src/shared/Core.Tests/GitTests.cs index a6905bb8f..b72e80b43 100644 --- a/src/shared/Core.Tests/GitTests.cs +++ b/src/shared/Core.Tests/GitTests.cs @@ -197,8 +197,7 @@ public void Git_Version_ReturnsVersion() var git = new GitProcess(trace, trace2, processManager, gitPath, Path.GetTempPath()); GitVersion version = git.Version; - Assert.NotEqual(new GitVersion(), version); - + Assert.NotEqual(GitVersion.Zero, version); } #region Test Helpers diff --git a/src/shared/Core.Tests/GitVersionTests.cs b/src/shared/Core.Tests/GitVersionTests.cs index 5b53a3da7..1e1d2a36d 100644 --- a/src/shared/Core.Tests/GitVersionTests.cs +++ b/src/shared/Core.Tests/GitVersionTests.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using Xunit; @@ -5,19 +6,984 @@ namespace GitCredentialManager.Tests { public class GitVersionTests { + [Fact] + public void GitVersion_Parse_ValidVersionString_ReturnsCorrectVersion() + { + GitVersion version = GitVersion.Parse("2.30.1"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Null(version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, version.Distribution); + Assert.Null(version.DistributionIdentifier); + Assert.Null(version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithBuild_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.30.1.windows.2"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Null(version.ReleaseCandidate); + Assert.Equal(GitDistributionType.GitForWindows, version.Distribution); + Assert.Equal("windows", version.DistributionIdentifier); + Assert.Equal(2, version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithBuildRevision_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.30.1.vfs.2.3"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Null(version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Microsoft, version.Distribution); + Assert.Equal("vfs", version.DistributionIdentifier); + Assert.Equal(2, version.Build); + Assert.Equal(3, version.Revision); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid")] + [InlineData("a.b.c")] + [InlineData("2.invalid.1")] + [InlineData("2.30.invalid")] + [InlineData("2.30")] + [InlineData("2")] + public void GitVersion_Parse_InvalidVersionString_ThrowsFormatException(string invalidVersion) + { + Assert.Throws(() => GitVersion.Parse(invalidVersion)); + } + + [Theory] + [InlineData("2.30.1.vfs.1.0.extra")] + [InlineData("2.30.1.vfs.1.0-extra")] + [InlineData("2.30.1.vfs.1.0.extra.1")] + [InlineData("2.30.1.vfs.1.0-extra.1")] + public void GitVersion_Parse_ExtraInformationAtEnd_IgnoresExtraInfo(string versionString) + { + var version = GitVersion.Parse(versionString); + + Assert.Equal("2.30.1.vfs.1.0", version.ToString()); + Assert.Equal(versionString, version.OriginalString); + } + + [Fact] + public void GitVersion_TryParse_ValidVersionString_ReturnsTrueAndCorrectVersion() + { + bool result = GitVersion.TryParse("2.30.1", out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("invalid")] + [InlineData("a.b.c")] + public void GitVersion_TryParse_InvalidVersionString_ReturnsFalseAndNullVersion(string invalidVersion) + { + bool result = GitVersion.TryParse(invalidVersion, out var version); + + Assert.False(result); + Assert.Null(version); + } + + [Fact] + public void GitVersion_ToString_StandardVersion_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 30, 1); + + Assert.Equal("2.30.1", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_VersionWithDistributionOnly_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 30, 1, GitDistributionType.Unknown) + { + DistributionIdentifier = "custom" + }; + + Assert.Equal("2.30.1.custom", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_VersionWithBuild_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 30, 1, GitDistributionType.GitForWindows, 1, 0); + + Assert.Equal("2.30.1.windows.1.0", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_VersionWithBuildRevision_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 30, 1, GitDistributionType.Microsoft, 1, 2); + + Assert.Equal("2.30.1.vfs.1.2", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_VersionWithZeroBuildRevision_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 30, 1, GitDistributionType.Microsoft, 0, 0); + + Assert.Equal("2.30.1.vfs.0.0", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_ParsedStandardVersion_RoundTripWorks() + { + var originalString = "2.30.1"; + var version = GitVersion.Parse(originalString); + + Assert.Equal(originalString, version.ToString()); + } + + [Fact] + public void GitVersion_ToString_ParsedDistributionVersion_RoundTripWorks() + { + var originalString = "2.30.1.vfs.1.2"; + var version = GitVersion.Parse(originalString); + + Assert.Equal(originalString, version.ToString()); + } + + [Fact] + public void GitVersion_ToString_ZeroVersionNumbers_ReturnsCorrectFormat() + { + var version = GitVersion.Zero; + + Assert.Equal("0.0.0", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_LargeVersionNumbers_ReturnsCorrectFormat() + { + var version = new GitVersion(999,888, 777 ); + + Assert.Equal("999.888.777", version.ToString()); + } + + [Fact] + public void GitVersion_ToString_AppleGit_ReturnsCorrectFormat() + { + var version = new GitVersion(2, 50, 1, GitDistributionType.Apple, 155); + + Assert.Equal("2.50.1 (Apple Git-155)", version.ToString()); + } + + [Fact] + public void GitVersion_CompareTo_SameVersion_ReturnsZero() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.1"); + + Assert.Equal(0, version1.CompareTo(version2)); + } + + [Fact] + public void GitVersion_CompareTo_SameReference_ReturnsZero() + { + var version = GitVersion.Parse("2.30.1"); + + Assert.Equal(0, version.CompareTo(version)); + } + + [Fact] + public void GitVersion_CompareTo_WithNull_ReturnsPositive() + { + var version = GitVersion.Parse("2.30.1"); + + Assert.True(version.CompareTo(null) > 0); + } + + [Theory] + [InlineData("2.30.1", "2.30.2", -1)] + [InlineData("2.30.2", "2.30.1", 1)] + [InlineData("2.29.1", "2.30.1", -1)] + [InlineData("2.31.1", "2.30.1", 1)] + [InlineData("1.30.1", "2.30.1", -1)] + [InlineData("3.30.1", "2.30.1", 1)] + public void GitVersion_CompareTo_DifferentVersions_ReturnsCorrectComparison(string str1, string str2, int expectedSign) + { + var version1 = GitVersion.Parse(str1); + var version2 = GitVersion.Parse(str2); + + var result = version1.CompareTo(version2); + Assert.Equal(expectedSign, Math.Sign(result)); + } + + [Theory] + [InlineData("2.30.1.windows.1.0", "2.30.1.windows.2.0", -1)] + [InlineData("2.30.1.windows.1.1", "2.30.1.windows.1.0", 1)] + public void GitVersion_CompareTo_VersionsWithDistribution_ReturnsCorrectComparison(string str1, string str2, int expectedSign) + { + var version1 = GitVersion.Parse(str1); + var version2 = GitVersion.Parse(str2); + + var result = version1.CompareTo(version2); + Assert.Equal(expectedSign, Math.Sign(result)); + } + + [Fact] + public void GitVersion_LessThanOperator_WithValidVersions_ReturnsCorrectResult() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.2"); + + Assert.True(version1 < version2); + Assert.False(version2 < version1); +#pragma warning disable CS1718 // Comparison made to same variable + Assert.False(version1 < version1); +#pragma warning restore CS1718 + } + + [Fact] + public void GitVersion_LessThanOperator_WithNull_ReturnsCorrectResult() + { + var version = GitVersion.Parse("2.30.1"); + + Assert.True(null < version); + Assert.False(version < null); + } + + [Fact] + public void GitVersion_LessThanOrEqualOperator_WithValidVersions_ReturnsCorrectResult() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.2"); + var version3 = GitVersion.Parse("2.30.1"); + + Assert.True(version1 <= version2); + Assert.True(version1 <= version3); + Assert.False(version2 <= version1); + } + + [Fact] + public void GitVersion_LessThanOrEqualOperator_WithNull_ReturnsCorrectResult() + { + var version = GitVersion.Parse("2.30.1"); + GitVersion? nullVersion = null; + + Assert.True(null <= version); +#pragma warning disable CS1718 // Comparison made to same variable + Assert.True(nullVersion <= nullVersion); +#pragma warning restore CS1718 + Assert.False(version <= null); + } + + [Fact] + public void GitVersion_GreaterThanOperator_WithValidVersions_ReturnsCorrectResult() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.2"); + + Assert.True(version2 > version1); + Assert.False(version1 > version2); +#pragma warning disable CS1718 // Comparison made to same variable + Assert.False(version1 > version1); +#pragma warning restore CS1718 + } + + [Fact] + public void GitVersion_GreaterThanOperator_WithNull_ReturnsCorrectResult() + { + var version = GitVersion.Parse("2.30.1"); + + Assert.True(version > null); + Assert.False(null > version); + } + + [Fact] + public void GitVersion_GreaterThanOrEqualOperator_WithValidVersions_ReturnsCorrectResult() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.2"); + var version3 = GitVersion.Parse("2.30.1"); + + Assert.True(version2 >= version1); + Assert.True(version1 >= version3); + Assert.False(version1 >= version2); + } + + [Fact] + public void GitVersion_GreaterThanOrEqualOperator_WithNull_ReturnsCorrectResult() + { + var version = GitVersion.Parse("2.30.1"); + GitVersion? nullVersion = null; + + Assert.True(version >= null); +#pragma warning disable CS1718 // Comparison made to same variable + Assert.True(nullVersion >= nullVersion); +#pragma warning restore CS1718 + Assert.False(null >= version); + } + + [Fact] + public void GitVersion_EqualityOperator_WithSameVersions_ReturnsTrue() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.1"); + + Assert.True(version1 == version2); + Assert.False(version1 != version2); + } + + [Fact] + public void GitVersion_EqualityOperator_WithDifferentVersions_ReturnsFalse() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.2"); + + Assert.False(version1 == version2); + Assert.True(version1 != version2); + } + + [Fact] + public void GitVersion_EqualityOperator_WithNull_ReturnsCorrectResult() + { + var version = GitVersion.Parse("2.30.1"); + GitVersion? nullVersion = null; + + Assert.False(version == null); + Assert.False(null == version); +#pragma warning disable CS1718 // Comparison made to same variable + Assert.True(nullVersion == nullVersion); + Assert.True(version != null); + Assert.True(null != version); + Assert.False(nullVersion != nullVersion); +#pragma warning restore CS1718 + } + + [Fact] + public void GitVersion_Equality_WorksCorrectly() + { + var version1 = new GitVersion (2, 30, 1); + var version2 = new GitVersion (2, 30, 1); + var version3 = new GitVersion (2, 30, 2); + + Assert.Equal(version1, version2); + Assert.NotEqual(version1, version3); + Assert.Equal(version1.GetHashCode(), version2.GetHashCode()); + } + + [Fact] + public void GitVersion_IsComparable_WithNull_ReturnsFalse() + { + var version = GitVersion.Parse("2.30.1"); + + Assert.False(version.IsComparableTo(null)); + } + + [Fact] + public void GitVersion_IsComparable_ReleaseCandidateVersions_ReturnsTrue() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1-rc2"); + var stable = GitVersion.Parse("2.30.1"); + + Assert.True(rc1.IsComparableTo(rc2)); + Assert.True(rc1.IsComparableTo(stable)); + Assert.True(stable.IsComparableTo(rc1)); + } + + [Fact] + public void GitVersion_IsComparable_WithSameDistribution_ReturnsTrue() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.2.windows.2.0"); + + Assert.True(version1.IsComparableTo(version2)); + } + + [Fact] + public void GitVersion_IsComparable_WithDifferentDistribution_ReturnsFalse() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + Assert.False(version1.IsComparableTo(version2)); + } + + [Fact] + public void GitVersion_IsComparable_BothWithoutDistribution_ReturnsTrue() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.31.0"); + + Assert.True(version1.IsComparableTo(version2)); + } + + [Fact] + public void GitVersion_IsComparable_OneWithDistributionOneWithout_ReturnsFalse() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.1.windows.1.0"); + + Assert.False(version1.IsComparableTo(version2)); + } + + [Fact] + public void GitVersion_CompareTo_ReleaseCandidateVsStable_ReleaseCandidateIsLess() + { + var rcVersion = GitVersion.Parse("2.30.1.rc1"); + var stableVersion = GitVersion.Parse("2.30.1"); + + Assert.True(rcVersion.CompareTo(stableVersion) < 0); + Assert.True(stableVersion.CompareTo(rcVersion) > 0); + } + + [Fact] + public void GitVersion_CompareTo_DifferentReleaseCandidate_ReturnsCorrectComparison() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1-rc2"); + + Assert.True(rc1.CompareTo(rc2) < 0); + Assert.True(rc2.CompareTo(rc1) > 0); + } + + [Fact] + public void GitVersion_CompareTo_SameReleaseCandidate_ReturnsZero() + { + var rc1 = GitVersion.Parse("2.30.1-rc5"); + var rc2 = GitVersion.Parse("2.30.1.rc5"); + + Assert.Equal(0, rc1.CompareTo(rc2)); + } + [Theory] - [InlineData(null, 1)] - [InlineData("2", 1)] - [InlineData("3", -1)] - [InlineData("2.33", 0)] - [InlineData("2.32.0", 1)] - [InlineData("2.33.0.windows.0.1", 0)] - [InlineData("2.33.0.2", -1)] - public void GitVersion_CompareTo_2_33_0(string input, int expectedCompare) - { - GitVersion baseline = new GitVersion(2, 33, 0); - GitVersion actual = new GitVersion(input); - Assert.Equal(expectedCompare, baseline.CompareTo(actual)); + [InlineData("2.30.1.rc1", "2.30.1", -1)] + [InlineData("2.30.1", "2.30.1.rc1", 1)] + [InlineData("2.30.1.rc1", "2.30.1.rc2", -1)] + [InlineData("2.30.1.rc10", "2.30.1.rc2", 1)] + [InlineData("2.30.0.rc1", "2.30.1.rc1", -1)] + public void GitVersion_CompareTo_ReleaseCandidateVersions_ReturnsCorrectComparison(string str1, string str2, + int expectedSign) + { + var version1 = GitVersion.Parse(str1); + var version2 = GitVersion.Parse(str2); + + var result = version1.CompareTo(version2); + Assert.Equal(expectedSign, Math.Sign(result)); + } + + [Fact] + public void GitVersion_CompareTo_IncompatibleVersions_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + var exception = Assert.Throws(() => version1.CompareTo(version2)); + Assert.NotNull(exception.Version1); + Assert.NotNull(exception.Version2); + Assert.Equal(version1, exception.Version1); + Assert.Equal(version2, exception.Version2); + } + + [Fact] + public void GitVersion_CompareTo_ReleaseCandidateWithDistributionVersions_ReturnsCorrectComparison() + { + var rc1Windows = GitVersion.Parse("2.30.1.rc1.windows.1.0"); + var rc2Windows = GitVersion.Parse("2.30.1-rc2.windows.1.0"); + var stableWindows = GitVersion.Parse("2.30.1.windows.1.0"); + + Assert.True(rc1Windows.CompareTo(rc2Windows) < 0); + Assert.True(rc1Windows.CompareTo(stableWindows) < 0); + Assert.True(stableWindows.CompareTo(rc1Windows) > 0); + } + + [Fact] + public void GitVersion_CompareTo_StandardVersionWithDistributionVersion_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1"); + var version2 = GitVersion.Parse("2.30.1.windows.1.0"); + + var exception = Assert.Throws(() => version1.CompareTo(version2)); + Assert.Equal(version1, exception.Version1); + Assert.Equal(version2, exception.Version2); + } + + [Fact] + public void GitVersion_LessThanOperator_ReleaseCandidateVersions_ReturnsCorrectResult() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1-rc2"); + var stable = GitVersion.Parse("2.30.1"); + + Assert.True(rc1 < rc2); + Assert.True(rc1 < stable); + Assert.False(rc2 < rc1); + Assert.False(stable < rc1); + } + + [Fact] + public void GitVersion_GreaterThanOperator_ReleaseCandidateVersions_ReturnsCorrectResult() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1-rc2"); + var stable = GitVersion.Parse("2.30.1"); + + Assert.True(rc2 > rc1); + Assert.True(stable > rc1); + Assert.False(rc1 > rc2); + Assert.False(rc1 > stable); + } + + [Fact] + public void GitVersion_LessThanOrEqualOperator_ReleaseCandidateVersions_ReturnsCorrectResult() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1.rc1"); + var rc3 = GitVersion.Parse("2.30.1.rc2"); + + Assert.True(rc1 <= rc2); + Assert.True(rc1 <= rc3); + Assert.False(rc3 <= rc1); + } + + [Fact] + public void GitVersion_GreaterThanOrEqualOperator_ReleaseCandidateVersions_ReturnsCorrectResult() + { + var rc1 = GitVersion.Parse("2.30.1.rc1"); + var rc2 = GitVersion.Parse("2.30.1.rc1"); + var rc3 = GitVersion.Parse("2.30.1.rc2"); + + Assert.True(rc2 >= rc1); + Assert.True(rc3 >= rc1); + Assert.False(rc1 >= rc3); + } + + [Fact] + public void GitVersion_LessThanOperator_IncompatibleVersions_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + Assert.Throws(() => version1 < version2); + } + + [Fact] + public void GitVersion_GreaterThanOperator_IncompatibleVersions_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + Assert.Throws(() => version1 > version2); + } + + [Fact] + public void GitVersion_LessThanOrEqualOperator_IncompatibleVersions_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + Assert.Throws(() => version1 <= version2); + } + + [Fact] + public void GitVersion_GreaterThanOrEqualOperator_IncompatibleVersions_ThrowsGitVersionMismatchException() + { + var version1 = GitVersion.Parse("2.30.1.windows.1.0"); + var version2 = GitVersion.Parse("2.30.1.vfs.1.0"); + + Assert.Throws(() => version1 >= version2); + } + + [Fact] + public void GitVersion_ToCoreVersion_StandardVersion_ReturnsSameVersion() + { + var version = GitVersion.Parse("2.30.1"); + var coreVersion = version.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Null(coreVersion.Revision); + Assert.Equal(version, coreVersion); + } + + [Fact] + public void GitVersion_ToCoreVersion_ReleaseCandidateDotVersion_KeepsReleaseCandidate() + { + var rcVersion = GitVersion.Parse("2.30.1.rc3"); + var coreVersion = rcVersion.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(3, coreVersion.ReleaseCandidate); + Assert.Equal("2.30.1.rc3", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_ReleaseCandidateDashVersion_KeepsReleaseCandidate() + { + var rcVersion = GitVersion.Parse("2.30.1-rc3"); + var coreVersion = rcVersion.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(3, coreVersion.ReleaseCandidate); + Assert.Equal("2.30.1.rc3", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_ReleaseCandidateDotWithDistribution_RemovesAllExtraInfo() + { + var rcVersion = GitVersion.Parse("2.30.1.rc2.windows.1"); + var coreVersion = rcVersion.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(2, coreVersion.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Equal("2.30.1.rc2", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_ReleaseCandidateDashWithDistribution_RemovesAllExtraInfo() + { + var rcVersion = GitVersion.Parse("2.30.1-rc2.windows.1"); + var coreVersion = rcVersion.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(2, coreVersion.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Equal("2.30.1.rc2", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_VersionWithDistributionOnly_RemovesDistribution() + { + var version = GitVersion.Parse("2.30.1.custom"); + var coreVersion = version.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Null(coreVersion.Revision); + Assert.Equal("2.30.1", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_VersionWithBuild_RemovesDistributionInfo() + { + var version = GitVersion.Parse("2.30.1.windows.2"); + var coreVersion = version.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Null(coreVersion.Revision); + Assert.Equal("2.30.1", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_VersionWithBuildRevision_RemovesAllDistributionInfo() + { + var version = GitVersion.Parse("2.30.1.vfs.2.3"); + var coreVersion = version.ToCoreVersion(); + + Assert.Equal(2, coreVersion.Major); + Assert.Equal(30, coreVersion.Minor); + Assert.Equal(1, coreVersion.Patch); + Assert.Equal(GitDistributionType.Core, coreVersion.Distribution); + Assert.Null(coreVersion.DistributionIdentifier); + Assert.Null(coreVersion.Build); + Assert.Null(coreVersion.Revision); + Assert.Equal("2.30.1", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_DifferentDistributionsSameCore_ProduceSameCoreVersion() + { + var windowsVersion = GitVersion.Parse("2.30.1.windows.1.0"); + var vfsVersion = GitVersion.Parse("2.30.1.vfs.2.3"); + + var windowsCore = windowsVersion.ToCoreVersion(); + var vfsCore = vfsVersion.ToCoreVersion(); + + Assert.Equal(windowsCore, vfsCore); + Assert.Equal("2.30.1", windowsCore.ToString()); + Assert.Equal("2.30.1", vfsCore.ToString()); + } + + [Fact] + public void GitVersion_ToCoreVersion_CoreVersionsAreComparable() + { + var windowsVersion = GitVersion.Parse("2.30.1.windows.1.0"); + var vfsVersion = GitVersion.Parse("2.30.2.vfs.1.0"); + + var windowsCore = windowsVersion.ToCoreVersion(); + var vfsCore = vfsVersion.ToCoreVersion(); + + // These should be comparable since they're both core versions + Assert.True(windowsCore.IsComparableTo(vfsCore)); + Assert.True(windowsCore < vfsCore); + } + + [Fact] + public void GitVersion_ToCoreVersion_PreservesOriginalVersionIntegrity() + { + var originalVersion = GitVersion.Parse("2.30.1.windows.2.3"); + var coreVersion = originalVersion.ToCoreVersion(); + + // Original version should be unchanged + Assert.Equal(2, originalVersion.Major); + Assert.Equal(30, originalVersion.Minor); + Assert.Equal(1, originalVersion.Patch); + Assert.Equal(GitDistributionType.GitForWindows, originalVersion.Distribution); + Assert.Equal("windows", originalVersion.DistributionIdentifier); + Assert.Equal(2, originalVersion.Build); + Assert.Equal(3, originalVersion.Revision); + Assert.Equal("2.30.1.windows.2.3", originalVersion.ToString()); + + // Core version should have no distribution info + Assert.Equal("2.30.1", coreVersion.ToString()); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDot_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.50.1.rc1"); + + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(1, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, version.Distribution); + Assert.Null(version.DistributionIdentifier); + Assert.Null(version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDash_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.50.1-rc1"); + + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(1, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, version.Distribution); + Assert.Null(version.DistributionIdentifier); + Assert.Null(version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDotAndDistribution_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.50.1.rc1.windows.1"); + + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(1, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.GitForWindows, version.Distribution); + Assert.Equal("windows", version.DistributionIdentifier); + Assert.Equal(1, version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDashAndDistribution_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.50.1-rc1.windows.1"); + + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(1, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.GitForWindows, version.Distribution); + Assert.Equal("windows", version.DistributionIdentifier); + Assert.Equal(1, version.Build); + Assert.Null(version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateAndBuildRevision_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.50.1.rc1.vfs.0.1"); + + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(1, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Microsoft, version.Distribution); + Assert.Equal("vfs", version.DistributionIdentifier); + Assert.Equal(0, version.Build); + Assert.Equal(1, version.Revision); + } + + [Fact] + public void GitVersion_Parse_VersionWithHigherReleaseCandidate_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.30.1.rc15"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(15, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Core, version.Distribution); + Assert.Null(version.DistributionIdentifier); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDotCaseInsensitive_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.30.1.RC2"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(2, version.ReleaseCandidate); + } + + [Fact] + public void GitVersion_Parse_VersionWithReleaseCandidateDashCaseInsensitive_ReturnsCorrectVersion() + { + var version = GitVersion.Parse("2.30.1-RC2"); + + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(2, version.ReleaseCandidate); + } + + [Theory] + [InlineData("2.30.1.rc", "rc")] + [InlineData("2.30.1.rc.1", "rc")] + [InlineData("2.30.1.rcabc", "rcabc")] + [InlineData("2.30.1.rc-1", "rc-1")] + public void GitVersion_Parse_InvalidReleaseCandidateDotFormats_ParsedAsDistributionId(string ver, string rcComponent) + { + bool result = GitVersion.TryParse(ver, out GitVersion? version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Null(version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Unknown, version.Distribution); + Assert.Equal(rcComponent, version.DistributionIdentifier); + } + + [Fact] + public void GitVersion_TryParse_ValidReleaseCandidateDotVersion_ReturnsTrueAndCorrectVersion() + { + bool result = GitVersion.TryParse("2.30.1.rc3", out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(3, version.ReleaseCandidate); + } + + [Fact] + public void GitVersion_TryParse_ValidReleaseCandidateDashVersion_ReturnsTrueAndCorrectVersion() + { + bool result = GitVersion.TryParse("2.30.1-rc3", out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(3, version.ReleaseCandidate); + } + + [Fact] + public void GitVersion_TryParse_ValidReleaseCandidateDotAndDashVersion_ReturnsTrueAndCorrectVersion() + { + bool result = GitVersion.TryParse("v2.30.1-rc3.rc0", out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(30, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Equal(3, version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Unknown, version.Distribution); + Assert.Equal("rc0", version.DistributionIdentifier); + } + + [Fact] + public void GitVersion_TryParse_AppleGitVersion_ReturnsTrueAndCorrectVersion() + { + bool result = GitVersion.TryParse("2.50.1 (Apple Git-155)", out var version); + + Assert.True(result); + Assert.NotNull(version); + Assert.Equal(2, version.Major); + Assert.Equal(50, version.Minor); + Assert.Equal(1, version.Patch); + Assert.Null(version.ReleaseCandidate); + Assert.Equal(GitDistributionType.Apple, version.Distribution); + Assert.Null(version.DistributionIdentifier); + Assert.Equal(155, version.Build); + Assert.Null(version.Revision); + } + + [Theory] + [InlineData("2.30.1", "2.30.1", "v2.30.1")] + [InlineData("v2.30.1-rc2", "2.30.1.rc2", "v2.30.1-rc2")] + [InlineData("2.30.1.windows.1", "2.30.1.windows.1", "v2.30.1.windows.1")] + [InlineData("v2.30.1-rc2.windows.1", "2.30.1.rc2.windows.1", "v2.30.1-rc2.windows.1")] + public void GitVersion_ToString_FormatsCorrectlyForAllStyles(string input, string expectedBuildNumber, string expectedTag) + { + var version = GitVersion.Parse(input); + Assert.Equal(expectedBuildNumber, version.ToString(GitVersionFormat.BuildNumber)); + Assert.Equal(expectedTag, version.ToString(GitVersionFormat.Tag)); + } + + [Theory] + [InlineData("2.30.1", "2.30.1")] + [InlineData("v2.30.1-rc2", "2.30.1.rc2")] + [InlineData("2.30.1.windows.1", "2.30.1.windows.1")] + [InlineData("v2.30.1-rc2.windows.1", "2.30.1.rc2.windows.1")] + public void GitVersion_ToString_DefaultOverload_UsesBuildNumberFormat(string input, string expected) + { + var version = GitVersion.Parse(input); + Assert.Equal(expected, version.ToString()); } } } diff --git a/src/shared/Core/Git.cs b/src/shared/Core/Git.cs index 0c58e0159..ab8321864 100644 --- a/src/shared/Core/Git.cs +++ b/src/shared/Core/Git.cs @@ -105,13 +105,9 @@ public GitVersion Version git.WaitForExit(); Match match = Regex.Match(data, @"^git version (?'value'.*)"); - if (match.Success) + if (!match.Success || !GitVersion.TryParse(match.Groups["value"].Value, out _version)) { - _version = new GitVersion(match.Groups["value"].Value); - } - else - { - _version = new GitVersion(); + _version = GitVersion.Zero; } } } diff --git a/src/shared/Core/GitVersion.cs b/src/shared/Core/GitVersion.cs index 1e33b8663..f843259ac 100644 --- a/src/shared/Core/GitVersion.cs +++ b/src/shared/Core/GitVersion.cs @@ -1,168 +1,544 @@ +#nullable enable using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.RegularExpressions; namespace GitCredentialManager { - public class GitVersion : IComparable, IComparable + public enum GitDistributionType { - private readonly string _originalString; - private List _components; + /// + /// Represents a base/core distribution of Git. + /// + Core, - public GitVersion(string versionString) + /// + /// Represents the Git for Windows fork of Git. Inline version identifier "windows". + /// + GitForWindows, + + /// + /// Represents the Microsoft fork of Git with VFS support. Inline version identifier "vfs". + /// + Microsoft, + + /// + /// Represents the Apple distribution of Git. Custom version string. + /// + Apple, + + /// + /// Represents an unknown distribution of Git that has an unknown inline version identifier. + /// + Unknown, + } + + public partial class GitVersion : IComparable, IEquatable + { + /// + /// Represents the lowest possible Git version. All other versions will be compared greater than this instance. + /// + public static readonly GitVersion Zero = new GitVersion(0, 0, 0); + + private static readonly Regex VersionRegex = CreateRegex(); + private static readonly Regex AppleVersionRegex = CreateAppleRegex(); + + public int Major { get; } + public int Minor { get; } + public int Patch { get; } + public int? ReleaseCandidate { get; set; } + public GitDistributionType Distribution { get; } + public string? DistributionIdentifier { get; set; } + public int? Build { get; } + public int? Revision { get; } + public string? CommitId { get; set; } + public string? OriginalString { get; private set; } + + private GitVersion() { - if (versionString is null) + } + + public GitVersion(int major, int minor, int patch, GitDistributionType distribution = GitDistributionType.Core, + int? build = null, int? revision = null) + { + Major = major; + Minor = minor; + Patch = patch; + Distribution = distribution; + switch (distribution) { - _components = new List(); - return; + case GitDistributionType.GitForWindows: + DistributionIdentifier = "windows"; + break; + + case GitDistributionType.Microsoft: + DistributionIdentifier = "vfs"; + break; + + case GitDistributionType.Apple: + // Only the build component is allowed in Apple Git - the revision component is not permitted + if (revision is not null) + throw new ArgumentException("Revision is not supported for the Apple distribution.", nameof(revision)); + // Apple's version format is special - we don't use an inline identifier + DistributionIdentifier = null; + break; + + case GitDistributionType.Unknown: + // No special handling + break; + + case GitDistributionType.Core: + DistributionIdentifier = null; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(distribution), distribution, null); } - _originalString = versionString; + // If the revision component is specified then so must build component + if (revision is not null && build is null) + throw new ArgumentNullException(nameof(build), "Build component cannot be null if revision is specified."); + + Build = build; + Revision = revision; + } + + public static GitVersion Parse(string str) => + TryParse(str, out GitVersion? version) +#if NETFRAMEWORK + ? version! +#else + ? version +#endif + : throw new FormatException($"Invalid Git version format: {str}"); - string[] splitVersion = versionString.Split('.'); - _components = new List(splitVersion.Length); +#if NETFRAMEWORK + public static bool TryParse(string str, out GitVersion? version) +#else + public static bool TryParse(string str, [NotNullWhen(true)] out GitVersion? version) +#endif + { + Match match = VersionRegex.Match(str); - foreach (string part in splitVersion) + if (match.Success) { - if (Int32.TryParse(part, out int component)) + int major, minor, patch; + string? distId = null; + GitDistributionType dist = GitDistributionType.Core; + int? rc = null, build = null, revision = null; + + // Major, minor, and patch components are required and must be valid integers. + if (!int.TryParse(match.Groups["major"].Value, out major) || + !int.TryParse(match.Groups["minor"].Value, out minor) || + !int.TryParse(match.Groups["patch"].Value, out patch)) { - _components.Add(component); + version = null; + return false; } - else + + // Release candidate is optional, but if present, it must be a valid integer. + if (match.Groups["rc"].Success && int.TryParse(match.Groups["rc"].Value, out int rcValue)) { - // Exit at the first non-integer component - break; + rc = rcValue; } + + // Distribution is optional, but if present, it must be a valid string. + // Build and revision are also optional, but if present, they must be valid integers + // and are relative to the distribution identifier. + if (match.Groups["dist"].Success) + { + distId = match.Groups["dist"].Value; + dist = distId.ToLowerInvariant() switch + { + "windows" => GitDistributionType.GitForWindows, + "vfs" => GitDistributionType.Microsoft, + _ => GitDistributionType.Unknown + }; + build = match.Groups["build"].Success ? int.Parse(match.Groups["build"].Value) : null; + revision = match.Groups["rev"].Success ? int.Parse(match.Groups["rev"].Value) : null; + } + + // Try to make sense of the remaining parts of the input string. + string rest = match.Groups["rest"].Value.Trim(); + + string? commitId = null; + if (!string.IsNullOrWhiteSpace(rest)) + { + // We have to handle Apple Git specially since their version format string looks like this: + // .. (Apple Git-) + // There is no revision version component for Apple Git. + var appleMatch = AppleVersionRegex.Match(rest); + if (appleMatch.Success) + { + build = int.Parse(appleMatch.Groups["build"].Value); + revision = null; + dist = GitDistributionType.Apple; + distId = null; + } + // We also check for a 'dirty-build' of Git; that is one that was built from a commit: + // ...g + // where is the commit ID. + else if (rest.StartsWith(".g", StringComparison.OrdinalIgnoreCase)) + { + commitId = rest.Substring(2).ToLowerInvariant(); + } + } + + version = new GitVersion(major, minor, patch, dist, build, revision) + { + ReleaseCandidate = rc, + CommitId = commitId, + DistributionIdentifier = distId, + OriginalString = str + }; + return true; } - } - public GitVersion(params int[] components) - { - _components = components.ToList(); + version = null; + return false; } - public override string ToString() - { - return string.Join(".", _components); - } + public override string ToString() => ToString(GitVersionFormat.BuildNumber); - public string OriginalString + public string ToString(GitVersionFormat format) { - get + var sb = new StringBuilder(); + + if (format == GitVersionFormat.Tag) { - if (_originalString is null) + sb.Append('v'); + } + + sb.Append($"{Major}.{Minor}.{Patch}"); + + if (ReleaseCandidate is not null) + { + switch (format) { - return ToString(); + case GitVersionFormat.BuildNumber: + sb.Append(".rc"); + break; + + case GitVersionFormat.Tag: + sb.Append("-rc"); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported GitVersionFormat"); } - return _originalString; + sb.Append(ReleaseCandidate); + } + + switch (Distribution) + { + case GitDistributionType.Core: + return sb.ToString(); + + case GitDistributionType.Apple: + sb.Append(" (Apple Git"); + if (Build is not null) + sb.Append($"-{Build}"); + sb.Append(')'); + return sb.ToString(); + + default: + sb.Append($".{DistributionIdentifier}"); + break; + } + + if (Build is not null) + { + sb.Append($".{Build}"); + } + else if (Revision is not null) + { + Debug.Fail("Build should not be null if Revision is set."); + return sb.ToString(); // Don't append Revision if Build is null as this would be misleading + } + + if (Revision is not null) + { + sb.Append($".{Revision}"); } + + if (CommitId is not null) + { + sb.Append($".g{CommitId}"); + } + + return sb.ToString(); } - public int CompareTo(object obj) + /// + /// Determines whether this can be compared with another . + /// Two versions are comparable if they have the same distribution. + /// + /// The other GitVersion to check compatibility with. + /// True if the versions can be compared; otherwise, false. + public bool IsComparableTo(GitVersion? other) { - if (obj is null) + if (other is null) { - return 1; + return false; } - GitVersion other = obj as GitVersion; - if (other == null) + if (Distribution != other.Distribution) { - throw new ArgumentException("A GitVersion object is required for comparison.", "obj"); + return false; } - return CompareTo(other); + return string.Equals(DistributionIdentifier, other.DistributionIdentifier, StringComparison.Ordinal); } - public int CompareTo(GitVersion other) + public int CompareTo(GitVersion? other) { + if (ReferenceEquals(this, other)) + { + return 0; + } + if (other is null) { return 1; } - // Compare for as many components as the two versions have in common. If a - // component does not exist in a components list, it is assumed to be 0. - int thisCount = _components.Count, otherCount = other._components.Count; - for (int i = 0; i < Math.Max(thisCount, otherCount); i++) + // Check for distribution mismatch and throw exception if they don't match + if (!IsComparableTo(other)) { - int thisComponent = i < thisCount ? _components[i] : 0; - int otherComponent = i < otherCount ? other._components[i] : 0; - if (thisComponent != otherComponent) - { - return thisComponent.CompareTo(otherComponent); - } + throw new GitVersionMismatchException(this, other); } - // No discrepencies found in versions - return 0; + var majorCmp = Major.CompareTo(other.Major); + if (majorCmp != 0) return majorCmp; + + var minorCmp = Minor.CompareTo(other.Minor); + if (minorCmp != 0) return minorCmp; + + var patchCmp = Patch.CompareTo(other.Patch); + if (patchCmp != 0) return patchCmp; + + // Compare release candidates: stable versions (null) are greater than release candidate versions + var rcCmp = (ReleaseCandidate, other.ReleaseCandidate) switch + { + (null, null) => 0, // Both are stable releases, equal + (null, _) => 1, // This is stable, other is RC -> this is greater + (_ , null) => -1, // This is RC, other is stable -> this is less + var (rc1, rc2) => rc1.Value.CompareTo(rc2.Value) // Both are RC, compare values + }; + if (rcCmp != 0) return rcCmp; + + // Since we've already verified distributions are equal, we can skip the dist comparison + var buildCmp = Build?.CompareTo(other.Build) ?? 0; + if (buildCmp != 0) return buildCmp; + + var revCmp = Revision?.CompareTo(other.Revision) ?? 0; + return revCmp; + + // Ignore the CommitID } - public static int Compare(GitVersion left, GitVersion right) + /// + /// Converts this GitVersion to a core version by ignoring distribution information. + /// This is useful for comparing versions without considering distribution-specific details. + /// + public GitVersion ToCoreVersion() { - if (object.ReferenceEquals(left, right)) + // Convert to a core version by ignoring distribution information + return new GitVersion(Major, Minor, Patch, GitDistributionType.Core) { - return 0; + ReleaseCandidate = ReleaseCandidate + }; + } + +#if NETFRAMEWORK + public override int GetHashCode() + { + return Major.GetHashCode() ^ + Minor.GetHashCode() ^ + Patch.GetHashCode() ^ + Distribution.GetHashCode() ^ + (Build?.GetHashCode() ?? 0) ^ + (Revision?.GetHashCode() ?? 0); + } +#else + public override int GetHashCode() + => HashCode.Combine(Major, Minor, Patch, Distribution, Build, Revision); +#endif + + /// + /// Check if this version is equal to another version. + /// + public bool Equals(GitVersion? other) + { + if (other is null) + { + return false; } - if (left is null) + if (ReferenceEquals(this, other)) { - return -1; + return true; } - return left.CompareTo(right); + return CompareTo(other) == 0; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { - GitVersion other = obj as GitVersion; - if (other is null) + if (obj is null) { return false; } - return this.CompareTo(other) == 0; + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return ((IEquatable)this).Equals((GitVersion)obj); } - public override int GetHashCode() + public static bool operator ==(GitVersion? left, GitVersion? right) { - return ToString().GetHashCode(); + return left is null && right is null || + left is not null && left.CompareTo(right) == 0; } - public static bool operator ==(GitVersion left, GitVersion right) + public static bool operator !=(GitVersion? left, GitVersion? right) + { + return !(left == right); + } + + public static bool operator <(GitVersion? left, GitVersion? right) { if (left is null) { - return right is null; + return right is not null; } - return left.Equals(right); + return left.CompareTo(right) < 0; } - public static bool operator !=(GitVersion left, GitVersion right) + public static bool operator >(GitVersion? left, GitVersion? right) { - return !(left == right); + if (left is null) + { + return false; + } + + return left.CompareTo(right) > 0; + } + + public static bool operator <=(GitVersion? left, GitVersion? right) + { + if (left is null) + { + return true; + } + + return left.CompareTo(right) <= 0; + } + + public static bool operator >=(GitVersion? left, GitVersion? right) + { + if (left is null) + { + return right is null; + } + + return left.CompareTo(right) >= 0; } - public static bool operator <(GitVersion left, GitVersion right) + private const string RegexPattern = + @"^v?(?'major'\d+)(?:\.(?'minor'\d+))(?:\.(?'patch'\d+))(?:[-.]rc(?'rc'\d+))?(?:\.(?'dist'[^\.]+)(?:\.(?'build'\d+)(?:\.(?'rev'\d+))?)?)?(?'rest'.+)?"; + +#if NETFRAMEWORK + private static Regex CreateRegex() + => new Regex(RegexPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); +#else + [GeneratedRegex(RegexPattern, RegexOptions.IgnoreCase)] + private static partial Regex CreateRegex(); +#endif + + private const string AppleRegexPattern = + @"\(Apple Git-(?'build'\d+)\)"; + +#if NETFRAMEWORK + private static Regex CreateAppleRegex() + => new Regex(AppleRegexPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); +#else + [GeneratedRegex(AppleRegexPattern, RegexOptions.IgnoreCase)] + private static partial Regex CreateAppleRegex(); +#endif + } + + public enum GitVersionFormat + { + /// + /// Format the version as Git build number. Example: "1.2.3.rc4". + /// + BuildNumber, + + /// + /// Format the version as a Git tag. Example: "v1.2.3-rc4". + /// + Tag, + } + + /// + /// Exception thrown when comparing GitVersion instances with different platform. + /// + public class GitVersionMismatchException : InvalidOperationException + { + public GitVersion Version1 { get; } + public GitVersion Version2 { get; } + + public GitVersionMismatchException(GitVersion version1, GitVersion version2) + : base(GetErrorMessage(version1, version2)) { - return Compare(left, right) < 0; + Version1 = version1; + Version2 = version2; } - public static bool operator >(GitVersion left, GitVersion right) + private static string GetErrorMessage(GitVersion version1, GitVersion version2) { - return Compare(left, right) > 0; + var sb = new StringBuilder("Cannot compare Git versions with different distribution: "); + + sb.Append($"'{version1.Distribution}'"); + if (version1.DistributionIdentifier is not null) + { + sb.Append($" (\"{version1.DistributionIdentifier}\")"); + } + + sb.Append($" and '{version2.Distribution}'"); + if (version2.DistributionIdentifier is not null) + { + sb.Append($" (\"{version2.DistributionIdentifier}\")"); + } + + return sb.ToString(); } - public static bool operator <=(GitVersion left, GitVersion right) + public GitVersionMismatchException(GitVersion version1, GitVersion version2, string message) + : base(message) { - return Compare(left, right) <= 0; + Version1 = version1; + Version2 = version2; } - public static bool operator >=(GitVersion left, GitVersion right) + public GitVersionMismatchException(GitVersion version1, GitVersion version2, string message, Exception innerException) + : base(message, innerException) { - return Compare(left, right) >= 0; + Version1 = version1; + Version2 = version2; } } -} \ No newline at end of file +} diff --git a/src/shared/TestInfrastructure/Objects/TestGit.cs b/src/shared/TestInfrastructure/Objects/TestGit.cs index f7a93a372..e52f50e5a 100644 --- a/src/shared/TestInfrastructure/Objects/TestGit.cs +++ b/src/shared/TestInfrastructure/Objects/TestGit.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -8,7 +7,7 @@ namespace GitCredentialManager.Tests.Objects { public class TestGit : IGit { - public GitVersion Version { get; set; } = new GitVersion("2.32.0.test.0"); + public GitVersion Version { get; set; } = GitVersion.Parse("2.32.0.test.0"); public string CurrentRepository { get; set; }