diff --git a/Buildvana.slnx b/Buildvana.slnx
index 1109e4fe..21087ba9 100644
--- a/Buildvana.slnx
+++ b/Buildvana.slnx
@@ -44,6 +44,7 @@
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2d7d32c8..129c2a5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- the GitHub token (`github.tokenEnv`, default `GITHUB_TOKEN`): names the environment variable that holds the token used for release operations.
- `buildvana.json` accepts a `git.identity` (`{ name, email }`) section describing the author/committer for automated commits. It is validated and exposed, but not yet wired to release commits.
- The `ThisAssemblyClass` SDK module, removed along with code generation tasks after v1.0.0-alpha.20, has been reintroduced. Setting the `GenerateThisAssemblyClass` property to `true` (default: `false`) in a C# project generates a `ThisAssembly` static class containing constants defined via `ThisAssemblyConstant` items, using the syntax documented in [docs/ConstantsSyntax.md](docs/ConstantsSyntax.md). A set of default constants (assembly version, company, product, etc.) is defined unless the `EnableDefaultThisAssemblyConstants` property is set to `false`; the class name and namespace can be customized via the `ThisAssemblyClassName` and `ThisAssemblyClassNamespace` properties. Unlike its previous incarnation, the feature is implemented as a Roslyn incremental source generator, and supports C# projects only: setting `GenerateThisAssemblyClass` to `true` in a project in any other language raises warning BVSDK2300.
+- Buildvana now computes project versions natively, replacing Nerdbank.GitVersioning end-to-end (see the corresponding breaking change below). The single source of truth for the version is a plain-text `VERSION` file at the repository root, holding a `MAJOR.MINOR[-[tag]]` specification; the patch number is the Git height of the version line, i.e. the number of commits since the last change of `MAJOR.MINOR`, computed with the same rules as Nerdbank.GitVersioning (counting from 1 at the bump commit, longest path across merges; prerelease-only edits to `VERSION` do not reset the height). Versioning policy lives in `buildvana.json`:
+ - `release.branches`: regular expressions (matched against the short branch name) identifying branches that produce public releases. On other branches, and in detached-HEAD state, the build is a non-public release, and the informational version carries a short commit ID (`1.2.3-preview.g0123456789` on prereleases, `1.2.3-g0123456789` on stable versions);
+ - `versioning.prereleaseTag`: the effective prerelease tag (e.g. `preview`), required when `VERSION` marks a prerelease line (the tag text in `VERSION` itself is informational only);
+ - `versioning.assemblyVersionPrecision` (`major` | `minor` | `build`, default `major`): how much of the computed version goes into `AssemblyVersion`.
+
+ With a `VERSION` file present, every project built with Buildvana SDK gets `$(Version)`, `$(PackageVersion)`, `$(AssemblyVersion)` (precision-controlled), `$(FileVersion)` (`MAJOR.MINOR.HEIGHT.0`), and `$(InformationalVersion)` computed at build time by a compiled task using LibGit2Sharp — no `git` executable and no external package required — under plain `dotnet build` as well as `MSBuild.exe` and `bv`. The `UseVersioning` property overrides the automatic module activation in both directions. `bv release` now computes versions the same way, in-process.
+- Versioned C# projects get the generated `ThisAssembly` class by default: when the `Versioning` module is active, `GenerateThisAssemblyClass` defaults to `true`, and the module contributes versioning constants alongside the default assembly-attribute constants: `SimpleVersion`, `SemVer`, `IsPublicRelease`, `IsPrerelease`, and `GitCommitId`. Compared with Nerdbank.GitVersioning's `ThisAssembly`, the `GitCommitDate`/`GitCommitAuthorDate` and `PublicKey`/`PublicKeyToken` constants are not provided, while `AssemblyDescription`, `SimpleVersion`, and `SemVer` are new.
### Changes to existing features
@@ -69,6 +76,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **BREAKING CHANGE**: The `--main-branch` global option has been removed, along with `bv`'s main-branch discovery. Documentation generation on release is now gated by `release.generateDocsFrom` in `buildvana.json`: a list of regular expressions matched against the current short branch name (default `["^main$", "^master$"]`, reproducing the previous main/master discovery). The human-curated changelog permalink in generated release notes now points at the release branch itself rather than the discovered main branch.
- **BREAKING CHANGE**: The `--unstable-changelog` and `--require-changelog` options of `bv release` have been removed with no CLI replacement; changelog policy is repository-stable, not per-invocation. Configure it in `buildvana.json` instead: `release.changelogUpdates` (`none` | `stable` | `all`, default `stable`) selects which releases update the changelog, and `release.emptyChangelog` provides substitute text for an empty "Unreleased changes" section (when unset, an empty section fails the release, matching the previous `--require-changelog` default of `true`).
- `bv`'s build commands (`clean`, `restore`, `build`, `test`, `pack`) and `release` now observe cancellation. Pressing Ctrl-C (or a host cancelling the operation) stops the pipeline promptly: it stops launching further steps and terminates the running `dotnet` child process instead of waiting for it to finish, then `bv` exits with code 130. Partial build output may be left behind on cancellation; `bv clean` recovers.
+- **BREAKING CHANGE**: Nerdbank.GitVersioning has been removed from Buildvana SDK and `bv`. Versions are now computed natively from a `VERSION` file and `buildvana.json` keys (see _New features_ above): the `NerdbankGitVersioning` SDK module is gone, the `Nerdbank.GitVersioning` package is no longer injected into projects, `version.json` is no longer read, and `bv` no longer invokes the `nbgv` CLI. The `GetBuildVersion` target name is retained (as a real target or a stub) for targets that depend on it. To migrate a repository:
+ - create a `VERSION` file at the repository root holding the `version` value from `version.json` (e.g. `2.0-preview`); Git heights remain comparable across the migration, so the computed version does not regress within a `MAJOR.MINOR` line;
+ - move `publicReleaseRefSpec` to `release.branches` in `buildvana.json`, converting refspec patterns to short branch-name regular expressions (e.g. `^refs/heads/main$` → `^main$`);
+ - move `release.firstUnstableTag` to `versioning.prereleaseTag`, and `assemblyVersion.precision` to `versioning.assemblyVersionPrecision`;
+ - delete `version.json` and remove `nbgv` from your tool manifest, if present.
+- **BREAKING CHANGE**: The `UseNerdbankGitVersioning` property has been renamed to `UseVersioning`. The old name still works as an alias (when `UseVersioning` itself is not set), but raises warning BVSDK2001 and will be removed in a future version.
+- **BREAKING CHANGE**: `version.json` features without a native equivalent are dropped: `pathFilters` is not supported (the version height is always computed from the whole history of the version line), nested `version.json` files are not supported (`VERSION` lives at the repository root only), and the NuGet package version scheme is fixed at SemVer 2.0.
### Bugs fixed in this release
diff --git a/README.md b/README.md
index 6b0bb143..6405d64a 100644
--- a/README.md
+++ b/README.md
@@ -59,7 +59,7 @@ However, Buildvana SDK has already been used successfully in production, for bot
### Benefits
- Helps you keep your project files clean and concise - even better than "plain" MSBuild SDKs
-- Single source of truth for assembly versions (via [`Nerdbank.GitVersioning`(https://github.com/dotnet/Nerdbank.GitVersioning)])
+- Single source of truth for assembly versions (a plain-text `VERSION` file, with the patch number computed from Git height)
- Single source of truth for package licenses and copyright notices
- More auto-generated assembly information (`ClsCompliant`, `COMVisible`)
- Automatic configuration of commonly-used code analyzers
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..6fc53064
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+2.0-preview
diff --git a/buildvana.json b/buildvana.json
new file mode 100644
index 00000000..e6f12306
--- /dev/null
+++ b/buildvana.json
@@ -0,0 +1,9 @@
+{
+ "release": {
+ "branches": ["^main$", "^v\\d+\\.\\d+$"]
+ },
+ "versioning": {
+ "prereleaseTag": "preview",
+ "assemblyVersionPrecision": "major"
+ }
+}
diff --git a/docs/DirectoryStructure.md b/docs/DirectoryStructure.md
index d2ebef87..bb106520 100644
--- a/docs/DirectoryStructure.md
+++ b/docs/DirectoryStructure.md
@@ -268,4 +268,12 @@ TODO
## `VERSION`
-TODO
+The single source of truth for the version of your product: a plain-text file, in the home directory, holding a single `MAJOR.MINOR[-[tag]]` version specification, for example:
+
+```text
+2.0-preview
+```
+
+The presence of `-` after the minor version marks a prerelease line; the tag text after it is optional and informational (the effective prerelease tag comes from the `versioning.prereleaseTag` key of `buildvana.json`). The patch number is not stored in the file: it is the Git height of the version line, i.e. the number of commits since the last change of `MAJOR.MINOR`.
+
+When a `VERSION` file is present, the Buildvana SDK computes `Version`, `AssemblyVersion`, `FileVersion`, and `InformationalVersion` for all projects in the repository; `bv` uses the same computation for releases and rewrites the file when advancing the version.
diff --git a/docs/SdkDiagnostics.md b/docs/SdkDiagnostics.md
index c746df99..da07ec70 100644
--- a/docs/SdkDiagnostics.md
+++ b/docs/SdkDiagnostics.md
@@ -16,7 +16,7 @@
- [StandardAnalyzers module (1700-1799)](#standardanalyzers-module-1700-1799)
- [XmlDocumentation module (1800-1899)](#xmldocumentation-module-1800-1899)
- [AlternatePack module (1900-1999)](#alternatepack-module-1900-1999)
-- [NerdbankGitVersioning module (2000-2099)](#nerdbankgitversioning-module-2000-2099)
+- [Versioning module (2000-2099)](#versioning-module-2000-2099)
- [ReleaseAssetList module (2100-2199)](#releaseassetlist-module-2100-2199)
- [Wine module (2200-2299)](#wine-module-2200-2299)
- [ThisAssemblyClass module (2300-2399)](#thisassemblyclass-module-2300-2399)
@@ -109,11 +109,12 @@ This module has no associated diagnostics.
| BVSDK1901 | Error | InnoSetup script '...' referenced by '...' does not exist. | An `InnoSetup` item's `Script` metadata refers to a non-existing file. |
| BVSDK1902 | Error | InnoSetup item '...' refers to non-existent PublishFolder '...'. | An `InnoSetup` item has a `SourcePublishFolder` metadata, but no `PublishFolder` item exists with the specified name. |
-## NerdbankGitVersioning module (2000-2099)
+## Versioning module (2000-2099)
-| Code | Severity | Message | Description |
-| --------- | :------: | ------------------------------------------ | -------------------------------------------------------------------------------------------------- |
-| BVSDK2000 | Error | Version specification JSON file not found. | A `version.json` or `.version.json` file for the project was not found within the repository root. |
+| Code | Severity | Message | Description |
+| --------- | :------: | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| BVSDK2000 | Error | Version file (VERSION) not found in home directory. | The `UseVersioning` property was explicitly set to `true`, but no `VERSION` file was found in the home directory. |
+| BVSDK2001 | Warning | The UseNerdbankGitVersioning property is deprecated; use UseVersioning instead. | The project sets the `UseNerdbankGitVersioning` property, which is a deprecated alias for `UseVersioning` kept for compatibility with older projects. |
## ReleaseAssetList module (2100-2199)
diff --git a/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs b/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs
index ac2605ed..973ffd31 100644
--- a/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs
+++ b/src/Buildvana.Core.JsonSchema/JsonSourceMap.Frame.cs
@@ -3,6 +3,9 @@
namespace Buildvana.Core.JsonSchema;
+// ReSharper disable once ClassCannotBeInstantiated -- false positive: ReSharper analyzes this partial part
+// in isolation, missing the instantiation in JsonSourceMap.cs (the class has a private constructor and is
+// created by its static Build method).
partial class JsonSourceMap
{
// Tracks one open container while walking the document, so a value's pointer can be built from its parent.
diff --git a/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj b/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj
new file mode 100644
index 00000000..d5b03336
--- /dev/null
+++ b/src/Buildvana.Core.Testing/Buildvana.Core.Testing.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Buildvana testing helpers
+ Shared test harnesses for Buildvana test projects, e.g. temporary Git repositories and home directories.
+ $(StandardTfm)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs b/src/Buildvana.Core.Testing/TempGitRepo.cs
similarity index 55%
rename from tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs
rename to src/Buildvana.Core.Testing/TempGitRepo.cs
index 509c07e4..084a8398 100644
--- a/tests/Buildvana.Core.Versioning.Tests/TempGitRepo.cs
+++ b/src/Buildvana.Core.Testing/TempGitRepo.cs
@@ -1,15 +1,26 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System;
+using System.IO;
using System.Text;
using LibGit2Sharp;
-internal sealed class TempGitRepo : IDisposable
+namespace Buildvana.Core.Testing;
+
+///
+/// A disposable real Git repository in a temporary directory, for tests exercising Git-dependent code.
+///
+public sealed class TempGitRepo : IDisposable
{
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private readonly Repository _repository;
+ ///
+ /// Initializes a new instance of the class, creating and initializing
+ /// a Git repository in a newly-created temporary directory.
+ ///
public TempGitRepo()
{
RootPath = Directory.CreateTempSubdirectory("bv-test-repo-").FullName;
@@ -17,15 +28,34 @@ public TempGitRepo()
_repository = new Repository(RootPath);
}
+ ///
+ /// Gets the full path of the repository's root directory.
+ ///
public string RootPath { get; }
+ ///
+ /// Gets the friendly name of the current branch.
+ ///
public string CurrentBranchName => _repository.Head.FriendlyName;
+ ///
+ /// Gets the SHA of the current HEAD commit.
+ ///
public string HeadSha => _repository.Head.Tip.Sha;
+ ///
+ /// Writes a file in the repository's root directory.
+ ///
+ /// The name of the file, relative to the root directory.
+ /// The content of the file.
+ /// The file encoding; UTF-8 without BOM if unspecified.
public void WriteFile(string name, string content, Encoding? encoding = null)
=> File.WriteAllText(Path.Combine(RootPath, name), content, encoding ?? Utf8NoBom);
+ ///
+ /// Stages all changes and creates a commit, which may be empty.
+ ///
+ /// The commit message.
public void CommitAll(string message = "Test commit")
{
Commands.Stage(_repository, "*");
@@ -33,16 +63,31 @@ public void CommitAll(string message = "Test commit")
_ = _repository.Commit(message, signature, signature, new CommitOptions { AllowEmptyCommit = true });
}
+ ///
+ /// Creates a new branch at the current HEAD commit and checks it out.
+ ///
+ /// The name of the new branch.
public void CheckoutNewBranch(string name)
{
var branch = _repository.CreateBranch(name);
_ = Commands.Checkout(_repository, branch);
}
+ ///
+ /// Checks out an existing branch.
+ ///
+ /// The name of the branch.
public void Checkout(string name) => _ = Commands.Checkout(_repository, _repository.Branches[name]);
+ ///
+ /// Checks out the current HEAD commit directly, detaching HEAD from any branch.
+ ///
public void CheckoutDetached() => _ = Commands.Checkout(_repository, _repository.Head.Tip);
+ ///
+ /// Merges a branch into the current branch, always creating a merge commit.
+ ///
+ /// The name of the branch to merge.
public void Merge(string branchName)
{
var signature = new Signature("Test", "test@example.com", DateTimeOffset.Now);
@@ -50,6 +95,7 @@ public void Merge(string branchName)
_ = _repository.Merge(_repository.Branches[branchName], signature, options);
}
+ ///
public void Dispose()
{
_repository.Dispose();
diff --git a/src/Buildvana.Core.Testing/TempHome.cs b/src/Buildvana.Core.Testing/TempHome.cs
new file mode 100644
index 00000000..79218d6a
--- /dev/null
+++ b/src/Buildvana.Core.Testing/TempHome.cs
@@ -0,0 +1,44 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using Buildvana.Core.HomeDirectory;
+
+namespace Buildvana.Core.Testing;
+
+///
+/// A disposable temporary home directory (with no Git repository), for tests exercising file-based code
+/// that resolves paths against a home directory.
+///
+public sealed class TempHome : IDisposable
+{
+ private readonly DirectoryInfo _directory = Directory.CreateTempSubdirectory("bv-test-home-");
+
+ ///
+ /// Gets the full path of the home directory.
+ ///
+ public string RootPath => _directory.FullName;
+
+ ///
+ /// Gets a home directory provider resolving to .
+ ///
+ public FixedHomeDirectoryProvider Provider => new(RootPath);
+
+ ///
+ /// Writes a file in the home directory.
+ ///
+ /// The name of the file, relative to the home directory.
+ /// The content of the file.
+ public void WriteFile(string name, string content) => File.WriteAllText(Path.Combine(RootPath, name), content);
+
+ ///
+ /// Reads a file in the home directory.
+ ///
+ /// The name of the file, relative to the home directory.
+ /// The content of the file.
+ public string ReadFile(string name) => File.ReadAllText(Path.Combine(RootPath, name));
+
+ ///
+ public void Dispose() => _directory.Delete(recursive: true);
+}
diff --git a/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs b/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs
new file mode 100644
index 00000000..b09ec6ad
--- /dev/null
+++ b/src/Buildvana.Core.Versioning/GitHeightCalculator-nativeLibrary.cs
@@ -0,0 +1,40 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.IO;
+using System.Runtime.InteropServices;
+using LibGit2Sharp;
+
+namespace Buildvana.Core.Versioning;
+
+partial class GitHeightCalculator
+{
+ // LibGit2Sharp normally locates its native library through the host's dependency context (deps.json).
+ // MSBuild task hosts load task assemblies without one, so when the conventional runtimes/{rid}/native
+ // directory exists next to the managed assembly, point LibGit2Sharp at it explicitly.
+ // This must happen before the first native call in the process, hence the static constructor: it is
+ // guaranteed to run before any member of the library's only Git touchpoint is used.
+ static GitHeightCalculator()
+ {
+ var baseDirectory = Path.GetDirectoryName(typeof(Repository).Assembly.Location);
+ if (string.IsNullOrEmpty(baseDirectory))
+ {
+ return;
+ }
+
+ var nativeDirectory = Path.Combine(baseDirectory, "runtimes", RuntimeInformation.RuntimeIdentifier, "native");
+ if (!Directory.Exists(nativeDirectory))
+ {
+ return;
+ }
+
+ try
+ {
+ GlobalSettings.NativeLibraryPath = nativeDirectory;
+ }
+ catch (LibGit2SharpException)
+ {
+ // The native library is already loaded, so resolution has evidently succeeded some other way.
+ }
+ }
+}
diff --git a/src/Buildvana.Core.Versioning/GitHeightCalculator.cs b/src/Buildvana.Core.Versioning/GitHeightCalculator.cs
index d2fb2d33..1389503d 100644
--- a/src/Buildvana.Core.Versioning/GitHeightCalculator.cs
+++ b/src/Buildvana.Core.Versioning/GitHeightCalculator.cs
@@ -26,7 +26,7 @@ namespace Buildvana.Core.Versioning;
/// This class is the library's only Git touchpoint; its public surface deliberately exposes no
/// LibGit2Sharp types.
///
-public sealed class GitHeightCalculator
+public sealed partial class GitHeightCalculator
{
private readonly string _versionFileName;
@@ -40,6 +40,28 @@ public GitHeightCalculator(string versionFileName)
_versionFileName = versionFileName;
}
+ ///
+ /// Gets an opaque token describing the repository state that depends on:
+ /// the current HEAD reference and the commit it points to. For a given version file name and
+ /// version specification, equal tokens guarantee equal results, because the
+ /// commit graph reachable from a commit is immutable.
+ ///
+ /// The directory of the Git repository.
+ /// A state token, or if there is no Git repository at
+ /// .
+ public static string? TryGetRepositoryStateToken(string repositoryDirectory)
+ {
+ Guard.IsNotNullOrEmpty(repositoryDirectory);
+ if (!Repository.IsValid(repositoryDirectory))
+ {
+ return null;
+ }
+
+ using var repository = new Repository(repositoryDirectory);
+ var head = repository.Head;
+ return FormattableString.Invariant($"{head.CanonicalName}\0{head.Tip?.Sha}");
+ }
+
///
/// Calculates the Git height of the version line identified by in the
/// repository at , along with the repository facts needed to
diff --git a/src/Buildvana.Core.Versioning/VersionFile.cs b/src/Buildvana.Core.Versioning/VersionFile.cs
new file mode 100644
index 00000000..70ebbb30
--- /dev/null
+++ b/src/Buildvana.Core.Versioning/VersionFile.cs
@@ -0,0 +1,127 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Security;
+using Buildvana.Core.HomeDirectory;
+using CommunityToolkit.Diagnostics;
+
+namespace Buildvana.Core.Versioning;
+
+///
+/// Represents the repository's version file, for reading the version specification and applying
+/// version advances.
+///
+///
+/// The version file (, in the home directory) is plain text holding a single
+/// MAJOR.MINOR[-[tag]] version specification; see the Parse method in
+/// for the accepted format.
+///
+public sealed class VersionFile
+{
+ ///
+ /// The name of the version file, relative to the home directory.
+ ///
+ public const string FileName = "VERSION";
+
+ private VersionFile(string absolutePath, VersionSpec spec)
+ {
+ Path = absolutePath;
+ Spec = spec;
+ }
+
+ ///
+ /// Gets the absolute path of the version file.
+ ///
+ public string Path { get; }
+
+ ///
+ /// Gets the version specification read from the version file, as possibly modified by
+ /// .
+ ///
+ public VersionSpec Spec { get; private set; }
+
+ ///
+ /// Constructs a instance by loading the repository's version file.
+ ///
+ /// The home directory provider used to locate the version file.
+ /// A newly-created , representing the loaded data.
+ /// The version file is absent, cannot be read, or does not
+ /// contain a valid version specification.
+ public static VersionFile Load(IHomeDirectoryProvider home)
+ {
+ Guard.IsNotNull(home);
+ var path = System.IO.Path.Combine(home.HomeDirectory, FileName);
+ return new(path, ReadSpec(path));
+ }
+
+ ///
+ /// Applies a version specification change to this instance.
+ ///
+ /// A constant representing the kind of change to apply.
+ /// If the property actually changed as a result of
+ /// , ; otherwise, .
+ ///
+ /// This method does not save the modified version file; call for that.
+ ///
+ public bool ApplyChange(VersionSpecChange change)
+ {
+ (Spec, var changed) = Spec.ApplyChange(change);
+ return changed;
+ }
+
+ ///
+ /// Saves the version file, possibly with a modified , back to the repository.
+ ///
+ /// The informational tag text to write after the prerelease marker,
+ /// usually the configured . It is only written when
+ /// is a prerelease and the tag fits the version file grammar; otherwise a bare
+ /// - marks the prerelease.
+ /// The version file cannot be written.
+ public void Save(string? prereleaseTag = null)
+ {
+ var text = Format(Spec, prereleaseTag);
+ try
+ {
+ File.WriteAllText(Path, text);
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException)
+ {
+ throw new BuildFailedException($"Could not write to {Path}: {e.Message}", e);
+ }
+ }
+
+ private static VersionSpec ReadSpec(string path)
+ {
+ BuildFailedException.ThrowIfNot(File.Exists(path), $"Version file {path} not found.");
+ string text;
+ try
+ {
+ text = File.ReadAllText(path);
+ }
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException)
+ {
+ throw new BuildFailedException($"Could not read from {path}: {e.Message}", e);
+ }
+
+ text = text.Trim();
+ BuildFailedException.ThrowIfNot(
+ VersionSpec.TryParse(text, out var spec),
+ $"{path} contains an invalid version specification '{text}'.");
+ return spec;
+ }
+
+ private static string Format(VersionSpec spec, string? prereleaseTag)
+ {
+ var stable = FormattableString.Invariant($"{spec.Major}.{spec.Minor}");
+ if (!spec.Prerelease)
+ {
+ return stable + "\n";
+ }
+
+ var tagged = stable + "-" + prereleaseTag;
+ var tagFits = prereleaseTag is not null && VersionSpec.TryParse(tagged, out _);
+ return (tagFits ? tagged : stable + "-") + "\n";
+ }
+}
diff --git a/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs b/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs
index 04ce94ce..34469d15 100644
--- a/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs
+++ b/src/Buildvana.Core.Versioning/VersionSpecExtensions-private.cs
@@ -5,6 +5,7 @@
namespace Buildvana.Core.Versioning;
+#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716
partial class VersionSpecExtensions
{
private static readonly Regex VersionSpecRegex = GetVersionSpecRegex();
diff --git a/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs b/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs
index 7090e9e2..9e8a763b 100644
--- a/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs
+++ b/src/Buildvana.Core.Versioning/VersionSpecExtensions.cs
@@ -75,4 +75,53 @@ public static bool TryParse([NotNullWhen(true)] string? str, [MaybeNullWhen(fals
return true;
}
}
+
+ extension(VersionSpec @this)
+ {
+ ///
+ /// Gets a that represents the same version as this instance and is not
+ /// a prerelease.
+ ///
+ /// This instance if it is not a prerelease; otherwise, a newly-created
+ /// .
+ public VersionSpec Stable() => @this.Prerelease ? @this with { Prerelease = false } : @this;
+
+ ///
+ /// Gets a that represents the same version as this instance and is
+ /// a prerelease.
+ ///
+ /// This instance if it is a prerelease; otherwise, a newly-created
+ /// .
+ public VersionSpec Unstable() => @this.Prerelease ? @this : @this with { Prerelease = true };
+
+ ///
+ /// Gets a that represents the next minor version with respect to this
+ /// instance. The new version line starts as a prerelease.
+ ///
+ /// A newly-created .
+ public VersionSpec NextMinor() => new(@this.Major, @this.Minor + 1, true);
+
+ ///
+ /// Gets a that represents the next major version with respect to this
+ /// instance. The new version line starts as a prerelease.
+ ///
+ /// A newly-created .
+ public VersionSpec NextMajor() => new(@this.Major + 1, 0, true);
+
+ ///
+ /// Gets a that represents the result of applying the specified change
+ /// to this instance.
+ ///
+ /// A constant representing the kind of change to apply.
+ /// A tuple of the result of applying to this instance, and a
+ /// value indicating whether the result differs from this instance.
+ public (VersionSpec Result, bool Changed) ApplyChange(VersionSpecChange change) => change switch
+ {
+ VersionSpecChange.Unstable => @this.Prerelease ? (@this, false) : (@this.Unstable(), true),
+ VersionSpecChange.Stable => @this.Prerelease ? (@this.Stable(), true) : (@this, false),
+ VersionSpecChange.Minor => (@this.NextMinor(), true),
+ VersionSpecChange.Major => (@this.NextMajor(), true),
+ _ => (@this, false),
+ };
+ }
}
diff --git a/src/Buildvana.Core.Versioning/VersioningService.cs b/src/Buildvana.Core.Versioning/VersioningService.cs
index f6ef459e..f63677c1 100644
--- a/src/Buildvana.Core.Versioning/VersioningService.cs
+++ b/src/Buildvana.Core.Versioning/VersioningService.cs
@@ -2,8 +2,6 @@
// See the LICENSE file in the project root for full license information.
using System;
-using System.IO;
-using System.Security;
using Buildvana.Core.Configuration;
using Buildvana.Core.ConsoleOutput;
using Buildvana.Core.HomeDirectory;
@@ -17,17 +15,12 @@ namespace Buildvana.Core.Versioning;
/// and exposes it in the string forms needed by builds and releases.
///
///
-/// The version file (, in the home directory) holds a
+/// The version file (, in the home directory) holds a
/// MAJOR.MINOR[-[tag]] specification; the patch number is the Git height of the version line
/// (see ). All values are computed once, on construction.
///
public sealed class VersioningService
{
- ///
- /// The name of the version file, relative to the home directory.
- ///
- public const string VersionFileName = "VERSION";
-
private const int ShortCommitIdLength = 10;
///
@@ -54,7 +47,7 @@ public VersioningService(
Guard.IsNotNull(heightCalculator);
var homeDirectory = home.HomeDirectory;
- Spec = ReadVersionFile(Path.Combine(homeDirectory, VersionFileName));
+ Spec = VersionFile.Load(home).Spec;
var prereleaseTag = Spec.Prerelease ? GetPrereleaseTag(settings) : null;
var facts = heightCalculator.Calculate(homeDirectory, Spec);
Height = facts.Height;
@@ -63,6 +56,7 @@ public VersioningService(
SimpleVersion = FormattableString.Invariant($"{Spec.Major}.{Spec.Minor}.{Height}");
SemVer = prereleaseTag is null ? SimpleVersion : $"{SimpleVersion}-{prereleaseTag}";
AssemblyVersion = ComputeAssemblyVersion(Spec, Height, settings.AssemblyVersionPrecision);
+ FileVersion = FormattableString.Invariant($"{SimpleVersion}.0");
InformationalVersion = ComputeInformationalVersion(SemVer, Spec.Prerelease, IsPublicRelease, CommitId);
var publicity = IsPublicRelease ? "public release" : "not a public release";
reporter.Info(FormattableString.Invariant($"Version {SemVer} (height {Height}, {publicity})"));
@@ -112,38 +106,24 @@ public VersioningService(
///
public string AssemblyVersion { get; }
+ ///
+ /// Gets the file version: at full precision, plus a fourth component
+ /// that is always 0.
+ ///
+ public string FileVersion { get; }
+
///
/// Gets the informational version: , plus a g-prefixed short commit ID
/// appended to the prerelease part when the build is not a public release.
///
public string InformationalVersion { get; }
- private static VersionSpec ReadVersionFile(string path)
- {
- BuildFailedException.ThrowIfNot(File.Exists(path), $"Version file {path} not found.");
- string text;
- try
- {
- text = File.ReadAllText(path);
- }
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException)
- {
- throw new BuildFailedException($"Could not read from {path}: {e.Message}", e);
- }
-
- text = text.Trim();
- BuildFailedException.ThrowIfNot(
- VersionSpec.TryParse(text, out var spec),
- $"{path} contains an invalid version specification '{text}'.");
- return spec;
- }
-
private static string GetPrereleaseTag(VersioningSettings settings)
{
var tag = settings.PrereleaseTag;
BuildFailedException.ThrowIf(
string.IsNullOrEmpty(tag),
- $"{VersionFileName} specifies a prerelease version, but versioning.prereleaseTag is not set in the configuration file.");
+ $"{VersionFile.FileName} specifies a prerelease version, but versioning.prereleaseTag is not set in the configuration file.");
var isValid = SemanticVersion.TryParse(FormattableString.Invariant($"0.0.0-{tag}"), out _);
BuildFailedException.ThrowIfNot(
isValid,
diff --git a/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj b/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj
index fe394ad0..302b4112 100644
--- a/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj
+++ b/src/Buildvana.Sdk.Tasks/Buildvana.Sdk.Tasks.csproj
@@ -19,7 +19,10 @@
+
+
+
diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs
new file mode 100644
index 00000000..f1e8fc36
--- /dev/null
+++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion-caching.cs
@@ -0,0 +1,93 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using Buildvana.Core.Configuration;
+using Buildvana.Core.ConsoleOutput;
+using Buildvana.Core.HomeDirectory;
+using Buildvana.Core.Versioning;
+using Buildvana.Sdk.Internal;
+
+namespace Buildvana.Sdk.Tasks;
+
+partial class ComputeVersion
+{
+ private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal);
+
+ private static CachedVersion GetOrComputeVersion(string homeDirectory, IReporter reporter)
+ {
+ homeDirectory = Path.GetFullPath(homeDirectory);
+ var fingerprint = ComputeFingerprint(homeDirectory);
+ if (fingerprint is null)
+ {
+ return ComputeCore(homeDirectory, reporter, null);
+ }
+
+ if (Cache.TryGetValue(homeDirectory, out var cached) && cached.Fingerprint == fingerprint)
+ {
+ return cached;
+ }
+
+ var computed = ComputeCore(homeDirectory, reporter, fingerprint);
+ Cache[homeDirectory] = computed;
+ return computed;
+ }
+
+ private static CachedVersion ComputeCore(string homeDirectory, IReporter reporter, string? fingerprint)
+ {
+ var service = new VersioningService(
+ reporter,
+ new FixedHomeDirectoryProvider(homeDirectory),
+ new VersioningSettings(BuildvanaConfigLoader.Load(homeDirectory)),
+ new GitHeightCalculator(VersionFile.FileName));
+ return new CachedVersion(
+ fingerprint,
+ service.SimpleVersion,
+ service.SemVer,
+ service.AssemblyVersion,
+ service.FileVersion,
+ service.InformationalVersion,
+ service.IsPublicRelease,
+ service.IsPrerelease,
+ service.CommitId ?? string.Empty,
+ service.Height);
+ }
+
+ // The fingerprint captures everything the computed version depends on: the working-tree VERSION file,
+ // the configuration file, and the repository state (the height walk only sees commits reachable from HEAD).
+ // A null fingerprint disables caching, letting VersioningService surface the proper error.
+ private static string? ComputeFingerprint(string homeDirectory)
+ {
+ try
+ {
+ var versionPath = Path.Combine(homeDirectory, VersionFile.FileName);
+ if (!File.Exists(versionPath))
+ {
+ return null;
+ }
+
+ var repositoryStateToken = GitHeightCalculator.TryGetRepositoryStateToken(homeDirectory);
+ if (repositoryStateToken is null)
+ {
+ return null;
+ }
+
+ string?[] parts =
+ [
+ repositoryStateToken,
+ File.ReadAllText(versionPath),
+ ReadOptionalFile(Path.Combine(homeDirectory, "buildvana.json")),
+ ReadOptionalFile(Path.Combine(homeDirectory, "buildvana.jsonc")),
+ ];
+ return string.Join('\0', parts);
+ }
+ catch (Exception e) when (!e.IsFatalException())
+ {
+ return null;
+ }
+ }
+
+ private static string? ReadOptionalFile(string path) => File.Exists(path) ? File.ReadAllText(path) : null;
+}
diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs
new file mode 100644
index 00000000..041f8a1a
--- /dev/null
+++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.CachedVersion.cs
@@ -0,0 +1,23 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Buildvana.Sdk.Tasks;
+
+partial class ComputeVersion
+{
+ ///
+ /// The computed version values for a home directory, together with the repository-state fingerprint
+ /// they were computed from ( if the state could not be fingerprinted).
+ ///
+ private sealed record CachedVersion(
+ string? Fingerprint,
+ string SimpleVersion,
+ string SemVer,
+ string AssemblyVersion,
+ string FileVersion,
+ string InformationalVersion,
+ bool IsPublicRelease,
+ bool IsPrerelease,
+ string CommitId,
+ int Height);
+}
diff --git a/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs
new file mode 100644
index 00000000..9e3b458b
--- /dev/null
+++ b/src/Buildvana.Sdk.Tasks/Tasks/ComputeVersion.cs
@@ -0,0 +1,67 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Globalization;
+using Buildvana.Core;
+using Buildvana.Sdk.Resources;
+using Microsoft.Build.Framework;
+
+namespace Buildvana.Sdk.Tasks;
+
+///
+/// Computes the version being built from the repository's VERSION file, Git height, and versioning
+/// settings, exposing it in the string forms consumed by builds.
+/// Results are cached per home directory for the lifetime of the task host process, keyed on the repository
+/// state, so that multi-project builds pay for a single Git graph walk.
+///
+public sealed partial class ComputeVersion : BuildvanaSdkTask
+{
+ [Required]
+ public string HomeDirectory { get; set; } = string.Empty;
+
+ [Output]
+ public string SimpleVersion { get; private set; } = string.Empty;
+
+ [Output]
+ public string SemVer { get; private set; } = string.Empty;
+
+ [Output]
+ public string AssemblyVersion { get; private set; } = string.Empty;
+
+ [Output]
+ public string FileVersion { get; private set; } = string.Empty;
+
+ [Output]
+ public string InformationalVersion { get; private set; } = string.Empty;
+
+ [Output]
+ public bool IsPublicRelease { get; private set; }
+
+ [Output]
+ public bool IsPrerelease { get; private set; }
+
+ [Output]
+ public string CommitId { get; private set; } = string.Empty;
+
+ [Output]
+ public int Height { get; private set; }
+
+ protected override Undefined Run()
+ {
+ BuildFailedException.ThrowIfNot(
+ !string.IsNullOrEmpty(HomeDirectory),
+ string.Format(CultureInfo.InvariantCulture, Strings.MissingParameterFmt, nameof(HomeDirectory)));
+
+ var version = GetOrComputeVersion(HomeDirectory, Reporter);
+ SimpleVersion = version.SimpleVersion;
+ SemVer = version.SemVer;
+ AssemblyVersion = version.AssemblyVersion;
+ FileVersion = version.FileVersion;
+ InformationalVersion = version.InformationalVersion;
+ IsPublicRelease = version.IsPublicRelease;
+ IsPrerelease = version.IsPrerelease;
+ CommitId = version.CommitId;
+ Height = version.Height;
+ return Undefined.Value;
+ }
+}
diff --git a/src/Buildvana.Sdk/Buildvana.Sdk.csproj b/src/Buildvana.Sdk/Buildvana.Sdk.csproj
index ab27a01a..042efbfa 100644
--- a/src/Buildvana.Sdk/Buildvana.Sdk.csproj
+++ b/src/Buildvana.Sdk/Buildvana.Sdk.csproj
@@ -67,7 +67,7 @@
<_SdkVersionPropsXml>
- $(NuGetPackageVersion)
+ $(PackageVersion)
$(BV_MinRoslynVersion)
$(BV_MinRoslynVersionHint)
@@ -272,8 +272,19 @@
<_PublishedBinaries Remove="$(OutputPath)\**\*.pdb" Condition="'$(Configuration)' != 'Debug'" />
+
+ <_PublishedLibGit2NativeBinaries Include="$(OutputPath)\**\libgit2-*.so;$(OutputPath)\**\libgit2-*.dylib" />
+ <_PublishedBinaries Remove="@(_PublishedLibGit2NativeBinaries)" />
+
+
<_PublishedBinaries Remove="@(_PublishedBinaries)" />
+ <_PublishedLibGit2NativeBinaries Remove="@(_PublishedLibGit2NativeBinaries)" />
diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets
deleted file mode 100644
index 025b2464..00000000
--- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/BeforeModules.targets
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
-
-
-
-
-
-
- $([MSBuild]::GetPathOfFileAbove('version.json', '$(MSBuildProjectDirectory)'))
- $([MSBuild]::GetPathOfFileAbove('.version.json', '$(MSBuildProjectDirectory)'))
-
-
-
-
-
- false
- true
-
-
-
-
- $([System.IO.Path]::GetDirectoryName('$(BV_VersionJsonPath)'))
-
-
-
-
-
-
-
-
-
- false
-
-
-
diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets
deleted file mode 100644
index e15d8244..00000000
--- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Core.targets
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- $(NpmPackageVersion)
-
-
-
-
-
diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets b/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets
deleted file mode 100644
index 64a0dadd..00000000
--- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.targets
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets b/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets
new file mode 100644
index 00000000..79fc0863
--- /dev/null
+++ b/src/Buildvana.Sdk/Modules/Versioning/BeforeModules.targets
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ false
+
+
+
+
+ $(HomeDirectory)VERSION
+
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+ true
+
+
+
diff --git a/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets
new file mode 100644
index 00000000..be504a85
--- /dev/null
+++ b/src/Buildvana.Sdk/Modules/Versioning/Module.Core.targets
@@ -0,0 +1,72 @@
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(BV_SemVer)
+ $(BV_SemVer)
+ $(BV_AssemblyVersion)
+ $(BV_FileVersion)
+ $(BV_InformationalVersion)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets
similarity index 67%
rename from src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets
rename to src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets
index 409a7886..b6c735cb 100644
--- a/src/Buildvana.Sdk/Modules/NerdbankGitVersioning/Module.Stubs.targets
+++ b/src/Buildvana.Sdk/Modules/Versioning/Module.Stubs.targets
@@ -1,7 +1,7 @@
diff --git a/src/Buildvana.Sdk/Modules/Versioning/Module.targets b/src/Buildvana.Sdk/Modules/Versioning/Module.targets
new file mode 100644
index 00000000..3c7aa7f8
--- /dev/null
+++ b/src/Buildvana.Sdk/Modules/Versioning/Module.targets
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/src/Buildvana.Sdk/Sdk/PackageVersions.props b/src/Buildvana.Sdk/Sdk/PackageVersions.props
index 474bfc2e..51de1dc5 100644
--- a/src/Buildvana.Sdk/Sdk/PackageVersions.props
+++ b/src/Buildvana.Sdk/Sdk/PackageVersions.props
@@ -9,7 +9,6 @@
-
diff --git a/src/Buildvana.Sdk/Sdk/Sdk.props b/src/Buildvana.Sdk/Sdk/Sdk.props
index 54ee1c73..7116c677 100644
--- a/src/Buildvana.Sdk/Sdk/Sdk.props
+++ b/src/Buildvana.Sdk/Sdk/Sdk.props
@@ -113,6 +113,7 @@
$(BV_BinariesDir)Buildvana.Sdk.Tasks.dll
+
diff --git a/src/Buildvana.Tool/Buildvana.Tool.csproj b/src/Buildvana.Tool/Buildvana.Tool.csproj
index 0548890d..94355c2b 100644
--- a/src/Buildvana.Tool/Buildvana.Tool.csproj
+++ b/src/Buildvana.Tool/Buildvana.Tool.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/Buildvana.Tool/Program.cs b/src/Buildvana.Tool/Program.cs
index 34fb76d0..0ef34ca6 100644
--- a/src/Buildvana.Tool/Program.cs
+++ b/src/Buildvana.Tool/Program.cs
@@ -10,6 +10,7 @@
using Buildvana.Core.HomeDirectory;
using Buildvana.Core.Json;
using Buildvana.Core.Process;
+using Buildvana.Core.Versioning;
using Buildvana.Tool.Build;
using Buildvana.Tool.CommandLine;
using Buildvana.Tool.Infrastructure.DependencyInjection;
@@ -190,6 +191,7 @@ private static ServiceProvider BuildServiceProvider(
.AddSingleton()
.AddSingleton()
.AddSingleton(ServerAdapter.Create)
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionFile.cs b/src/Buildvana.Tool/Services/Versioning/VersionFile.cs
deleted file mode 100644
index 9a9a4af6..00000000
--- a/src/Buildvana.Tool/Services/Versioning/VersionFile.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using System.Text.Json.Nodes;
-using Buildvana.Core;
-using Buildvana.Core.HomeDirectory;
-using Buildvana.Core.Json;
-using CommunityToolkit.Diagnostics;
-
-namespace Buildvana.Tool.Services.Versioning;
-
-///
-/// Represents the version.json file, for the purpose of applying version advances.
-///
-internal sealed class VersionFile
-{
- private const string VersionJsonPath = "version.json";
- private const string VersionPropertyName = "version";
- private const string DefaultFirstUnstableTag = "preview";
-
- private readonly IJsonHelper _jsonHelper;
-
- private VersionFile(
- IJsonHelper jsonHelper,
- string absolutePath,
- VersionSpec versionSpec,
- string firstUnstableTag)
- {
- _jsonHelper = jsonHelper;
- Path = absolutePath;
- VersionSpec = versionSpec;
- FirstUnstableTag = firstUnstableTag;
- }
-
- ///
- /// Gets the absolute path of the version.json file.
- ///
- public string Path { get; }
-
- ///
- /// Gets a representing the "version" value in the version.json file.
- ///
- public VersionSpec VersionSpec { get; private set; }
-
- ///
- /// Gets the unstable tag to use for version advances.
- ///
- /// Either the "release.firstUnstableTag" value read from version.json, or "preview" as a default value.
- public string FirstUnstableTag { get; }
-
- ///
- /// Constructs a instance by loading the repository's version.json file.
- ///
- /// The home directory provider used to resolve the path of version.json.
- /// The JSON helper used to load and rewrite the file.
- /// A newly-created , representing the loaded data.
- public static VersionFile Load(IHomeDirectoryProvider home, IJsonHelper jsonHelper)
- {
- Guard.IsNotNull(home);
- Guard.IsNotNull(jsonHelper);
-
- var path = System.IO.Path.GetFullPath(VersionJsonPath, home.HomeDirectory);
- var json = jsonHelper.LoadObject(path);
- var versionStr = jsonHelper.GetPropertyValue(json, VersionPropertyName, path + " file");
- BuildFailedException.ThrowIfNot(VersionSpec.TryParse(versionStr, out var versionSpec), $"{VersionJsonPath} contains invalid version specification '{versionStr}'.");
- var firstUnstableTag = DefaultFirstUnstableTag;
- var release = json["release"];
- if (release is not null)
- {
- var firstUnstableTagNode = release["firstUnstableTag"];
- if (firstUnstableTagNode is JsonValue firstUnstableTagValue && firstUnstableTagValue.TryGetValue(out var firstUnstableTagStr) && !string.IsNullOrEmpty(firstUnstableTagStr))
- {
- firstUnstableTag = firstUnstableTagStr;
- }
- }
-
- return new(jsonHelper, path, versionSpec, firstUnstableTag);
- }
-
- ///
- /// Applies a version spec change to this instance.
- ///
- /// A constant representing the kind of change to apply.
- /// If the property is actually changed as a result of , ;
- /// otherwise, .
- ///
- /// This method does not save the modified version.json file; you will have to call
- /// the method if this method returns .
- ///
- public bool ApplyVersionSpecChange(VersionSpecChange change)
- {
- (VersionSpec, var changed) = VersionSpec.ApplyChange(change, FirstUnstableTag);
- return changed;
- }
-
- ///
- /// Saves the version.json file, possibly with a modified , back to the repository.
- ///
- public void Save()
- {
- var newVersion = VersionSpec.ToString();
- var rewritten = _jsonHelper.RewriteStringValues(
- Path,
- (propertyPath, _) => propertyPath is [VersionPropertyName] ? newVersion : null);
-
- // Load already validated that a top-level string "version" property exists, so a no-op here
- // means either the on-disk file changed underneath us or VersionSpec.ToString() produced the
- // same string the file already held — both cases would let the release flow stage stale data.
- BuildFailedException.ThrowIfNot(rewritten, $"Could not update {VersionJsonPath}: expected a top-level string '{VersionPropertyName}' property to rewrite.");
- }
-}
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs b/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs
deleted file mode 100644
index 79dbc640..00000000
--- a/src/Buildvana.Tool/Services/Versioning/VersionIncrement.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-namespace Buildvana.Tool.Services.Versioning;
-
-///
-/// Specifies a kind of version increment.
-///
-///
-/// The values of this enum are sorted in ascending order of importance, so that they may be compared.
-///
-internal enum VersionIncrement
-{
- ///
- /// Represents no version advancement.
- ///
- None,
-
- ///
- /// Represents an increment of minor version.
- ///
- Minor,
-
- ///
- /// Represents an increment of major version and reset of minor version.
- ///
- Major,
-}
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs b/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs
new file mode 100644
index 00000000..3974a692
--- /dev/null
+++ b/src/Buildvana.Tool/Services/Versioning/VersionService.VersionIncrement.cs
@@ -0,0 +1,26 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace Buildvana.Tool.Services.Versioning;
+
+partial class VersionService
+{
+ ///
+ /// Represents a version increment.
+ ///
+ ///
+ /// Constants are defined in ascending order of importance, so that they can be compared
+ /// with relational operators.
+ ///
+ private enum VersionIncrement
+ {
+ /// No version increment.
+ None,
+
+ /// Increment of the minor version.
+ Minor,
+
+ /// Increment of the major version.
+ Major,
+ }
+}
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionService.cs b/src/Buildvana.Tool/Services/Versioning/VersionService.cs
index 4228b6f7..b3d6af34 100644
--- a/src/Buildvana.Tool/Services/Versioning/VersionService.cs
+++ b/src/Buildvana.Tool/Services/Versioning/VersionService.cs
@@ -3,8 +3,8 @@
using Buildvana.Core;
using Buildvana.Core.ConsoleOutput;
-using Buildvana.Core.Json;
-using Buildvana.Core.Process;
+using Buildvana.Core.HomeDirectory;
+using Buildvana.Core.Versioning;
using Buildvana.Tool.Services.Git;
using Buildvana.Tool.Services.PublicApiFiles;
using CommunityToolkit.Diagnostics;
@@ -13,42 +13,49 @@
namespace Buildvana.Tool.Services.Versioning;
///
-/// Provides methods to manage project versioning.
+/// Exposes the version being built, computed by , alongside
+/// release-flow version policy: consistency checks against published versions and computation
+/// of the version specification change to apply upon release.
///
-internal sealed class VersionService
+internal sealed partial class VersionService
{
private readonly IReporter _reporter;
- private readonly IJsonHelper _jsonHelper;
- private readonly IProcessRunner _processRunner;
+ private readonly IHomeDirectoryProvider _home;
+ private readonly VersioningSettings _settings;
+ private readonly GitHeightCalculator _heightCalculator;
private readonly PublicApiFilesService _publicApiFiles;
+ private VersioningService _current;
+
///
/// Initializes a new instance of the class.
///
public VersionService(
IReporter reporter,
- IJsonHelper jsonHelper,
- IProcessRunner processRunner,
+ IHomeDirectoryProvider home,
+ VersioningSettings settings,
GitService git,
PublicApiFilesService publicApiFiles)
{
Guard.IsNotNull(reporter);
- Guard.IsNotNull(jsonHelper);
- Guard.IsNotNull(processRunner);
+ Guard.IsNotNull(home);
+ Guard.IsNotNull(settings);
Guard.IsNotNull(git);
Guard.IsNotNull(publicApiFiles);
_reporter = reporter;
- _jsonHelper = jsonHelper;
- _processRunner = processRunner;
+ _home = home;
+ _settings = settings;
_publicApiFiles = publicApiFiles;
- (CurrentStr, Current, IsPublicRelease, IsPrerelease) = GetVersionInformationFromNbgv();
+ _heightCalculator = new(VersionFile.FileName);
+ _current = new(reporter, home, settings, _heightCalculator);
+ Current = SemanticVersion.Parse(_current.SemVer);
(Latest, LatestStable) = git.GetLatestVersions();
}
///
- /// Gets the version to build, as a string computed by Nerdbank.GitVersioning.
+ /// Gets the version to build, as a string.
///
- public string CurrentStr { get; private set; }
+ public string CurrentStr => _current.SemVer;
///
/// Gets the version to build, as a SemanticVersion object.
@@ -58,14 +65,14 @@ public VersionService(
///
/// Gets a value indicating whether a public release can be built.
///
- /// If Git's HEAD is on a public release branch, as indicated in version.json, ;
- /// otherwise, .
- public bool IsPublicRelease { get; private set; }
+ /// If Git's HEAD is on a public release branch, as configured in release.branches,
+ /// ; otherwise, .
+ public bool IsPublicRelease => _current.IsPublicRelease;
///
/// Gets a value indicating whether the version to build is a prerelease.
///
- public bool IsPrerelease { get; private set; }
+ public bool IsPrerelease => _current.IsPrerelease;
///
/// Gets the latest published version, if any, as a SemanticVersion object.
@@ -118,9 +125,9 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha
{
// Determine how we are currently already incrementing version
var currentVersionIncrement = LatestStable == null ? VersionIncrement.None
- : Current.Major > LatestStable.Major ? VersionIncrement.Major
- : Current.Minor > LatestStable.Minor ? VersionIncrement.Minor
- : VersionIncrement.None;
+ : Current.Major > LatestStable.Major ? VersionIncrement.Major
+ : Current.Minor > LatestStable.Minor ? VersionIncrement.Minor
+ : VersionIncrement.None;
_reporter.Info($"Current version increment: {currentVersionIncrement}");
// Determine the kind of change in public API
@@ -132,7 +139,8 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha
// When the major version is 0, "anything MAY change" according to SemVer;
// by convention, we increment the minor version for breaking changes (0.x -> 0.(x+1))
var isMajorVersionZero = LatestStable is { Major: 0 };
- var semanticVersionIncrement = publicApiChangeKind switch {
+ var semanticVersionIncrement = publicApiChangeKind switch
+ {
ApiChangeKind.Breaking => isMajorVersionZero ? VersionIncrement.Minor : VersionIncrement.Major,
ApiChangeKind.Additive => isMajorVersionZero ? VersionIncrement.None : VersionIncrement.Minor,
_ => VersionIncrement.None,
@@ -141,7 +149,8 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha
// Determine the requested version increment, if any.
_reporter.Info($"Requested version spec change: {requestedChange}");
- var requestedVersionIncrement = requestedChange switch {
+ var requestedVersionIncrement = requestedChange switch
+ {
VersionSpecChange.Major => VersionIncrement.Major,
VersionSpecChange.Minor => VersionIncrement.Minor,
_ => VersionIncrement.None,
@@ -161,11 +170,13 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha
// Determine the actual version spec change to apply:
// - forget any increment-related change (already accounted for via requestedVersionIncrement)
// - set the change to the required increment if any, otherwise leave it as is (None, Unstable, Stable)
- var actualChange = requestedChange switch {
+ var actualChange = requestedChange switch
+ {
VersionSpecChange.Major or VersionSpecChange.Minor => VersionSpecChange.None,
_ => requestedChange,
};
- actualChange = actualVersionIncrement switch {
+ actualChange = actualVersionIncrement switch
+ {
VersionIncrement.Major => VersionSpecChange.Major,
VersionIncrement.Minor => VersionSpecChange.Minor,
_ => actualChange,
@@ -177,22 +188,9 @@ public VersionSpecChange ComputeVersionSpecChange(VersionSpecChange requestedCha
///
/// Update version information, typically after a commit.
///
- public void Update() => (CurrentStr, Current, IsPublicRelease, IsPrerelease) = GetVersionInformationFromNbgv();
-
- private (string CurrentStr, SemanticVersion Current, bool IsPublicRelease, bool IsPrerelease) GetVersionInformationFromNbgv()
+ public void Update()
{
- // Block synchronously: this method runs from the constructor (and Update()), neither of which is async.
- var result = _processRunner
- .RunAsync("dotnet", ["nbgv", "get-version", "--format", "json"])
- .GetAwaiter()
- .GetResult();
-
- var json = _jsonHelper.ParseObject(result.StandardOutput, "The output of nbgv");
- var currentStr = _jsonHelper.GetPropertyValue(json, "SemVer2", "the output of nbgv");
- return (
- currentStr,
- SemanticVersion.Parse(currentStr),
- _jsonHelper.GetPropertyValue(json, "PublicRelease", "the output of nbgv"),
- !string.IsNullOrEmpty(_jsonHelper.GetPropertyValue(json, "PrereleaseVersion", "the output of nbgv")));
+ _current = new(_reporter, _home, _settings, _heightCalculator);
+ Current = SemanticVersion.Parse(_current.SemVer);
}
}
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs b/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs
deleted file mode 100644
index 31ff2e45..00000000
--- a/src/Buildvana.Tool/Services/Versioning/VersionSpec.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Text.RegularExpressions;
-
-namespace Buildvana.Tool.Services.Versioning;
-
-///
-/// Represents a Major.Minor[-Tag] version as found in version.json.
-///
-internal sealed partial record VersionSpec
-{
- private static readonly Regex VersionSpecRegex = GetVersionSpecRegex();
-
- private VersionSpec(int major, int minor, string tag)
- {
- Major = major;
- Minor = minor;
- Tag = tag;
- }
-
- ///
- /// Gets the major version.
- ///
- public int Major { get; }
-
- ///
- /// Gets the minor version.
- ///
- public int Minor { get; }
-
- ///
- /// Gets the current unstable tag.
- ///
- /// The current unstable tag, or the empty string if the current version is stable.
- public string Tag { get; }
-
- ///
- /// Gets a value indicating whether this instance has an unstable tag.
- ///
- public bool HasTag => !string.IsNullOrEmpty(Tag);
-
- ///
- /// Attempts to parse a from the specified string.
- ///
- /// The string to parse.
- /// When this method returns , a newly-created .
- /// This parameter is passed uninitialized.
- /// if successful, otherwise.
- public static bool TryParse(string str, [MaybeNullWhen(false)] out VersionSpec result)
- {
- var match = VersionSpecRegex.Match(str);
- if (!match.Success)
- {
- result = null;
- return false;
- }
-
- result = new(
- int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture),
- int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture),
- match.Groups["tag"].Value);
-
- return true;
- }
-
- ///
- public override string ToString() => $"{Major}.{Minor}{(HasTag ? "-" + Tag : null)}";
-
- ///
- /// Gets an instance of that represents the same version as the current instance and has no unstable tag.
- ///
- /// If this instance has no unstable tag, this instance; otherwise, a newly-created
- /// that represents the same version as the current instance and has no unstable tag.
- public VersionSpec Stable() => HasTag ? new(Major, Minor, string.Empty) : this;
-
- ///
- /// Gets an instance of that represents the same version as the current instance and has the specified unstable tag.
- ///
- /// The unstable tag of the returned instance.
- /// If this instance's property is equal to the given , this instance;
- /// otherwise, a newly-created that represents the same version as the current instance and has the specified unstable tag.
- public VersionSpec Unstable(string tag) => string.Equals(Tag, tag, StringComparison.Ordinal) ? this : new(Major, Minor, tag);
-
- ///
- /// Gets an instance of that represents the next minor version with respect to the current instance and has the specified unstable tag.
- ///
- /// The unstable tag of the returned instance.
- /// A newly-created .
- public VersionSpec NextMinor(string tag) => new(Major, Minor + 1, tag);
-
- ///
- /// Gets an instance of that represents the next major version with respect to the current instance and has the specified unstable tag.
- ///
- /// The unstable tag of the returned instance.
- /// A newly-created .
- public VersionSpec NextMajor(string tag) => new(Major + 1, 0, tag);
-
- ///
- /// Gets an instance of that represents the result of applying the specified change to the current instance.
- ///
- /// A constant representing the kind of change to apply.
- /// If the returned instance has an unstable tag, the unstable tag of the returned instance; otherwise, this parameter is ignored.
- ///
- /// A tuple of the following values:
- ///
- /// -
- /// Result
- /// The result of applying to the current instance.
- ///
- /// -
- /// Changed
- /// If Result is equal to the current instance, ; otherwise, .
- ///
- ///
- ///
- public (VersionSpec Result, bool Changed) ApplyChange(VersionSpecChange change, string tag)
- => change switch {
- VersionSpecChange.Unstable => HasTag ? (this, false) : (Unstable(tag), true),
- VersionSpecChange.Stable => HasTag ? (Stable(), true) : (this, false),
- VersionSpecChange.Minor => (NextMinor(tag), true),
- VersionSpecChange.Major => (NextMajor(tag), true),
- _ => (this, false),
- };
-
- [GeneratedRegex(@"(?-imsx)^v?(?0|[1-9][0-9]*)\.(?0|[1-9][0-9]*)(-(?.*))?$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant)]
- private static partial Regex GetVersionSpecRegex();
-}
diff --git a/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs b/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs
deleted file mode 100644
index 8c3eeb35..00000000
--- a/src/Buildvana.Tool/Services/Versioning/VersionSpecChange.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-namespace Buildvana.Tool.Services.Versioning;
-
-///
-/// Specifies how to modify the version specification upon publishing a release.
-///
-internal enum VersionSpecChange
-{
- ///
- /// Do not force a version increment; do not modify the unstable tag.
- ///
- None,
-
- ///
- /// Do not force a version increment; add an unstable tag if not present.
- ///
- Unstable,
-
- ///
- /// Do not force a version increment; remove the unstable tag if present.
- ///
- Stable,
-
- ///
- /// Force a minor version increment with respect to the latest stable version; add an unstable tag.
- ///
- Minor,
-
- ///
- /// Force a major version increment and minor version reset with respect to the latest stable version; add an unstable tag.
- ///
- Major,
-}
diff --git a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs
index 184d9390..f235ca6e 100644
--- a/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs
+++ b/src/Buildvana.Tool/Subcommands/ReleaseCommand.cs
@@ -12,7 +12,7 @@
using Buildvana.Core.Configuration;
using Buildvana.Core.ConsoleOutput;
using Buildvana.Core.HomeDirectory;
-using Buildvana.Core.Json;
+using Buildvana.Core.Versioning;
using Buildvana.Tool.Build;
using Buildvana.Tool.Infrastructure;
using Buildvana.Tool.Infrastructure.Execution;
@@ -42,7 +42,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
await pipeline.RunThroughAsync(BuildStep.Test, configuration, cancellationToken).ConfigureAwait(false);
var home = services.GetRequiredService();
- var jsonHelper = services.GetRequiredService();
+ var versioningSettings = services.GetRequiredService();
var server = services.GetRequiredService();
var version = services.GetRequiredService();
var dotnet = services.GetRequiredService();
@@ -91,12 +91,12 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
// Modify version file if required.
if (versionSpecChange != VersionSpecChange.None)
{
- var versionFile = VersionFile.Load(home, jsonHelper);
- var previousVersionSpec = versionFile.VersionSpec;
- if (versionFile.ApplyVersionSpecChange(versionSpecChange))
+ var versionFile = VersionFile.Load(home);
+ var previousVersionSpec = versionFile.Spec;
+ if (versionFile.ApplyChange(versionSpecChange))
{
- reporter.Info($"Version spec changed from {previousVersionSpec} to {versionFile.VersionSpec}.");
- versionFile.Save();
+ reporter.Info($"Version spec changed from {previousVersionSpec} to {versionFile.Spec}.");
+ versionFile.Save(versioningSettings.PrereleaseTag);
release.UpdateRepository(versionFile.Path);
}
else
@@ -180,7 +180,7 @@ public async Task ExecuteAsync(CancellationToken cancellationToken)
// Ensure that the release tag doesn't already exist.
// This assumes that full repo history has been checked out;
- // however, that is already a prerequisite for using Nerdbank.GitVersioning.
+ // however, that is already a prerequisite for computing the Git height.
BuildFailedException.ThrowIfNot(!git.TagExists(version.CurrentStr), $"Tag '{version.CurrentStr}' already exists in repository.");
// Artifact pass (Restore→Pack, no Clean): rebuild against the resolved version and make artifacts.
diff --git a/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs b/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs
index c19beb63..f0354ca5 100644
--- a/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs
+++ b/src/Buildvana.Tool/Subcommands/ReleaseSettings.cs
@@ -7,9 +7,9 @@
using System.Text.RegularExpressions;
using Buildvana.Core;
using Buildvana.Core.Configuration;
+using Buildvana.Core.Versioning;
using Buildvana.Tool.CommandLine;
using Buildvana.Tool.Services;
-using Buildvana.Tool.Services.Versioning;
using CommunityToolkit.Diagnostics;
namespace Buildvana.Tool.Subcommands;
diff --git a/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj b/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj
index 6f81faf1..de7f516f 100644
--- a/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj
+++ b/tests/Buildvana.Core.Versioning.Tests/Buildvana.Core.Versioning.Tests.csproj
@@ -8,11 +8,11 @@
-
+
diff --git a/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs b/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs
deleted file mode 100644
index b4221384..00000000
--- a/tests/Buildvana.Core.Versioning.Tests/FixedHomeDirectoryProvider.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
-// See the LICENSE file in the project root for full license information.
-
-using Buildvana.Core.HomeDirectory;
-
-internal sealed class FixedHomeDirectoryProvider(string homeDirectory) : IHomeDirectoryProvider
-{
- public string HomeDirectory => homeDirectory;
-}
diff --git a/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs b/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs
index a2eba6b3..8685362c 100644
--- a/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs
+++ b/tests/Buildvana.Core.Versioning.Tests/GitHeightCalculatorTests.cs
@@ -3,6 +3,7 @@
using System.Text;
using Buildvana.Core;
+using Buildvana.Core.Testing;
using Buildvana.Core.Versioning;
internal sealed class GitHeightCalculatorTests
@@ -173,6 +174,56 @@ public async Task Calculate_DetachedHead_ReportsEmptyBranchName()
await Assert.That(result.Height).IsEqualTo(1);
}
+ [Test]
+ public async Task TryGetRepositoryStateToken_WithoutRepository_ReturnsNull()
+ {
+ var directory = Directory.CreateTempSubdirectory("bv-test-");
+ try
+ {
+ var token = GitHeightCalculator.TryGetRepositoryStateToken(directory.FullName);
+ await Assert.That(token).IsNull();
+ }
+ finally
+ {
+ directory.Delete(recursive: true);
+ }
+ }
+
+ [Test]
+ public async Task TryGetRepositoryStateToken_UnchangedState_ReturnsEqualTokens()
+ {
+ using var repo = new TempGitRepo();
+ repo.CommitAll();
+ var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ await Assert.That(first).IsNotNull();
+ await Assert.That(second).IsEqualTo(first);
+ }
+
+ [Test]
+ public async Task TryGetRepositoryStateToken_NewCommit_ChangesToken()
+ {
+ using var repo = new TempGitRepo();
+ repo.CommitAll();
+ var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ repo.CommitAll();
+ var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ await Assert.That(second).IsNotNull();
+ await Assert.That(string.Equals(second, first, StringComparison.Ordinal)).IsFalse();
+ }
+
+ [Test]
+ public async Task TryGetRepositoryStateToken_DetachedHead_ChangesToken()
+ {
+ using var repo = new TempGitRepo();
+ repo.CommitAll();
+ var first = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ repo.CheckoutDetached();
+ var second = GitHeightCalculator.TryGetRepositoryStateToken(repo.RootPath);
+ await Assert.That(second).IsNotNull();
+ await Assert.That(string.Equals(second, first, StringComparison.Ordinal)).IsFalse();
+ }
+
private static GitHeightResult Calculate(TempGitRepo repo, int major, int minor)
=> new GitHeightCalculator("VERSION").Calculate(repo.RootPath, new VersionSpec(major, minor, false));
}
diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs
new file mode 100644
index 00000000..f23efcd3
--- /dev/null
+++ b/tests/Buildvana.Core.Versioning.Tests/VersionFileTests.cs
@@ -0,0 +1,110 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Buildvana.Core;
+using Buildvana.Core.Testing;
+using Buildvana.Core.Versioning;
+
+internal sealed class VersionFileTests
+{
+ [Test]
+ public async Task Load_MissingFile_Throws()
+ {
+ using var home = new TempHome();
+ var exception = CatchLoad(home);
+ await Assert.That(exception).IsNotNull();
+ await Assert.That(exception!.Message).Contains("not found");
+ }
+
+ [Test]
+ public async Task Load_MalformedFile_Throws()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "banana\n");
+ var exception = CatchLoad(home);
+ await Assert.That(exception).IsNotNull();
+ await Assert.That(exception!.Message).Contains("banana");
+ }
+
+ [Test]
+ public async Task Load_ValidFile_ParsesSpecAndExposesPath()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3-preview\n");
+ var versionFile = VersionFile.Load(home.Provider);
+ await Assert.That(versionFile.Spec).IsEqualTo(new VersionSpec(2, 3, true));
+ await Assert.That(versionFile.Path).IsEqualTo(Path.Combine(home.RootPath, VersionFile.FileName));
+ }
+
+ [Test]
+ public async Task ApplyChange_UpdatesSpecAndReportsChange()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3-x\n");
+ var versionFile = VersionFile.Load(home.Provider);
+ await Assert.That(versionFile.ApplyChange(VersionSpecChange.Stable)).IsTrue();
+ await Assert.That(versionFile.Spec).IsEqualTo(new VersionSpec(2, 3, false));
+ await Assert.That(versionFile.ApplyChange(VersionSpecChange.Stable)).IsFalse();
+ }
+
+ [Test]
+ public async Task Save_StableSpec_WritesCanonicalForm()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3\n");
+ VersionFile.Load(home.Provider).Save("preview");
+ await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3\n");
+ }
+
+ [Test]
+ public async Task Save_PrereleaseSpecWithConformingTag_WritesTag()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3-\n");
+ VersionFile.Load(home.Provider).Save("preview");
+ await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-preview\n");
+ }
+
+ [Test]
+ public async Task Save_PrereleaseSpecWithNoTag_WritesBareMarker()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3-x\n");
+ VersionFile.Load(home.Provider).Save();
+ await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-\n");
+ }
+
+ [Test]
+ public async Task Save_PrereleaseSpecWithNonConformingTag_WritesBareMarker()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3-x\n");
+ VersionFile.Load(home.Provider).Save("beta.1");
+ await Assert.That(home.ReadFile(VersionFile.FileName)).IsEqualTo("2.3-\n");
+ }
+
+ [Test]
+ public async Task Save_AfterApplyChange_RoundTrips()
+ {
+ using var home = new TempHome();
+ home.WriteFile(VersionFile.FileName, "2.3\n");
+ var versionFile = VersionFile.Load(home.Provider);
+ _ = versionFile.ApplyChange(VersionSpecChange.Minor);
+ versionFile.Save("preview");
+ var reloaded = VersionFile.Load(home.Provider);
+ await Assert.That(reloaded.Spec).IsEqualTo(new VersionSpec(2, 4, true));
+ }
+
+ private static BuildFailedException? CatchLoad(TempHome home)
+ {
+ try
+ {
+ _ = VersionFile.Load(home.Provider);
+ return null;
+ }
+ catch (BuildFailedException e)
+ {
+ return e;
+ }
+ }
+}
diff --git a/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs
new file mode 100644
index 00000000..7e51ea80
--- /dev/null
+++ b/tests/Buildvana.Core.Versioning.Tests/VersionSpecChangeTests.cs
@@ -0,0 +1,73 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Buildvana.Core.Versioning;
+
+internal sealed class VersionSpecChangeTests
+{
+ [Test]
+ public async Task Stable_OnPrerelease_ClearsPrereleaseMarker()
+ {
+ var result = new VersionSpec(2, 3, true).Stable();
+ await Assert.That(result).IsEqualTo(new VersionSpec(2, 3, false));
+ }
+
+ [Test]
+ public async Task Stable_OnStable_ReturnsEqualSpec()
+ {
+ var spec = new VersionSpec(2, 3, false);
+ await Assert.That(spec.Stable()).IsEqualTo(spec);
+ }
+
+ [Test]
+ public async Task Unstable_OnStable_SetsPrereleaseMarker()
+ {
+ var result = new VersionSpec(2, 3, false).Unstable();
+ await Assert.That(result).IsEqualTo(new VersionSpec(2, 3, true));
+ }
+
+ [Test]
+ public async Task Unstable_OnPrerelease_ReturnsEqualSpec()
+ {
+ var spec = new VersionSpec(2, 3, true);
+ await Assert.That(spec.Unstable()).IsEqualTo(spec);
+ }
+
+ [Test]
+ public async Task NextMinor_IncrementsMinorAndStartsPrerelease()
+ {
+ var result = new VersionSpec(2, 3, false).NextMinor();
+ await Assert.That(result).IsEqualTo(new VersionSpec(2, 4, true));
+ }
+
+ [Test]
+ public async Task NextMajor_IncrementsMajorResetsMinorAndStartsPrerelease()
+ {
+ var result = new VersionSpec(2, 3, false).NextMajor();
+ await Assert.That(result).IsEqualTo(new VersionSpec(3, 0, true));
+ }
+
+ [Test]
+ [Arguments(false, VersionSpecChange.None, 2, 3, false, false)]
+ [Arguments(true, VersionSpecChange.None, 2, 3, true, false)]
+ [Arguments(false, VersionSpecChange.Unstable, 2, 3, true, true)]
+ [Arguments(true, VersionSpecChange.Unstable, 2, 3, true, false)]
+ [Arguments(false, VersionSpecChange.Stable, 2, 3, false, false)]
+ [Arguments(true, VersionSpecChange.Stable, 2, 3, false, true)]
+ [Arguments(false, VersionSpecChange.Minor, 2, 4, true, true)]
+ [Arguments(true, VersionSpecChange.Minor, 2, 4, true, true)]
+ [Arguments(false, VersionSpecChange.Major, 3, 0, true, true)]
+ [Arguments(true, VersionSpecChange.Major, 3, 0, true, true)]
+ public async Task ApplyChange_ComputesExpectedResult(
+ bool prerelease,
+ VersionSpecChange change,
+ int expectedMajor,
+ int expectedMinor,
+ bool expectedPrerelease,
+ bool expectedChanged)
+ {
+ var (result, changed) = new VersionSpec(2, 3, prerelease).ApplyChange(change);
+ await Assert.That(result).IsEqualTo(new VersionSpec(expectedMajor, expectedMinor, expectedPrerelease));
+ await Assert.That(changed).IsEqualTo(expectedChanged);
+ }
+}
diff --git a/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs b/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs
index 560d2149..425e1ed6 100644
--- a/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs
+++ b/tests/Buildvana.Core.Versioning.Tests/VersioningServiceTests.cs
@@ -4,6 +4,8 @@
using Buildvana.Core;
using Buildvana.Core.Configuration;
using Buildvana.Core.ConsoleOutput;
+using Buildvana.Core.HomeDirectory;
+using Buildvana.Core.Testing;
using Buildvana.Core.Versioning;
internal sealed class VersioningServiceTests
@@ -138,12 +140,22 @@ public async Task Constructor_DefaultPrecision_IsMajor()
await Assert.That(service.AssemblyVersion).IsEqualTo("2.0.0.0");
}
+ [Test]
+ public async Task Constructor_FileVersion_IsFullPrecisionRegardlessOfAssemblyVersionPrecision()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "2.3\n");
+ repo.CommitAll();
+ var service = CreateService(repo, new BuildvanaConfig());
+ await Assert.That(service.FileVersion).IsEqualTo("2.3.1.0");
+ }
+
private static VersioningService CreateService(TempGitRepo repo, BuildvanaConfig config)
=> new(
NullReporter.Instance,
new FixedHomeDirectoryProvider(repo.RootPath),
new VersioningSettings(config),
- new GitHeightCalculator(VersioningService.VersionFileName));
+ new GitHeightCalculator(VersionFile.FileName));
private static BuildFailedException? CatchCreate(TempGitRepo repo, BuildvanaConfig config)
{
diff --git a/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj b/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj
index 6409d9fb..d57601a8 100644
--- a/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj
+++ b/tests/Buildvana.Sdk.Tasks.Tests/Buildvana.Sdk.Tasks.Tests.csproj
@@ -15,6 +15,7 @@
+
diff --git a/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs
new file mode 100644
index 00000000..e75397cb
--- /dev/null
+++ b/tests/Buildvana.Sdk.Tasks.Tests/ComputeVersionTests.cs
@@ -0,0 +1,113 @@
+// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Buildvana.Core.Testing;
+using Buildvana.Sdk.Tasks;
+
+internal sealed class ComputeVersionTests
+{
+ [Test]
+ public async Task Execute_WithMissingHomeDirectory_LogsError()
+ {
+ var engine = new RecordingBuildEngine();
+ var task = new ComputeVersion { BuildEngine = engine };
+ await Assert.That(task.Execute()).IsFalse();
+ await Assert.That(engine.Errors.Count).IsEqualTo(1);
+ await Assert.That(engine.Errors[0].Message!).Contains("BVSDK1050");
+ }
+
+ [Test]
+ public async Task Execute_StableNonPublic_ComputesVersionOutputs()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "1.2\n");
+ repo.CommitAll();
+ var task = RunTask(repo);
+ await Assert.That(task.SimpleVersion).IsEqualTo("1.2.1");
+ await Assert.That(task.SemVer).IsEqualTo("1.2.1");
+ await Assert.That(task.AssemblyVersion).IsEqualTo("1.0.0.0");
+ await Assert.That(task.FileVersion).IsEqualTo("1.2.1.0");
+ await Assert.That(task.InformationalVersion).IsEqualTo($"1.2.1-g{repo.HeadSha[..10]}");
+ await Assert.That(task.IsPublicRelease).IsFalse();
+ await Assert.That(task.IsPrerelease).IsFalse();
+ await Assert.That(task.CommitId).IsEqualTo(repo.HeadSha);
+ await Assert.That(task.Height).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Execute_WithConfigurationFile_HonorsVersioningSettings()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "1.2-x\n");
+ repo.WriteFile(
+ "buildvana.json",
+ """{ "release": { "branches": ["^rel$"] }, "versioning": { "prereleaseTag": "preview", "assemblyVersionPrecision": "minor" } }""");
+ repo.CommitAll();
+ repo.CheckoutNewBranch("rel");
+ var task = RunTask(repo);
+ await Assert.That(task.SemVer).IsEqualTo("1.2.1-preview");
+ await Assert.That(task.AssemblyVersion).IsEqualTo("1.2.0.0");
+ await Assert.That(task.InformationalVersion).IsEqualTo("1.2.1-preview");
+ await Assert.That(task.IsPublicRelease).IsTrue();
+ await Assert.That(task.IsPrerelease).IsTrue();
+ }
+
+ [Test]
+ public async Task Execute_WithUnchangedRepositoryState_UsesCachedResult()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "1.2\n");
+ repo.CommitAll();
+ var firstEngine = new RecordingBuildEngine();
+ _ = RunTask(repo, firstEngine);
+ var secondEngine = new RecordingBuildEngine();
+ var second = RunTask(repo, secondEngine);
+
+ // The versioning service logs the computed version; a cache hit computes nothing and stays silent.
+ await Assert.That(firstEngine.Messages.Count(m => m.Message!.Contains("Version 1.2.1", StringComparison.Ordinal))).IsEqualTo(1);
+ await Assert.That(secondEngine.Messages.Count(m => m.Message!.Contains("Version 1.2.1", StringComparison.Ordinal))).IsEqualTo(0);
+ await Assert.That(second.SemVer).IsEqualTo("1.2.1");
+ await Assert.That(second.Height).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Execute_AfterNewCommit_RecomputesVersion()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "1.2\n");
+ repo.CommitAll();
+ var first = RunTask(repo);
+ await Assert.That(first.Height).IsEqualTo(1);
+ repo.CommitAll();
+ var engine = new RecordingBuildEngine();
+ var second = RunTask(repo, engine);
+ await Assert.That(second.Height).IsEqualTo(2);
+ await Assert.That(second.SemVer).IsEqualTo("1.2.2");
+ await Assert.That(engine.Messages.Count(m => m.Message!.Contains("Version 1.2.2", StringComparison.Ordinal))).IsEqualTo(1);
+ }
+
+ [Test]
+ public async Task Execute_AfterVersionFileChange_RecomputesVersion()
+ {
+ using var repo = new TempGitRepo();
+ repo.WriteFile("VERSION", "1.2\n");
+ repo.CommitAll();
+ var first = RunTask(repo);
+ await Assert.That(first.SemVer).IsEqualTo("1.2.1");
+ repo.WriteFile("VERSION", "1.3\n");
+ var second = RunTask(repo);
+ await Assert.That(second.SemVer).IsEqualTo("1.3.0");
+ await Assert.That(second.Height).IsEqualTo(0);
+ }
+
+ private static ComputeVersion RunTask(TempGitRepo repo, RecordingBuildEngine? engine = null)
+ {
+ var task = new ComputeVersion
+ {
+ BuildEngine = engine ?? new RecordingBuildEngine(),
+ HomeDirectory = repo.RootPath,
+ };
+ _ = task.Execute();
+ return task;
+ }
+}
diff --git a/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs b/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs
index b36770f1..be023511 100644
--- a/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs
+++ b/tests/Buildvana.Tool.Tests/ReleaseSettingsTests.cs
@@ -3,8 +3,8 @@
using Buildvana.Core;
using Buildvana.Core.Configuration;
+using Buildvana.Core.Versioning;
using Buildvana.Tool.Services;
-using Buildvana.Tool.Services.Versioning;
using Buildvana.Tool.Subcommands;
internal sealed class ReleaseSettingsTests