Skip to content

Sanitize Participant #4588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/GitVersion.Core.Tests/Helpers/ParticipantSanitizerTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentException>(() => 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<ArgumentException>(() => ParticipantSanitizer.SanitizeParticipant(value));
exception.Message.ShouldBe("The value cannot end with a folder separator ('/'). (Parameter 'participant')");
}
}
7 changes: 7 additions & 0 deletions src/GitVersion.Core/Core/RegexPatterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -227,6 +228,9 @@ internal static partial class Output
[StringSyntax(StringSyntaxAttribute.Regex)]
internal const string VisualBasicAssemblyAttributeRegexPattern = @"(\s*\<Assembly:\s*(?:.*)\>\s*$(\r?\n)?)";

[StringSyntax(StringSyntaxAttribute.Regex)]
internal const string SanitizeParticipantRegexPattern = "[^a-zA-Z0-9]";

[GeneratedRegex(AssemblyVersionRegexPattern, Options)]
public static partial Regex AssemblyVersionRegex();

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/GitVersion.Core/GitVersion.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<InternalsVisibleTo Include="GitVersion.Output.Tests" />
<InternalsVisibleTo Include="GitVersion.App.Tests" />
<InternalsVisibleTo Include="GitVersion.MsBuild.Tests" />
<InternalsVisibleTo Include="GitVersion.Testing" />
</ItemGroup>

</Project>
8 changes: 5 additions & 3 deletions src/GitVersion.Testing/Fixtures/SequenceDiagram.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using GitVersion.Testing.Helpers;
using GitVersion.Testing.Internal;

namespace GitVersion.Testing;
Expand Down Expand Up @@ -39,11 +40,12 @@ public SequenceDiagram()
/// </summary>
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);
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/GitVersion.Testing/GitVersion.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="System.Reflection.Metadata" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\GitVersion.Core\Helpers\FileSystemHelper.cs" Link="Helpers\FileSystemHelper.cs" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="GitVersion.BuildAgents.Tests" />
<InternalsVisibleTo Include="GitVersion.Configuration.Tests" />
Expand All @@ -19,4 +16,7 @@
<InternalsVisibleTo Include="GitVersion.App.Tests" />
<InternalsVisibleTo Include="GitVersion.MsBuild.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GitVersion.Core\GitVersion.Core.csproj" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions src/GitVersion.Testing/Helpers/ParticipantSanitizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using GitVersion.Core;

namespace GitVersion.Testing.Helpers;

public static class ParticipantSanitizer
{
/// <summary>
/// Converts a participant identifier to a standardized format that won't break PlantUml.
/// </summary>
/// <param name="participant">The participant identifier to convert. This value cannot be null, empty, or consist only of whitespace.</param>
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));
}
}
}