From 4cacfe84c9c6d92fc556079644bf678547e35d6f Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 27 Jan 2026 13:05:51 +0000 Subject: [PATCH] gitversion: support forks/RCs/dirty Git versions Enhance the GitVersion type to be aware of different forks of Git including Git for Windows, Microsoft, and Apple. Also add support for release candidates found in the version string - these can appear as either a -rcN or .rcN format depending on if this is a tag or a build version. We support both types. Finally also capture the Commit ID from a dirty build version, i.e, ...g Signed-off-by: Matthew John Cheetham --- src/shared/Core.Tests/GitTests.cs | 3 +- src/shared/Core.Tests/GitVersionTests.cs | 990 +++++++++++++++++- src/shared/Core/Git.cs | 8 +- src/shared/Core/GitVersion.cs | 530 ++++++++-- .../TestInfrastructure/Objects/TestGit.cs | 3 +- 5 files changed, 1435 insertions(+), 99 deletions(-) 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; }