Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docfx/docs/versionJson.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The content of the version.json file is a JSON serialized object with these prop
"precision": "revision" // optional. Use when you want a more precise assembly version than the default major.minor.
},
"versionHeightOffset": "zOffset", // optional. Use when you need to add/subtract a fixed value from the computed version height.
"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.
"semVer1NumericIdentifierPadding": 4, // optional. Use when your -prerelease includes numeric identifiers and need semver1 support.
"gitCommitIdShortFixedLength": 10, // optional. Set the commit ID abbreviation length.
"gitCommitIdShortAutoMinimum": 0, // optional. Set to use the short commit ID abbreviation provided by the git repository.
Expand Down Expand Up @@ -85,4 +86,27 @@ that assumes linear versioning.

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.

## Version Height Offset

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.

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.

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.

### Example

```json
{
"version": "1.0-beta",
"versionHeightOffset": 100,
"versionHeightOffsetAppliesTo": "1.0-beta"
}
```

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".

> [!NOTE]
> 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`.

[Learn more about pathFilters](path-filters.md).
4 changes: 2 additions & 2 deletions src/NerdBank.GitVersioning/LibGit2/LibGit2GitExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ internal static Version GetIdAsVersionHelper(this Commit? commit, VersionOptions
// and forbids 0xffff as a value.
if (versionHeightPosition.HasValue)
{
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0);
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.EffectiveVersionHeightOffset ?? 0);
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
switch (versionHeightPosition.Value)
{
Expand Down Expand Up @@ -344,7 +344,7 @@ private static bool IsVersionHeightMismatch(Version version, VersionOptions vers
{
int expectedVersionHeight = SemanticVersion.ReadVersionPosition(version, position.Value);

int actualVersionOffset = versionOptions.VersionHeightOffsetOrDefault;
int actualVersionOffset = versionOptions.EffectiveVersionHeightOffset;
int actualVersionHeight = GetCommitHeight(commit, tracker, c => CommitMatchesVersion(c, version, position.Value - 1, tracker));
return expectedVersionHeight != actualVersionHeight + actualVersionOffset;
}
Expand Down
2 changes: 1 addition & 1 deletion src/NerdBank.GitVersioning/Managed/ManagedGitContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private Version GetIdAsVersionHelper(VersionOptions? versionOptions, int version
// and forbids 0xffff as a value.
if (versionHeightPosition.HasValue)
{
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.VersionHeightOffset ?? 0);
int adjustedVersionHeight = versionHeight == 0 ? 0 : versionHeight + (versionOptions?.EffectiveVersionHeightOffset ?? 0);
Verify.Operation(adjustedVersionHeight <= MaximumBuildNumberOrRevisionComponent, "Git height is {0}, which is greater than the maximum allowed {0}.", adjustedVersionHeight, MaximumBuildNumberOrRevisionComponent);
switch (versionHeightPosition.Value)
{
Expand Down
3 changes: 2 additions & 1 deletion src/NerdBank.GitVersioning/ReleaseManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,9 @@ private void UpdateVersion(LibGit2Context context, SemanticVersion oldVersion, S
{
if (versionOptions.VersionHeightOffset != -1 && versionOptions.VersionHeightPosition.HasValue && SemanticVersion.WillVersionChangeResetVersionHeight(versionOptions.Version, newVersion, versionOptions.VersionHeightPosition.Value))
{
// The version will be reset by this change, so remove the version height offset property.
// The version will be reset by this change, so remove the version height offset properties.
versionOptions.VersionHeightOffset = null;
versionOptions.VersionHeightOffsetAppliesTo = null;
}

versionOptions.Version = newVersion;
Expand Down
61 changes: 60 additions & 1 deletion src/NerdBank.GitVersioning/VersionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ public class VersionOptions : IEquatable<VersionOptions>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private int? buildNumberOffset;

/// <summary>
/// Backing field for the <see cref="VersionHeightOffsetAppliesTo"/> property.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private SemanticVersion? versionHeightOffsetAppliesTo;

/// <summary>
/// Backing field for the <see cref="SemVer1NumericIdentifierPadding"/> property.
/// </summary>
Expand Down Expand Up @@ -146,6 +152,7 @@ public VersionOptions(VersionOptions copyFrom)
this.version = copyFrom.version;
this.assemblyVersion = copyFrom.assemblyVersion is object ? new AssemblyVersionOptions(copyFrom.assemblyVersion) : null;
this.buildNumberOffset = copyFrom.buildNumberOffset;
this.versionHeightOffsetAppliesTo = copyFrom.versionHeightOffsetAppliesTo;
this.semVer1NumericIdentifierPadding = copyFrom.semVer1NumericIdentifierPadding;
this.gitCommitIdShortFixedLength = copyFrom.gitCommitIdShortFixedLength;
this.gitCommitIdShortAutoMinimum = copyFrom.gitCommitIdShortAutoMinimum;
Expand Down Expand Up @@ -368,6 +375,57 @@ public int VersionHeightOffsetOrDefault
#pragma warning restore CS0618
}

/// <summary>
/// Gets the effective version height offset, taking into account the <see cref="VersionHeightOffsetAppliesTo"/> property.
/// </summary>
/// <returns>
/// The version height offset if it applies to the current version, or 0 if the version has changed
/// such that the offset should no longer be applied.
/// </returns>
[JsonIgnore]
public int EffectiveVersionHeightOffset
{
get
{
// Check if the offset applies to the current version
if (this.VersionHeightOffsetAppliesTo is object &&
this.Version is object &&
this.VersionHeightPosition.HasValue)
{
// If the version would be reset by a change from VersionHeightOffsetAppliesTo to Version,
// then the offset does not apply.
if (SemanticVersion.WillVersionChangeResetVersionHeight(
this.VersionHeightOffsetAppliesTo,
this.Version,
this.VersionHeightPosition.Value))
{
return 0;
}
}

return this.VersionHeightOffsetOrDefault;
}
}

/// <summary>
/// Gets or sets the version to which the <see cref="VersionHeightOffset"/> applies.
/// When the <see cref="Version"/> property changes such that the version height would be reset,
/// and this property does not match the new version, the <see cref="VersionHeightOffset"/> will be ignored.
/// </summary>
/// <value>A semantic version, or <see langword="null"/> to indicate no constraint.</value>
/// <remarks>
/// This property is typically used in conjunction with <see cref="VersionHeightOffset"/> to ensure
/// that the offset is only applied when the version matches the expected version. When the version
/// changes such that the version height would reset, this property can be used to automatically
/// stop applying the offset without needing to manually remove it from all version.json files.
/// </remarks>
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public SemanticVersion? VersionHeightOffsetAppliesTo
{
get => this.versionHeightOffsetAppliesTo;
set => this.SetIfNotReadOnly(ref this.versionHeightOffsetAppliesTo, value);
}

/// <summary>
/// Gets or sets the minimum number of digits to use for numeric identifiers in SemVer 1.
/// </summary>
Expand Down Expand Up @@ -1667,7 +1725,8 @@ public bool Equals(VersionOptions? x, VersionOptions? y)
&& NuGetPackageVersionOptions.EqualWithDefaultsComparer.Singleton.Equals(x.NuGetPackageVersionOrDefault, y.NuGetPackageVersionOrDefault)
&& CloudBuildOptions.EqualWithDefaultsComparer.Singleton.Equals(x.CloudBuildOrDefault, y.CloudBuildOrDefault)
&& ReleaseOptions.EqualWithDefaultsComparer.Singleton.Equals(x.ReleaseOrDefault, y.ReleaseOrDefault)
&& x.VersionHeightOffset == y.VersionHeightOffset;
&& x.VersionHeightOffset == y.VersionHeightOffset
&& EqualityComparer<SemanticVersion?>.Default.Equals(x.VersionHeightOffsetAppliesTo, y.VersionHeightOffsetAppliesTo);
}

/// <inheritdoc />
Expand Down
5 changes: 5 additions & 0 deletions src/NerdBank.GitVersioning/VersionOptionsContractResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetOrDefault != 0;
}

if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.VersionHeightOffsetAppliesTo))
{
property.ShouldSerialize = instance => ((VersionOptions)instance).VersionHeightOffsetAppliesTo is not null;
}

if (property.DeclaringType == typeof(VersionOptions) && member.Name == nameof(VersionOptions.NuGetPackageVersion))
{
property.ShouldSerialize = instance => !((VersionOptions)instance).NuGetPackageVersionOrDefault.IsDefault;
Expand Down
8 changes: 7 additions & 1 deletion src/NerdBank.GitVersioning/VersionOracle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,13 @@ public string PrereleaseVersion
/// when calculating the integer to use as the <see cref="BuildNumber"/>
/// or elsewhere that the {height} macro is used.
/// </summary>
public int VersionHeightOffset => this.VersionOptions?.VersionHeightOffsetOrDefault ?? 0;
/// <remarks>
/// This property returns the effective version height offset, which takes into account
/// the <see cref="VersionOptions.VersionHeightOffsetAppliesTo"/> property. If that property
/// is set and the version has changed such that the version height would be reset, this
/// will return 0 instead of the configured offset.
/// </remarks>
public int VersionHeightOffset => this.VersionOptions?.EffectiveVersionHeightOffset ?? 0;

/// <summary>
/// Gets or sets the ref (branch or tag) being built.
Expand Down
5 changes: 5 additions & 0 deletions src/NerdBank.GitVersioning/version.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
"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.",
"default": 0
},
"versionHeightOffsetAppliesTo": {
"type": "string",
"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.",
"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\\}))*)?$"
},
"buildNumberOffset": {
"type": "integer",
"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.",
Expand Down
41 changes: 41 additions & 0 deletions test/Nerdbank.GitVersioning.Tests/ReleaseManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,47 @@ public void PrepareRelease_WithCustomCommitMessagePattern(string initialVersion,
Assert.Equal(expectedCommitMessage, releaseBranchCommit.MessageShort);
}

[Fact]
public void PrepareRelease_ResetsVersionHeightOffsetAppliesTo()
{
// create and configure repository
this.InitializeSourceControl();

var initialVersionOptions = new VersionOptions()
{
Version = SemanticVersion.Parse("1.0-beta"),
VersionHeightOffset = 5,
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("1.0-beta"),
};

var expectedReleaseVersionOptions = new VersionOptions()
{
Version = SemanticVersion.Parse("1.0"),
VersionHeightOffset = 5,
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("1.0-beta"),
};

var expectedMainVersionOptions = new VersionOptions()
{
Version = SemanticVersion.Parse("1.1-alpha"),
};

// create version.json
this.WriteVersionFile(initialVersionOptions);

Commit tipBeforePrepareRelease = this.LibGit2Repository.Head.Tip;

var releaseManager = new ReleaseManager();
releaseManager.PrepareRelease(this.RepoPath);

this.SetContextToHead();
VersionOptions newVersion = this.Context.VersionFile.GetVersion();
Assert.Equal(expectedMainVersionOptions, newVersion);

VersionOptions releaseVersion = this.GetVersionOptions(committish: this.LibGit2Repository.Branches["v1.0"].Tip.Sha);
Assert.Equal(expectedReleaseVersionOptions, releaseVersion);
}

/// <inheritdoc/>
protected override void InitializeSourceControl(bool withInitialCommit = true)
{
Expand Down
57 changes: 57 additions & 0 deletions test/Nerdbank.GitVersioning.Tests/VersionOracleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,63 @@ public void HeightInBuildMetadata()
Assert.Equal(2, oracle.VersionHeightOffset);
}

[Fact]
public void VersionHeightOffsetAppliesTo_Matching()
{
// When VersionHeightOffsetAppliesTo matches the current version, the offset should be applied
VersionOptions workingCopyVersion = new VersionOptions
{
Version = SemanticVersion.Parse("7.8.9-beta"),
VersionHeightOffset = 5,
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8.9-beta"),
};
this.WriteVersionFile(workingCopyVersion);
this.InitializeSourceControl();
var oracle = new VersionOracle(this.Context);

// The offset should be applied because the version matches
Assert.Equal(5, oracle.VersionHeightOffset);
Assert.Equal(1, oracle.VersionHeight);
}

[Fact]
public void VersionHeightOffsetAppliesTo_NotMatching()
{
// When VersionHeightOffsetAppliesTo doesn't match the current version, the offset should NOT be applied
VersionOptions workingCopyVersion = new VersionOptions
{
Version = SemanticVersion.Parse("7.9-beta"),
VersionHeightOffset = 5,
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8-beta"),
};
this.WriteVersionFile(workingCopyVersion);
this.InitializeSourceControl();
var oracle = new VersionOracle(this.Context);

// The offset should NOT be applied because the version changed (7.8 -> 7.9)
Assert.Equal(0, oracle.VersionHeightOffset);
Assert.Equal(1, oracle.VersionHeight);
}

[Fact]
public void VersionHeightOffsetAppliesTo_BuildNumberChange()
{
// When VersionHeightOffsetAppliesTo has a different build number, the offset should NOT be applied
VersionOptions workingCopyVersion = new VersionOptions
{
Version = SemanticVersion.Parse("7.9-beta"),
VersionHeightOffset = 5,
VersionHeightOffsetAppliesTo = SemanticVersion.Parse("7.8-beta"),
};
this.WriteVersionFile(workingCopyVersion);
this.InitializeSourceControl();
var oracle = new VersionOracle(this.Context);

// The offset should NOT be applied because the minor version changed (7.8 -> 7.9)
Assert.Equal(0, oracle.VersionHeightOffset);
Assert.Equal(1, oracle.VersionHeight);
}

[Theory]
[InlineData("7.8.9-foo.25", "7.8.9-foo-0025")]
[InlineData("7.8.9-foo.25s", "7.8.9-foo-25s")]
Expand Down
Loading