Skip to content

Commit 2ad35fe

Browse files
authored
Merge pull request #119 from AArnott/BuildNumberFromVersionJson
Allow user control of build number integer
2 parents 14d6ff9 + 4d80666 commit 2ad35fe

13 files changed

+382
-28
lines changed

doc/versionJson.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ The content of the version.json file is a JSON serialized object with these prop
2424
"version": "x.y-prerelease", // required
2525
"assemblyVersion": "x.y", // optional. Use when x.y for AssemblyVersionAttribute differs from the default version property.
2626
"buildNumberOffset": "zOffset", // optional. Use when you need to add/subtract a fixed value from the computed build number.
27+
"semVer1NumericIdentifierPadding": 4, // optional. Use when your -prerelease includes numeric identifiers and need semver1 support.
2728
"publicReleaseRefSpec": [
2829
"^refs/heads/master$", // we release out of master
29-
"^refs/tags/v\\d\\.\\d" // we also release tags starting with vN.N
30+
"^refs/tags/v\\d+\\.\\d+" // we also release tags starting with vN.N
3031
],
3132
"cloudBuild": {
3233
"setVersionVariables": true,
@@ -43,6 +44,11 @@ The content of the version.json file is a JSON serialized object with these prop
4344

4445
The `x` and `y` variables are for your use to specify a version that is meaningful
4546
to your customers. Consider using [semantic versioning][semver] for guidance.
47+
You may optionally supply a third integer in the version (i.e. x.y.z),
48+
in which case the git version height is specified as the fourth integer,
49+
which only appears in certain version representations.
50+
Alternatively, you can include the git version height in the -prerelease tag using
51+
syntax such as: `1.2.3-beta.{height}`
4652

4753
The optional -prerelease tag allows you to indicate that you are building prerelease software.
4854

src/NerdBank.GitVersioning.Tests/BuildIntegrationTests.cs

+35-1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ public async Task GetBuildVersion_Without_Git()
9898
Assert.Equal("3.4.0", buildResult.AssemblyInformationalVersion);
9999
}
100100

101+
[Fact]
102+
public async Task GetBuildVersion_WithThreeVersionIntegers()
103+
{
104+
VersionOptions workingCopyVersion = new VersionOptions
105+
{
106+
Version = SemanticVersion.Parse("7.8.9-beta.3"),
107+
SemVer1NumericIdentifierPadding = 1,
108+
};
109+
this.WriteVersionFile(workingCopyVersion);
110+
this.InitializeSourceControl();
111+
var buildResult = await this.BuildAsync();
112+
this.AssertStandardProperties(workingCopyVersion, buildResult);
113+
}
114+
101115
[Fact]
102116
public async Task GetBuildVersion_Without_Git_HighPrecisionAssemblyVersion()
103117
{
@@ -381,6 +395,21 @@ public async Task GetBuildVersion_Minus1BuildOffset_NotYetCommitted()
381395
this.AssertStandardProperties(versionOptions, buildResult);
382396
}
383397

398+
[Theory]
399+
[InlineData(0)]
400+
[InlineData(21)]
401+
public async Task GetBuildVersion_BuildNumberSpecifiedInVersionJson(int buildNumber)
402+
{
403+
var versionOptions = new VersionOptions
404+
{
405+
Version = SemanticVersion.Parse("14.0." + buildNumber),
406+
};
407+
this.WriteVersionFile(versionOptions);
408+
this.InitializeSourceControl();
409+
var buildResult = await this.BuildAsync();
410+
this.AssertStandardProperties(versionOptions, buildResult);
411+
}
412+
384413
[Fact]
385414
public async Task PublicRelease_RegEx_Unsatisfied()
386415
{
@@ -832,7 +861,7 @@ private void AssertStandardProperties(VersionOptions versionOptions, BuildResult
832861
string pkgVersionSuffix = buildResult.PublicRelease
833862
? string.Empty
834863
: $"-g{commitIdShort}";
835-
Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{versionOptions.Version.Prerelease}{pkgVersionSuffix}", buildResult.NuGetPackageVersion);
864+
Assert.Equal($"{idAsVersion.Major}.{idAsVersion.Minor}.{idAsVersion.Build}{GetSemVer1PrereleaseTag(versionOptions)}{pkgVersionSuffix}", buildResult.NuGetPackageVersion);
836865

837866
var buildNumberOptions = versionOptions.CloudBuild?.BuildNumber ?? new VersionOptions.CloudBuildNumberOptions();
838867
if (buildNumberOptions.Enabled)
@@ -864,6 +893,11 @@ private void AssertStandardProperties(VersionOptions versionOptions, BuildResult
864893
}
865894
}
866895

896+
private static string GetSemVer1PrereleaseTag(VersionOptions versionOptions)
897+
{
898+
return versionOptions.Version.Prerelease?.Replace('.', '-');
899+
}
900+
867901
private async Task<BuildResults> BuildAsync(string target = Targets.GetBuildVersion, LoggerVerbosity logVerbosity = LoggerVerbosity.Detailed, bool assertSuccessfulBuild = true)
868902
{
869903
var eventLogger = new MSBuildLogger { Verbosity = LoggerVerbosity.Minimal };

src/NerdBank.GitVersioning.Tests/GitExtensionsTests.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public void GetIdAsVersion_MissingVersionTxt()
172172
}
173173

174174
[Fact]
175-
public void GetIdAsVersion_VersionFileNeverCheckedIn()
175+
public void GetIdAsVersion_VersionFileNeverCheckedIn_3Ints()
176176
{
177177
this.AddCommits();
178178
var expectedVersion = new Version(1, 1, 0);
@@ -182,6 +182,23 @@ public void GetIdAsVersion_VersionFileNeverCheckedIn()
182182
Assert.Equal(expectedVersion.Major, actualVersion.Major);
183183
Assert.Equal(expectedVersion.Minor, actualVersion.Minor);
184184
Assert.Equal(expectedVersion.Build, actualVersion.Build);
185+
186+
// Height is expressed in the 4th integer since 3 were specified in version.json.
187+
// height is 0 since the change hasn't been committed.
188+
Assert.Equal(0, actualVersion.Revision);
189+
}
190+
191+
[Fact]
192+
public void GetIdAsVersion_VersionFileNeverCheckedIn_2Ints()
193+
{
194+
this.AddCommits();
195+
var expectedVersion = new Version(1, 1);
196+
var unstagedVersionData = VersionOptions.FromVersion(expectedVersion);
197+
string versionFilePath = VersionFile.SetVersion(this.RepoPath, unstagedVersionData);
198+
Version actualVersion = this.Repo.GetIdAsVersion();
199+
Assert.Equal(expectedVersion.Major, actualVersion.Major);
200+
Assert.Equal(expectedVersion.Minor, actualVersion.Minor);
201+
Assert.Equal(0, actualVersion.Build); // height is 0 since the change hasn't been committed.
185202
Assert.Equal(this.Repo.Head.Commits.First().GetTruncatedCommitIdAsUInt16(), actualVersion.Revision);
186203
}
187204

src/NerdBank.GitVersioning.Tests/NerdBank.GitVersioning.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</EmbeddedResource>
1313
<EmbeddedResource Include="Keys\*.snk" />
1414
<EmbeddedResource Include="Keys\*.pfx" />
15+
<EmbeddedResource Include="..\NerdBank.GitVersioning\version.schema.json" Link="version.schema.json" />
1516
<EmbeddedResource Include="test.prj" />
1617
<EmbeddedResource Include="repos\submodules.7z" />
1718
</ItemGroup>
@@ -21,6 +22,7 @@
2122
</ItemGroup>
2223
<ItemGroup>
2324
<PackageReference Include="7z.NET" Version="1.0.3" />
25+
<PackageReference Include="Newtonsoft.Json.Schema" Version="2.0.11" />
2426
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
2527
<PackageReference Include="Microsoft.Build" Version="14.3" Condition=" '$(TargetFramework)' == 'net452' " />
2628
<PackageReference Include="Xunit.Combinatorial" Version="1.1.12" />

src/NerdBank.GitVersioning.Tests/VersionOracleTests.cs

+121
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,125 @@ public void Submodule_RecognizedWithCorrectVersion()
2828
Assert.Equal("3ea7f010c3", oracleB.GitCommitIdShort);
2929
}
3030
}
31+
32+
[Fact]
33+
public void MajorMinorPrereleaseBuildMetadata()
34+
{
35+
VersionOptions workingCopyVersion = new VersionOptions
36+
{
37+
Version = SemanticVersion.Parse("7.8-beta.3+metadata.4"),
38+
};
39+
this.WriteVersionFile(workingCopyVersion);
40+
this.InitializeSourceControl();
41+
var oracle = VersionOracle.Create(this.RepoPath);
42+
Assert.Equal("7.8", oracle.MajorMinorVersion.ToString());
43+
Assert.Equal(oracle.VersionHeight, oracle.BuildNumber);
44+
45+
Assert.Equal("-beta.3", oracle.PrereleaseVersion);
46+
////Assert.Equal("+metadata.4", oracle.BuildMetadataFragment);
47+
48+
Assert.Equal(1, oracle.VersionHeight);
49+
Assert.Equal(0, oracle.VersionHeightOffset);
50+
}
51+
52+
[Fact]
53+
public void MajorMinorBuildPrereleaseBuildMetadata()
54+
{
55+
VersionOptions workingCopyVersion = new VersionOptions
56+
{
57+
Version = SemanticVersion.Parse("7.8.9-beta.3+metadata.4"),
58+
};
59+
this.WriteVersionFile(workingCopyVersion);
60+
this.InitializeSourceControl();
61+
var oracle = VersionOracle.Create(this.RepoPath);
62+
Assert.Equal("7.8", oracle.MajorMinorVersion.ToString());
63+
Assert.Equal(9, oracle.BuildNumber);
64+
Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision);
65+
66+
Assert.Equal("-beta.3", oracle.PrereleaseVersion);
67+
////Assert.Equal("+metadata.4", oracle.BuildMetadataFragment);
68+
69+
Assert.Equal(1, oracle.VersionHeight);
70+
Assert.Equal(0, oracle.VersionHeightOffset);
71+
}
72+
73+
[Fact]
74+
public void HeightInPrerelease()
75+
{
76+
VersionOptions workingCopyVersion = new VersionOptions
77+
{
78+
Version = SemanticVersion.Parse("7.8.9-beta.{height}.foo"),
79+
BuildNumberOffset = 2,
80+
};
81+
this.WriteVersionFile(workingCopyVersion);
82+
this.InitializeSourceControl();
83+
var oracle = VersionOracle.Create(this.RepoPath);
84+
Assert.Equal("7.8", oracle.MajorMinorVersion.ToString());
85+
Assert.Equal(9, oracle.BuildNumber);
86+
Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision);
87+
88+
Assert.Equal("-beta." + (oracle.VersionHeight + oracle.VersionHeightOffset) + ".foo", oracle.PrereleaseVersion);
89+
90+
Assert.Equal(1, oracle.VersionHeight);
91+
Assert.Equal(2, oracle.VersionHeightOffset);
92+
}
93+
94+
[Fact(Skip = "Build metadata not yet retained from version.json")]
95+
public void HeightInBuildMetadata()
96+
{
97+
VersionOptions workingCopyVersion = new VersionOptions
98+
{
99+
Version = SemanticVersion.Parse("7.8.9-beta+another.{height}.foo"),
100+
BuildNumberOffset = 2,
101+
};
102+
this.WriteVersionFile(workingCopyVersion);
103+
this.InitializeSourceControl();
104+
var oracle = VersionOracle.Create(this.RepoPath);
105+
Assert.Equal("7.8", oracle.MajorMinorVersion.ToString());
106+
Assert.Equal(9, oracle.BuildNumber);
107+
Assert.Equal(oracle.VersionHeight + oracle.VersionHeightOffset, oracle.Version.Revision);
108+
109+
Assert.Equal("-beta", oracle.PrereleaseVersion);
110+
Assert.Equal("+another." + (oracle.VersionHeight + oracle.VersionHeightOffset) + ".foo", oracle.BuildMetadataFragment);
111+
112+
Assert.Equal(1, oracle.VersionHeight);
113+
Assert.Equal(2, oracle.VersionHeightOffset);
114+
}
115+
116+
[Theory]
117+
[InlineData("7.8.9-foo.25", "7.8.9-foo-0025")]
118+
[InlineData("7.8.9-foo.25s", "7.8.9-foo-25s")]
119+
[InlineData("7.8.9-foo.s25", "7.8.9-foo-s25")]
120+
[InlineData("7.8.9-foo.25.bar-24.13-11", "7.8.9-foo-0025-bar-24-13-11")]
121+
[InlineData("7.8.9-25.bar.baz-25", "7.8.9-0025-bar-baz-25")]
122+
[InlineData("7.8.9-foo.5.bar.1.43.baz", "7.8.9-foo-0005-bar-0001-0043-baz")]
123+
public void SemVer1PrereleaseConversion(string semVer2, string semVer1)
124+
{
125+
VersionOptions workingCopyVersion = new VersionOptions
126+
{
127+
Version = SemanticVersion.Parse(semVer2),
128+
BuildNumberOffset = 2,
129+
};
130+
this.WriteVersionFile(workingCopyVersion);
131+
this.InitializeSourceControl();
132+
var oracle = VersionOracle.Create(this.RepoPath);
133+
oracle.PublicRelease = true;
134+
Assert.Equal(semVer1, oracle.SemVer1);
135+
}
136+
137+
[Fact]
138+
public void SemVer1PrereleaseConversionPadding()
139+
{
140+
VersionOptions workingCopyVersion = new VersionOptions
141+
{
142+
Version = SemanticVersion.Parse("7.8.9-foo.25"),
143+
BuildNumberOffset = 2,
144+
SemVer1NumericIdentifierPadding = 3,
145+
};
146+
this.WriteVersionFile(workingCopyVersion);
147+
this.InitializeSourceControl();
148+
var oracle = VersionOracle.Create(this.RepoPath);
149+
oracle.PublicRelease = true;
150+
Assert.Equal("7.8.9-foo-025", oracle.SemVer1);
151+
}
31152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.IO;
2+
using System.Reflection;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
using Newtonsoft.Json.Schema;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
9+
public class VersionSchemaTests
10+
{
11+
private readonly ITestOutputHelper Logger;
12+
13+
private readonly JSchema schema;
14+
15+
private JObject json;
16+
17+
public VersionSchemaTests(ITestOutputHelper logger)
18+
{
19+
this.Logger = logger;
20+
using (var schemaStream = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream($"{ThisAssembly.RootNamespace}.version.schema.json")))
21+
{
22+
this.schema = JSchema.Load(new JsonTextReader(schemaStream));
23+
}
24+
}
25+
26+
[Fact]
27+
public void VersionField_BasicScenarios()
28+
{
29+
json = JObject.Parse(@"{ ""version"": ""2.3"" }");
30+
Assert.True(json.IsValid(this.schema));
31+
json = JObject.Parse(@"{ ""version"": ""2.3-beta"" }");
32+
Assert.True(json.IsValid(this.schema));
33+
json = JObject.Parse(@"{ ""version"": ""2.3-beta-final"" }");
34+
Assert.True(json.IsValid(this.schema));
35+
json = JObject.Parse(@"{ ""version"": ""2.3-beta.2"" }");
36+
Assert.True(json.IsValid(this.schema));
37+
json = JObject.Parse(@"{ ""version"": ""2.3-beta.0"" }");
38+
Assert.True(json.IsValid(this.schema));
39+
json = JObject.Parse(@"{ ""version"": ""2.3-beta.01"" }");
40+
Assert.True(json.IsValid(this.schema));
41+
json = JObject.Parse(@"{ ""version"": ""1.2.3"" }");
42+
Assert.True(json.IsValid(this.schema));
43+
json = JObject.Parse(@"{ ""version"": ""1.2.3.4"" }");
44+
Assert.True(json.IsValid(this.schema));
45+
46+
json = JObject.Parse(@"{ ""version"": ""02.3"" }");
47+
Assert.False(json.IsValid(this.schema));
48+
json = JObject.Parse(@"{ ""version"": ""2.03"" }");
49+
Assert.False(json.IsValid(this.schema));
50+
}
51+
52+
[Fact]
53+
public void VersionField_HeightMacroPlacement()
54+
{
55+
// Valid uses
56+
json = JObject.Parse(@"{ ""version"": ""2.3.0-{height}"" }");
57+
Assert.True(json.IsValid(this.schema));
58+
json = JObject.Parse(@"{ ""version"": ""2.3.0-{height}.beta"" }");
59+
Assert.True(json.IsValid(this.schema));
60+
json = JObject.Parse(@"{ ""version"": ""2.3.0-beta.{height}"" }");
61+
Assert.True(json.IsValid(this.schema));
62+
json = JObject.Parse(@"{ ""version"": ""2.3.0-beta+{height}"" }");
63+
Assert.True(json.IsValid(this.schema));
64+
65+
// Invalid uses
66+
json = JObject.Parse(@"{ ""version"": ""2.3.{height}-beta"" }");
67+
Assert.False(json.IsValid(this.schema));
68+
json = JObject.Parse(@"{ ""version"": ""2.3.0-beta-{height}"" }");
69+
Assert.False(json.IsValid(this.schema));
70+
json = JObject.Parse(@"{ ""version"": ""2.3.0-beta+height-{height}"" }");
71+
Assert.False(json.IsValid(this.schema));
72+
}
73+
}

src/NerdBank.GitVersioning/GitExtensions.cs

+30-16
Original file line numberDiff line numberDiff line change
@@ -512,26 +512,40 @@ private static void AddReachableCommitsFrom(Commit startingCommit, HashSet<Commi
512512
private static Version GetIdAsVersionHelper(Commit commit, VersionOptions versionOptions, string repoRelativeProjectDirectory, int? versionHeight)
513513
{
514514
var baseVersion = versionOptions?.Version?.Version ?? Version0;
515+
int buildNumber = baseVersion.Build;
516+
int revision = baseVersion.Revision;
515517

516-
// The compiler (due to WinPE header requirements) only allows 16-bit version components,
517-
// and forbids 0xffff as a value.
518-
// The build number is set to the git height. This helps ensure that
519-
// within a major.minor release, each patch has an incrementing integer.
520-
// The revision is set to the first two bytes of the git commit ID.
521-
if (!versionHeight.HasValue)
518+
if (revision < 0)
522519
{
523-
versionHeight = commit != null
524-
? commit.GetHeight(c => CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory))
525-
: 0;
526-
}
520+
// The compiler (due to WinPE header requirements) only allows 16-bit version components,
521+
// and forbids 0xffff as a value.
522+
// The build number is set to the git height. This helps ensure that
523+
// within a major.minor release, each patch has an incrementing integer.
524+
// The revision is set to the first two bytes of the git commit ID.
525+
if (!versionHeight.HasValue)
526+
{
527+
versionHeight = commit != null
528+
? commit.GetHeight(c => CommitMatchesMajorMinorVersion(c, baseVersion, repoRelativeProjectDirectory))
529+
: 0;
530+
}
527531

528-
int build = versionHeight.Value == 0 ? 0 : versionHeight.Value + (versionOptions?.BuildNumberOffset ?? 0);
529-
Verify.Operation(build <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", build, MaximumBuildNumberOrRevisionComponent);
530-
int revision = commit != null
531-
? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16())
532-
: 0;
532+
int adjustedVersionHeight = versionHeight.Value == 0 ? 0 : versionHeight.Value + (versionOptions?.BuildNumberOffset ?? 0);
533+
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
534+
535+
if (buildNumber < 0)
536+
{
537+
buildNumber = adjustedVersionHeight;
538+
revision = commit != null
539+
? Math.Min(MaximumBuildNumberOrRevisionComponent, commit.GetTruncatedCommitIdAsUInt16())
540+
: 0;
541+
}
542+
else
543+
{
544+
revision = adjustedVersionHeight;
545+
}
546+
}
533547

534-
return new Version(baseVersion.Major, baseVersion.Minor, build, revision);
548+
return new Version(baseVersion.Major, baseVersion.Minor, buildNumber, revision);
535549
}
536550

537551
/// <summary>

0 commit comments

Comments
 (0)