From cacd5ab24223cc00a6c6dd96b3617c70c59cd919 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:56:43 +0100 Subject: [PATCH 01/17] Add `CreativeCoders.GitTool` project and implement versioning utilities - Introduced `CreativeCoders.GitTool.Base` with `VersionBuilder` for managing semantic versions. - Added `VersionAutoIncrement` enum to support automated version increment logic. - Integrated new `.csproj` file for unit tests and updated the solution file with the new project reference. --- GitTools.sln | 7 ++ .../VersionBuilder.cs | 106 ++++++++++++++++++ .../Create/CreateReleaseOptions.cs | 7 ++ .../CreativeCoders.GitTool.csproj | 32 ++++++ 4 files changed, 152 insertions(+) create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs create mode 100644 tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj diff --git a/GitTools.sln b/GitTools.sln index d6b018b..efa992d 100644 --- a/GitTools.sln +++ b/GitTools.sln @@ -80,6 +80,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool.Cli. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool.Cli.Commands", "source\GitTool\CreativeCoders.GitTool.Cli.Commands\CreativeCoders.GitTool.Cli.Commands.csproj", "{80C7112B-7D72-4585-BDFA-101289319B2C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool", "tests\CreativeCoders.GitTool\CreativeCoders.GitTool.csproj", "{EB39081F-6139-40B2-BEE7-E024DE6118CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -136,6 +138,10 @@ Global {80C7112B-7D72-4585-BDFA-101289319B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {80C7112B-7D72-4585-BDFA-101289319B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {80C7112B-7D72-4585-BDFA-101289319B2C}.Release|Any CPU.Build.0 = Release|Any CPU + {EB39081F-6139-40B2-BEE7-E024DE6118CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB39081F-6139-40B2-BEE7-E024DE6118CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB39081F-6139-40B2-BEE7-E024DE6118CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB39081F-6139-40B2-BEE7-E024DE6118CD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -159,6 +165,7 @@ Global {D65676E3-6820-464B-8493-597E3E1DCD98} = {90C87656-57A8-4AE9-8619-DA08AA137D6A} {46156AAB-B9F6-4C99-AD0A-9F370DB87679} = {D65676E3-6820-464B-8493-597E3E1DCD98} {80C7112B-7D72-4585-BDFA-101289319B2C} = {D65676E3-6820-464B-8493-597E3E1DCD98} + {EB39081F-6139-40B2-BEE7-E024DE6118CD} = {B7558045-D005-49D4-9205-55A32B6C99DE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D38E5978-8FB3-474B-BDCB-18853E21A429} diff --git a/source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs b/source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs new file mode 100644 index 0000000..abf96d9 --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace CreativeCoders.GitTool.Base; + +public class VersionBuilder +{ + private const int MajorPartIndex = 0; + private const int MinorPartIndex = 1; + private const int PatchPartIndex = 2; + + private readonly VersionFormatKind _formatKind; + + private readonly List _versionParts = []; + + public VersionBuilder(string version, VersionFormatKind formatKind = VersionFormatKind.Strict) + { + _formatKind = formatKind; + _versionParts.AddRange(SplitVersionParts(version)); + } + + private IEnumerable SplitVersionParts(string version) + { + var versionParts = version.Split('.').ToList(); + + if (versionParts.Count > 3) + { + throw new VersionFormatException(version); + } + + for (var i = 0; i < versionParts.Count; i++) + { + var versionPart = versionParts[i]; + + if (versionPart.Trim() != versionPart && _formatKind == VersionFormatKind.Strict) + { + throw new VersionFormatException(version, + $"The version part '{versionPart}' contains leading or trailing whitespace."); + } + + if (!int.TryParse(versionPart.Trim(), out var versionPartInt)) + { + throw new VersionFormatException(version, $"The version part '{versionPart}' is not a valid integer."); + } + + versionParts[i] = versionPartInt.ToString(); + } + + while (versionParts.Count < 3) + { + versionParts.Add("0"); + } + + return versionParts; + } + + private static string BuildVersion(IEnumerable versionParts) + { + return string.Join(".", versionParts); + } + + public void IncrementPatch(int incrementBy = 1) + { + IncrementPart(PatchPartIndex, incrementBy); + } + + public void IncrementMinor(int incrementBy = 1) + { + IncrementPart(MinorPartIndex, incrementBy); + } + + public void IncrementMajor(int incrementBy = 1) + { + IncrementPart(MajorPartIndex, incrementBy); + } + + private void IncrementPart(int partIndex, int incrementBy) + { + if (!int.TryParse(_versionParts[partIndex], out var versionPartNumber)) + { + throw new InvalidOperationException(); + } + + _versionParts[partIndex] = (versionPartNumber + incrementBy).ToString(); + } + + public string Build() + { + return BuildVersion(_versionParts); + } +} + +public enum VersionFormatKind +{ + Strict, + Loose +} + +[PublicAPI] +public class VersionFormatException(string? version, string? message = null) : Exception(message ?? + $"The version '{version}' has an invalid format. The version must consist of at least 3 parts separated by dots (e.g. '1.0.0').") +{ + public string? Version { get; } = version; +} diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index 9d37fb3..0a203ea 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -13,3 +13,10 @@ public class CreateReleaseOptions [OptionParameter('a', PushAllTagsLongName)] public bool PushAllTags { get; set; } } + +public enum VersionAutoIncrement +{ + Major, + Minor, + Patch +} diff --git a/tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj b/tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj new file mode 100644 index 0000000..b6ed026 --- /dev/null +++ b/tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj @@ -0,0 +1,32 @@ + + + + enable + enable + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + From 60a4ac39fbfe3504aca443568b65dff63fc87361 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:02:59 +0100 Subject: [PATCH 02/17] Add unit tests for `VersionBuilder` and integrate `CreativeCoders.GitTool.Base` into the test project - Introduced `VersionBuilderTests` covering version parsing, validation, and increment logic. - Updated solution to include `CreativeCoders.GitTool.Tests` project. - Added project reference to `CreativeCoders.GitTool.Base` in the test project. --- GitTools.sln | 2 +- .../Base/VersionBuilderTests.cs | 170 ++++++++++++++++++ .../CreativeCoders.GitTool.Tests.csproj} | 8 +- 3 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs rename tests/{CreativeCoders.GitTool/CreativeCoders.GitTool.csproj => CreativeCoders.GitTool.Tests/CreativeCoders.GitTool.Tests.csproj} (91%) diff --git a/GitTools.sln b/GitTools.sln index efa992d..466eb8a 100644 --- a/GitTools.sln +++ b/GitTools.sln @@ -80,7 +80,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool.Cli. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool.Cli.Commands", "source\GitTool\CreativeCoders.GitTool.Cli.Commands\CreativeCoders.GitTool.Cli.Commands.csproj", "{80C7112B-7D72-4585-BDFA-101289319B2C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool", "tests\CreativeCoders.GitTool\CreativeCoders.GitTool.csproj", "{EB39081F-6139-40B2-BEE7-E024DE6118CD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreativeCoders.GitTool.Tests", "tests\CreativeCoders.GitTool.Tests\CreativeCoders.GitTool.Tests.csproj", "{EB39081F-6139-40B2-BEE7-E024DE6118CD}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs new file mode 100644 index 0000000..04ab127 --- /dev/null +++ b/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs @@ -0,0 +1,170 @@ +using System.Diagnostics.CodeAnalysis; +using AwesomeAssertions; +using CreativeCoders.GitTool.Base; +using Xunit; + +namespace CreativeCoders.GitTool.Tests.Base; + +public class VersionBuilderTests +{ + [Theory] + [InlineData("1.2.3", "1.2.3")] + [InlineData("1.2", "1.2.0")] + [InlineData("1", "1.0.0")] + public void Build_ValidVersion_ReturnsCorrectVersionString(string version, string expected) + { + // Arrange + var builder = new VersionBuilder(version); + + // Act + var result = builder.Build(); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData(" 1.2.3 ", VersionFormatKind.Loose, "1.2.3")] + [InlineData("1 . 2 . 3", VersionFormatKind.Loose, "1.2.3")] + public void Build_VersionWithWhitespaceInLooseMode_ReturnsCleanedVersion(string version, + VersionFormatKind formatKind, string expected) + { + // Arrange + var builder = new VersionBuilder(version, formatKind); + + // Act + var result = builder.Build(); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData(" 1.2.3")] + [InlineData("1.2.3 ")] + [InlineData("1. 2.3")] + [SuppressMessage("ReSharper", "RedundantArgumentDefaultValue")] + public void Constructor_VersionWithWhitespaceInStrictMode_ThrowsVersionFormatException(string version) + { + // Act + Action act = () => _ = new VersionBuilder(version, VersionFormatKind.Strict); + + // Assert + act.Should().Throw() + .And.Version.Should().Be(version); + } + + [Theory] + [InlineData("1.2.3.4")] + [InlineData("a.b.c")] + [InlineData("1.2.c")] + public void Constructor_InvalidVersionFormat_ThrowsVersionFormatException(string version) + { + // Act + Action act = () => _ = new VersionBuilder(version); + + // Assert + act.Should().Throw() + .And.Version.Should().Be(version); + } + + [Fact] + public void IncrementPatch_DefaultIncrement_IncrementsByOne() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementPatch(); + var result = builder.Build(); + + // Assert + result.Should().Be("1.2.4"); + } + + [Fact] + public void IncrementPatch_CustomIncrement_IncrementsByValue() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementPatch(5); + var result = builder.Build(); + + // Assert + result.Should().Be("1.2.8"); + } + + [Fact] + public void IncrementMinor_DefaultIncrement_IncrementsByOne() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMinor(); + var result = builder.Build(); + + // Assert + result.Should().Be("1.3.3"); + } + + [Fact] + public void IncrementMinor_CustomIncrement_IncrementsByValue() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMinor(10); + var result = builder.Build(); + + // Assert + result.Should().Be("1.12.3"); + } + + [Fact] + public void IncrementMajor_DefaultIncrement_IncrementsByOne() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMajor(); + var result = builder.Build(); + + // Assert + result.Should().Be("2.2.3"); + } + + [Fact] + public void IncrementMajor_CustomIncrement_IncrementsByValue() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMajor(2); + var result = builder.Build(); + + // Assert + result.Should().Be("3.2.3"); + } + + [Fact] + public void MultipleIncrements_ReturnsCorrectFinalVersion() + { + // Arrange + var builder = new VersionBuilder("1.1.1"); + + // Act + builder.IncrementMajor(); + builder.IncrementMinor(2); + builder.IncrementPatch(3); + var result = builder.Build(); + + // Assert + result.Should().Be("2.3.4"); + } +} diff --git a/tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj b/tests/CreativeCoders.GitTool.Tests/CreativeCoders.GitTool.Tests.csproj similarity index 91% rename from tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj rename to tests/CreativeCoders.GitTool.Tests/CreativeCoders.GitTool.Tests.csproj index b6ed026..f81dccf 100644 --- a/tests/CreativeCoders.GitTool/CreativeCoders.GitTool.csproj +++ b/tests/CreativeCoders.GitTool.Tests/CreativeCoders.GitTool.Tests.csproj @@ -5,6 +5,10 @@ enable + + + + @@ -25,8 +29,4 @@ - - - - From bed66d1c2f7d6b3ecc6b5be2cf346358cfec11d6 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:27:49 +0100 Subject: [PATCH 03/17] - Migrate to `CreativeCoders` 6.6.0 across all projects and refactor code for enhanced versioning management. - Introduce `VersionBuilder` improvements: return-chaining methods and better validation logic. - Add `VersionUtils` for parsing and validating semantic versions. - Enhance `CreateReleaseCommand` with auto-incremented version generation. - Refactor tag commands with better error handling and messaging. - Introduce `VersionFormatKind` and `VersionFormatException` for version handling. - Upgrade `.NET` dependencies to match `CreativeCoders` version updates. - Add CLI options validation for better user feedback. --- build/Build.csproj | 2 +- .../CreativeCoders.Git.Abstractions.csproj | 2 +- ...ders.Git.Auth.CredentialManagerCore.csproj | 4 +- .../CreativeCoders.Git.csproj | 4 +- .../CreativeCoders.GitTool.Base.csproj | 6 +- .../ReturnCodes.cs | 2 + .../{ => Versioning}/VersionBuilder.cs | 32 +++------ .../Versioning/VersionFormatException.cs | 11 +++ .../Versioning/VersionFormatKind.cs | 7 ++ .../Versioning/VersionUtils.cs | 36 ++++++++++ ...CreativeCoders.GitTool.Cli.Commands.csproj | 4 +- .../Create/CreateReleaseCommand.cs | 70 +++++++++++++++---- .../Create/CreateReleaseOptions.cs | 25 ++++--- .../Create/VersionAutoIncrement.cs | 9 +++ .../TagGroup/Delete/DeleteTagCommand.cs | 22 +++++- .../CreativeCoders.GitTool.Cli.GtApp.csproj | 4 +- .../Program.cs | 1 + .../CreativeCoders.GitTool.GitHub.csproj | 6 +- .../CreativeCoders.GitTool.GitLab.csproj | 2 +- ...uth.CredentialManagerCore.UnitTests.csproj | 2 +- .../Base/VersionBuilderTests.cs | 1 + 21 files changed, 188 insertions(+), 64 deletions(-) rename source/GitTool/CreativeCoders.GitTool.Base/{ => Versioning}/VersionBuilder.cs (72%) create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatKind.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs diff --git a/build/Build.csproj b/build/Build.csproj index 7ea113f..caf68a7 100644 --- a/build/Build.csproj +++ b/build/Build.csproj @@ -8,7 +8,7 @@ - + diff --git a/source/Git/CreativeCoders.Git.Abstractions/CreativeCoders.Git.Abstractions.csproj b/source/Git/CreativeCoders.Git.Abstractions/CreativeCoders.Git.Abstractions.csproj index dced856..38dcfa6 100644 --- a/source/Git/CreativeCoders.Git.Abstractions/CreativeCoders.Git.Abstractions.csproj +++ b/source/Git/CreativeCoders.Git.Abstractions/CreativeCoders.Git.Abstractions.csproj @@ -5,7 +5,7 @@ - + diff --git a/source/Git/CreativeCoders.Git.Auth.CredentialManagerCore/CreativeCoders.Git.Auth.CredentialManagerCore.csproj b/source/Git/CreativeCoders.Git.Auth.CredentialManagerCore/CreativeCoders.Git.Auth.CredentialManagerCore.csproj index 001eb62..9d8b465 100644 --- a/source/Git/CreativeCoders.Git.Auth.CredentialManagerCore/CreativeCoders.Git.Auth.CredentialManagerCore.csproj +++ b/source/Git/CreativeCoders.Git.Auth.CredentialManagerCore/CreativeCoders.Git.Auth.CredentialManagerCore.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/source/Git/CreativeCoders.Git/CreativeCoders.Git.csproj b/source/Git/CreativeCoders.Git/CreativeCoders.Git.csproj index 0457a51..4795f10 100644 --- a/source/Git/CreativeCoders.Git/CreativeCoders.Git.csproj +++ b/source/Git/CreativeCoders.Git/CreativeCoders.Git.csproj @@ -5,9 +5,9 @@ - + - + diff --git a/source/GitTool/CreativeCoders.GitTool.Base/CreativeCoders.GitTool.Base.csproj b/source/GitTool/CreativeCoders.GitTool.Base/CreativeCoders.GitTool.Base.csproj index 2a3f55e..613a962 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/CreativeCoders.GitTool.Base.csproj +++ b/source/GitTool/CreativeCoders.GitTool.Base/CreativeCoders.GitTool.Base.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs b/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs index 6511294..2d26c03 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs @@ -17,4 +17,6 @@ public static class ReturnCodes public const int FeatureBranchAlreadyExistsLocal = -7; public const int FeatureBranchAlreadyExistsRemote = -8; + + public const int LocalTagNotFound = -9; } diff --git a/source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs similarity index 72% rename from source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs rename to source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs index abf96d9..1428ff8 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/VersionBuilder.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; -namespace CreativeCoders.GitTool.Base; +namespace CreativeCoders.GitTool.Base.Versioning; public class VersionBuilder { @@ -61,22 +60,22 @@ private static string BuildVersion(IEnumerable versionParts) return string.Join(".", versionParts); } - public void IncrementPatch(int incrementBy = 1) + public VersionBuilder IncrementPatch(int incrementBy = 1) { - IncrementPart(PatchPartIndex, incrementBy); + return IncrementPart(PatchPartIndex, incrementBy); } - public void IncrementMinor(int incrementBy = 1) + public VersionBuilder IncrementMinor(int incrementBy = 1) { - IncrementPart(MinorPartIndex, incrementBy); + return IncrementPart(MinorPartIndex, incrementBy); } - public void IncrementMajor(int incrementBy = 1) + public VersionBuilder IncrementMajor(int incrementBy = 1) { - IncrementPart(MajorPartIndex, incrementBy); + return IncrementPart(MajorPartIndex, incrementBy); } - private void IncrementPart(int partIndex, int incrementBy) + private VersionBuilder IncrementPart(int partIndex, int incrementBy) { if (!int.TryParse(_versionParts[partIndex], out var versionPartNumber)) { @@ -84,6 +83,8 @@ private void IncrementPart(int partIndex, int incrementBy) } _versionParts[partIndex] = (versionPartNumber + incrementBy).ToString(); + + return this; } public string Build() @@ -91,16 +92,3 @@ public string Build() return BuildVersion(_versionParts); } } - -public enum VersionFormatKind -{ - Strict, - Loose -} - -[PublicAPI] -public class VersionFormatException(string? version, string? message = null) : Exception(message ?? - $"The version '{version}' has an invalid format. The version must consist of at least 3 parts separated by dots (e.g. '1.0.0').") -{ - public string? Version { get; } = version; -} diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs new file mode 100644 index 0000000..8a4b490 --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs @@ -0,0 +1,11 @@ +using System; +using JetBrains.Annotations; + +namespace CreativeCoders.GitTool.Base.Versioning; + +[PublicAPI] +public class VersionFormatException(string? version, string? message = null) : Exception(message ?? + $"The version '{version}' has an invalid format. The version must consist of at least 3 parts separated by dots (e.g. '1.0.0').") +{ + public string? Version { get; } = version; +} \ No newline at end of file diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatKind.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatKind.cs new file mode 100644 index 0000000..4cd38e3 --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatKind.cs @@ -0,0 +1,7 @@ +namespace CreativeCoders.GitTool.Base.Versioning; + +public enum VersionFormatKind +{ + Strict, + Loose +} \ No newline at end of file diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs new file mode 100644 index 0000000..25419a9 --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; + +namespace CreativeCoders.GitTool.Base.Versioning; + +public static class VersionUtils +{ + public static bool IsValidVersion(string version, out string normalizedVersion, + bool ignoreTrailingVersionPrefix = true) + { + if (ignoreTrailingVersionPrefix) + { + version = RemoveTrailingVersionPrefix(version); + } + + var versionParts = version.Split('.'); + + var isValidVersion = versionParts.Length == 3 && versionParts.All(x => int.TryParse(x, out _)); + + normalizedVersion = isValidVersion ? string.Join(".", versionParts) : string.Empty; + + return isValidVersion; + } + + public static string RemoveTrailingVersionPrefix(string version) + { + string[] versionPrefixes = ["version", "v"]; + + var versionPrefix = + versionPrefixes.FirstOrDefault(x => version.StartsWith(x, StringComparison.OrdinalIgnoreCase)); + + return string.IsNullOrWhiteSpace(versionPrefix) + ? version + : version[versionPrefix.Length..]; + } +} diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/CreativeCoders.GitTool.Cli.Commands.csproj b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/CreativeCoders.GitTool.Cli.Commands.csproj index 6ec2d32..925be94 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/CreativeCoders.GitTool.Cli.Commands.csproj +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/CreativeCoders.GitTool.Cli.Commands.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 5944a16..8b9dc45 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -3,6 +3,7 @@ using CreativeCoders.Git.Abstractions; using CreativeCoders.Git.Abstractions.Branches; using CreativeCoders.GitTool.Base; +using CreativeCoders.GitTool.Base.Versioning; using JetBrains.Annotations; using Spectre.Console; @@ -17,23 +18,14 @@ public class CreateReleaseCommand( IGitRepository gitRepository) : ICliCommand { + private const string DefaultBaseVersionForIncrement = "0.0.0"; + private readonly IGitServiceProviders _gitServiceProviders = Ensure.NotNull(gitServiceProviders); private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); private readonly IGitRepository _gitRepository = Ensure.NotNull(gitRepository); - private async Task MergeDevelopToMain(IGitRepository repository, string mainBranchName, - CreateReleaseOptions options) - { - var provider = await _gitServiceProviders.GetServiceProviderAsync(repository, null); - - var createPullRequest = new GitCreatePullRequest(repository.Info.RemoteUri, - $"Release {options.Version}", "develop", mainBranchName); - - _ = await provider.CreatePullRequestAsync(createPullRequest); - } - public async Task ExecuteAsync(CreateReleaseOptions options) { var mainBranchName = GitBranchNames.Local.GetCanonicalName(_gitRepository.Info.MainBranch); @@ -46,7 +38,9 @@ public async Task ExecuteAsync(CreateReleaseOptions options) await MergeDevelopToMain(_gitRepository, mainBranchName, options); } - var tagName = $"v{options.Version}"; + var version = CreateVersion(options); + + var tagName = $"v{version}"; _ansiConsole.WriteLine($"Create tag '{tagName}'"); @@ -55,7 +49,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) _gitRepository.Pull(); var versionTag = - _gitRepository.Tags.CreateTagWithMessage(tagName, $"Version {options.Version}", mainBranchName); + _gitRepository.Tags.CreateTagWithMessage(tagName, $"Version {version}", mainBranchName); if (options.PushAllTags) { @@ -72,4 +66,54 @@ public async Task ExecuteAsync(CreateReleaseOptions options) return CommandResult.Success; } + + private string CreateVersion(CreateReleaseOptions options) + { + if (!string.IsNullOrWhiteSpace(options.Version)) + { + return new VersionBuilder(options.Version).Build(); + } + + var greatestVersion = GetVersionTags().OrderByDescending(x => x).FirstOrDefault(); + + var versionBuilder = + new VersionBuilder(string.IsNullOrWhiteSpace(greatestVersion) + ? DefaultBaseVersionForIncrement + : greatestVersion); + + switch (options.VersionIncrement!) + { + case VersionAutoIncrement.Major: versionBuilder.IncrementMajor(); break; + case VersionAutoIncrement.Minor: versionBuilder.IncrementMinor(); break; + case VersionAutoIncrement.Patch: versionBuilder.IncrementPatch(); break; + default: + throw new InvalidOperationException("Unknown VersionAutoIncrement"); + } + + return versionBuilder.Build(); + } + + private IEnumerable GetVersionTags() + { + _gitRepository.FetchAllTags("origin"); + + foreach (var tag in _gitRepository.Tags) + { + if (VersionUtils.IsValidVersion(tag.Name.Friendly, out var version)) + { + yield return version; + } + } + } + + private async Task MergeDevelopToMain(IGitRepository repository, string mainBranchName, + CreateReleaseOptions options) + { + var provider = await _gitServiceProviders.GetServiceProviderAsync(repository, null); + + var createPullRequest = new GitCreatePullRequest(repository.Info.RemoteUri, + $"Release {options.Version}", "develop", mainBranchName); + + _ = await provider.CreatePullRequestAsync(createPullRequest); + } } diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index 0a203ea..f5fd32d 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -1,22 +1,29 @@ -using CreativeCoders.SysConsole.Cli.Parsing; +using CreativeCoders.Cli.Core; +using CreativeCoders.SysConsole.Cli.Parsing; using JetBrains.Annotations; namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.Create; [PublicAPI] -public class CreateReleaseOptions +public class CreateReleaseOptions : IOptionsValidation { private const string PushAllTagsLongName = "alltags"; - [OptionValue(0, IsRequired = true)] public string? Version { get; set; } + [OptionValue(0, IsRequired = false)] public string? Version { get; set; } [OptionParameter('a', PushAllTagsLongName)] public bool PushAllTags { get; set; } -} -public enum VersionAutoIncrement -{ - Major, - Minor, - Patch + [OptionParameter('i', "increment", HelpText = "Version increment")] + public VersionAutoIncrement? VersionIncrement { get; set; } + + public Task ValidateAsync() + { + if (string.IsNullOrWhiteSpace(Version) && VersionIncrement == null) + { + return Task.FromResult(OptionsValidationResult.Invalid(["Version or version increment must be specified"])); + } + + return Task.FromResult(OptionsValidationResult.Valid()); + } } diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs new file mode 100644 index 0000000..260123c --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs @@ -0,0 +1,9 @@ +namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.Create; + +public enum VersionAutoIncrement +{ + None, + Major, + Minor, + Patch +} diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagCommand.cs index 219f084..01dbc75 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/TagGroup/Delete/DeleteTagCommand.cs @@ -1,6 +1,8 @@ using CreativeCoders.Cli.Core; +using CreativeCoders.Cli.Hosting.Exceptions; using CreativeCoders.Core; using CreativeCoders.Git.Abstractions; +using CreativeCoders.GitTool.Base; using CreativeCoders.SysConsole.Core; using JetBrains.Annotations; using Spectre.Console; @@ -19,9 +21,25 @@ public Task ExecuteAsync(DeleteTagOptions options) { _ansiConsole.WriteLine($"Deleting tag '{options.TagName}'..."); - _gitRepository.Tags.DeleteTag(options.TagName); + var localTagExists = + _gitRepository.Tags.Any(x => x.Name.Friendly == options.TagName || x.Name.Canonical == options.TagName); - _ansiConsole.MarkupLine($"Tag '{options.TagName}' deleted successfully".ToSuccessMarkup()); + if (!localTagExists && !options.DeleteOnRemote) + { + throw new CliCommandAbortException($"Tag '{options.TagName}' not found", ReturnCodes.LocalTagNotFound); + } + + if (localTagExists) + { + _gitRepository.Tags.DeleteTag(options.TagName); + _ansiConsole.MarkupLine($"Tag '{options.TagName}' deleted successfully".ToSuccessMarkup()); + } + else + { + _ansiConsole.MarkupLine( + $"Skip deleting local tag '{options.TagName}' because it does not exist locally." + .ToInfoMarkup()); + } if (options.DeleteOnRemote) { diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/CreativeCoders.GitTool.Cli.GtApp.csproj b/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/CreativeCoders.GitTool.Cli.GtApp.csproj index 535ceea..6bd35e1 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/CreativeCoders.GitTool.Cli.GtApp.csproj +++ b/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/CreativeCoders.GitTool.Cli.GtApp.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/Program.cs b/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/Program.cs index 8f16600..c5640d6 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/Program.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.GtApp/Program.cs @@ -25,6 +25,7 @@ internal static async Task Main(string[] args) var result = await CliHostBuilder.Create() .ConfigureServices(ConfigureServices) .PrintFooterText([string.Empty]) + .UseValidation() .EnableHelp(HelpCommandKind.CommandOrArgument) .ScanAssemblies(typeof(ShowConfigCommand).Assembly) .Build() diff --git a/source/GitTool/CreativeCoders.GitTool.GitHub/CreativeCoders.GitTool.GitHub.csproj b/source/GitTool/CreativeCoders.GitTool.GitHub/CreativeCoders.GitTool.GitHub.csproj index f078473..579ed7a 100644 --- a/source/GitTool/CreativeCoders.GitTool.GitHub/CreativeCoders.GitTool.GitHub.csproj +++ b/source/GitTool/CreativeCoders.GitTool.GitHub/CreativeCoders.GitTool.GitHub.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/source/GitTool/CreativeCoders.GitTool.GitLab/CreativeCoders.GitTool.GitLab.csproj b/source/GitTool/CreativeCoders.GitTool.GitLab/CreativeCoders.GitTool.GitLab.csproj index e51450e..0332cb7 100644 --- a/source/GitTool/CreativeCoders.GitTool.GitLab/CreativeCoders.GitTool.GitLab.csproj +++ b/source/GitTool/CreativeCoders.GitTool.GitLab/CreativeCoders.GitTool.GitLab.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests.csproj b/tests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests.csproj index 9c75280..546ee84 100644 --- a/tests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests.csproj +++ b/tests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests/CreativeCoders.Git.Auth.CredentialManagerCore.UnitTests.csproj @@ -7,7 +7,7 @@ - + diff --git a/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs index 04ab127..9f58920 100644 --- a/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs +++ b/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using AwesomeAssertions; using CreativeCoders.GitTool.Base; +using CreativeCoders.GitTool.Base.Versioning; using Xunit; namespace CreativeCoders.GitTool.Tests.Base; From 9ec380f51872e088eac1f033390479d82c5ef6a3 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:36:29 +0100 Subject: [PATCH 04/17] Refactor versioning logic and add `ListVersionsCommand` - Introduce `ListVersionsCommand` to display version tags, with sorting options. - Add `GitRepositoryExtensions` and `VersionTag` for improved version tag handling. - Replace `CreateReleaseCommand`'s version tag fetching logic with reusable methods. - Implement `VersionComparer` for semantic version comparison. - Minor enhancements to `VersionBuilder` and `VersionUtils` for parsing and validation. --- .../Versioning/GitRepositoryExtensions.cs | 18 ++++++++ .../Versioning/VersionBuilder.cs | 12 +++++- .../Versioning/VersionComparer.cs | 42 +++++++++++++++++++ .../Versioning/VersionTag.cs | 11 +++++ .../Versioning/VersionUtils.cs | 2 +- .../Create/CreateReleaseCommand.cs | 24 ++++------- .../ListVersions/ListVersionsCommand.cs | 40 ++++++++++++++++++ .../ListVersions/ListVersionsOptions.cs | 11 +++++ 8 files changed, 142 insertions(+), 18 deletions(-) create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/GitRepositoryExtensions.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsCommand.cs create mode 100644 source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsOptions.cs diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/GitRepositoryExtensions.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/GitRepositoryExtensions.cs new file mode 100644 index 0000000..3cedf9f --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/GitRepositoryExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using CreativeCoders.Git.Abstractions; + +namespace CreativeCoders.GitTool.Base.Versioning; + +public static class GitRepositoryExtensions +{ + public static IEnumerable GetVersionTags(this IGitRepository gitRepository) + { + foreach (var tag in gitRepository.Tags) + { + if (VersionUtils.IsValidVersion(tag.Name.Friendly, out var version)) + { + yield return new VersionTag(version, tag); + } + } + } +} diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs index 1428ff8..36d6557 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs @@ -20,7 +20,7 @@ public VersionBuilder(string version, VersionFormatKind formatKind = VersionForm _versionParts.AddRange(SplitVersionParts(version)); } - private IEnumerable SplitVersionParts(string version) + private List SplitVersionParts(string version) { var versionParts = version.Split('.').ToList(); @@ -91,4 +91,14 @@ public string Build() { return BuildVersion(_versionParts); } + + public int GetVersionPart(int partIndex) + { + if (partIndex < 0 || partIndex > 2) + { + throw new ArgumentOutOfRangeException(nameof(partIndex)); + } + + return int.Parse(_versionParts[partIndex]); + } } diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs new file mode 100644 index 0000000..605b9db --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace CreativeCoders.GitTool.Base.Versioning; + +public class VersionComparer : IComparer +{ + public int Compare(string? x, string? y) + { + if (x == null && y == null) + { + return 0; + } + + if (x == null) + { + return -1; + } + + if (y == null) + { + return 1; + } + + var versionBuilderX = new VersionBuilder(x); + var versionBuilderY = new VersionBuilder(y); + + for (var i = 0; i < 3; i++) + { + var versionPartX = versionBuilderX.GetVersionPart(i); + var versionPartY = versionBuilderY.GetVersionPart(i); + + var versionPartComparison = versionPartX.CompareTo(versionPartY); + + if (versionPartComparison != 0) + { + return versionPartComparison; + } + } + + return 0; + } +} diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs new file mode 100644 index 0000000..803b6cb --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs @@ -0,0 +1,11 @@ +using CreativeCoders.Core; +using CreativeCoders.Git.Abstractions.Tags; + +namespace CreativeCoders.GitTool.Base.Versioning; + +public class VersionTag(string version, IGitTag tag) +{ + public string Version { get; } = Ensure.NotNull(version); + + public IGitTag Tag { get; } = Ensure.NotNull(tag); +} diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs index 25419a9..38f6719 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs @@ -15,7 +15,7 @@ public static bool IsValidVersion(string version, out string normalizedVersion, var versionParts = version.Split('.'); - var isValidVersion = versionParts.Length == 3 && versionParts.All(x => int.TryParse(x, out _)); + var isValidVersion = versionParts.All(x => int.TryParse(x, out _)); normalizedVersion = isValidVersion ? string.Join(".", versionParts) : string.Empty; diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 8b9dc45..261fccd 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -74,12 +74,17 @@ private string CreateVersion(CreateReleaseOptions options) return new VersionBuilder(options.Version).Build(); } - var greatestVersion = GetVersionTags().OrderByDescending(x => x).FirstOrDefault(); + _gitRepository.FetchAllTags("origin"); + + var greatestVersion = _gitRepository + .GetVersionTags() + .OrderByDescending(x => x.Version, new VersionComparer()) + .FirstOrDefault(); var versionBuilder = - new VersionBuilder(string.IsNullOrWhiteSpace(greatestVersion) + new VersionBuilder(string.IsNullOrWhiteSpace(greatestVersion?.Version) ? DefaultBaseVersionForIncrement - : greatestVersion); + : greatestVersion.Version); switch (options.VersionIncrement!) { @@ -93,19 +98,6 @@ private string CreateVersion(CreateReleaseOptions options) return versionBuilder.Build(); } - private IEnumerable GetVersionTags() - { - _gitRepository.FetchAllTags("origin"); - - foreach (var tag in _gitRepository.Tags) - { - if (VersionUtils.IsValidVersion(tag.Name.Friendly, out var version)) - { - yield return version; - } - } - } - private async Task MergeDevelopToMain(IGitRepository repository, string mainBranchName, CreateReleaseOptions options) { diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsCommand.cs new file mode 100644 index 0000000..d81b06f --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsCommand.cs @@ -0,0 +1,40 @@ +using CreativeCoders.Cli.Core; +using CreativeCoders.Core; +using CreativeCoders.Git.Abstractions; +using CreativeCoders.Git.Abstractions.Tags; +using CreativeCoders.GitTool.Base.Versioning; +using CreativeCoders.SysConsole.Core; +using Spectre.Console; + +namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.ListVersions; + +[CliCommand([ReleaseCommandGroup.Name, "list-versions"], Description = "Lists version tags")] +public class ListVersionsCommand(IAnsiConsole ansiConsole, IGitRepository gitRepository) + : ICliCommand +{ + private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); + + private readonly IGitRepository _gitRepository = Ensure.NotNull(gitRepository); + + public Task ExecuteAsync(ListVersionsOptions options) + { + _ansiConsole.WriteLines("Version tags:", string.Empty); + + _gitRepository.FetchAllTags("origin"); + + var versionTags = _gitRepository.GetVersionTags(); + + var versionComparer = new VersionComparer(); + + var orderedVersionTags = options.SortDescending + ? versionTags.OrderByDescending(x => x.Version, versionComparer) + : versionTags.OrderBy(x => x.Version, versionComparer); + + foreach (var versionTag in orderedVersionTags) + { + _ansiConsole.WriteLine($"- {versionTag.Version}: {versionTag.Tag.Name.Friendly}"); + } + + return Task.FromResult(CommandResult.Success); + } +} diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsOptions.cs new file mode 100644 index 0000000..f03b60d --- /dev/null +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/ListVersions/ListVersionsOptions.cs @@ -0,0 +1,11 @@ +using CreativeCoders.SysConsole.Cli.Parsing; +using JetBrains.Annotations; + +namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.ListVersions; + +[UsedImplicitly] +public class ListVersionsOptions +{ + [OptionParameter('d', "descending", HelpText = "Sorts versions descending")] + public bool SortDescending { get; set; } +} From d624b4cbdce00eaa8d08be50bfc4b7f1cc0fb989 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:37:51 +0100 Subject: [PATCH 05/17] Refactor `VersionComparer` to use `switch` expression for null checks, simplifying comparison logic. --- .../Versioning/VersionComparer.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs index 605b9db..96c0ee1 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionComparer.cs @@ -6,14 +6,12 @@ public class VersionComparer : IComparer { public int Compare(string? x, string? y) { - if (x == null && y == null) + switch (x) { - return 0; - } - - if (x == null) - { - return -1; + case null when y == null: + return 0; + case null: + return -1; } if (y == null) From 084d08230de3c2e23fa39e01e942e14c90520d6f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:23:42 +0100 Subject: [PATCH 06/17] Add confirmation prompt for auto-incremented version in `CreateReleaseCommand` - Introduced `ConfirmAutoIncrementVersion` option to validate user intent during version increment. - Added return code for release creation abortion. - Updated `VersionAutoIncrement` enum to reorder values for consistency. --- .../CreativeCoders.GitTool.Base/ReturnCodes.cs | 2 ++ .../ReleaseGroup/Create/CreateReleaseCommand.cs | 15 +++++++++++++++ .../ReleaseGroup/Create/CreateReleaseOptions.cs | 3 +++ .../ReleaseGroup/Create/VersionAutoIncrement.cs | 5 ++--- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs b/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs index 2d26c03..be4b019 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/ReturnCodes.cs @@ -19,4 +19,6 @@ public static class ReturnCodes public const int FeatureBranchAlreadyExistsRemote = -8; public const int LocalTagNotFound = -9; + + public const int ReleaseCreationAborted = -10; } diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 261fccd..0e7ed13 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -1,4 +1,5 @@ using CreativeCoders.Cli.Core; +using CreativeCoders.Cli.Hosting.Exceptions; using CreativeCoders.Core; using CreativeCoders.Git.Abstractions; using CreativeCoders.Git.Abstractions.Branches; @@ -40,6 +41,20 @@ public async Task ExecuteAsync(CreateReleaseOptions options) var version = CreateVersion(options); + if (options is { VersionIncrement: not null, ConfirmAutoIncrementVersion: true }) + { + _ansiConsole.WriteLine($"Version will be incremented to '{version}'"); + + if (!await _ansiConsole.PromptAsync(new TextPrompt("Do you want to continue?").DefaultValue(true)) + .ConfigureAwait(false)) + { + throw new CliCommandAbortException("Release creation aborted.", ReturnCodes.ReleaseCreationAborted) + { + IsError = false + }; + } + } + var tagName = $"v{version}"; _ansiConsole.WriteLine($"Create tag '{tagName}'"); diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index f5fd32d..cfb075f 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -17,6 +17,9 @@ public class CreateReleaseOptions : IOptionsValidation [OptionParameter('i', "increment", HelpText = "Version increment")] public VersionAutoIncrement? VersionIncrement { get; set; } + [OptionParameter('c', "confirm", HelpText = "Confirm auto increment version")] + public bool ConfirmAutoIncrementVersion { get; set; } + public Task ValidateAsync() { if (string.IsNullOrWhiteSpace(Version) && VersionIncrement == null) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs index 260123c..9a7170d 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/VersionAutoIncrement.cs @@ -2,8 +2,7 @@ namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.Create; public enum VersionAutoIncrement { - None, - Major, + Patch, Minor, - Patch + Major } From dbab44bd7b50ff2c64fe07a5b458e80317ea07c2 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:25:51 +0100 Subject: [PATCH 07/17] Enhance `CreateReleaseCommand` console output with markup and success messages --- .../ReleaseGroup/Create/CreateReleaseCommand.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 0e7ed13..4daea53 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -5,6 +5,7 @@ using CreativeCoders.Git.Abstractions.Branches; using CreativeCoders.GitTool.Base; using CreativeCoders.GitTool.Base.Versioning; +using CreativeCoders.SysConsole.Core; using JetBrains.Annotations; using Spectre.Console; @@ -43,7 +44,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) if (options is { VersionIncrement: not null, ConfirmAutoIncrementVersion: true }) { - _ansiConsole.WriteLine($"Version will be incremented to '{version}'"); + _ansiConsole.MarkupLine($"Version will be incremented to '{version}'".ToInfoMarkup()); if (!await _ansiConsole.PromptAsync(new TextPrompt("Do you want to continue?").DefaultValue(true)) .ConfigureAwait(false)) @@ -57,7 +58,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) var tagName = $"v{version}"; - _ansiConsole.WriteLine($"Create tag '{tagName}'"); + _ansiConsole.WriteLine($"Creating tag '{tagName}'"); _gitRepository.Branches.CheckOut(mainBranchName); @@ -66,17 +67,23 @@ public async Task ExecuteAsync(CreateReleaseOptions options) var versionTag = _gitRepository.Tags.CreateTagWithMessage(tagName, $"Version {version}", mainBranchName); + _ansiConsole.MarkupLine($"Tag '{tagName}' created".ToSuccessMarkup()); + if (options.PushAllTags) { - _ansiConsole.WriteLine("Push all tags to remote"); + _ansiConsole.WriteLine("Pushing all tags to remote"); _gitRepository.Tags.PushAllTags(); + + _ansiConsole.MarkupLine("All tags pushed successfully".ToSuccessMarkup()); } else { _ansiConsole.WriteLine($"Push tag '{versionTag.Name.Canonical}'"); _gitRepository.Tags.PushTag(versionTag); + + _ansiConsole.MarkupLine($"Tag '{versionTag.Name.Canonical}' pushed successfully".ToSuccessMarkup()); } return CommandResult.Success; From b466bb2175c8b888b8daddb6627abe3ba80c9b09 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:26:55 +0100 Subject: [PATCH 08/17] Refine `CreateReleaseCommand` console messages for improved clarity and consistency. --- .../ReleaseGroup/Create/CreateReleaseCommand.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 4daea53..5f54f59 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -58,7 +58,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) var tagName = $"v{version}"; - _ansiConsole.WriteLine($"Creating tag '{tagName}'"); + _ansiConsole.WriteLine($"Creating tag '{tagName}'..."); _gitRepository.Branches.CheckOut(mainBranchName); @@ -71,7 +71,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) if (options.PushAllTags) { - _ansiConsole.WriteLine("Pushing all tags to remote"); + _ansiConsole.WriteLine("Pushing all tags to remote..."); _gitRepository.Tags.PushAllTags(); @@ -79,7 +79,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) } else { - _ansiConsole.WriteLine($"Push tag '{versionTag.Name.Canonical}'"); + _ansiConsole.WriteLine($"Pushing tag '{versionTag.Name.Canonical}'..."); _gitRepository.Tags.PushTag(versionTag); From 99dda6e6623c26d74c5828a2dbd9a074074763ae Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:34:29 +0100 Subject: [PATCH 09/17] Add support for resetting lower version parts during version auto-increment - Extend `VersionBuilder` methods (`IncrementMinor`, `IncrementMajor`) to allow resetting lower parts. - Introduce `ResetLowerVersionPartsOnAutoInc` option in `CreateReleaseOptions` for configurable behavior. - Update `CreateReleaseCommand` to handle the new reset logic during version auto-increment. --- .../Versioning/VersionBuilder.cs | 48 +++++++++++++++---- .../Create/CreateReleaseCommand.cs | 6 ++- .../Create/CreateReleaseOptions.cs | 3 ++ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs index 36d6557..a3c2b19 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs @@ -65,14 +65,29 @@ public VersionBuilder IncrementPatch(int incrementBy = 1) return IncrementPart(PatchPartIndex, incrementBy); } - public VersionBuilder IncrementMinor(int incrementBy = 1) + public VersionBuilder IncrementMinor(int incrementBy = 1, bool resetPatch = true) { - return IncrementPart(MinorPartIndex, incrementBy); + IncrementPart(MinorPartIndex, incrementBy); + + if (resetPatch) + { + Patch = 0; + } + + return this; } - public VersionBuilder IncrementMajor(int incrementBy = 1) + public VersionBuilder IncrementMajor(int incrementBy = 1, bool resetMinorAndPatch = true) { - return IncrementPart(MajorPartIndex, incrementBy); + IncrementPart(MajorPartIndex, incrementBy); + + if (resetMinorAndPatch) + { + Minor = 0; + Patch = 0; + } + + return this; } private VersionBuilder IncrementPart(int partIndex, int incrementBy) @@ -94,11 +109,26 @@ public string Build() public int GetVersionPart(int partIndex) { - if (partIndex < 0 || partIndex > 2) - { - throw new ArgumentOutOfRangeException(nameof(partIndex)); - } + return partIndex is < 0 or > 2 + ? throw new ArgumentOutOfRangeException(nameof(partIndex)) + : int.Parse(_versionParts[partIndex]); + } + + public int Major + { + get => GetVersionPart(MajorPartIndex); + set => _versionParts[MajorPartIndex] = value.ToString(); + } - return int.Parse(_versionParts[partIndex]); + public int Minor + { + get => GetVersionPart(MinorPartIndex); + set => _versionParts[MinorPartIndex] = value.ToString(); + } + + public int Patch + { + get => GetVersionPart(PatchPartIndex); + set => _versionParts[PatchPartIndex] = value.ToString(); } } diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 5f54f59..ae586e6 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -110,8 +110,10 @@ private string CreateVersion(CreateReleaseOptions options) switch (options.VersionIncrement!) { - case VersionAutoIncrement.Major: versionBuilder.IncrementMajor(); break; - case VersionAutoIncrement.Minor: versionBuilder.IncrementMinor(); break; + case VersionAutoIncrement.Major: + versionBuilder.IncrementMajor(1, options.ResetLowerVersionPartsOnAutoInc); break; + case VersionAutoIncrement.Minor: + versionBuilder.IncrementMinor(1, options.ResetLowerVersionPartsOnAutoInc); break; case VersionAutoIncrement.Patch: versionBuilder.IncrementPatch(); break; default: throw new InvalidOperationException("Unknown VersionAutoIncrement"); diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index cfb075f..b33711f 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -17,6 +17,9 @@ public class CreateReleaseOptions : IOptionsValidation [OptionParameter('i', "increment", HelpText = "Version increment")] public VersionAutoIncrement? VersionIncrement { get; set; } + [OptionParameter('r', "resetlower", HelpText = "Reset lower version parts on auto increment")] + public bool ResetLowerVersionPartsOnAutoInc { get; set; } = true; + [OptionParameter('c', "confirm", HelpText = "Confirm auto increment version")] public bool ConfirmAutoIncrementVersion { get; set; } From a2614dd9220cadc36b5fbf1515d53060b1f94361 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:36:50 +0100 Subject: [PATCH 10/17] Refactor `CreateReleaseCommand` to use `ConfirmationPrompt` for user confirmation --- .../ReleaseGroup/Create/CreateReleaseCommand.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index ae586e6..389acaf 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -46,8 +46,12 @@ public async Task ExecuteAsync(CreateReleaseOptions options) { _ansiConsole.MarkupLine($"Version will be incremented to '{version}'".ToInfoMarkup()); - if (!await _ansiConsole.PromptAsync(new TextPrompt("Do you want to continue?").DefaultValue(true)) - .ConfigureAwait(false)) + var prompt = new ConfirmationPrompt("Do you want to continue?") + { + DefaultValue = true + }; + + if (!await _ansiConsole.PromptAsync(prompt).ConfigureAwait(false)) { throw new CliCommandAbortException("Release creation aborted.", ReturnCodes.ReleaseCreationAborted) { From 73224f25ab3cfb1446fda983ad4d1b38fdb6d600 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:55:55 +0100 Subject: [PATCH 11/17] Add comprehensive unit tests for versioning utilities and enhance `VersionBuilder` - Added `VersionComparerTests` and `VersionUtilsTests` to validate version comparison, parsing, and normalization logic. - Enhanced `VersionBuilder` with additional methods to increment versions without resetting lower parts. - Updated `VersionBuilderTests` to include test cases for new methods and edge cases. - Improved `VersionUtils.RemoveTrailingVersionPrefix` to handle empty or null inputs gracefully. --- .../Versioning/VersionBuilder.cs | 6 +- .../Versioning/VersionTag.cs | 2 + .../Versioning/VersionUtils.cs | 5 ++ .../{ => Versioning}/VersionBuilderTests.cs | 90 +++++++++++++++++-- .../Base/Versioning/VersionComparerTests.cs | 78 ++++++++++++++++ .../Base/Versioning/VersionUtilsTests.cs | 64 +++++++++++++ 6 files changed, 235 insertions(+), 10 deletions(-) rename tests/CreativeCoders.GitTool.Tests/Base/{ => Versioning}/VersionBuilderTests.cs (63%) create mode 100644 tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionComparerTests.cs create mode 100644 tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs index a3c2b19..de4bf55 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionBuilder.cs @@ -6,9 +6,9 @@ namespace CreativeCoders.GitTool.Base.Versioning; public class VersionBuilder { - private const int MajorPartIndex = 0; - private const int MinorPartIndex = 1; - private const int PatchPartIndex = 2; + public const int MajorPartIndex = 0; + public const int MinorPartIndex = 1; + public const int PatchPartIndex = 2; private readonly VersionFormatKind _formatKind; diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs index 803b6cb..45b8811 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionTag.cs @@ -1,8 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using CreativeCoders.Core; using CreativeCoders.Git.Abstractions.Tags; namespace CreativeCoders.GitTool.Base.Versioning; +[ExcludeFromCodeCoverage] public class VersionTag(string version, IGitTag tag) { public string Version { get; } = Ensure.NotNull(version); diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs index 38f6719..b526df1 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs @@ -24,6 +24,11 @@ public static bool IsValidVersion(string version, out string normalizedVersion, public static string RemoveTrailingVersionPrefix(string version) { + if (string.IsNullOrEmpty(version)) + { + return string.Empty; + } + string[] versionPrefixes = ["version", "v"]; var versionPrefix = diff --git a/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs similarity index 63% rename from tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs rename to tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs index 9f58920..bc1e813 100644 --- a/tests/CreativeCoders.GitTool.Tests/Base/VersionBuilderTests.cs +++ b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs @@ -1,10 +1,9 @@ using System.Diagnostics.CodeAnalysis; using AwesomeAssertions; -using CreativeCoders.GitTool.Base; using CreativeCoders.GitTool.Base.Versioning; using Xunit; -namespace CreativeCoders.GitTool.Tests.Base; +namespace CreativeCoders.GitTool.Tests.Base.Versioning; public class VersionBuilderTests { @@ -107,6 +106,20 @@ public void IncrementMinor_DefaultIncrement_IncrementsByOne() builder.IncrementMinor(); var result = builder.Build(); + // Assert + result.Should().Be("1.3.0"); + } + + [Fact] + public void IncrementMinor_WithOutPatchReset_IncrementsByOne() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMinor(1, false); + var result = builder.Build(); + // Assert result.Should().Be("1.3.3"); } @@ -122,7 +135,7 @@ public void IncrementMinor_CustomIncrement_IncrementsByValue() var result = builder.Build(); // Assert - result.Should().Be("1.12.3"); + result.Should().Be("1.12.0"); } [Fact] @@ -135,6 +148,20 @@ public void IncrementMajor_DefaultIncrement_IncrementsByOne() builder.IncrementMajor(); var result = builder.Build(); + // Assert + result.Should().Be("2.0.0"); + } + + [Fact] + public void IncrementMajor_WithOutResetMinorAndPatch_MinorAndPatchAreResetToZero() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + builder.IncrementMajor(1, false); + var result = builder.Build(); + // Assert result.Should().Be("2.2.3"); } @@ -150,22 +177,71 @@ public void IncrementMajor_CustomIncrement_IncrementsByValue() var result = builder.Build(); // Assert - result.Should().Be("3.2.3"); + result.Should().Be("3.0.0"); } [Fact] - public void MultipleIncrements_ReturnsCorrectFinalVersion() + public void MultipleIncrements_WithOutResetLowerVersionParts_ReturnsCorrectFinalVersion() { // Arrange var builder = new VersionBuilder("1.1.1"); // Act - builder.IncrementMajor(); - builder.IncrementMinor(2); + builder.IncrementMajor(1, false); + builder.IncrementMinor(2, false); builder.IncrementPatch(3); var result = builder.Build(); // Assert result.Should().Be("2.3.4"); } + + [Fact] + public void MajorMinorPatch_Get_ReturnCorrectVersionParts() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + var major = builder.Major; + var minor = builder.Minor; + var patch = builder.Patch; + + // Assert + major.Should().Be(1); + minor.Should().Be(2); + patch.Should().Be(3); + } + + [Fact] + public void GetVersionPart_Get_ReturnCorrectVersionParts() + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + var major = builder.GetVersionPart(VersionBuilder.MajorPartIndex); + var minor = builder.GetVersionPart(VersionBuilder.MinorPartIndex); + var patch = builder.GetVersionPart(VersionBuilder.PatchPartIndex); + + // Assert + major.Should().Be(1); + minor.Should().Be(2); + patch.Should().Be(3); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public void GetVersionPart_InvalidIndex_ThrowsArgumentOutOfRangeException(int partIndex) + { + // Arrange + var builder = new VersionBuilder("1.2.3"); + + // Act + Action act = () => _ = builder.GetVersionPart(partIndex); + + // Assert + act.Should().Throw(); + } } diff --git a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionComparerTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionComparerTests.cs new file mode 100644 index 0000000..2b03ec7 --- /dev/null +++ b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionComparerTests.cs @@ -0,0 +1,78 @@ +using CreativeCoders.GitTool.Base.Versioning; +using AwesomeAssertions; +using Xunit; + +namespace CreativeCoders.GitTool.Tests.Base.Versioning; + +public class VersionComparerTests +{ + [Theory] + [InlineData("1.0.0", "1.0.0", 0)] + [InlineData("2.0.0", "1.0.0", 1)] + [InlineData("1.0.0", "2.0.0", -1)] + [InlineData("1.1.0", "1.0.0", 1)] + [InlineData("1.0.0", "1.1.0", -1)] + [InlineData("1.0.1", "1.0.0", 1)] + [InlineData("1.0.0", "1.0.1", -1)] + [InlineData("1.1", "1.1.0", 0)] + [InlineData("1", "1.0.0", 0)] + [InlineData("1.2", "1.1", 1)] + [InlineData("1.1", "1.2", -1)] + [InlineData("1.10", "1.2", 1)] + public void Compare_PositiveCases_ReturnsExpectedResult(string x, string y, int expected) + { + // Arrange + var comparer = new VersionComparer(); + + // Act + var result = comparer.Compare(x, y); + + // Assert + if (expected == 0) + { + result.Should().Be(0); + } + else if (expected > 0) + { + result.Should().BeGreaterThan(0); + } + else + { + result.Should().BeLessThan(0); + } + } + + [Theory] + [InlineData(null, null, 0)] + [InlineData(null, "1.0.0", -1)] + [InlineData("1.0.0", null, 1)] + public void Compare_NullValues_ReturnsExpectedResult(string? x, string? y, int expected) + { + // Arrange + var comparer = new VersionComparer(); + + // Act + var result = comparer.Compare(x, y); + + // Assert + result.Should().Be(expected); + } + + [Theory] + [InlineData("invalid")] + [InlineData("1.0.a")] + [InlineData("1.0.0.0")] + public void Compare_InvalidVersionFormat_ThrowsVersionFormatException(string invalidVersion) + { + // Arrange + var comparer = new VersionComparer(); + + // Act + var actX = () => comparer.Compare(invalidVersion, "1.0.0"); + var actY = () => comparer.Compare("1.0.0", invalidVersion); + + // Assert + actX.Should().Throw(); + actY.Should().Throw(); + } +} diff --git a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs new file mode 100644 index 0000000..8db64f5 --- /dev/null +++ b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs @@ -0,0 +1,64 @@ +using AwesomeAssertions; +using CreativeCoders.GitTool.Base.Versioning; +using Xunit; + +namespace CreativeCoders.GitTool.Tests.Base.Versioning; + +public class VersionUtilsTests +{ + [Theory] + [InlineData("1.2.3", true, "1.2.3")] + [InlineData("v1.2.3", true, "1.2.3")] + [InlineData("version1.0", true, "1.0")] + [InlineData("1.2", true, "1.2")] + [InlineData("V1.2", true, "1.2")] + [InlineData("VERSION1.2", true, "1.2")] + [InlineData("1.2.3", false, "1.2.3")] + public void IsValidVersion_ValidVersions_ReturnsTrueAndNormalizedVersion(string version, bool ignorePrefix, + string expectedNormalized) + { + // Act + var result = VersionUtils.IsValidVersion(version, out var normalizedVersion, ignorePrefix); + + // Assert + result.Should().BeTrue(); + normalizedVersion.Should().Be(expectedNormalized); + } + + [Theory] + [InlineData("1.a.3", true)] + [InlineData("v1.2.x", true)] + [InlineData("1..2", true)] + [InlineData("", true)] + [InlineData(" ", true)] + [InlineData("v1.2.3", false)] + [InlineData("version1.0", false)] + public void IsValidVersion_InvalidVersions_ReturnsFalseAndEmptyNormalizedVersion(string version, bool ignorePrefix) + { + // Act + var result = VersionUtils.IsValidVersion(version, out var normalizedVersion, ignorePrefix); + + // Assert + result.Should().BeFalse(); + normalizedVersion.Should().BeEmpty(); + } + + [Theory] + [InlineData("v1.2.3", "1.2.3")] + [InlineData("version1.0", "1.0")] + [InlineData("V2.0", "2.0")] + [InlineData("VERSION3.1", "3.1")] + [InlineData("1.2.3", "1.2.3")] + [InlineData("v", "")] + [InlineData("version", "")] + [InlineData("", "")] + [InlineData(null, "")] + public void RemoveTrailingVersionPrefix_VariousInputs_ReturnsExpectedResult(string version, string expected) + { + // Act + var result = VersionUtils.RemoveTrailingVersionPrefix(version); + + // Assert + result.Should().Be(expected); + } +} From 7fbdfee467bd19370114653c9884a12928833df2 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:57:56 +0100 Subject: [PATCH 12/17] Rename auto-increment confirmation option for improved clarity in `CreateReleaseOptions` --- .../ReleaseGroup/Create/CreateReleaseCommand.cs | 2 +- .../ReleaseGroup/Create/CreateReleaseOptions.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index 389acaf..c8487e9 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -42,7 +42,7 @@ public async Task ExecuteAsync(CreateReleaseOptions options) var version = CreateVersion(options); - if (options is { VersionIncrement: not null, ConfirmAutoIncrementVersion: true }) + if (options is { VersionIncrement: not null, NoConfirmAutoIncrementVersion: false }) { _ansiConsole.MarkupLine($"Version will be incremented to '{version}'".ToInfoMarkup()); diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index b33711f..90d10b9 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -20,8 +20,8 @@ public class CreateReleaseOptions : IOptionsValidation [OptionParameter('r', "resetlower", HelpText = "Reset lower version parts on auto increment")] public bool ResetLowerVersionPartsOnAutoInc { get; set; } = true; - [OptionParameter('c', "confirm", HelpText = "Confirm auto increment version")] - public bool ConfirmAutoIncrementVersion { get; set; } + [OptionParameter("nc", "noconfirm", HelpText = "No confirmation for auto increment version")] + public bool NoConfirmAutoIncrementVersion { get; set; } public Task ValidateAsync() { From 46069cb8e2a21d2e0982814cdac0c76c816f8534 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sat, 14 Feb 2026 18:11:17 +0100 Subject: [PATCH 13/17] Fix test name in `VersionBuilderTests` to reflect correct behavior of minor and patch increments --- .../Base/Versioning/VersionBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs index bc1e813..9fa7f03 100644 --- a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs +++ b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionBuilderTests.cs @@ -153,7 +153,7 @@ public void IncrementMajor_DefaultIncrement_IncrementsByOne() } [Fact] - public void IncrementMajor_WithOutResetMinorAndPatch_MinorAndPatchAreResetToZero() + public void IncrementMajor_WithOutResetMinorAndPatch_MinorAndPatchAreNotResetToZero() { // Arrange var builder = new VersionBuilder("1.2.3"); From 6388bfc35c1c92cd5e8de4296a7deaed5c3b5ba7 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:38:54 +0100 Subject: [PATCH 14/17] Refactor `CreateReleaseCommand` to simplify code by removing unused `IGitServiceProviders` and `MergeDevelopToMain` logic. --- .../Create/CreateReleaseCommand.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index c8487e9..efe09b3 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -16,14 +16,11 @@ namespace CreativeCoders.GitTool.Cli.Commands.ReleaseGroup.Create; Description = "Creates a release by creating a version tag")] public class CreateReleaseCommand( IAnsiConsole ansiConsole, - IGitServiceProviders gitServiceProviders, IGitRepository gitRepository) : ICliCommand { private const string DefaultBaseVersionForIncrement = "0.0.0"; - private readonly IGitServiceProviders _gitServiceProviders = Ensure.NotNull(gitServiceProviders); - private readonly IAnsiConsole _ansiConsole = Ensure.NotNull(ansiConsole); private readonly IGitRepository _gitRepository = Ensure.NotNull(gitRepository); @@ -32,14 +29,6 @@ public async Task ExecuteAsync(CreateReleaseOptions options) { var mainBranchName = GitBranchNames.Local.GetCanonicalName(_gitRepository.Info.MainBranch); - if (_gitRepository.Branches["develop"] != null) - { - _ansiConsole.WriteLine( - $"Repository has a develop branch. So first a merge from develop -> {mainBranchName} must be done."); - - await MergeDevelopToMain(_gitRepository, mainBranchName, options); - } - var version = CreateVersion(options); if (options is { VersionIncrement: not null, NoConfirmAutoIncrementVersion: false }) @@ -125,15 +114,4 @@ private string CreateVersion(CreateReleaseOptions options) return versionBuilder.Build(); } - - private async Task MergeDevelopToMain(IGitRepository repository, string mainBranchName, - CreateReleaseOptions options) - { - var provider = await _gitServiceProviders.GetServiceProviderAsync(repository, null); - - var createPullRequest = new GitCreatePullRequest(repository.Info.RemoteUri, - $"Release {options.Version}", "develop", mainBranchName); - - _ = await provider.CreatePullRequestAsync(createPullRequest); - } } From f368913053f0d1f4c1fabd36b5c8fafc8986dcd8 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:41:28 +0100 Subject: [PATCH 15/17] Rename `RemoveTrailingVersionPrefix` to `RemoveLeadingVersionPrefix` across `VersionUtils` and associated tests. --- .../Versioning/VersionUtils.cs | 8 ++++---- .../Base/Versioning/VersionUtilsTests.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs index b526df1..0573fbe 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionUtils.cs @@ -6,11 +6,11 @@ namespace CreativeCoders.GitTool.Base.Versioning; public static class VersionUtils { public static bool IsValidVersion(string version, out string normalizedVersion, - bool ignoreTrailingVersionPrefix = true) + bool ignoreLeadingVersionPrefix = true) { - if (ignoreTrailingVersionPrefix) + if (ignoreLeadingVersionPrefix) { - version = RemoveTrailingVersionPrefix(version); + version = RemoveLeadingVersionPrefix(version); } var versionParts = version.Split('.'); @@ -22,7 +22,7 @@ public static bool IsValidVersion(string version, out string normalizedVersion, return isValidVersion; } - public static string RemoveTrailingVersionPrefix(string version) + public static string RemoveLeadingVersionPrefix(string version) { if (string.IsNullOrEmpty(version)) { diff --git a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs index 8db64f5..525a1af 100644 --- a/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs +++ b/tests/CreativeCoders.GitTool.Tests/Base/Versioning/VersionUtilsTests.cs @@ -53,10 +53,10 @@ public void IsValidVersion_InvalidVersions_ReturnsFalseAndEmptyNormalizedVersion [InlineData("version", "")] [InlineData("", "")] [InlineData(null, "")] - public void RemoveTrailingVersionPrefix_VariousInputs_ReturnsExpectedResult(string version, string expected) + public void RemoveLeadingVersionPrefix_VariousInputs_ReturnsExpectedResult(string version, string expected) { // Act - var result = VersionUtils.RemoveTrailingVersionPrefix(version); + var result = VersionUtils.RemoveLeadingVersionPrefix(version); // Assert result.Should().Be(expected); From b4f7b199a36b020494548072ee37264f409c478f Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:44:26 +0100 Subject: [PATCH 16/17] Ensure mutual exclusivity between `Version` and `VersionIncrement` in `CreateReleaseOptions` validation and update help texts for clarity. --- .../ReleaseGroup/Create/CreateReleaseCommand.cs | 10 +++++++--- .../ReleaseGroup/Create/CreateReleaseOptions.cs | 11 +++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs index efe09b3..7984d19 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseCommand.cs @@ -104,10 +104,14 @@ private string CreateVersion(CreateReleaseOptions options) switch (options.VersionIncrement!) { case VersionAutoIncrement.Major: - versionBuilder.IncrementMajor(1, options.ResetLowerVersionPartsOnAutoInc); break; + versionBuilder.IncrementMajor(1, options.ResetLowerVersionPartsOnAutoInc); + break; case VersionAutoIncrement.Minor: - versionBuilder.IncrementMinor(1, options.ResetLowerVersionPartsOnAutoInc); break; - case VersionAutoIncrement.Patch: versionBuilder.IncrementPatch(); break; + versionBuilder.IncrementMinor(1, options.ResetLowerVersionPartsOnAutoInc); + break; + case VersionAutoIncrement.Patch: + versionBuilder.IncrementPatch(); + break; default: throw new InvalidOperationException("Unknown VersionAutoIncrement"); } diff --git a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs index 90d10b9..d37ebb0 100644 --- a/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs +++ b/source/GitTool/CreativeCoders.GitTool.Cli.Commands/ReleaseGroup/Create/CreateReleaseOptions.cs @@ -9,12 +9,13 @@ public class CreateReleaseOptions : IOptionsValidation { private const string PushAllTagsLongName = "alltags"; - [OptionValue(0, IsRequired = false)] public string? Version { get; set; } + [OptionValue(0, IsRequired = false, HelpText = "Version for release. If set version increment is not allowed.")] + public string? Version { get; set; } [OptionParameter('a', PushAllTagsLongName)] public bool PushAllTags { get; set; } - [OptionParameter('i', "increment", HelpText = "Version increment")] + [OptionParameter('i', "increment", HelpText = "Version auto increment. If set version is not allowed.")] public VersionAutoIncrement? VersionIncrement { get; set; } [OptionParameter('r', "resetlower", HelpText = "Reset lower version parts on auto increment")] @@ -30,6 +31,12 @@ public Task ValidateAsync() return Task.FromResult(OptionsValidationResult.Invalid(["Version or version increment must be specified"])); } + if (!string.IsNullOrWhiteSpace(Version) && VersionIncrement != null) + { + return Task.FromResult( + OptionsValidationResult.Invalid(["Version and version increment are mutually exclusive"])); + } + return Task.FromResult(OptionsValidationResult.Valid()); } } From ca794b0dafab6cb8edd0c4e16825f47f79e6ddee Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:45:35 +0100 Subject: [PATCH 17/17] Update `VersionFormatException` message for clearer description of valid version formats --- .../Versioning/VersionFormatException.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs index 8a4b490..7de9365 100644 --- a/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs +++ b/source/GitTool/CreativeCoders.GitTool.Base/Versioning/VersionFormatException.cs @@ -5,7 +5,7 @@ namespace CreativeCoders.GitTool.Base.Versioning; [PublicAPI] public class VersionFormatException(string? version, string? message = null) : Exception(message ?? - $"The version '{version}' has an invalid format. The version must consist of at least 3 parts separated by dots (e.g. '1.0.0').") + $"The version '{version}' has an invalid format. The version must consist of 1 to 3 numeric parts separated by dots (e.g. '1.0.0').") { public string? Version { get; } = version; -} \ No newline at end of file +}