Skip to content

Commit ead5232

Browse files
authored
Merge pull request #1279 from dotnet/copilot/add-version-height-offset-property
Add `versionHeightOffsetAppliesTo` property to version.json
2 parents 0454778 + 0c7d75d commit ead5232

File tree

10 files changed

+204
-6
lines changed

10 files changed

+204
-6
lines changed

docfx/docs/versionJson.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ The content of the version.json file is a JSON serialized object with these prop
3434
"precision": "revision" // optional. Use when you want a more precise assembly version than the default major.minor.
3535
},
3636
"versionHeightOffset": "zOffset", // optional. Use when you need to add/subtract a fixed value from the computed version height.
37+
"versionHeightOffsetAppliesTo": "x.y-prerelease", // optional. Specifies the version to which versionHeightOffset applies. When the version changes such that version height would reset, and this doesn't match the new version, versionHeightOffset is ignored.
3738
"semVer1NumericIdentifierPadding": 4, // optional. Use when your -prerelease includes numeric identifiers and need semver1 support.
3839
"gitCommitIdShortFixedLength": 10, // optional. Set the commit ID abbreviation length.
3940
"gitCommitIdShortAutoMinimum": 0, // optional. Set to use the short commit ID abbreviation provided by the git repository.
@@ -85,4 +86,27 @@ that assumes linear versioning.
8586

8687
When the `cloudBuild.buildNumber.includeCommitId.where` property is set to `fourthVersionComponent`, the first 15 bits of the commit hash is used to create the 4th integer in the version number.
8788

89+
## Version Height Offset
90+
91+
The `versionHeightOffset` property allows you to add or subtract a fixed value from the git version height. This is typically used as a temporary workaround when migrating from another versioning system or when correcting version numbering discrepancies.
92+
93+
The `versionHeightOffsetAppliesTo` property can be used in conjunction with `versionHeightOffset` to ensure that the offset is only applied when the version matches a specific value. When the `version` property changes such that the version height would be reset, and `versionHeightOffsetAppliesTo` does not match the new version, the `versionHeightOffset` will be automatically ignored.
94+
95+
This allows version height offsets to implicitly reset as intended when the version changes, without having to manually remove the offset properties from all `version.json` files in the repository.
96+
97+
### Example
98+
99+
```json
100+
{
101+
"version": "1.0-beta",
102+
"versionHeightOffset": 100,
103+
"versionHeightOffsetAppliesTo": "1.0-beta"
104+
}
105+
```
106+
107+
In this example, the offset of 100 will be applied as long as the version remains "1.0-beta". When you update the version to "1.1-alpha" (which would reset the version height), the offset will be automatically ignored because "1.1-alpha" does not match "1.0-beta".
108+
109+
> [!NOTE]
110+
> This feature is particularly useful when a `version.json` file uses `"inherit": true` to get the version from a parent `version.json` file higher in the source tree. In such cases, you can set `versionHeightOffset` and `versionHeightOffsetAppliesTo` in the inheriting file without having to update it when the parent version changes. The offset will automatically stop applying when the inherited version no longer matches `versionHeightOffsetAppliesTo`.
111+
88112
[Learn more about pathFilters](path-filters.md).

src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ internal static Version GetIdAsVersionHelper(this Commit? commit, VersionOptions
231231
// and forbids 0xffff as a value.
232232
if (versionHeightPosition.HasValue)
233233
{
234-
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0);
234+
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.EffectiveVersionHeightOffset ?? 0);
235235
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
236236
switch (versionHeightPosition.Value)
237237
{
@@ -344,7 +344,7 @@ private static bool IsVersionHeightMismatch(Version version, VersionOptions vers
344344
{
345345
int expectedVersionHeight = SemanticVersion.ReadVersionPosition(version, position.Value);
346346

347-
int actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault;
347+
int actualVersionOffset = versionOptions.EffectiveVersionHeightOffset;
348348
int actualVersionHeight = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, version, position.Value - 1, tracker));
349349
return expectedVersionHeight != actualVersionHeight + actualVersionOffset;
350350
}

src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ private Version GetIdAsVersionHelper(VersionOptions? versionOptions, int version
176176
// and forbids 0xffff as a value.
177177
if (versionHeightPosition.HasValue)
178178
{
179-
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0);
179+
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.EffectiveVersionHeightOffset ?? 0);
180180
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
181181
switch (versionHeightPosition.Value)
182182
{

src/NerdBank.GitVersioning/ReleaseManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ private void UpdateVersion(LibGit2Context context, SemanticVersion oldVersion, S
282282
{
283283
if (versionOptions.VersionHeightOffset != -1 && versionOptions.VersionHeightPosition.HasValue && SemanticVersion.WillVersionChangeResetVersionHeight(versionOptions.Version, newVersion, versionOptions.VersionHeightPosition.Value))
284284
{
285-
// The version will be reset by this change, so remove the version height offset property.
285+
// The version will be reset by this change, so remove the version height offset properties.
286286
versionOptions.VersionHeightOffset = null;
287+
versionOptions.VersionHeightOffsetAppliesTo = null;
287288
}
288289

289290
versionOptions.Version = newVersion;

src/NerdBank.GitVersioning/VersionOptions.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ public class VersionOptions : IEquatable<VersionOptions>
7373
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
7474
private int? buildNumberOffset;
7575

76+
/// <summary>
77+
/// Backing field for the <see cref="VersionHeightOffsetAppliesTo"/> property.
78+
/// </summary>
79+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
80+
private SemanticVersion? versionHeightOffsetAppliesTo;
81+
7682
/// <summary>
7783
/// Backing field for the <see cref="SemVer1NumericIdentifierPadding"/> property.
7884
/// </summary>
@@ -146,6 +152,7 @@ public VersionOptions(VersionOptions copyFrom)
146152
this.version = copyFrom.version;
147153
this.assemblyVersion = copyFrom.assemblyVersion is object ? new AssemblyVersionOptions(copyFrom.assemblyVersion) : null;
148154
this.buildNumberOffset = copyFrom.buildNumberOffset;
155+
this.versionHeightOffsetAppliesTo = copyFrom.versionHeightOffsetAppliesTo;
149156
this.semVer1NumericIdentifierPadding = copyFrom.semVer1NumericIdentifierPadding;
150157
this.gitCommitIdShortFixedLength = copyFrom.gitCommitIdShortFixedLength;
151158
this.gitCommitIdShortAutoMinimum = copyFrom.gitCommitIdShortAutoMinimum;
@@ -368,6 +375,57 @@ public int VersionHeightOffsetOrDefault
368375
#pragma warning restore CS0618
369376
}
370377

378+
/// <summary>
379+
/// Gets the effective version height offset, taking into account the <see cref="VersionHeightOffsetAppliesTo"/> property.
380+
/// </summary>
381+
/// <returns>
382+
/// The version height offset if it applies to the current version, or 0 if the version has changed
383+
/// such that the offset should no longer be applied.
384+
/// </returns>
385+
[JsonIgnore]
386+
public int EffectiveVersionHeightOffset
387+
{
388+
get
389+
{
390+
// Check if the offset applies to the current version
391+
if (this.VersionHeightOffsetAppliesTo is object &&
392+
this.Version is object &&
393+
this.VersionHeightPosition.HasValue)
394+
{
395+
// If the version would be reset by a change from VersionHeightOffsetAppliesTo to Version,
396+
// then the offset does not apply.
397+
if (SemanticVersion.WillVersionChangeResetVersionHeight(
398+
this.VersionHeightOffsetAppliesTo,
399+
this.Version,
400+
this.VersionHeightPosition.Value))
401+
{
402+
return 0;
403+
}
404+
}
405+
406+
return this.VersionHeightOffsetOrDefault;
407+
}
408+
}
409+
410+
/// <summary>
411+
/// Gets or sets the version to which the <see cref="VersionHeightOffset"/> applies.
412+
/// When the <see cref="Version"/> property changes such that the version height would be reset,
413+
/// and this property does not match the new version, the <see cref="VersionHeightOffset"/> will be ignored.
414+
/// </summary>
415+
/// <value>A semantic version, or <see langword="null"/> to indicate no constraint.</value>
416+
/// <remarks>
417+
/// This property is typically used in conjunction with <see cref="VersionHeightOffset"/> to ensure
418+
/// that the offset is only applied when the version matches the expected version. When the version
419+
/// changes such that the version height would reset, this property can be used to automatically
420+
/// stop applying the offset without needing to manually remove it from all version.json files.
421+
/// </remarks>
422+
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
423+
public SemanticVersion? VersionHeightOffsetAppliesTo
424+
{
425+
get => this.versionHeightOffsetAppliesTo;
426+
set => this.SetIfNotReadOnly(ref this.versionHeightOffsetAppliesTo, value);
427+
}
428+
371429
/// <summary>
372430
/// Gets or sets the minimum number of digits to use for numeric identifiers in SemVer 1.
373431
/// </summary>
@@ -1667,7 +1725,8 @@ public bool Equals(VersionOptions? x, VersionOptions? y)
16671725
&& NuGetPackageVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.NuGetPackageVersionOrDefault, y.NuGetPackageVersionOrDefault)
16681726
&& CloudBuildOptions.EqualWithDefaultsComparer.Singleton.Equals(x.CloudBuildOrDefault, y.CloudBuildOrDefault)
16691727
&& ReleaseOptions.EqualWithDefaultsComparer.Singleton.Equals(x.ReleaseOrDefault, y.ReleaseOrDefault)
1670-
&& x.VersionHeightOffset == y.VersionHeightOffset;
1728+
&& x.VersionHeightOffset == y.VersionHeightOffset
1729+
&& EqualityComparer<SemanticVersion?>.Default.Equals(x.VersionHeightOffsetAppliesTo, y.VersionHeightOffsetAppliesTo);
16711730
}
16721731

16731732
/// <inheritdoc />

src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
8686
property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetOrDefault != 0;
8787
}
8888

89+
if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.VersionHeightOffsetAppliesTo))
90+
{
91+
property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetAppliesTo is not null;
92+
}
93+
8994
if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.NuGetPackageVersion))
9095
{
9196
property.ShouldSerialize = instance => !((VersionOptions)instance).NuGetPackageVersionOrDefault.IsDefault;

src/NerdBank.GitVersioning/VersionOracle.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,13 @@ public string PrereleaseVersion
306306
/// when calculating the integer to use as the <see cref="BuildNumber"/>
307307
/// or elsewhere that the {height} macro is used.
308308
/// </summary>
309-
public int VersionHeightOffset => this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0;
309+
/// <remarks>
310+
/// This property returns the effective version height offset, which takes into account
311+
/// the <see cref="VersionOptions.VersionHeightOffsetAppliesTo"/> property. If that property
312+
/// is set and the version has changed such that the version height would be reset, this
313+
/// will return 0 instead of the configured offset.
314+
/// </remarks>
315+
public int VersionHeightOffset => this.VersionOptions?.EffectiveVersionHeightOffset ?? 0;
310316

311317
/// <summary>
312318
/// Gets or sets the ref (branch or tag) being built.

src/NerdBank.GitVersioning/version.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@
6868
"description": "A number to add to the git height when calculating the version height (which typically appears as the 3rd integer in a computed version). May be negative, but not of greater magnitude than the original git height.",
6969
"default": 0
7070
},
71+
"versionHeightOffsetAppliesTo": {
72+
"type": "string",
73+
"description": "The version to which the versionHeightOffset applies. When the version property changes such that the version height would be reset, and this property does not match the new version, the versionHeightOffset will be ignored. This allows the offset to implicitly reset as intended without having to manually remove it from all version.json files.",
74+
"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\\}))*)?$"
75+
},
7176
"buildNumberOffset": {
7277
"type": "integer",
7378
"description": "OBSOLETE by v3.0. Use \"versionHeightOffset\" instead. A number to add to the git height when calculating the version height (which typically appears as the 3rd integer in a computed version). May be negative, but not of greater magnitude than the original git height.",

test/Nerdbank.GitVersioning.Tests/ReleaseManagerTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,47 @@ public void PrepareRelease_WithCustomCommitMessagePattern(string initialVersion,
703703
Assert.Equal(expectedCommitMessage, releaseBranchCommit.MessageShort);
704704
}
705705

706+
[Fact]
707+
public void PrepareRelease_ResetsVersionHeightOffsetAppliesTo()
708+
{
709+
// create and configure repository
710+
this.InitializeSourceControl();
711+
712+
var initialVersionOptions = new VersionOptions()
713+
{
714+
Version = SemanticVersion.Parse("1.0-beta"),
715+
VersionHeightOffset = 5,
716+
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("1.0-beta"),
717+
};
718+
719+
var expectedReleaseVersionOptions = new VersionOptions()
720+
{
721+
Version = SemanticVersion.Parse("1.0"),
722+
VersionHeightOffset = 5,
723+
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("1.0-beta"),
724+
};
725+
726+
var expectedMainVersionOptions = new VersionOptions()
727+
{
728+
Version = SemanticVersion.Parse("1.1-alpha"),
729+
};
730+
731+
// create version.json
732+
this.WriteVersionFile(initialVersionOptions);
733+
734+
Commit tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip;
735+
736+
var releaseManager = new ReleaseManager();
737+
releaseManager.PrepareRelease(this.RepoPath);
738+
739+
this.SetContextToHead();
740+
VersionOptions newVersion = this.Context.VersionFile.GetVersion();
741+
Assert.Equal(expectedMainVersionOptions, newVersion);
742+
743+
VersionOptions releaseVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches["v1.0"].Tip.Sha);
744+
Assert.Equal(expectedReleaseVersionOptions, releaseVersion);
745+
}
746+
706747
/// <inheritdoc/>
707748
protected override void InitializeSourceControl(bool withInitialCommit = true)
708749
{

test/Nerdbank.GitVersioning.Tests/VersionOracleTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,63 @@ public void HeightInBuildMetadata()
219219
Assert.Equal(2, oracle.VersionHeightOffset);
220220
}
221221

222+
[Fact]
223+
public void VersionHeightOffsetAppliesTo_Matching()
224+
{
225+
// When VersionHeightOffsetAppliesTo matches the current version, the offset should be applied
226+
VersionOptions workingCopyVersion = new VersionOptions
227+
{
228+
Version = SemanticVersion.Parse("7.8.9-beta"),
229+
VersionHeightOffset = 5,
230+
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8.9-beta"),
231+
};
232+
this.WriteVersionFile(workingCopyVersion);
233+
this.InitializeSourceControl();
234+
var oracle = new VersionOracle(this.Context);
235+
236+
// The offset should be applied because the version matches
237+
Assert.Equal(5, oracle.VersionHeightOffset);
238+
Assert.Equal(1, oracle.VersionHeight);
239+
}
240+
241+
[Fact]
242+
public void VersionHeightOffsetAppliesTo_NotMatching()
243+
{
244+
// When VersionHeightOffsetAppliesTo doesn't match the current version, the offset should NOT be applied
245+
VersionOptions workingCopyVersion = new VersionOptions
246+
{
247+
Version = SemanticVersion.Parse("7.9-beta"),
248+
VersionHeightOffset = 5,
249+
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8-beta"),
250+
};
251+
this.WriteVersionFile(workingCopyVersion);
252+
this.InitializeSourceControl();
253+
var oracle = new VersionOracle(this.Context);
254+
255+
// The offset should NOT be applied because the version changed (7.8 -> 7.9)
256+
Assert.Equal(0, oracle.VersionHeightOffset);
257+
Assert.Equal(1, oracle.VersionHeight);
258+
}
259+
260+
[Fact]
261+
public void VersionHeightOffsetAppliesTo_BuildNumberChange()
262+
{
263+
// When VersionHeightOffsetAppliesTo has a different build number, the offset should NOT be applied
264+
VersionOptions workingCopyVersion = new VersionOptions
265+
{
266+
Version = SemanticVersion.Parse("7.9-beta"),
267+
VersionHeightOffset = 5,
268+
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8-beta"),
269+
};
270+
this.WriteVersionFile(workingCopyVersion);
271+
this.InitializeSourceControl();
272+
var oracle = new VersionOracle(this.Context);
273+
274+
// The offset should NOT be applied because the minor version changed (7.8 -> 7.9)
275+
Assert.Equal(0, oracle.VersionHeightOffset);
276+
Assert.Equal(1, oracle.VersionHeight);
277+
}
278+
222279
[Theory]
223280
[InlineData("7.8.9-foo.25", "7.8.9-foo-0025")]
224281
[InlineData("7.8.9-foo.25s", "7.8.9-foo-25s")]

0 commit comments

Comments
 (0)