Skip to content

Commit 8acd159

Browse files
John-LeitchcincuranetCopilottarekgh
authored
Fixed LoggerMessageGenerator message escaping. (#123787)
This PR addresses bug #123786 by escaping all non-printable characters (`< 0x20`) in the emitted message. `\r` and `\n` are emitted using the same escape sequence, while other non-printable characters are emitted as hex escape sequences e.g. `\x00`. Escaping of `\` is also addressed. Finally, an additional test was added to cover these behaviors. --------- Co-authored-by: Jiri Cincura ↹ <jiri@cincura.net> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tarek Mahmoud Sayed <10833894+tarekgh@users.noreply.github.com> Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
1 parent 39e5215 commit 8acd159

10 files changed

Lines changed: 66 additions & 62 deletions

src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Text;
88
using System.Threading;
99
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.CSharp;
1011

1112
namespace Microsoft.Extensions.Logging.Generators
1213
{
@@ -181,7 +182,7 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation)
181182
string formatMethodEnd = formatMethodBegin.Length > 0 ? ")" : "";
182183

183184
_builder.Append($@"
184-
{nestedIndentation}return {formatMethodBegin}$""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}""{formatMethodEnd};
185+
{nestedIndentation}return {formatMethodBegin}${SymbolDisplay.FormatLiteral(lm.Message, quote: true)}{formatMethodEnd};
185186
{nestedIndentation}}}
186187
");
187188
_builder.Append($@"
@@ -280,7 +281,7 @@ private void GenCases(LoggerMethod lm, string nestedIndentation)
280281
_builder.AppendLine($" {nestedIndentation}{index++} => new global::System.Collections.Generic.KeyValuePair<string, object?>(\"{name}\", this.{NormalizeSpecialSymbol(p.CodeName)}),");
281282
}
282283

283-
_builder.AppendLine($" {nestedIndentation}{index++} => new global::System.Collections.Generic.KeyValuePair<string, object?>(\"{{OriginalFormat}}\", \"{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}\"),");
284+
_builder.AppendLine($" {nestedIndentation}{index++} => new global::System.Collections.Generic.KeyValuePair<string, object?>(\"{{OriginalFormat}}\", {SymbolDisplay.FormatLiteral(lm.Message, quote: true)}),");
284285
}
285286

286287
private void GenCallbackArguments(LoggerMethod lm)
@@ -406,7 +407,7 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
406407

407408
GenDefineTypes(lm, brackets: true);
408409

409-
_builder.Append(@$"({level}, new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), ""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}"", new global::Microsoft.Extensions.Logging.LogDefineOptions() {{ SkipEnabledCheck = true }});
410+
_builder.Append(@$"({level}, new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), {SymbolDisplay.FormatLiteral(lm.Message, quote: true)}, new global::Microsoft.Extensions.Logging.LogDefineOptions() {{ SkipEnabledCheck = true }});
410411
");
411412
}
412413

@@ -576,55 +577,6 @@ public static string Enumerate(global::System.Collections.IEnumerable? enumerabl
576577
}
577578
}
578579

579-
private static string ConvertEndOfLineAndQuotationCharactersToEscapeForm(string s)
580-
{
581-
int index = 0;
582-
while (index < s.Length)
583-
{
584-
if (s[index] is '\n' or '\r' or '"')
585-
{
586-
break;
587-
}
588-
index++;
589-
}
590-
591-
if (index >= s.Length)
592-
{
593-
return s;
594-
}
595-
596-
StringBuilder sb = new StringBuilder(s.Length);
597-
sb.Append(s, 0, index);
598-
599-
while (index < s.Length)
600-
{
601-
switch (s[index])
602-
{
603-
case '\n':
604-
sb.Append('\\');
605-
sb.Append('n');
606-
break;
607-
608-
case '\r':
609-
sb.Append('\\');
610-
sb.Append('r');
611-
break;
612-
613-
case '"':
614-
sb.Append('\\');
615-
sb.Append('"');
616-
break;
617-
618-
default:
619-
sb.Append(s[index]);
620-
break;
621-
}
622-
623-
index++;
624-
}
625-
626-
return sb.ToString();
627-
}
628580
/// <summary>
629581
/// Checks if variableOrTemplateName contains a special symbol ('@') as starting char
630582
/// </summary>

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithLoggerFromPrimaryConstructor.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M0Callback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
public partial void M0()

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithLoggerFromPrimaryConstructorWithParameterUsedInMethod.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M0Callback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
public partial void M0()

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithLoggerInFieldAndFromPrimaryConstructor.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M0Callback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
public partial void M0()

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMultipleClassesStableOrder.generated.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __LogACallback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(1, nameof(LogA)), "Message from ClassA", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(1, nameof(LogA)), "Message from ClassA", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
static partial void LogA(global::Microsoft.Extensions.Logging.ILogger logger)
@@ -25,7 +25,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
2525
{
2626
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
2727
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __LogBCallback =
28-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(2, nameof(LogB)), "Message from ClassB", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
28+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(2, nameof(LogB)), "Message from ClassB", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
2929

3030
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
3131
static partial void LogB(global::Microsoft.Extensions.Logging.ILogger logger)
@@ -43,7 +43,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
4343
{
4444
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
4545
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __LogCCallback =
46-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(3, nameof(LogC)), "Message from ClassC", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
46+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(3, nameof(LogC)), "Message from ClassC", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
4747

4848
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
4949
static partial void LogC(global::Microsoft.Extensions.Logging.ILogger logger)

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithNestedClass.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses.NestedNamesp
1717
{
1818
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1919
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M9Callback =
20-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(9, nameof(M9)), "M9", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
20+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(9, nameof(M9)), "M9", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
2121

2222
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
2323
public static partial void M9(global::Microsoft.Extensions.Logging.ILogger logger)

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithNestedClassWithGenericTypesWithAttributes.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
99
{
1010
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1111
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, A, B, C, global::System.Exception?> __M0Callback =
12-
global::Microsoft.Extensions.Logging.LoggerMessage.Define<A, B, C>(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(42, nameof(M0)), "a = {a}; b = {b}; c = {c}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
12+
global::Microsoft.Extensions.Logging.LoggerMessage.Define<A, B, C>(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(42, nameof(M0)), "a = {a}; b = {b}; c = {c}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1313

1414
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1515
public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, A a, B b, C c)

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M0Callback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger)

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithTwoParams.generated.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
77
{
88
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
99
private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Int32, global::System.Collections.Generic.IEnumerable<global::System.Int32>, global::System.Exception?> __M0Callback =
10-
global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.Int32, global::System.Collections.Generic.IEnumerable<global::System.Int32>>(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0 {a1} {a2}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
10+
global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.Int32, global::System.Collections.Generic.IEnumerable<global::System.Int32>>(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0 {a1} {a2}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
1111

1212
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")]
1313
public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, global::System.Int32 a1, global::System.Collections.Generic.IEnumerable<global::System.Int32> a2)

src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,58 @@ public void GenericTypeParameterAttributesAreRetained()
277277
Assert.NotNull(type.GenericTypeParameters[1].GetCustomAttribute<TestClasses.BarAttribute>());
278278
}
279279

280+
[Theory]
281+
[InlineData(@"Foo \\ bar: {foo}", null)]
282+
[InlineData(@"Foo \\\\ bar: {foo}", null)]
283+
[InlineData(@"Foo \"" bar: {foo}", null)]
284+
[InlineData(@"Foo \x22 bar: {foo}", @"Foo \"" bar: {foo}")]
285+
[InlineData(@"Foo \u0022 bar: {foo}", @"Foo \"" bar: {foo}")]
286+
[InlineData(@"Foo \r bar: {foo}", null)]
287+
[InlineData(@"Foo \x0d bar: {foo}", @"Foo \r bar: {foo}")]
288+
[InlineData(@"Foo \u000d bar: {foo}", @"Foo \r bar: {foo}")]
289+
[InlineData(@"Foo \n bar: {foo}", null)]
290+
[InlineData(@"Foo \x0a bar: {foo}", @"Foo \n bar: {foo}")]
291+
[InlineData(@"Foo \u000a bar: {foo}", @"Foo \n bar: {foo}")]
292+
[InlineData(@"Foo \0 bar: {foo}", null)]
293+
[InlineData(@"Foo \x00 bar: {foo}", @"Foo \0 bar: {foo}")]
294+
[InlineData(@"Foo \u0000 bar: {foo}", @"Foo \0 bar: {foo}")]
295+
[InlineData(@"Foo \x1f bar: {foo}", @"Foo \u001f bar: {foo}")]
296+
[InlineData(@"Foo \u001f bar: {foo}", null)]
297+
public async Task EmittedMessageIsWellFormed(string message, string? expectedMessage)
298+
{
299+
var code =
300+
$$"""
301+
namespace Test
302+
{
303+
using Microsoft.Extensions.Logging;
304+
305+
partial class C
306+
{
307+
[LoggerMessage(EventId = 5230, Level = LogLevel.Information, Message = "{{message}}")]
308+
static partial void Test(ILogger logger, string foo);
309+
}
310+
}
311+
""";
312+
313+
var (diagnostics, generatedSources) = await RoslynTestUtils
314+
.RunGenerator(
315+
new LoggerMessageGenerator(),
316+
new[] { typeof(ILogger).Assembly, typeof(LoggerMessageAttribute).Assembly },
317+
new[] { code },
318+
includeBaseReferences: true)
319+
.ConfigureAwait(false);
320+
321+
Assert.Empty(diagnostics);
322+
Assert.Single(generatedSources);
323+
324+
var generatedSource = generatedSources[0];
325+
var src = generatedSource.SourceText.ToString();
326+
Assert.Contains($"\"{expectedMessage ?? message}\"", src);
327+
328+
var generatedSourceDiagnostics = generatedSource.SyntaxTree.GetDiagnostics();
329+
Assert.Empty(generatedSourceDiagnostics);
330+
}
331+
280332
private async Task VerifyAgainstBaselineUsingFile(string filename, string testSourceCode)
281333
{
282334
string baseline = LineEndingsHelper.Normalize(File.ReadAllText(Path.Combine("Baselines", filename)));

0 commit comments

Comments
 (0)