Skip to content

Commit c763ca0

Browse files
committed
Add main application configuration component.
Add main application configuration component, which will ensure the exectuable is on the PATH (on Windows) and configure itself and Git's credential helper in the user's config.
1 parent 4f6300a commit c763ca0

File tree

3 files changed

+371
-2
lines changed

3 files changed

+371
-2
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Threading.Tasks;
6+
using Microsoft.Git.CredentialManager.Tests.Objects;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace Microsoft.Git.CredentialManager.Tests
11+
{
12+
public class ApplicationTests
13+
{
14+
#region Common configuration tests
15+
16+
[Fact]
17+
public async Task Application_ConfigureAsync_HelperSet_DoesNothing()
18+
{
19+
const string emptyHelper = "";
20+
const string gcmConfigName = "manager-core";
21+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
22+
string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
23+
24+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
25+
26+
var environment = new Mock<IEnvironment>();
27+
28+
var config = new TestGitConfiguration();
29+
config.Dictionary[key] = new List<string>
30+
{
31+
emptyHelper, gcmConfigName
32+
};
33+
34+
await application.ConfigureAsync(
35+
environment.Object, EnvironmentVariableTarget.User,
36+
config, GitConfigurationLevel.Global);
37+
38+
Assert.Single(config.Dictionary);
39+
Assert.True(config.Dictionary.TryGetValue(key, out var actualValues));
40+
Assert.Equal(2, actualValues.Count);
41+
Assert.Equal(emptyHelper, actualValues[0]);
42+
Assert.Equal(gcmConfigName, actualValues[1]);
43+
}
44+
45+
[Fact]
46+
public async Task Application_ConfigureAsync_HelperSetWithOthersPreceding_DoesNothing()
47+
{
48+
const string emptyHelper = "";
49+
const string gcmConfigName = "manager-core";
50+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
51+
string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
52+
53+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
54+
55+
var environment = new Mock<IEnvironment>();
56+
57+
var config = new TestGitConfiguration();
58+
config.Dictionary[key] = new List<string>
59+
{
60+
"foo", "bar", emptyHelper, gcmConfigName
61+
};
62+
63+
await application.ConfigureAsync(
64+
environment.Object, EnvironmentVariableTarget.User,
65+
config, GitConfigurationLevel.Global);
66+
67+
Assert.Single(config.Dictionary);
68+
Assert.True(config.Dictionary.TryGetValue(key, out var actualValues));
69+
Assert.Equal(4, actualValues.Count);
70+
Assert.Equal("foo", actualValues[0]);
71+
Assert.Equal("bar", actualValues[1]);
72+
Assert.Equal(emptyHelper, actualValues[2]);
73+
Assert.Equal(gcmConfigName, actualValues[3]);
74+
}
75+
76+
[Fact]
77+
public async Task Application_ConfigureAsync_HelperSetWithOthersFollowing_ClearsEntriesSetsHelper()
78+
{
79+
const string emptyHelper = "";
80+
const string gcmConfigName = "manager-core";
81+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
82+
string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
83+
84+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
85+
86+
var environment = new Mock<IEnvironment>();
87+
88+
var config = new TestGitConfiguration();
89+
config.Dictionary[key] = new List<string>
90+
{
91+
"bar", emptyHelper, executablePath, "foo"
92+
};
93+
94+
await application.ConfigureAsync(
95+
environment.Object, EnvironmentVariableTarget.User,
96+
config, GitConfigurationLevel.Global);
97+
98+
Assert.Single(config.Dictionary);
99+
Assert.True(config.Dictionary.TryGetValue(key, out var actualValues));
100+
Assert.Equal(2, actualValues.Count);
101+
Assert.Equal(emptyHelper, actualValues[0]);
102+
Assert.Equal(gcmConfigName, actualValues[1]);
103+
}
104+
105+
[Fact]
106+
public async Task Application_ConfigureAsync_HelperNotSet_SetsHelper()
107+
{
108+
const string emptyHelper = "";
109+
const string gcmConfigName = "manager-core";
110+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
111+
string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
112+
113+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
114+
115+
var environment = new Mock<IEnvironment>();
116+
117+
var config = new TestGitConfiguration();
118+
119+
await application.ConfigureAsync(
120+
environment.Object, EnvironmentVariableTarget.User,
121+
config, GitConfigurationLevel.Global);
122+
123+
Assert.Single(config.Dictionary);
124+
Assert.True(config.Dictionary.TryGetValue(key, out var actualValues));
125+
Assert.Equal(2, actualValues.Count);
126+
Assert.Equal(emptyHelper, actualValues[0]);
127+
Assert.Equal(gcmConfigName, actualValues[1]);
128+
}
129+
130+
[Fact]
131+
public async Task Application_UnconfigureAsync_HelperSet_RemovesEntries()
132+
{
133+
const string emptyHelper = "";
134+
const string gcmConfigName = "manager-core";
135+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
136+
string key = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
137+
138+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
139+
140+
var environment = new Mock<IEnvironment>();
141+
var config = new TestGitConfiguration(new Dictionary<string, IList<string>>
142+
{
143+
[key] = new List<string> {emptyHelper, gcmConfigName}
144+
});
145+
146+
await application.UnconfigureAsync(
147+
environment.Object, EnvironmentVariableTarget.User,
148+
config, GitConfigurationLevel.Global);
149+
150+
Assert.Empty(config.Dictionary);
151+
}
152+
153+
#endregion
154+
155+
#region Windows-specific configuration tests
156+
157+
[PlatformFact(Platform.Windows)]
158+
public async Task Application_ConfigureAsync_User_PathSet_DoesNothing()
159+
{
160+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
161+
162+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
163+
164+
var environment = new Mock<IEnvironment>();
165+
environment.Setup(x => x.IsDirectoryOnPath("/usr/local/share/gcm-core")).Returns(true);
166+
167+
var config = new TestGitConfiguration();
168+
169+
await application.ConfigureAsync(
170+
environment.Object, EnvironmentVariableTarget.User,
171+
config, GitConfigurationLevel.Global);
172+
173+
environment.Verify(x => x.AddDirectoryToPath(It.IsAny<string>(), It.IsAny<EnvironmentVariableTarget>()), Times.Never);
174+
}
175+
176+
[PlatformFact(Platform.Windows)]
177+
public async Task Application_ConfigureAsync_User_PathNotSet_SetsUserPath()
178+
{
179+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
180+
181+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
182+
183+
var environment = new Mock<IEnvironment>();
184+
environment.Setup(x => x.IsDirectoryOnPath("/usr/local/share/gcm-core")).Returns(false);
185+
186+
var config = new TestGitConfiguration();
187+
188+
await application.ConfigureAsync(
189+
environment.Object, EnvironmentVariableTarget.User,
190+
config, GitConfigurationLevel.Global);
191+
192+
environment.Verify(x => x.AddDirectoryToPath(directoryPath, EnvironmentVariableTarget.User), Times.Once);
193+
}
194+
195+
[PlatformFact(Platform.Windows)]
196+
public async Task Application_UnconfigureAsync_User_PathSet_RemovesFromUserPath()
197+
{
198+
const string executablePath = "/usr/local/share/gcm-core/git-credential-manager-core";
199+
200+
IConfigurableComponent application = new Application(new TestCommandContext(), executablePath);
201+
202+
var environment = new Mock<IEnvironment>();
203+
environment.Setup(x => x.IsDirectoryOnPath("/usr/local/share/gcm-core")).Returns(true);
204+
205+
var config = new TestGitConfiguration();
206+
207+
await application.UnconfigureAsync(
208+
environment.Object, EnvironmentVariableTarget.User,
209+
config, GitConfigurationLevel.Global);
210+
211+
environment.Verify(x => x.RemoveDirectoryFromPath(directoryPath, EnvironmentVariableTarget.User), Times.Once);
212+
}
213+
214+
#endregion
215+
}
216+
}

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

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,52 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text.RegularExpressions;
47
using System.Threading.Tasks;
58
using Microsoft.Git.CredentialManager.Commands;
69
using Microsoft.Git.CredentialManager.Interop;
710

811
namespace Microsoft.Git.CredentialManager
912
{
10-
public class Application : ApplicationBase
13+
public class Application : ApplicationBase, IConfigurableComponent
1114
{
1215
private readonly string _appPath;
1316
private readonly IHostProviderRegistry _providerRegistry;
17+
private readonly IConfigurationService _configurationService;
1418

1519
public Application(ICommandContext context, string appPath)
20+
: this(context, new HostProviderRegistry(context), new ConfigurationService(context), appPath)
21+
{
22+
}
23+
24+
internal Application(ICommandContext context,
25+
IHostProviderRegistry providerRegistry,
26+
IConfigurationService configurationService,
27+
string appPath)
1628
: base(context)
1729
{
30+
EnsureArgument.NotNull(providerRegistry, nameof(providerRegistry));
31+
EnsureArgument.NotNull(configurationService, nameof(configurationService));
1832
EnsureArgument.NotNullOrWhiteSpace(appPath, nameof(appPath));
1933

2034
_appPath = appPath;
21-
_providerRegistry = new HostProviderRegistry(context);
35+
_providerRegistry = providerRegistry;
36+
_configurationService = configurationService;
37+
38+
_configurationService.AddComponent(this);
2239
}
2340

2441
public void RegisterProviders(params IHostProvider[] providers)
2542
{
2643
_providerRegistry.Register(providers);
44+
45+
// Add any providers that are also configurable components to the configuration service
46+
foreach (IConfigurableComponent configurableProvider in providers.OfType<IConfigurableComponent>())
47+
{
48+
_configurationService.AddComponent(configurableProvider);
49+
}
2750
}
2851

2952
protected override async Task<int> RunInternalAsync(string[] args)
@@ -36,6 +59,8 @@ protected override async Task<int> RunInternalAsync(string[] args)
3659
new GetCommand(_providerRegistry),
3760
new StoreCommand(_providerRegistry),
3861
new EraseCommand(_providerRegistry),
62+
new ConfigureCommand(_configurationService),
63+
new UnconfigureCommand(_configurationService),
3964
new VersionCommand(),
4065
new HelpCommand(appName),
4166
};
@@ -111,5 +136,114 @@ protected bool WriteException(Exception ex)
111136

112137
return true;
113138
}
139+
140+
#region IConfigurableComponent
141+
142+
string IConfigurableComponent.Name => "Git Credential Manager";
143+
144+
Task IConfigurableComponent.ConfigureAsync(
145+
IEnvironment environment, EnvironmentVariableTarget environmentTarget,
146+
IGitConfiguration configuration, GitConfigurationLevel configurationLevel)
147+
{
148+
// NOTE: We currently only update the PATH in Windows installations and leave putting the GCM executable
149+
// on the PATH on other platform to their installers.
150+
if (PlatformUtils.IsWindows())
151+
{
152+
string directoryPath = Path.GetDirectoryName(_appPath);
153+
if (!environment.IsDirectoryOnPath(directoryPath))
154+
{
155+
Context.Trace.WriteLine("Adding application to PATH...");
156+
environment.AddDirectoryToPath(directoryPath, environmentTarget);
157+
}
158+
else
159+
{
160+
Context.Trace.WriteLine("Application is already on the PATH.");
161+
}
162+
}
163+
164+
string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
165+
string gitConfigAppName = GetGitConfigAppName();
166+
167+
using (IGitConfiguration targetConfig = configuration.GetFilteredConfiguration(configurationLevel))
168+
{
169+
/*
170+
* We are looking for the following to be considered already set:
171+
*
172+
* [credential]
173+
* ... # any number of helper entries
174+
* helper = # an empty value to reset/clear any previous entries
175+
* helper = {gitConfigAppName} # the expected executable value in the last position & directly following the empty value
176+
*
177+
*/
178+
179+
string[] currentValues = targetConfig.GetMultivarValue(helperKey, Constants.RegexPatterns.Any).ToArray();
180+
if (currentValues.Length < 2 ||
181+
!string.IsNullOrWhiteSpace(currentValues[currentValues.Length - 2]) || // second to last entry is empty
182+
currentValues[currentValues.Length - 1] != gitConfigAppName) // last entry is the expected executable
183+
{
184+
Context.Trace.WriteLine("Updating Git credential helper configuration...");
185+
186+
// Clear any existing entries in the configuration.
187+
targetConfig.DeleteMultivarEntry(helperKey, Constants.RegexPatterns.Any);
188+
189+
// Add an empty value for `credential.helper`, which has the effect of clearing any helper value
190+
// from any lower-level Git configuration, then add a second value which is the actual executable path.
191+
targetConfig.SetValue(helperKey, string.Empty);
192+
targetConfig.SetMultivarValue(helperKey, Constants.RegexPatterns.None, gitConfigAppName);
193+
}
194+
else
195+
{
196+
Context.Trace.WriteLine("Credential helper configuration is already set correctly.");
197+
}
198+
}
199+
200+
return Task.CompletedTask;
201+
}
202+
203+
Task IConfigurableComponent.UnconfigureAsync(
204+
IEnvironment environment, EnvironmentVariableTarget environmentTarget,
205+
IGitConfiguration configuration, GitConfigurationLevel configurationLevel)
206+
{
207+
string helperKey = $"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
208+
string gitConfigAppName = GetGitConfigAppName();
209+
210+
using (IGitConfiguration targetConfig = configuration.GetFilteredConfiguration(configurationLevel))
211+
{
212+
Context.Trace.WriteLine("Removing Git credential helper configuration...");
213+
214+
// Clear any blank 'reset' entries
215+
targetConfig.DeleteMultivarEntry(helperKey, Constants.RegexPatterns.Empty);
216+
217+
// Clear GCM executable entries
218+
targetConfig.DeleteMultivarEntry(helperKey, Regex.Escape(gitConfigAppName));
219+
}
220+
221+
// NOTE: We currently only update the PATH in Windows installations and leave removing the GCM executable
222+
// on the PATH on other platform to their installers.
223+
// Remove GCM executable from the PATH
224+
if (PlatformUtils.IsWindows())
225+
{
226+
Context.Trace.WriteLine("Removing application from the PATH...");
227+
string directoryPath = Path.GetDirectoryName(_appPath);
228+
environment.RemoveDirectoryFromPath(directoryPath, environmentTarget);
229+
}
230+
231+
return Task.CompletedTask;
232+
}
233+
234+
private string GetGitConfigAppName()
235+
{
236+
const string gitCredentialPrefix = "git-credential-";
237+
238+
string appName = Path.GetFileNameWithoutExtension(_appPath);
239+
if (appName != null && appName.StartsWith(gitCredentialPrefix, StringComparison.OrdinalIgnoreCase))
240+
{
241+
return appName.Substring(gitCredentialPrefix.Length);
242+
}
243+
244+
return _appPath;
245+
}
246+
247+
#endregion
114248
}
115249
}

0 commit comments

Comments
 (0)