Skip to content

Commit 564045e

Browse files
committed
autodetect: set credential.provider after autodetect
After we've detected the host provider from an auto-detection network probe, set the `credential.provider` setting so we can avoid the expensive operation in the future. If we fail to set this then warn the user and ask them to set the configuration manually.
1 parent 7edf9cd commit 564045e

File tree

3 files changed

+124
-18
lines changed

3 files changed

+124
-18
lines changed

docs/autodetect.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ hostname can be made. Only one HTTP `HEAD` call is made per credential request
3131
received by Git. To avoid this network call, please [explicitly configure](#explicit-configuration)
3232
the host provider for your self-hosted instance.
3333

34+
After a successful detection of the host provider, GCM will automatically set
35+
the [`credential.provider` configuration entry](configuration.md#credentialprovider)
36+
for that remote to avoid needing to perform this expensive network call in
37+
future requests.
38+
3439
### Timeout
3540

3641
You can control how long GCM will wait for a response to the remote network call
@@ -41,7 +46,7 @@ Git configuration setting to the maximum number of milliseconds to wait.
4146
The default value is 2000 milliseconds (2 seconds). You can prevent the network
4247
call altogether by setting a zero or negative value, for example -1.
4348

44-
## Explicit configuration
49+
## Manual configuration
4550

4651
If the auto-detection mechanism fails to select the correct host provider, or
4752
if the remote probing network call is causing performance issues, you can

src/shared/Core.Tests/HostProviderRegistryTests.cs

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Net;
45
using System.Net.Http;
56
using System.Threading.Tasks;
@@ -48,7 +49,8 @@ public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_ReturnsSupp
4849
{
4950
var context = new TestCommandContext();
5051
var registry = new HostProviderRegistry(context);
51-
var input = new InputArguments(new Dictionary<string, string>());
52+
var remote = new Uri("https://example.com");
53+
InputArguments input = CreateInputArguments(remote);
5254

5355
var provider1Mock = new Mock<IHostProvider>();
5456
var provider2Mock = new Mock<IHostProvider>();
@@ -66,12 +68,69 @@ public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_ReturnsSupp
6668
Assert.Same(provider2Mock.Object, result);
6769
}
6870

71+
[Fact]
72+
public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_SetsProviderGlobalConfig()
73+
{
74+
var context = new TestCommandContext();
75+
var registry = new HostProviderRegistry(context);
76+
var remote = new Uri("https://example.com");
77+
InputArguments input = CreateInputArguments(remote);
78+
79+
string providerId = "myProvider";
80+
string configKey = string.Format(CultureInfo.InvariantCulture,
81+
"{0}.https://example.com.{1}",
82+
Constants.GitConfiguration.Credential.SectionName,
83+
Constants.GitConfiguration.Credential.Provider);
84+
85+
var providerMock = new Mock<IHostProvider>();
86+
providerMock.Setup(x => x.Id).Returns(providerId);
87+
providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
88+
89+
registry.Register(providerMock.Object, HostProviderPriority.Normal);
90+
91+
IHostProvider result = await registry.GetProviderAsync(input);
92+
93+
Assert.Same(providerMock.Object, result);
94+
Assert.True(context.Git.Configuration.Global.TryGetValue(configKey, out IList<string> config));
95+
Assert.Equal(1, config.Count);
96+
Assert.Equal(providerId, config[0]);
97+
}
98+
99+
[Fact]
100+
public async Task HostProviderRegistry_GetProvider_Auto_HasProviders_SetsProviderGlobalConfig_HostWithPath()
101+
{
102+
var context = new TestCommandContext();
103+
var registry = new HostProviderRegistry(context);
104+
var remote = new Uri("https://example.com/alice/repo.git/");
105+
InputArguments input = CreateInputArguments(remote);
106+
107+
string providerId = "myProvider";
108+
string configKey = string.Format(CultureInfo.InvariantCulture,
109+
"{0}.https://example.com/alice/repo.git.{1}", // expect any trailing slash to be removed
110+
Constants.GitConfiguration.Credential.SectionName,
111+
Constants.GitConfiguration.Credential.Provider);
112+
113+
var providerMock = new Mock<IHostProvider>();
114+
providerMock.Setup(x => x.Id).Returns(providerId);
115+
providerMock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(true);
116+
117+
registry.Register(providerMock.Object, HostProviderPriority.Normal);
118+
119+
IHostProvider result = await registry.GetProviderAsync(input);
120+
121+
Assert.Same(providerMock.Object, result);
122+
Assert.True(context.Git.Configuration.Global.TryGetValue(configKey, out IList<string> config));
123+
Assert.Equal(1, config.Count);
124+
Assert.Equal(providerId, config[0]);
125+
}
126+
69127
[Fact]
70128
public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProviders_ReturnsFirstRegistered()
71129
{
72130
var context = new TestCommandContext();
73131
var registry = new HostProviderRegistry(context);
74-
var input = new InputArguments(new Dictionary<string, string>());
132+
var remote = new Uri("https://example.com");
133+
InputArguments input = CreateInputArguments(remote);
75134

76135
var provider1Mock = new Mock<IHostProvider>();
77136
var provider2Mock = new Mock<IHostProvider>();
@@ -94,7 +153,8 @@ public async Task HostProviderRegistry_GetProvider_Auto_MultipleValidProvidersMu
94153
{
95154
var context = new TestCommandContext();
96155
var registry = new HostProviderRegistry(context);
97-
var input = new InputArguments(new Dictionary<string, string>());
156+
var remote = new Uri("https://example.com");
157+
InputArguments input = CreateInputArguments(remote);
98158

99159
var provider1Mock = new Mock<IHostProvider>();
100160
var provider2Mock = new Mock<IHostProvider>();
@@ -152,7 +212,8 @@ public async Task HostProviderRegistry_GetProvider_AutoProviderSpecified_Returns
152212
Settings = {ProviderOverride = Constants.ProviderIdAuto}
153213
};
154214
var registry = new HostProviderRegistry(context);
155-
var input = new InputArguments(new Dictionary<string, string>());
215+
var remote = new Uri("https://example.com");
216+
InputArguments input = CreateInputArguments(remote);
156217

157218
var provider1Mock = new Mock<IHostProvider>();
158219
var provider2Mock = new Mock<IHostProvider>();
@@ -181,7 +242,8 @@ public async Task HostProviderRegistry_GetProvider_UnknownProviderSpecified_Retu
181242
Settings = {ProviderOverride = "provider42"}
182243
};
183244
var registry = new HostProviderRegistry(context);
184-
var input = new InputArguments(new Dictionary<string, string>());
245+
var remote = new Uri("https://example.com");
246+
InputArguments input = CreateInputArguments(remote);
185247

186248
var provider1Mock = new Mock<IHostProvider>();
187249
var provider2Mock = new Mock<IHostProvider>();
@@ -239,7 +301,8 @@ public async Task HostProviderRegistry_GetProvider_AutoLegacyAuthoritySpecified_
239301
Settings = {LegacyAuthorityOverride = Constants.AuthorityIdAuto}
240302
};
241303
var registry = new HostProviderRegistry(context);
242-
var input = new InputArguments(new Dictionary<string, string>());
304+
var remote = new Uri("https://example.com");
305+
InputArguments input = CreateInputArguments(remote);
243306

244307
var provider1Mock = new Mock<IHostProvider>();
245308
var provider2Mock = new Mock<IHostProvider>();
@@ -266,13 +329,7 @@ public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_ReturnsSupp
266329
var context = new TestCommandContext();
267330
var registry = new HostProviderRegistry(context);
268331
var remoteUri = new Uri("https://provider2.onprem.example.com");
269-
var input = new InputArguments(
270-
new Dictionary<string, string>
271-
{
272-
["protocol"] = remoteUri.Scheme,
273-
["host"] = remoteUri.Host
274-
}
275-
);
332+
InputArguments input = CreateInputArguments(remoteUri);
276333

277334
var provider1Mock = new Mock<IHostProvider>();
278335
provider1Mock.Setup(x => x.IsSupported(It.IsAny<InputArguments>())).Returns(false);
@@ -408,5 +465,21 @@ public async Task HostProviderRegistry_GetProvider_Auto_NetworkProbe_NoNetwork_R
408465
httpHandler.AssertRequest(HttpMethod.Head, remoteUri, 1);
409466
Assert.Same(lowProviderMock.Object, result);
410467
}
468+
469+
public static InputArguments CreateInputArguments(Uri uri)
470+
{
471+
var dict = new Dictionary<string, string>
472+
{
473+
["protocol"] = uri.Scheme,
474+
["host"] = uri.IsDefaultPort ? uri.Host : $"{uri.Host}:{uri.Port}"
475+
};
476+
477+
if (!string.IsNullOrWhiteSpace(uri.AbsolutePath) && uri.AbsolutePath != "/")
478+
{
479+
dict["path"] = uri.AbsolutePath.TrimEnd('/');
480+
}
481+
482+
return new InputArguments(dict);
483+
}
411484
}
412485
}

src/shared/Core/HostProviderRegistry.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Linq;
45
using System.Net.Http;
56
using System.Threading.Tasks;
@@ -143,10 +144,15 @@ public async Task<IHostProvider> GetProviderAsync(InputArguments input)
143144

144145
//
145146
// Auto-detection
147+
// Perform auto-detection network probe and remember the result
146148
//
147149
_context.Trace.WriteLine("Performing auto-detection of host provider.");
148150

149151
var uri = input.GetRemoteUri();
152+
if (uri is null)
153+
{
154+
throw new Exception("Unable to detect host provider without a remote URL");
155+
}
150156

151157
var probeTimeout = TimeSpan.FromMilliseconds(_context.Settings.AutoDetectProviderTimeout);
152158
_context.Trace.WriteLine($"Auto-detect probe timeout is {probeTimeout.TotalSeconds} ms.");
@@ -209,10 +215,32 @@ async Task<IHostProvider> MatchProviderAsync(HostProviderPriority priority)
209215
}
210216

211217
// Match providers starting with the highest priority
212-
return await MatchProviderAsync(HostProviderPriority.High) ??
213-
await MatchProviderAsync(HostProviderPriority.Normal) ??
214-
await MatchProviderAsync(HostProviderPriority.Low) ??
215-
throw new Exception("No host provider available to service this request.");
218+
IHostProvider match = await MatchProviderAsync(HostProviderPriority.High) ??
219+
await MatchProviderAsync(HostProviderPriority.Normal) ??
220+
await MatchProviderAsync(HostProviderPriority.Low) ??
221+
throw new Exception("No host provider available to service this request.");
222+
223+
// Set the host provider explicitly for future calls
224+
IGitConfiguration gitConfig = _context.Git.GetConfiguration();
225+
var keyName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}",
226+
Constants.GitConfiguration.Credential.SectionName, uri.ToString().TrimEnd('/'),
227+
Constants.GitConfiguration.Credential.Provider);
228+
229+
try
230+
{
231+
_context.Trace.WriteLine($"Remembering host provider for '{uri}' as '{match.Id}'...");
232+
gitConfig.Set(GitConfigurationLevel.Global, keyName, match.Id);
233+
}
234+
catch (Exception ex)
235+
{
236+
_context.Trace.WriteLine("Failed to set host provider!");
237+
_context.Trace.WriteException(ex);
238+
239+
_context.Streams.Error.WriteLine("warning: failed to remember result of host provider detection!");
240+
_context.Streams.Error.WriteLine($"warning: try setting this manually: `git config --global {keyName} {match.Id}`");
241+
}
242+
243+
return match;
216244
}
217245

218246
public void Dispose()

0 commit comments

Comments
 (0)