Skip to content

Commit 32823c7

Browse files
authored
Merge pull request #6 from Techsola/upgrading_broke_system_commandline
Upgrading System.CommandLine broke the app
2 parents 0e4b187 + bf07a59 commit 32823c7

File tree

5 files changed

+181
-4
lines changed

5 files changed

+181
-4
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace TfvcMigrator.Tests;
2+
3+
public static class EntryPointTests
4+
{
5+
[Test]
6+
public static async Task No_System_CommandLine_failure_for_minimal_arguments()
7+
{
8+
var arguments = await CommandVerifier.VerifyArgumentsAsync(async () =>
9+
await Program.Main(new[]
10+
{
11+
"http://someurl",
12+
"$/SomePath",
13+
"--authors", "authors.txt",
14+
}));
15+
16+
arguments[0].ShouldBe(new Uri("http://someurl"));
17+
arguments[1].ShouldBe("$/SomePath");
18+
arguments[2].ShouldBe("authors.txt");
19+
}
20+
21+
[Test]
22+
public static async Task No_System_CommandLine_failure_for_all_arguments()
23+
{
24+
var arguments = await CommandVerifier.VerifyArgumentsAsync(async () =>
25+
await Program.Main(new[]
26+
{
27+
"http://someurl",
28+
"$/SomePath",
29+
"--authors", "authors.txt",
30+
"--out-dir", "somedir",
31+
"--min-changeset", "42",
32+
"--max-changeset", "43",
33+
"--root-path-changes", "CS1234:$/New/Path", "CS1235:$/Another/Path",
34+
"--pat", "somepat",
35+
}));
36+
37+
arguments[0].ShouldBe(new Uri("http://someurl"));
38+
arguments[1].ShouldBe("$/SomePath");
39+
arguments[2].ShouldBe("authors.txt");
40+
arguments[3].ShouldBe("somedir");
41+
arguments[4].ShouldBe(42);
42+
arguments[5].ShouldBe(43);
43+
arguments[6].ShouldBeOfType<ImmutableArray<RootPathChange>>().ShouldBe(new[]
44+
{
45+
new RootPathChange(1234, "$/New/Path"),
46+
new RootPathChange(1235, "$/Another/Path"),
47+
});
48+
arguments[7].ShouldBe("somepat");
49+
}
50+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using System.Reflection;
2+
using System.Reflection.Emit;
3+
4+
namespace TfvcMigrator;
5+
6+
public static class CommandVerifier
7+
{
8+
private static readonly AsyncLocal<Action<object?[]>?> ActiveInterceptor = new();
9+
private static readonly FieldInfo ActiveInterceptorField = typeof(CommandVerifier).GetField(nameof(ActiveInterceptor), BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly)!;
10+
11+
public static async Task<object?[]> VerifyArgumentsAsync(Func<Task> invokeInterceptedDelegateAsync)
12+
{
13+
var arguments = (object?[]?)null;
14+
15+
var previousValue = ActiveInterceptor.Value;
16+
ActiveInterceptor.Value = interceptedArguments =>
17+
{
18+
if (arguments is not null)
19+
throw new InvalidOperationException("The intercepted delegate was invoked more than once.");
20+
21+
arguments = interceptedArguments;
22+
};
23+
try
24+
{
25+
await invokeInterceptedDelegateAsync().ConfigureAwait(false);
26+
}
27+
finally
28+
{
29+
ActiveInterceptor.Value = previousValue;
30+
}
31+
32+
return arguments ?? throw new InvalidOperationException("The intercepted delegate was not invoked.");
33+
}
34+
35+
public static Delegate Intercept(Delegate @delegate)
36+
{
37+
if (@delegate.Target is not null)
38+
throw new ArgumentException("Only static lambdas and static methods are supported.", nameof(@delegate));
39+
40+
var parameters = @delegate.Method.GetParameters();
41+
42+
// System.Linq.Expressions creates a method with an initial closure parameter which System.CommandLine
43+
// doesn't tolerate.
44+
var interceptingMethod = new DynamicMethod(@delegate.Method.Name, @delegate.Method.ReturnType, Array.ConvertAll(parameters, p => p.ParameterType));
45+
46+
for (var i = 0; i < parameters.Length; i++)
47+
{
48+
var parameter = parameters[i];
49+
interceptingMethod.DefineParameter(i + 1, parameter.Attributes, parameter.Name);
50+
}
51+
52+
var generator = interceptingMethod.GetILGenerator();
53+
54+
var interceptorLocal = generator.DeclareLocal(typeof(Action<object[]>));
55+
56+
generator.Emit(OpCodes.Ldsfld, ActiveInterceptorField);
57+
generator.Emit(OpCodes.Callvirt, ActiveInterceptorField.FieldType.GetProperty(nameof(ActiveInterceptor.Value), BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)!.GetMethod!);
58+
generator.Emit(OpCodes.Stloc, interceptorLocal);
59+
generator.Emit(OpCodes.Ldloc, interceptorLocal);
60+
61+
var noInterceptionLabel = generator.DefineLabel();
62+
generator.Emit(OpCodes.Brfalse_S, noInterceptionLabel);
63+
64+
// Call the intercepting delegate
65+
generator.Emit(OpCodes.Ldloc, interceptorLocal);
66+
generator.Emit(OpCodes.Ldc_I4, parameters.Length);
67+
generator.Emit(OpCodes.Newarr, typeof(object));
68+
69+
for (var i = 0; i < parameters.Length; i++)
70+
{
71+
generator.Emit(OpCodes.Dup); // object[]
72+
generator.Emit(OpCodes.Ldc_I4, i);
73+
generator.Emit(OpCodes.Ldarg, i);
74+
75+
if (parameters[i].ParameterType is { IsValueType: true } valueType)
76+
generator.Emit(OpCodes.Box, valueType);
77+
78+
generator.Emit(OpCodes.Stelem_Ref);
79+
}
80+
81+
generator.Emit(OpCodes.Callvirt, interceptorLocal.LocalType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)!);
82+
83+
if (@delegate.Method.ReturnType != typeof(void))
84+
{
85+
if (@delegate.Method.ReturnType == typeof(Task))
86+
{
87+
generator.Emit(OpCodes.Call, typeof(Task).GetProperty(nameof(Task.CompletedTask), BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)!.GetMethod!);
88+
}
89+
else
90+
{
91+
throw new NotImplementedException();
92+
}
93+
}
94+
95+
generator.Emit(OpCodes.Ret);
96+
97+
// Call original method
98+
generator.MarkLabel(noInterceptionLabel);
99+
100+
for (var i = 0; i < parameters.Length; i++)
101+
generator.Emit(OpCodes.Ldarg, i);
102+
103+
generator.Emit(OpCodes.Call, @delegate.Method);
104+
generator.Emit(OpCodes.Ret);
105+
106+
return interceptingMethod.CreateDelegate(@delegate.GetType());
107+
}
108+
}

src/TfvcMigrator/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.CommandLine;
2+
using System.CommandLine.NamingConventionBinder;
23
using System.Globalization;
34
using System.Text;
45
using LibGit2Sharp;
@@ -30,13 +31,13 @@ public static Task Main(string[] args)
3031
parseArgument: result => result.Tokens.Select(token => ParseRootPathChange(token.Value)).ToImmutableArray())
3132
{
3233
Arity = ArgumentArity.OneOrMore,
34+
AllowMultipleArgumentsPerToken = true,
3335
Description = "Followed by one or more arguments with the format CS1234:$/New/Path. Changes the path that is mapped as the Git repository root to a new path during a specified changeset.",
3436
},
3537
new Option<string?>("--pat") { Description = "Optional PAT, required to access TFVC repositories hosted on Azure DevOps Services. If not provided Default Client Credentials will be used, these are only suitable for on-premise TFS/Azure DevOps Server." },
3638
};
3739

38-
command.SetHandler(
39-
new Func<Uri, string, string, string?, int?, int?, ImmutableArray<RootPathChange>, string?, Task>(MigrateAsync));
40+
command.Handler = CommandHandler.Create(CommandVerifier.Intercept(MigrateAsync));
4041

4142
return command.InvokeAsync(args);
4243
}

src/TfvcMigrator/RootPathChange.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace TfvcMigrator;
22

33
[DebuggerDisplay("{ToString(),nq}")]
4-
public sealed class RootPathChange
4+
public sealed class RootPathChange : IEquatable<RootPathChange?>
55
{
66
public RootPathChange(int changeset, string newSourceRootPath)
77
{
@@ -18,5 +18,22 @@ public RootPathChange(int changeset, string newSourceRootPath)
1818
public int Changeset { get; }
1919
public string NewSourceRootPath { get; }
2020

21+
public override bool Equals(object? obj)
22+
{
23+
return Equals(obj as RootPathChange);
24+
}
25+
26+
public bool Equals(RootPathChange? other)
27+
{
28+
return other != null &&
29+
Changeset == other.Changeset &&
30+
NewSourceRootPath == other.NewSourceRootPath;
31+
}
32+
33+
public override int GetHashCode()
34+
{
35+
return HashCode.Combine(Changeset, NewSourceRootPath);
36+
}
37+
2138
public override string ToString() => $"CS{Changeset}:{NewSourceRootPath}";
2239
}

src/TfvcMigrator/TfvcMigrator.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
<ItemGroup>
2424
<PackageReference Include="LibGit2Sharp" Version="0.27.0-preview-0175" />
2525
<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="16.170.0" />
26-
<PackageReference Include="System.CommandLine" Version="2.0.0-beta2.21617.1" />
26+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta3.22114.1" />
27+
<PackageReference Include="System.CommandLine.NamingConventionBinder" Version="2.0.0-beta3.22114.1" />
2728
<PackageReference Include="TaskTupleAwaiter" Version="2.0.0" />
2829
</ItemGroup>
2930

0 commit comments

Comments
 (0)