diff --git a/doc/versionJson.md b/doc/versionJson.md index 2a9e20e6..4e041d66 100644 --- a/doc/versionJson.md +++ b/doc/versionJson.md @@ -24,9 +24,10 @@ The content of the version.json file is a JSON serialized object with these prop "version": "x.y-prerelease", // required "assemblyVersion": "x.y", // optional. Use when x.y for AssemblyVersionAttribute differs from the default version property. "buildNumberOffset": "zOffset", // optional. Use when you need to add/subtract a fixed value from the computed build number. + "semVer1NumericIdentifierPadding": 4, // optional. Use when your -prerelease includes numeric identifiers and need semver1 support. "publicReleaseRefSpec": [ "^refs/heads/master$", // we release out of master - "^refs/tags/v\\d\\.\\d" // we also release tags starting with vN.N + "^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N ], "cloudBuild": { "setVersionVariables": true, @@ -43,6 +44,11 @@ The content of the version.json file is a JSON serialized object with these prop The `x` and `y` variables are for your use to specify a version that is meaningful to your customers. Consider using [semantic versioning][semver] for guidance. +You may optionally supply a third integer in the version (i.e. x.y.z), +in which case the git version height is specified as the fourth integer, +which only appears in certain version representations. +Alternatively, you can include the git version height in the -prerelease tag using +syntax such as: `1.2.3-beta.{height}` The optional -prerelease tag allows you to indicate that you are building prerelease software. diff --git a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs index c99fbd0f..e9bf90ff 100644 --- a/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs +++ b/src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs @@ -98,6 +98,20 @@ public async Task GetBuildVersion_Without_Git() Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion); } + [Fact] + public async Task GetBuildVersion_WithThreeVersionIntegers() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-beta.3"), + SemVer1NumericIdentifierPadding = 1, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var buildResult = await this.BuildAsync(); + this.AssertStandardProperties(workingCopyVersion, buildResult); + } + [Fact] public async Task GetBuildVersion_Without_Git_HighPrecisionAssemblyVersion() { @@ -381,6 +395,21 @@ public async Task GetBuildVersion_Minus1BuildOffset_NotYetCommitted() this.AssertStandardProperties(versionOptions, buildResult); } + [Theory] + [InlineData(0)] + [InlineData(21)] + public async Task GetBuildVersion_BuildNumberSpecifiedInVersionJson(int buildNumber) + { + var versionOptions = new VersionOptions + { + Version = SemanticVersion.Parse("14.0." + buildNumber), + }; + this.WriteVersionFile(versionOptions); + this.InitializeSourceControl(); + var buildResult = await this.BuildAsync(); + this.AssertStandardProperties(versionOptions, buildResult); + } + [Fact] public async Task PublicRelease_RegEx_Unsatisfied() { @@ -832,7 +861,7 @@ private void AssertStandardProperties(VersionOptions versionOptions, BuildResult string pkgVersionSuffix = buildResult.PublicRelease ? string.Empty : $"-g{commitIdShort}"; - Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{versionOptions.Version.Prerelease}{pkgVersionSuffix}", buildResult.NuGetPackageVersion); + Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVer1PrereleaseTag(versionOptions)}{pkgVersionSuffix}", buildResult.NuGetPackageVersion); var buildNumberOptions = versionOptions.CloudBuild?.BuildNumber ?? new VersionOptions.CloudBuildNumberOptions(); if (buildNumberOptions.Enabled) @@ -864,6 +893,11 @@ private void AssertStandardProperties(VersionOptions versionOptions, BuildResult } } + private static string GetSemVer1PrereleaseTag(VersionOptions versionOptions) + { + return versionOptions.Version.Prerelease?.Replace('.', '-'); + } + private async Task BuildAsync(string target = Targets.GetBuildVersion, LoggerVerbosity logVerbosity = LoggerVerbosity.Detailed, bool assertSuccessfulBuild = true) { var eventLogger = new MSBuildLogger { Verbosity = LoggerVerbosity.Minimal }; diff --git a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs index a5bf0eef..b13f66ae 100644 --- a/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs +++ b/src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs @@ -172,7 +172,7 @@ public void GetIdAsVersion_MissingVersionTxt() } [Fact] - public void GetIdAsVersion_VersionFileNeverCheckedIn() + public void GetIdAsVersion_VersionFileNeverCheckedIn_3Ints() { this.AddCommits(); var expectedVersion = new Version(1, 1, 0); @@ -182,6 +182,23 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn() Assert.Equal(expectedVersion.Major, actualVersion.Major); Assert.Equal(expectedVersion.Minor, actualVersion.Minor); Assert.Equal(expectedVersion.Build, actualVersion.Build); + + // Height is expressed in the 4th integer since 3 were specified in version.json. + // height is 0 since the change hasn't been committed. + Assert.Equal(0, actualVersion.Revision); + } + + [Fact] + public void GetIdAsVersion_VersionFileNeverCheckedIn_2Ints() + { + this.AddCommits(); + var expectedVersion = new Version(1, 1); + var unstagedVersionData = VersionOptions.FromVersion(expectedVersion); + string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData); + Version actualVersion = this.Repo.GetIdAsVersion(); + Assert.Equal(expectedVersion.Major, actualVersion.Major); + Assert.Equal(expectedVersion.Minor, actualVersion.Minor); + Assert.Equal(0, actualVersion.Build); // height is 0 since the change hasn't been committed. Assert.Equal(this.Repo.Head.Commits.First().GetTruncatedCommitIdAsUInt16(), actualVersion.Revision); } diff --git a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj index 5dfadae5..53963af9 100644 --- a/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj +++ b/src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj @@ -12,6 +12,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs index 0869c2b1..9a374cbd 100644 --- a/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs +++ b/src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs @@ -28,4 +28,125 @@ public void Submodule_RecognizedWithCorrectVersion() Assert.Equal("3ea7f010c3", oracleB.GitCommitIdShort); } } + + [Fact] + public void MajorMinorPrereleaseBuildMetadata() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8-beta.3+metadata.4"), + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + Assert.Equal("7.8", oracle.MajorMinorVersion.ToString()); + Assert.Equal(oracle.VersionHeight, oracle.BuildNumber); + + Assert.Equal("-beta.3", oracle.PrereleaseVersion); + ////Assert.Equal("+metadata.4", oracle.BuildMetadataFragment); + + Assert.Equal(1, oracle.VersionHeight); + Assert.Equal(0, oracle.VersionHeightOffset); + } + + [Fact] + public void MajorMinorBuildPrereleaseBuildMetadata() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-beta.3+metadata.4"), + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + Assert.Equal("7.8", oracle.MajorMinorVersion.ToString()); + Assert.Equal(9, oracle.BuildNumber); + Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision); + + Assert.Equal("-beta.3", oracle.PrereleaseVersion); + ////Assert.Equal("+metadata.4", oracle.BuildMetadataFragment); + + Assert.Equal(1, oracle.VersionHeight); + Assert.Equal(0, oracle.VersionHeightOffset); + } + + [Fact] + public void HeightInPrerelease() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-beta.{height}.foo"), + BuildNumberOffset = 2, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + Assert.Equal("7.8", oracle.MajorMinorVersion.ToString()); + Assert.Equal(9, oracle.BuildNumber); + Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision); + + Assert.Equal("-beta." + (oracle.VersionHeight + oracle.VersionHeightOffset) + ".foo", oracle.PrereleaseVersion); + + Assert.Equal(1, oracle.VersionHeight); + Assert.Equal(2, oracle.VersionHeightOffset); + } + + [Fact(Skip = "Build metadata not yet retained from version.json")] + public void HeightInBuildMetadata() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-beta+another.{height}.foo"), + BuildNumberOffset = 2, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + Assert.Equal("7.8", oracle.MajorMinorVersion.ToString()); + Assert.Equal(9, oracle.BuildNumber); + Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision); + + Assert.Equal("-beta", oracle.PrereleaseVersion); + Assert.Equal("+another." + (oracle.VersionHeight + oracle.VersionHeightOffset) + ".foo", oracle.BuildMetadataFragment); + + Assert.Equal(1, oracle.VersionHeight); + Assert.Equal(2, oracle.VersionHeightOffset); + } + + [Theory] + [InlineData("7.8.9-foo.25", "7.8.9-foo-0025")] + [InlineData("7.8.9-foo.25s", "7.8.9-foo-25s")] + [InlineData("7.8.9-foo.s25", "7.8.9-foo-s25")] + [InlineData("7.8.9-foo.25.bar-24.13-11", "7.8.9-foo-0025-bar-24-13-11")] + [InlineData("7.8.9-25.bar.baz-25", "7.8.9-0025-bar-baz-25")] + [InlineData("7.8.9-foo.5.bar.1.43.baz", "7.8.9-foo-0005-bar-0001-0043-baz")] + public void SemVer1PrereleaseConversion(string semVer2, string semVer1) + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse(semVer2), + BuildNumberOffset = 2, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + oracle.PublicRelease = true; + Assert.Equal(semVer1, oracle.SemVer1); + } + + [Fact] + public void SemVer1PrereleaseConversionPadding() + { + VersionOptions workingCopyVersion = new VersionOptions + { + Version = SemanticVersion.Parse("7.8.9-foo.25"), + BuildNumberOffset = 2, + SemVer1NumericIdentifierPadding = 3, + }; + this.WriteVersionFile(workingCopyVersion); + this.InitializeSourceControl(); + var oracle = VersionOracle.Create(this.RepoPath); + oracle.PublicRelease = true; + Assert.Equal("7.8.9-foo-025", oracle.SemVer1); + } } diff --git a/src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs b/src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs new file mode 100644 index 00000000..82233a01 --- /dev/null +++ b/src/NerdBank.GitVersioning.Tests/VersionSchemaTests.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using Xunit; +using Xunit.Abstractions; + +public class VersionSchemaTests +{ + private readonly ITestOutputHelper Logger; + + private readonly JSchema schema; + + private JObject json; + + public VersionSchemaTests(ITestOutputHelper logger) + { + this.Logger = logger; + using (var schemaStream = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.version.schema.json"))) + { + this.schema = JSchema.Load(new JsonTextReader(schemaStream)); + } + } + + [Fact] + public void VersionField_BasicScenarios() + { + json = JObject.Parse(@"{ ""version"": ""2.3"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3-beta"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3-beta-final"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3-beta.2"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3-beta.0"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3-beta.01"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""1.2.3"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""1.2.3.4"" }"); + Assert.True(json.IsValid(this.schema)); + + json = JObject.Parse(@"{ ""version"": ""02.3"" }"); + Assert.False(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.03"" }"); + Assert.False(json.IsValid(this.schema)); + } + + [Fact] + public void VersionField_HeightMacroPlacement() + { + // Valid uses + json = JObject.Parse(@"{ ""version"": ""2.3.0-{height}"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3.0-{height}.beta"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3.0-beta.{height}"" }"); + Assert.True(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3.0-beta+{height}"" }"); + Assert.True(json.IsValid(this.schema)); + + // Invalid uses + json = JObject.Parse(@"{ ""version"": ""2.3.{height}-beta"" }"); + Assert.False(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3.0-beta-{height}"" }"); + Assert.False(json.IsValid(this.schema)); + json = JObject.Parse(@"{ ""version"": ""2.3.0-beta+height-{height}"" }"); + Assert.False(json.IsValid(this.schema)); + } +} diff --git a/src/NerdBank.GitVersioning/GitExtensions.cs b/src/NerdBank.GitVersioning/GitExtensions.cs index 4fee2ac7..3b098a60 100644 --- a/src/NerdBank.GitVersioning/GitExtensions.cs +++ b/src/NerdBank.GitVersioning/GitExtensions.cs @@ -512,26 +512,40 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory)) - : 0; - } + // The compiler (due to WinPE header requirements) only allows 16-bit version components, + // and forbids 0xffff as a value. + // The build number is set to the git height. This helps ensure that + // within a major.minor release, each patch has an incrementing integer. + // The revision is set to the first two bytes of the git commit ID. + if (!versionHeight.HasValue) + { + versionHeight = commit != null + ? commit.GetHeight(c => CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory)) + : 0; + } - int build = versionHeight.Value == 0 ? 0 : versionHeight.Value + (versionOptions?.BuildNumberOffset ?? 0); - Verify.Operation(build <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", build, MaximumBuildNumberOrRevisionComponent); - int revision = commit != null - ? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16()) - : 0; + int adjustedVersionHeight = versionHeight.Value == 0 ? 0 : versionHeight.Value + (versionOptions?.BuildNumberOffset ?? 0); + Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent); + + if (buildNumber < 0) + { + buildNumber = adjustedVersionHeight; + revision = commit != null + ? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16()) + : 0; + } + else + { + revision = adjustedVersionHeight; + } + } - return new Version(baseVersion.Major, baseVersion.Minor, build, revision); + return new Version(baseVersion.Major, baseVersion.Minor, buildNumber, revision); } /// diff --git a/src/NerdBank.GitVersioning/SemanticVersion.cs b/src/NerdBank.GitVersioning/SemanticVersion.cs index 41d7161e..55325851 100644 --- a/src/NerdBank.GitVersioning/SemanticVersion.cs +++ b/src/NerdBank.GitVersioning/SemanticVersion.cs @@ -23,12 +23,27 @@ public class SemanticVersion : IEquatable /// /// The regex pattern that a prerelease must match. /// - private static readonly Regex PrereleasePattern = new Regex(@"^-(?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*$"); + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex PrereleasePattern = new Regex("-(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); /// /// The regex pattern that build metadata must match. /// - private static readonly Regex BuildMetadataPattern = new Regex(@"^\+(?:[0-9A-Za-z-]+)(?:\.[0-9A-Za-z-]+)*$"); + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex BuildMetadataPattern = new Regex("\\+(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*", RegexOptions.IgnoreCase); + + /// + /// The regular expression with capture groups for semantic versioning, + /// allowing for macros such as {height}. + /// + /// + /// Keep in sync with the regex for the version field found in the version.schema.json file. + /// + private static readonly Regex FullSemVerWithMacrosPattern = new Regex("^v?(?0|[1-9][0-9]*)\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*))?)?(?" + PrereleasePattern + ")?(?" + BuildMetadataPattern + ")?$", RegexOptions.IgnoreCase); /// /// Initializes a new instance of the class. @@ -90,7 +105,7 @@ public static bool TryParse(string semanticVersion, out SemanticVersion version) { Requires.NotNullOrEmpty(semanticVersion, nameof(semanticVersion)); - Match m = FullSemVerPattern.Match(semanticVersion); + Match m = FullSemVerWithMacrosPattern.Match(semanticVersion); if (m.Success) { var major = int.Parse(m.Groups["major"].Value); diff --git a/src/NerdBank.GitVersioning/VersionOptions.cs b/src/NerdBank.GitVersioning/VersionOptions.cs index 4fadc3d8..961dad38 100644 --- a/src/NerdBank.GitVersioning/VersionOptions.cs +++ b/src/NerdBank.GitVersioning/VersionOptions.cs @@ -59,6 +59,12 @@ public class VersionOptions : IEquatable [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public int BuildNumberOffset { get; set; } + /// + /// Gets or sets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? SemVer1NumericIdentifierPadding { get; set; } + /// /// Gets or sets an array of regular expressions that describes branch or tag names that should /// be built with PublicRelease=true as the default value on build servers. diff --git a/src/NerdBank.GitVersioning/VersionOracle.cs b/src/NerdBank.GitVersioning/VersionOracle.cs index c8bc7bca..2f1f22f0 100644 --- a/src/NerdBank.GitVersioning/VersionOracle.cs +++ b/src/NerdBank.GitVersioning/VersionOracle.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -14,6 +15,11 @@ /// public class VersionOracle { + /// + /// A regex that matches on numeric identifiers for prerelease or build metadata. + /// + private static readonly Regex NumericIdentifierRegex = new Regex(@"(? /// The cloud build suppport, if any. /// @@ -63,6 +69,9 @@ public VersionOracle(string projectDirectory, LibGit2Sharp.Repository repo, IClo // Override the typedVersion with the special build number and revision components, when available. this.Version = repo?.GetIdAsVersion(relativeRepoProjectDirectory, this.VersionHeight) ?? this.VersionOptions?.Version.Version; this.Version = this.Version ?? new Version(0, 0); + this.VersionHeightOffset = this.VersionOptions?.BuildNumberOffset ?? 0; + + this.PrereleaseVersion = ReplaceMacros(this.VersionOptions?.Version.Prerelease ?? string.Empty); this.CloudBuildNumberOptions = this.VersionOptions?.CloudBuild?.BuildNumber ?? new VersionOptions.CloudBuildNumberOptions(); @@ -145,7 +154,7 @@ public IEnumerable BuildMetadataWithCommitId /// /// Gets the prerelease version information. /// - public string PrereleaseVersion => this.VersionOptions?.Version.Prerelease ?? string.Empty; + public string PrereleaseVersion { get; } /// /// Gets the version information without a Revision component. @@ -155,7 +164,7 @@ public IEnumerable BuildMetadataWithCommitId : new Version(this.Version.Major, this.Version.Minor); /// - /// Gets the build number (git height + offset) for this version. + /// Gets the build number (i.e. third integer, or PATCH) for this version. /// public int BuildNumber => Math.Max(0, this.Version.Build); @@ -184,6 +193,13 @@ public IEnumerable BuildMetadataWithCommitId /// public int VersionHeight { get; } + /// + /// The offset to add to the + /// when calculating the integer to use as the + /// or elsewhere that the {height} macro is used. + /// + public int VersionHeightOffset { get; } + private string BuildingRef { get; } /// @@ -238,7 +254,7 @@ public IDictionary CloudBuildVersionVars /// when is false. /// public string SemVer1 => - $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersion}{this.SemVer1BuildMetadata}"; + $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersionSemVer1}{this.SemVer1BuildMetadata}"; /// /// Gets a SemVer 2.0 compliant string that represents this version, including a +gCOMMITID suffix @@ -247,14 +263,23 @@ public IDictionary CloudBuildVersionVars public string SemVer2 => $"{this.Version.ToStringSafe(3)}{this.PrereleaseVersion}{this.SemVer2BuildMetadata}"; + /// + /// Gets the minimum number of digits to use for numeric identifiers in SemVer 1. + /// + public int SemVer1NumericIdentifierPadding => this.VersionOptions?.SemVer1NumericIdentifierPadding ?? 4; + private string SemVer1BuildMetadata => this.PublicRelease ? string.Empty : $"-g{this.GitCommitIdShort}"; private string SemVer2BuildMetadata => FormatBuildMetadata(this.PublicRelease ? this.BuildMetadata : this.BuildMetadataWithCommitId); + private string PrereleaseVersionSemVer1 => MakePrereleaseSemVer1Compliant(this.PrereleaseVersion, SemVer1NumericIdentifierPadding); + private VersionOptions.CloudBuildNumberOptions CloudBuildNumberOptions { get; } + private int VersionHeightWithOffset => this.VersionHeight + this.VersionHeightOffset; + private static string FormatBuildMetadata(IEnumerable identifiers) => (identifiers?.Any() ?? false) ? "+" + string.Join(".", identifiers) : string.Empty; @@ -323,5 +348,38 @@ private static Version GetAssemblyVersion(Version version, VersionOptions versio precision >= VersionOptions.VersionPrecision.Revision ? version.Revision : 0); return assemblyVersion.EnsureNonNegativeComponents(4); } + + /// + /// Replaces any macros found in a prerelease or build metadata string. + /// + /// The prerelease or build metadata. + /// The specified string, with macros substituted for actual values. + private string ReplaceMacros(string prereleaseOrBuildMetadata) => prereleaseOrBuildMetadata?.Replace("{height}", this.VersionHeightWithOffset.ToString(CultureInfo.InvariantCulture)); + + /// + /// Converts a semver 2 compliant "-beta.5" prerelease tag to a semver 1 compatible one. + /// + /// The semver 2 prerelease tag, including its leading hyphen. + /// The minimum number of digits to use for any numeric identifier. + /// A semver 1 compliant prerelease tag. For example "-beta-0005". + private static string MakePrereleaseSemVer1Compliant(string prerelease, int paddingSize) + { + if (string.IsNullOrEmpty(prerelease)) + { + return prerelease; + } + + string paddingFormatter = "{0:" + new string('0', paddingSize) + "}"; + + string semver1 = prerelease; + + // Identify numeric identifiers and pad them. + Assumes.True(prerelease.StartsWith("-")); + semver1 = "-" + NumericIdentifierRegex.Replace(semver1.Substring(1), m => string.Format(CultureInfo.InvariantCulture, paddingFormatter, int.Parse(m.Groups[1].Value))); + + semver1 = semver1.Replace('.', '-'); + + return semver1; + } } } diff --git a/src/NerdBank.GitVersioning/version.schema.json b/src/NerdBank.GitVersioning/version.schema.json index d14ba1ec..85076d43 100644 --- a/src/NerdBank.GitVersioning/version.schema.json +++ b/src/NerdBank.GitVersioning/version.schema.json @@ -6,8 +6,8 @@ "properties": { "version": { "type": "string", - "description": "The default x.y-pre version to use as the basis for version calculations.", - "pattern": "^v?(?0|[1-9][0-9]*)\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*))?(?-[\\da-z\\-]+(?:\\.[\\da-z\\-]+)*)?(?\\+[\\da-z\\-]+(?:\\.[\\da-z\\-]+)*)?$" + "description": "The major.minor-pre version to use as the basis for version calculations. If {height} is not used in this value and the value has fewer than the full major.minor.build.revision specified, \".{height}\" will be appended by the build.", + "pattern": "^v?(?0|[1-9][0-9]*)\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*)(?:\\.(?0|[1-9][0-9]*))?)?(?-(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*)?(?\\+(?:[\\da-z\\-]+|\\{height\\})(?:\\.(?:[\\da-z\\-]+|\\{height\\}))*)?$" }, "assemblyVersion": { "description": "The x.y version to use particularly for the AssemblyVersionAttribute instead of the default. This is useful when maintaining assembly binding compatibility on the desktop .NET Framework is important even though AssemblyFileVersion may change.", @@ -32,6 +32,13 @@ "description": "A number to add to the git height when calculating the build number.", "default": 0 }, + "semVer1NumericIdentifierPadding": { + "type": "integer", + "description": "The minimum number of digits to use for numeric identifiers in SemVer 1.", + "default": 4, + "minimum": 1, + "maximum": 6 + }, "publicReleaseRefSpec": { "type": "array", "description": "An array of regular expressions that may match a ref (branch or tag) that should be built with PublicRelease=true as the default value. The ref matched against is in its canonical form (e.g. refs/heads/master)", diff --git a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs index 0a686272..ecae9350 100644 --- a/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs +++ b/src/Nerdbank.GitVersioning.Tasks/GetBuildVersion.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Globalization; using System.IO; using System.Linq; using Microsoft.Build.Framework; diff --git a/src/version.json b/src/version.json index 137db249..3387fc6c 100644 --- a/src/version.json +++ b/src/version.json @@ -1,6 +1,6 @@ { "$schema": "NerdBank.GitVersioning/version.schema.json", - "version": "1.6", + "version": "2.0-beta", "assemblyVersion": { "precision": "revision" },