diff --git a/src/GitVersion.Core.Tests/Helpers/ParticipantSanitizerTests.cs b/src/GitVersion.Core.Tests/Helpers/ParticipantSanitizerTests.cs new file mode 100644 index 0000000000..ad5f854bd3 --- /dev/null +++ b/src/GitVersion.Core.Tests/Helpers/ParticipantSanitizerTests.cs @@ -0,0 +1,67 @@ +using GitVersion.Testing.Helpers; + +namespace GitVersion.Core.Tests.Helpers; + +[TestFixture] +public class ParticipantSanitizerTests +{ + [TestCase("feature/1234-is-id-with-something-kebab", "feature_1234_is_id_with_something_kebab")] + [TestCase("feature/1234-IsSomethingPascalCase", "feature_1234_IsSomethingPascalCase")] + [TestCase("feature/Caps-lower-something-kebab", "feature_Caps_lower_something_kebab")] + [TestCase("feature/Caps-lower-is-kebab", "feature_Caps_lower_is_kebab")] + [TestCase("kebab-folder/1234-is-id-with-something-kebab", "kebab_folder_1234_is_id_with_something_kebab")] + [TestCase("kebab-folder/1234-IsSomethingPascalCase", "kebab_folder_1234_IsSomethingPascalCase")] + [TestCase("kebab-folder/Caps-lower-something-kebab", "kebab_folder_Caps_lower_something_kebab")] + [TestCase("kebab-folder/Caps-lower-is-kebab", "kebab_folder_Caps_lower_is_kebab")] + [TestCase("PascalCaseFolder/1234-is-id-with-something-kebab", "PascalCaseFolder_1234_is_id_with_something_kebab")] + [TestCase("PascalCaseFolder/1234-IsSomethingPascalCase", "PascalCaseFolder_1234_IsSomethingPascalCase")] + [TestCase("PascalCaseFolder/Caps-lower-something-kebab", "PascalCaseFolder_Caps_lower_something_kebab")] + [TestCase("PascalCaseFolder/Caps-lower-is-kebab", "PascalCaseFolder_Caps_lower_is_kebab")] + [TestCase("1234-is-id-with-something-kebab", "1234_is_id_with_something_kebab")] + [TestCase("1234-IsSomethingPascalCase", "1234_IsSomethingPascalCase")] + [TestCase("Caps-lower-something-kebab", "Caps_lower_something_kebab")] + [TestCase("Caps-lower-is-kebab", "Caps_lower_is_kebab")] + [TestCase("feature/all-lower-is-kebab", "feature_all_lower_is_kebab")] + [TestCase("feature/24321-Upperjustoneword", "feature_24321_Upperjustoneword")] + [TestCase("feature/justoneword", "feature_justoneword")] + [TestCase("feature/PascalCase", "feature_PascalCase")] + [TestCase("feature/PascalCase-with-kebab", "feature_PascalCase_with_kebab")] + [TestCase("feature/12414", "feature_12414")] + [TestCase("feature/12414/12342-FeatureStoryTaskWithShortDescription", "feature_12414_12342_FeatureStoryTaskWithShortDescription")] + [TestCase("feature/12414/12342-Short-description", "feature_12414_12342_Short_description")] + [TestCase("feature/12414/12342-short-description", "feature_12414_12342_short_description")] + [TestCase("feature/12414/12342-Short-Description", "feature_12414_12342_Short_Description")] + [TestCase("release/1.0.0", "release_1_0_0")] + [TestCase("releases", "releases")] + [TestCase("feature", "feature")] + [TestCase("feature/tfs1-Short-description", "feature_tfs1_Short_description")] + [TestCase("feature/f2-Short-description", "feature_f2_Short_description")] + [TestCase("feature/bug1", "feature_bug1")] + [TestCase("f2", "f2")] + [TestCase("feature/f2", "feature_f2")] + [TestCase("feature/story2", "feature_story2")] + [TestCase("master", "master")] + [TestCase("develop", "develop")] + [TestCase("main", "main")] + public void SanitizeValidParticipant_ShouldReturnExpectedResult(string input, string expected) + { + var actual = ParticipantSanitizer.SanitizeParticipant(input); + actual.ShouldBe(expected); + } + + [TestCase("")] + [TestCase(" ")] + public void SanitizeEmptyOrWhitespaceParticipant_ShouldThrow(string value) + { + var exception = Should.Throw(() => ParticipantSanitizer.SanitizeParticipant(value)); + exception.Message.ShouldBe("The value cannot be an empty string or composed entirely of whitespace. (Parameter 'participant')"); + } + + [TestCase("feature/")] + [TestCase("/")] + public void SanitizeInvalidParticipant_ShouldThrow(string value) + { + var exception = Should.Throw(() => ParticipantSanitizer.SanitizeParticipant(value)); + exception.Message.ShouldBe("The value cannot end with a folder separator ('/'). (Parameter 'participant')"); + } +} diff --git a/src/GitVersion.Core/Core/RegexPatterns.cs b/src/GitVersion.Core/Core/RegexPatterns.cs index a9ad425397..57d805fdab 100644 --- a/src/GitVersion.Core/Core/RegexPatterns.cs +++ b/src/GitVersion.Core/Core/RegexPatterns.cs @@ -53,6 +53,7 @@ public static Regex GetOrAdd([StringSyntax(StringSyntaxAttribute.Regex)] string [Output.CsharpAssemblyAttributeRegexPattern] = Output.CsharpAssemblyAttributeRegex, [Output.FsharpAssemblyAttributeRegexPattern] = Output.FsharpAssemblyAttributeRegex, [Output.VisualBasicAssemblyAttributeRegexPattern] = Output.VisualBasicAssemblyAttributeRegex, + [Output.SanitizeParticipantRegexPattern] = Output.SanitizeParticipantRegex, [VersionCalculation.DefaultMajorRegexPattern] = VersionCalculation.DefaultMajorRegex, [VersionCalculation.DefaultMinorRegexPattern] = VersionCalculation.DefaultMinorRegex, [VersionCalculation.DefaultPatchRegexPattern] = VersionCalculation.DefaultPatchRegex, @@ -227,6 +228,9 @@ internal static partial class Output [StringSyntax(StringSyntaxAttribute.Regex)] internal const string VisualBasicAssemblyAttributeRegexPattern = @"(\s*\\s*$(\r?\n)?)"; + [StringSyntax(StringSyntaxAttribute.Regex)] + internal const string SanitizeParticipantRegexPattern = "[^a-zA-Z0-9]"; + [GeneratedRegex(AssemblyVersionRegexPattern, Options)] public static partial Regex AssemblyVersionRegex(); @@ -244,6 +248,9 @@ internal static partial class Output [GeneratedRegex(VisualBasicAssemblyAttributeRegexPattern, Options | RegexOptions.Multiline)] public static partial Regex VisualBasicAssemblyAttributeRegex(); + + [GeneratedRegex(SanitizeParticipantRegexPattern, Options)] + public static partial Regex SanitizeParticipantRegex(); } internal static partial class VersionCalculation diff --git a/src/GitVersion.Core/GitVersion.Core.csproj b/src/GitVersion.Core/GitVersion.Core.csproj index bc311c70f1..0843eba8da 100644 --- a/src/GitVersion.Core/GitVersion.Core.csproj +++ b/src/GitVersion.Core/GitVersion.Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs b/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs index bf6a82ad86..35e73d21cf 100644 --- a/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs +++ b/src/GitVersion.Testing/Fixtures/SequenceDiagram.cs @@ -1,3 +1,4 @@ +using GitVersion.Testing.Helpers; using GitVersion.Testing.Internal; namespace GitVersion.Testing; @@ -39,11 +40,12 @@ public SequenceDiagram() /// public void Participant(string participant, string? @as = null) { - this.participants.Add(participant, @as ?? participant); - if (@as == null) + var cleanParticipant = ParticipantSanitizer.SanitizeParticipant(@as ?? participant); + this.participants.Add(participant, cleanParticipant); + if (participant == cleanParticipant) this.diagramBuilder.AppendLineFormat("participant {0}", participant); else - this.diagramBuilder.AppendLineFormat("participant \"{0}\" as {1}", participant, @as); + this.diagramBuilder.AppendLineFormat("participant \"{0}\" as {1}", participant, cleanParticipant); } /// diff --git a/src/GitVersion.Testing/GitVersion.Testing.csproj b/src/GitVersion.Testing/GitVersion.Testing.csproj index 97b286db82..08c0eba912 100644 --- a/src/GitVersion.Testing/GitVersion.Testing.csproj +++ b/src/GitVersion.Testing/GitVersion.Testing.csproj @@ -8,9 +8,6 @@ - - - @@ -19,4 +16,7 @@ + + + diff --git a/src/GitVersion.Testing/Helpers/ParticipantSanitizer.cs b/src/GitVersion.Testing/Helpers/ParticipantSanitizer.cs new file mode 100644 index 0000000000..826728b19b --- /dev/null +++ b/src/GitVersion.Testing/Helpers/ParticipantSanitizer.cs @@ -0,0 +1,26 @@ +using GitVersion.Core; + +namespace GitVersion.Testing.Helpers; + +public static class ParticipantSanitizer +{ + /// + /// Converts a participant identifier to a standardized format that won't break PlantUml. + /// + /// The participant identifier to convert. This value cannot be null, empty, or consist only of whitespace. + public static string SanitizeParticipant(string participant) + { + GuardAgainstInvalidParticipants(participant); + + return RegexPatterns.Output.SanitizeParticipantRegex().Replace(participant, "_"); + } + + private static void GuardAgainstInvalidParticipants(string participant) + { + ArgumentException.ThrowIfNullOrWhiteSpace(participant); + if (participant.EndsWith('/')) + { + throw new ArgumentException("The value cannot end with a folder separator ('/').", nameof(participant)); + } + } +}