Skip to content

Commit 8a48eb0

Browse files
committed
registry: enable registration of providers with priorities
Enable host providers to be registered and enumerated for auto-detection in a simple priority order: high, normal, low.
1 parent 6ec2db2 commit 8a48eb0

File tree

6 files changed

+123
-41
lines changed

6 files changed

+123
-41
lines changed

src/shared/Git-Credential-Manager/Program.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ public static void Main(string[] args)
1717
using (var context = new CommandContext())
1818
using (var app = new Application(context, appPath))
1919
{
20-
// Register all supported host providers
21-
app.RegisterProviders(
22-
new AzureReposHostProvider(context),
23-
new BitbucketHostProvider(context),
24-
new GitHubHostProvider(context),
25-
new GenericHostProvider(context)
26-
);
20+
// Register all supported host providers at the normal priority.
21+
// The generic provider should never win against a more specific one, so register it with low priority.
22+
app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal);
23+
app.RegisterProvider(new BitbucketHostProvider(context), HostProviderPriority.Normal);
24+
app.RegisterProvider(new GitHubHostProvider(context), HostProviderPriority.Normal);
25+
app.RegisterProvider(new GenericHostProvider(context), HostProviderPriority.Low);
2726

2827
// Run!
2928
int exitCode = app.RunAsync(args)

src/shared/Microsoft.Git.CredentialManager.Tests/HostProviderRegistryTests.cs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void HostProviderRegistry_Register_AutoProviderId_ThrowException()
1919
var provider = new Mock<IHostProvider>();
2020
provider.Setup(x => x.Id).Returns(Constants.ProviderIdAuto);
2121

22-
Assert.Throws<ArgumentException>(() => registry.Register(provider.Object));
22+
Assert.Throws<ArgumentException>(() => registry.Register(provider.Object, HostProviderPriority.Normal));
2323
}
2424

2525
[Fact]
@@ -30,7 +30,7 @@ public void HostProviderRegistry_Register_AutoAuthorityId_ThrowException()
3030
var provider = new Mock<IHostProvider>();
3131
provider.Setup(x => x.SupportedAuthorityIds).Returns(new[]{"foo", Constants.AuthorityIdAuto, "bar"});
3232

33-
Assert.Throws<ArgumentException>(() => registry.Register(provider.Object));
33+
Assert.Throws<ArgumentException>(() => registry.Register(provider.Object, HostProviderPriority.Normal));
3434
}
3535

3636
[Fact]
@@ -57,7 +57,9 @@ public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_ReturnsSupp
5757
provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
5858
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
5959

60-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
60+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
61+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
62+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
6163

6264
IHostProvider result = await registry.GetProviderAsync(input);
6365

@@ -78,13 +80,41 @@ public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProviders_R
7880
provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
7981
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
8082

81-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
83+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
84+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
85+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
8286

8387
IHostProvider result = await registry.GetProviderAsync(input);
8488

8589
Assert.Same(provider1Mock.Object, result);
8690
}
8791

92+
[Fact]
93+
public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProvidersMultipleLevels_ReturnsFirstHighestRegistered()
94+
{
95+
var context = new TestCommandContext();
96+
var registry = new HostProviderRegistry(context);
97+
var input = new InputArguments(new Dictionary<string, string>());
98+
99+
var provider1Mock = new Mock<IHostProvider>();
100+
var provider2Mock = new Mock<IHostProvider>();
101+
var provider3Mock = new Mock<IHostProvider>();
102+
var provider4Mock = new Mock<IHostProvider>();
103+
provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
104+
provider2Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
105+
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
106+
provider4Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
107+
108+
registry.Register(provider1Mock.Object, HostProviderPriority.Low);
109+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
110+
registry.Register(provider3Mock.Object, HostProviderPriority.High);
111+
registry.Register(provider4Mock.Object, HostProviderPriority.Low);
112+
113+
IHostProvider result = await registry.GetProviderAsync(input);
114+
115+
Assert.Same(provider3Mock.Object, result);
116+
}
117+
88118
[Fact]
89119
public async Task HostProviderRegistry_GetProvider_ProviderSpecified_ReturnsProvider()
90120
{
@@ -105,7 +135,9 @@ public async Task HostProviderRegistry_GetProvider_ProviderSpecified_ReturnsProv
105135
provider3Mock.Setup(x => x.Id).Returns("provider3");
106136
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
107137

108-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
138+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
139+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
140+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
109141

110142
IHostProvider result = await registry.GetProviderAsync(input);
111143

@@ -132,7 +164,9 @@ public async Task HostProviderRegistry_GetProvider_AutoProviderSpecified_Returns
132164
provider3Mock.Setup(x => x.Id).Returns("provider3");
133165
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
134166

135-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
167+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
168+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
169+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
136170

137171
IHostProvider result = await registry.GetProviderAsync(input);
138172

@@ -159,7 +193,9 @@ public async Task HostProviderRegistry_GetProvider_UnknownProviderSpecified_Retu
159193
provider3Mock.Setup(x => x.Id).Returns("provider3");
160194
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
161195

162-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
196+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
197+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
198+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
163199

164200
IHostProvider result = await registry.GetProviderAsync(input);
165201

@@ -186,7 +222,9 @@ public async Task HostProviderRegistry_GetProvider_LegacyAuthoritySpecified_Retu
186222
provider3Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{"authorityD"});
187223
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
188224

189-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
225+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
226+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
227+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
190228

191229
IHostProvider result = await registry.GetProviderAsync(input);
192230

@@ -213,7 +251,9 @@ public async Task HostProviderRegistry_GetProvider_AutoLegacyAuthoritySpecified_
213251
provider3Mock.Setup(x => x.SupportedAuthorityIds).Returns(new[]{"authorityD"});
214252
provider3Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
215253

216-
registry.Register(provider1Mock.Object, provider2Mock.Object, provider3Mock.Object);
254+
registry.Register(provider1Mock.Object, HostProviderPriority.Normal);
255+
registry.Register(provider2Mock.Object, HostProviderPriority.Normal);
256+
registry.Register(provider3Mock.Object, HostProviderPriority.Normal);
217257

218258
IHostProvider result = await registry.GetProviderAsync(input);
219259

src/shared/Microsoft.Git.CredentialManager/Application.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ internal Application(ICommandContext context,
3838
_configurationService.AddComponent(this);
3939
}
4040

41-
public void RegisterProviders(params IHostProvider[] providers)
41+
public void RegisterProvider(IHostProvider provider, HostProviderPriority priority)
4242
{
43-
_providerRegistry.Register(providers);
43+
_providerRegistry.Register(provider, priority);
4444

45-
// Add any providers that are also configurable components to the configuration service
46-
foreach (IConfigurableComponent configurableProvider in providers.OfType<IConfigurableComponent>())
45+
// If the provider is also a configurable component, add that to the configuration service
46+
if (provider is IConfigurableComponent configurableProvider)
4747
{
4848
_configurationService.AddComponent(configurableProvider);
4949
}

src/shared/Microsoft.Git.CredentialManager/EnumerableExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System;
34
using System.Collections.Generic;
45
using System.Linq;
56

@@ -25,5 +26,11 @@ public static IEnumerable<TSource> ConcatMany<TSource>(this IEnumerable<TSource>
2526

2627
return result;
2728
}
29+
30+
public static bool TryGetFirst<TSource>(this IEnumerable<TSource> collection, Func<TSource, bool> predicate, out TSource result)
31+
{
32+
result = collection.FirstOrDefault(predicate);
33+
return !(result is null);
34+
}
2835
}
2936
}

src/shared/Microsoft.Git.CredentialManager/HostProviderRegistry.cs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

88
namespace Microsoft.Git.CredentialManager
99
{
10+
/// <summary>
11+
/// Priority in which host providers are queried during auto-detection.
12+
/// </summary>
13+
public enum HostProviderPriority
14+
{
15+
Low = 0,
16+
Normal = 1,
17+
High = 2,
18+
}
19+
1020
/// <summary>
1121
/// Represents a collection of <see cref="IHostProvider"/>s which are selected based on Git credential query
1222
/// <see cref="InputArguments"/>.
@@ -17,11 +27,12 @@ namespace Microsoft.Git.CredentialManager
1727
public interface IHostProviderRegistry : IDisposable
1828
{
1929
/// <summary>
20-
/// Add the given <see cref="IHostProvider"/>(s) to this registry.
30+
/// Add the given <see cref="IHostProvider"/> to this registry.
2131
/// </summary>
22-
/// <param name="hostProviders">A collection of providers to register.</param>
32+
/// <param name="hostProvider">Host provider to register.</param>
33+
/// <param name="priority">Priority at which the provider will be considered when auto-detecting.</param>
2334
/// <remarks>Providers will be disposed of when this registry instance is disposed itself.</remarks>
24-
void Register(params IHostProvider[] hostProviders);
35+
void Register(IHostProvider hostProvider, HostProviderPriority priority);
2536

2637
/// <summary>
2738
/// Select a <see cref="IHostProvider"/> that can service the Git credential query based on the
@@ -33,44 +44,47 @@ public interface IHostProviderRegistry : IDisposable
3344
}
3445

3546
/// <summary>
36-
/// A simple host provider registry where each provider is queried in registration order until the first
47+
/// Host provider registry where each provider is queried by priority order until the first
3748
/// provider that supports the credential query is found.
3849
/// </summary>
3950
public class HostProviderRegistry : IHostProviderRegistry
4051
{
4152
private readonly ICommandContext _context;
42-
private readonly List<IHostProvider> _hostProviders;
53+
private readonly IDictionary<HostProviderPriority, ICollection<IHostProvider>> _hostProviders;
4354

4455
public HostProviderRegistry(ICommandContext context)
4556
{
4657
EnsureArgument.NotNull(context, nameof(context));
4758

4859
_context = context;
49-
_hostProviders = new List<IHostProvider>();
60+
_hostProviders = new Dictionary<HostProviderPriority, ICollection<IHostProvider>>();
5061
}
5162

52-
public void Register(params IHostProvider[] hostProviders)
63+
public void Register(IHostProvider hostProvider, HostProviderPriority priority)
5364
{
54-
if (hostProviders == null)
55-
{
56-
throw new ArgumentNullException(nameof(hostProviders));
57-
}
65+
EnsureArgument.NotNull(hostProvider, nameof(hostProvider));
5866

59-
if (hostProviders.Any(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, Constants.ProviderIdAuto)))
67+
if (StringComparer.OrdinalIgnoreCase.Equals(hostProvider.Id, Constants.ProviderIdAuto))
6068
{
6169
throw new ArgumentException(
6270
$"A host provider cannot be registered with the ID '{Constants.ProviderIdAuto}'",
63-
nameof(hostProviders));
71+
nameof(hostProvider));
6472
}
6573

66-
if (hostProviders.SelectMany(x => x.SupportedAuthorityIds).Any(y => StringComparer.OrdinalIgnoreCase.Equals(y, Constants.AuthorityIdAuto)))
74+
if (hostProvider.SupportedAuthorityIds.Any(y => StringComparer.OrdinalIgnoreCase.Equals(y, Constants.AuthorityIdAuto)))
6775
{
6876
throw new ArgumentException(
6977
$"A host provider cannot be registered with the legacy authority ID '{Constants.AuthorityIdAuto}'",
70-
nameof(hostProviders));
78+
nameof(hostProvider));
79+
}
80+
81+
if (!_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))
82+
{
83+
providers = new List<IHostProvider>();
84+
_hostProviders[priority] = providers;
7185
}
7286

73-
_hostProviders.AddRange(hostProviders);
87+
providers.Add(hostProvider);
7488
}
7589

7690
public Task<IHostProvider> GetProviderAsync(InputArguments input)
@@ -86,7 +100,9 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
86100

87101
if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.ProviderIdAuto, providerId))
88102
{
89-
provider = _hostProviders.FirstOrDefault(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, providerId));
103+
provider = _hostProviders
104+
.SelectMany(x => x.Value)
105+
.FirstOrDefault(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, providerId));
90106

91107
if (provider is null)
92108
{
@@ -110,7 +126,9 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
110126

111127
if (!StringComparer.OrdinalIgnoreCase.Equals(Constants.AuthorityIdAuto, authority))
112128
{
113-
provider = _hostProviders.FirstOrDefault(x => x.SupportedAuthorityIds.Contains(authority, StringComparer.OrdinalIgnoreCase));
129+
provider = _hostProviders
130+
.SelectMany(x => x.Value)
131+
.FirstOrDefault(x => x.SupportedAuthorityIds.Contains(authority, StringComparer.OrdinalIgnoreCase));
114132

115133
if (provider is null)
116134
{
@@ -128,7 +146,25 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
128146
// Auto-detection
129147
//
130148
_context.Trace.WriteLine("Performing auto-detection of host provider.");
131-
provider = _hostProviders.FirstOrDefault(x => x.IsSupported(input));
149+
150+
IHostProvider MatchProvider(HostProviderPriority priority)
151+
{
152+
if (_hostProviders.TryGetValue(priority, out ICollection<IHostProvider> providers))
153+
{
154+
_context.Trace.WriteLine($"Checking against {providers.Count} host providers registered with priority '{priority}'.");
155+
156+
if (providers.TryGetFirst(x => x.IsSupported(input), out IHostProvider match))
157+
{
158+
return match;
159+
}
160+
}
161+
162+
return null;
163+
}
164+
165+
provider = MatchProvider(HostProviderPriority.High) ??
166+
MatchProvider(HostProviderPriority.Normal) ??
167+
MatchProvider(HostProviderPriority.Low);
132168

133169
if (provider is null)
134170
{
@@ -141,7 +177,7 @@ public Task<IHostProvider> GetProviderAsync(InputArguments input)
141177
public void Dispose()
142178
{
143179
// Dispose of all registered providers to give them a chance to clean up and release any resources
144-
foreach (IHostProvider provider in _hostProviders)
180+
foreach (IHostProvider provider in _hostProviders.Values)
145181
{
146182
provider.Dispose();
147183
}

src/shared/TestInfrastructure/Objects/TestHostProviderRegistry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class TestHostProviderRegistry : IHostProviderRegistry
1010

1111
#region IHostProviderRegistry
1212

13-
void IHostProviderRegistry.Register(params IHostProvider[] hostProviders)
13+
void IHostProviderRegistry.Register(IHostProvider hostProvider, HostProviderPriority priority)
1414
{
1515
}
1616

0 commit comments

Comments
 (0)