diff --git a/.github/shared.yml b/.github/shared.yml index 038eadf..2ae05e5 100644 --- a/.github/shared.yml +++ b/.github/shared.yml @@ -141,7 +141,7 @@ definitions: os: ubuntu-24.04 exe: verlite - rid: osx-x64 - os: macos-13 + os: macos-14-large exe: verlite steps: - *checkout diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 7e51337..d3b135a 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -88,7 +88,7 @@ jobs: os: ubuntu-24.04 exe: verlite - rid: osx-x64 - os: macos-13 + os: macos-14-large exe: verlite steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f7dc41b..8df2f97 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -88,7 +88,7 @@ jobs: os: ubuntu-24.04 exe: verlite - rid: osx-x64 - os: macos-13 + os: macos-14-large exe: verlite steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 4891d70..125b57c 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -91,7 +91,7 @@ jobs: os: ubuntu-24.04 exe: verlite - rid: osx-x64 - os: macos-13 + os: macos-14-large exe: verlite steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 0e7319f..572b3a3 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ See [docs/VersionCalculation.md](docs/VersionCalculation.md) for further reading | The remote endpoint to use when fetching tags and commits. | -r, --remote, VerliteRemote | origin | | Generate version strings and embed them via a source generator. | VerliteEmbedVersion | true | | Use a shadow repo (partial, only commits) to read commit history. | --enable-shadow-repo, VerliteEnableShadowRepo | false | +| Any change except for ignored files causes a version bump. | --dirty-version-bump- VerliteDirtyVersionBump | false | ## Comparison with GitVersion @@ -247,6 +248,7 @@ namespace Verlite public const string BuildMetadata; public const string Commit; public const string Height; + public const bool Dirty; } } ``` diff --git a/src/Verlite.CLI/JsonOutput.cs b/src/Verlite.CLI/JsonOutput.cs index 47437e2..e3d48a7 100644 --- a/src/Verlite.CLI/JsonOutput.cs +++ b/src/Verlite.CLI/JsonOutput.cs @@ -9,7 +9,8 @@ public static string GenerateOutput( SemVer version, Commit? commit, TaggedVersion? lastTag, - int? height) + int? height, + bool? dirty) { var sb = new StringBuilder(); sb.AppendLine("{"); @@ -21,6 +22,7 @@ public static string GenerateOutput( sb.AppendString(1, "prerelease", version.Prerelease); sb.AppendString(1, "meta", version.BuildMetadata); sb.AppendInteger(1, "height", height); + sb.AppendBool(1, "dirty", dirty); if (lastTag is null) { sb.AppendLine("\t" + @"""lastTag"": null"); @@ -63,5 +65,20 @@ private static void AppendInteger(this StringBuilder sb, int indentation, string else sb.AppendLine(","); } + private static void AppendBool(this StringBuilder sb, int indentation, string key, bool? value, bool final = false) + { + var valuestr = value switch + { + null => "null", + true => "true", + false=> "false", + }; + sb.Append(new string('\t', indentation)); + sb.Append($@"""{key}"": ""{valuestr}"""); + if (final) + sb.AppendLine(""); + else + sb.AppendLine(","); + } } } diff --git a/src/Verlite.CLI/Program.cs b/src/Verlite.CLI/Program.cs index df485b2..b4bf094 100644 --- a/src/Verlite.CLI/Program.cs +++ b/src/Verlite.CLI/Program.cs @@ -93,6 +93,10 @@ public static async Task Main(string[] args) aliases: new[] { "--enable-shadow-repo" }, getDefaultValue: () => false, description: "Use a shadow repro for shallow clones using filter branches to fetch only commits."), + new Option( + aliases: new[] { "--dirty-version-bump" }, + getDefaultValue: () => false, + description: "Any non-ignored modifications to the repo result in a version bump."), new Option( aliases: new[] { "--auto-increment", "-a" }, isDefault: true, @@ -123,6 +127,7 @@ private static async Task RootCommandAsync( bool autoFetch, bool enableLightweightTags, bool enableShadowRepo, + bool dirtyVersionBump, AutoIncrement autoIncrement, string filterTags, string remote, @@ -148,12 +153,14 @@ private static async Task RootCommandAsync( QueryRemoteTags = autoFetch, AutoIncrement = autoIncrement.Value(), Remote = remote, + DirtyVersionBump = dirtyVersionBump, }; var version = opts.VersionOverride ?? new SemVer(); Commit? commit = null; TaggedVersion? lastTag = null; int? height = null; + bool? dirty = null; if (opts.VersionOverride is null) { @@ -169,6 +176,7 @@ private static async Task RootCommandAsync( commit = string.IsNullOrEmpty(revision) ? await repo.GetHead() : await repo.ParseRevision(revision); + dirty = await repo.GetDirty(); (version, lastTag, height) = await VersionCalculator.FromRepository3(repo, commit, opts, log, tagFilter); } @@ -182,7 +190,7 @@ private static async Task RootCommandAsync( Show.prerelease => version.Prerelease?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, Show.metadata => version.BuildMetadata?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, Show.height => height?.ToString(CultureInfo.InvariantCulture) ?? string.Empty, - Show.json => JsonOutput.GenerateOutput(version, commit, lastTag, height), + Show.json => JsonOutput.GenerateOutput(version, commit, lastTag, height, dirty), _ => throw new NotImplementedException(), }; diff --git a/src/Verlite.Core/GitRepoInspector.cs b/src/Verlite.Core/GitRepoInspector.cs index 9c6697c..7e7ef6b 100644 --- a/src/Verlite.Core/GitRepoInspector.cs +++ b/src/Verlite.Core/GitRepoInspector.cs @@ -464,5 +464,19 @@ public void Dispose() PrimaryCatfile?.Dispose(); ShadowRepo?.Dispose(); } + + /// + public async Task GetDirty() + { + try + { + var (stdout, stderr) = await Git("status", "--porcelain"); + return !string.IsNullOrWhiteSpace(stdout); + } + catch (CommandException) + { + return true; + } + } } } diff --git a/src/Verlite.Core/HeightCalculator.cs b/src/Verlite.Core/HeightCalculator.cs index 2c13f2d..73ca5fa 100644 --- a/src/Verlite.Core/HeightCalculator.cs +++ b/src/Verlite.Core/HeightCalculator.cs @@ -69,7 +69,8 @@ await repo.GetHead(), /// The log to output verbose diagnostics to. /// A filter to test tags against. A value of null means do not filter. /// A task containing the height, and, if found, the tagged version. - public static async Task<(int height, TaggedVersion?)> FromRepository2( + [Obsolete("Use FromRepository3")] + public static Task<(int height, TaggedVersion?)> FromRepository2( IRepoInspector repo, Commit? commit, string tagPrefix, @@ -77,6 +78,39 @@ await repo.GetHead(), bool fetchTags, ILogger? log, ITagFilter? tagFilter) + { + return FromRepository3( + repo, + commit, + tagPrefix, + queryRemoteTags, + fetchTags, + dirtyBumpsHeight: false, + log, + tagFilter); + } + + /// + /// Calculate the height from a repository by walking, from the head, the primary parents until a version tag is found. + /// + /// The repo to walk. + /// The commit for which to find a version. + /// What version tags are prefixed with. + /// Whether to query local or local and remote tags. + /// Whether to fetch tags we don't yet have locally. + /// Should a dirty repo bump the height. + /// The log to output verbose diagnostics to. + /// A filter to test tags against. A value of null means do not filter. + /// A task containing the height, and, if found, the tagged version. + public static async Task<(int height, TaggedVersion?)> FromRepository3( + IRepoInspector repo, + Commit? commit, + string tagPrefix, + bool queryRemoteTags, + bool fetchTags, + bool dirtyBumpsHeight, + ILogger? log, + ITagFilter? tagFilter) { var tags = await repo.GetTags(queryRemoteTags ? QueryTarget.Local | QueryTarget.Remote : QueryTarget.Local); @@ -108,10 +142,18 @@ await repo.GetHead(), } } - return candidates + var (height, taggedVersion) = candidates .OrderByDescending(x => x.version is not null) .ThenByDescending(x => x.version?.Version) .First(); + + if (dirtyBumpsHeight && await repo.GetDirty()) + { + log?.Normal($"Local repo is dirty, bumping height from {height} to {height + 1}."); + height += 1; + } + + return (height, taggedVersion); } /// diff --git a/src/Verlite.Core/IRepoInspector.cs b/src/Verlite.Core/IRepoInspector.cs index 6633670..bdea69a 100644 --- a/src/Verlite.Core/IRepoInspector.cs +++ b/src/Verlite.Core/IRepoInspector.cs @@ -70,6 +70,11 @@ public interface IRepoInspector /// A task containing the commit, or null if there is none. Task GetHead(); /// + /// Check if a repo has changes. + /// + /// true if there are changes. + Task GetDirty(); + /// /// Parse a revision to find the commit it points to. /// /// The revision to parse. diff --git a/src/Verlite.Core/PublicAPI.Unshipped.txt b/src/Verlite.Core/PublicAPI.Unshipped.txt index 79b781a..5d14923 100644 --- a/src/Verlite.Core/PublicAPI.Unshipped.txt +++ b/src/Verlite.Core/PublicAPI.Unshipped.txt @@ -1,3 +1,7 @@ #nullable enable +static Verlite.HeightCalculator.FromRepository3(Verlite.IRepoInspector! repo, Verlite.Commit? commit, string! tagPrefix, bool queryRemoteTags, bool fetchTags, bool dirtyBumpsHeight, Verlite.ILogger? log, Verlite.ITagFilter? tagFilter) -> System.Threading.Tasks.Task<(int height, Verlite.TaggedVersion?)>! static Verlite.SemVer.CompareMetadata(string? left, string? right) -> int static Verlite.SemVer.ComparePrerelease(string? left, string? right) -> int +Verlite.GitRepoInspector.GetDirty() -> System.Threading.Tasks.Task! +Verlite.VersionCalculationOptions.DirtyVersionBump.get -> bool +Verlite.VersionCalculationOptions.DirtyVersionBump.set -> void diff --git a/src/Verlite.Core/VersionCalculationOptions.cs b/src/Verlite.Core/VersionCalculationOptions.cs index ac36242..92eb6a1 100644 --- a/src/Verlite.Core/VersionCalculationOptions.cs +++ b/src/Verlite.Core/VersionCalculationOptions.cs @@ -42,6 +42,10 @@ public class VersionCalculationOptions /// public VersionPart AutoIncrement { get; set; } = VersionPart.Patch; /// + /// Whether the version should be bumped if the repo is considered to be dirty. + /// + public bool DirtyVersionBump { get; set; } + /// /// The remote endpoint to use when fetching tags and commits. /// public string Remote { get; set; } = "origin"; diff --git a/src/Verlite.Core/VersionCalculator.cs b/src/Verlite.Core/VersionCalculator.cs index 2af1e77..b6c5b1b 100644 --- a/src/Verlite.Core/VersionCalculator.cs +++ b/src/Verlite.Core/VersionCalculator.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; namespace Verlite @@ -175,12 +174,13 @@ public static async Task FromRepository( if (options.VersionOverride.HasValue) return (options.VersionOverride.Value, null, null); - var (height, lastTag) = await HeightCalculator.FromRepository2( + var (height, lastTag) = await HeightCalculator.FromRepository3( repo: repo, commit: commit, tagPrefix: options.TagPrefix, queryRemoteTags: options.QueryRemoteTags, fetchTags: options.QueryRemoteTags, + dirtyBumpsHeight: options.DirtyVersionBump, log: log, tagFilter: tagFilter); diff --git a/src/Verlite.MsBuild/GetVersionTask.cs b/src/Verlite.MsBuild/GetVersionTask.cs index 72c5c81..e217ecf 100644 --- a/src/Verlite.MsBuild/GetVersionTask.cs +++ b/src/Verlite.MsBuild/GetVersionTask.cs @@ -22,6 +22,7 @@ public sealed partial class GetVersionTask : MsBuildTask public string FilterTags { get; set; } = ""; public string Remote { get; set; } = ""; public bool EnableShadowRepo { get; set; } + public bool DirtyVersionBump { get; set; } public override bool Execute() { @@ -106,11 +107,14 @@ private async Task ExecuteAsync() if (!string.IsNullOrWhiteSpace(Remote)) opts.Remote = Remote; + + opts.DirtyVersionBump = DirtyVersionBump; } var version = opts.VersionOverride ?? new SemVer(); var commitString = string.Empty; var heightString = string.Empty; + var dirtyString = string.Empty; if (opts.VersionOverride is null) { using var repo = await GitRepoInspector.FromPath(ProjectDirectory, opts.Remote, log, commandRunner); @@ -122,10 +126,12 @@ private async Task ExecuteAsync() var commit = await repo.GetHead(); commitString = commit?.Id ?? string.Empty; + dirtyString = await repo.GetDirty() ? "true" : "false"; int? height; (version, _, height) = await VersionCalculator.FromRepository3(repo, commit, opts, log, tagFilter); heightString = height?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + } Version = version.ToString(); @@ -136,6 +142,7 @@ private async Task ExecuteAsync() VersionBuildMetadata = version.BuildMetadata ?? string.Empty; Commit = commitString; Height = heightString; + Dirty = dirtyString; return true; } @@ -147,5 +154,6 @@ private async Task ExecuteAsync() [Output] public string VersionBuildMetadata { get; private set; } = ""; [Output] public string Commit { get; private set; } = ""; [Output] public string Height { get; private set; } = ""; + [Output] public string Dirty { get; private set; } = ""; } } diff --git a/src/Verlite.MsBuild/VersionEmbedGenerator.cs b/src/Verlite.MsBuild/VersionEmbedGenerator.cs index b62c8ef..2cf4192 100644 --- a/src/Verlite.MsBuild/VersionEmbedGenerator.cs +++ b/src/Verlite.MsBuild/VersionEmbedGenerator.cs @@ -25,6 +25,7 @@ public void Execute(GeneratorExecutionContext context) context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.VerliteBuildMetadata", out var meta); context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.VerliteCommit", out var commit); context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.VerliteHeight", out var height); + context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.VerliteDirty", out var dirty); version ??= string.Empty; major ??= string.Empty; @@ -34,6 +35,14 @@ public void Execute(GeneratorExecutionContext context) meta ??= string.Empty; commit ??= string.Empty; height ??= string.Empty; + dirty ??= string.Empty; + + var normalizedDirty = dirty.Trim() switch + { + "true" or "1" => "true", + "false" or "0" => "false", + _ => "false", + }; var coreVersion = string.IsNullOrEmpty(version) ? string.Empty : $"{major}.{minor}.{patch}"; @@ -57,6 +66,7 @@ internal static class Version public const string BuildMetadata = ""{meta}""; public const string Commit = ""{commit}""; public const string Height = ""{height}""; + public const bool Dirty = {normalizedDirty}; }} }} "; diff --git a/src/Verlite.MsBuild/build/Verlite.MsBuild.targets b/src/Verlite.MsBuild/build/Verlite.MsBuild.targets index 8e61814..efa92d5 100644 --- a/src/Verlite.MsBuild/build/Verlite.MsBuild.targets +++ b/src/Verlite.MsBuild/build/Verlite.MsBuild.targets @@ -29,6 +29,7 @@ This file has been modified by Ashleigh Adams and adapted for Verlite --> + @@ -59,6 +60,7 @@ This file has been modified by Ashleigh Adams and adapted for Verlite --> + AutoIncrement="$(VerliteAutoIncrement)" FilterTags="$(VerliteFilterTags)" Remote="$(VerliteRemote)" - EnableShadowRepo="$(VerliteEnableShadowRepo)"> + EnableShadowRepo="$(VerliteEnableShadowRepo)" + DirtyVersionBump="$(VerliteDirtyVersionBump)"> @@ -81,6 +84,7 @@ This file has been modified by Ashleigh Adams and adapted for Verlite --> + @@ -97,6 +101,8 @@ This file has been modified by Ashleigh Adams and adapted for Verlite --> + + diff --git a/tests/UnitTests/GitRepoInspectorTests.cs b/tests/UnitTests/GitRepoInspectorTests.cs index 5cabb42..95e5897 100644 --- a/tests/UnitTests/GitRepoInspectorTests.cs +++ b/tests/UnitTests/GitRepoInspectorTests.cs @@ -602,5 +602,42 @@ public async Task AccessingInvalidBlobAsCommitThrows() await Assert.ThrowsAsync(() => repo.GetParents(new Commit(tree))); } + + [Fact] + public async Task NewUnstagedFilesAreDirty() + { + await TestRepo.Git("init"); + await TestRepo.Git("commit", "--allow-empty", "-m", "first"); + + await File.WriteAllTextAsync(Path.Combine(TestRepo.RootPath, "readme.md"), "# Hello"); + + using var repo = await TestRepo.MakeInspector(); + (await repo.GetDirty()).Should().BeTrue(); + } + + [Fact] + public async Task IgnoredFilesNotDirty() + { + await TestRepo.Git("init"); + await File.WriteAllTextAsync(Path.Combine(TestRepo.RootPath, ".gitignore"), "readme.md"); + await TestRepo.Git("add", ".gitignore"); + await TestRepo.Git("commit", "-m", "first"); + await File.WriteAllTextAsync(Path.Combine(TestRepo.RootPath, "readme.md"), "# Hello"); + + using var repo = await TestRepo.MakeInspector(); + (await repo.GetDirty()).Should().BeFalse(); + } + + [Fact] + public async Task StagedChangesDirty() + { + await TestRepo.Git("init"); + await TestRepo.Git("commit", "--allow-empty", "-m", "first"); + await File.WriteAllTextAsync(Path.Combine(TestRepo.RootPath, "readme.md"), "# Hello"); + await TestRepo.Git("add", "readme.md"); + + using var repo = await TestRepo.MakeInspector(); + (await repo.GetDirty()).Should().BeTrue(); + } } } diff --git a/tests/UnitTests/Mocks/MockRepoInspector.cs b/tests/UnitTests/Mocks/MockRepoInspector.cs index 1a2753a..577b3d7 100644 --- a/tests/UnitTests/Mocks/MockRepoInspector.cs +++ b/tests/UnitTests/Mocks/MockRepoInspector.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; +using System.Diagnostics.CodeAnalysis; namespace UnitTests { @@ -77,6 +78,7 @@ public void Merge(params MockRepoBranch[] others) } } + [SuppressMessage("Naming", "CA1721:Property names should not match get methods")] public sealed class MockRepoInspector : IRepoInspector { private class InternalCommit @@ -174,5 +176,11 @@ async Task IRepoInspector.GetTags(QueryTarget queryTarget) return new TagContainer(tags); } + + public bool Dirty { get; set; } + public Task GetDirty() + { + return Task.FromResult(Dirty); + } } }